Changeset 14716


Ignore:
Timestamp:
Mar 10, 2016, 7:05:13 PM (17 months ago)
Author:
mikedld
Message:

#6089: Beautified JavaScript? (patch by skybon)

Location:
trunk
Files:
1 added
11 edited

Legend:

Unmodified
Added
Removed
  • trunk/web/javascript/common.js

    r14523 r14716  
    1111    scroll_timeout;
    1212
    13 if (!Array.indexOf){
    14         Array.prototype.indexOf = function(obj){
    15                 var i, len;
    16                 for (i=0, len=this.length; i<len; i++)
    17                         if (this[i]==obj)
    18                                 return i;
    19                 return -1;
    20         }
    21 }
     13if (!Array.indexOf) {
     14    Array.prototype.indexOf = function (obj) {
     15        var i, len;
     16        for (i = 0, len = this.length; i < len; i++) {
     17            if (this[i] == obj) {
     18                return i;
     19            };
     20        };
     21        return -1;
     22    };
     23};
    2224
    2325// http://forum.jquery.com/topic/combining-ui-dialog-and-tabs
    2426$.fn.tabbedDialog = function (dialog_opts) {
    25         this.tabs({selected: 0});
    26         this.dialog(dialog_opts);
    27         this.find('.ui-tab-dialog-close').append(this.parent().find('.ui-dialog-titlebar-close'));
    28         this.find('.ui-tab-dialog-close').css({'position':'absolute','right':'0', 'top':'16px'});
    29         this.find('.ui-tab-dialog-close > a').css({'float':'none','padding':'0'});
    30         var tabul = this.find('ul:first');
    31         this.parent().addClass('ui-tabs').prepend(tabul).draggable('option','handle',tabul);
    32         this.siblings('.ui-dialog-titlebar').remove();
    33         tabul.addClass('ui-dialog-titlebar');
     27    this.tabs({
     28        selected: 0
     29    });
     30    this.dialog(dialog_opts);
     31    this.find('.ui-tab-dialog-close').append(this.parent().find('.ui-dialog-titlebar-close'));
     32    this.find('.ui-tab-dialog-close').css({
     33        'position': 'absolute',
     34        'right': '0',
     35        'top': '16px'
     36    });
     37    this.find('.ui-tab-dialog-close > a').css({
     38        'float': 'none',
     39        'padding': '0'
     40    });
     41    var tabul = this.find('ul:first');
     42    this.parent().addClass('ui-tabs').prepend(tabul).draggable('option', 'handle', tabul);
     43    this.siblings('.ui-dialog-titlebar').remove();
     44    tabul.addClass('ui-dialog-titlebar');
    3445}
    3546
    36 $(document).ready(function() {
    37 
    38         // IE8 and below don’t support ES5 Date.now()
    39         if (!Date.now) {
    40                 Date.now = function() {
    41                         return +new Date();
    42                 };
    43         }
    44 
    45         // IE specific fixes here
    46         if ($.browser.msie) {
    47                 try {
    48                         document.execCommand("BackgroundImageCache", false, true);
    49                 } catch(err) {}
    50                 $('.dialog_container').css('height',$(window).height()+'px');
    51         }
    52 
    53         if ($.browser.safari) {
    54                 // Move search field's margin down for the styled input
    55                 $('#torrent_search').css('margin-top', 3);
    56         }
    57         if (isMobileDevice){
    58                 window.onload = function(){ setTimeout(function() { window.scrollTo(0,1); },500); };
    59                 window.onorientationchange = function(){ setTimeout(function() { window.scrollTo(0,1); },100); };
    60                 if (window.navigator.standalone)
    61                         // Fix min height for isMobileDevice when run in full screen mode from home screen
    62                         // so the footer appears in the right place
    63                         $('body div#torrent_container').css('min-height', '338px');
    64                 $("label[for=torrent_upload_url]").text("URL: ");
    65         } else {
    66                 // Fix for non-Safari-3 browsers: dark borders to replace shadows.
    67                 $('div.dialog_container div.dialog_window').css('border', '1px solid #777');
    68         }
    69 
    70         // Initialise the dialog controller
    71         dialog = new Dialog();
    72 
    73         // Initialise the main Transmission controller
    74         transmission = new Transmission();
     47$(document).ready(function () {
     48
     49    // IE8 and below don’t support ES5 Date.now()
     50    if (!Date.now) {
     51        Date.now = function () {
     52            return +new Date();
     53        };
     54    };
     55
     56    // IE specific fixes here
     57    if ($.browser.msie) {
     58        try {
     59            document.execCommand("BackgroundImageCache", false, true);
     60        } catch (err) {};
     61        $('.dialog_container').css('height', $(window).height() + 'px');
     62    };
     63
     64    if ($.browser.safari) {
     65        // Move search field's margin down for the styled input
     66        $('#torrent_search').css('margin-top', 3);
     67    };
     68
     69    if (isMobileDevice) {
     70        window.onload = function () {
     71            setTimeout(function () {
     72                window.scrollTo(0, 1);
     73            }, 500);
     74        };
     75        window.onorientationchange = function () {
     76            setTimeout(function () {
     77                window.scrollTo(0, 1);
     78            }, 100);
     79        };
     80        if (window.navigator.standalone) {
     81            // Fix min height for isMobileDevice when run in full screen mode from home screen
     82            // so the footer appears in the right place
     83            $('body div#torrent_container').css('min-height', '338px');
     84        };
     85        $("label[for=torrent_upload_url]").text("URL: ");
     86    } else {
     87        // Fix for non-Safari-3 browsers: dark borders to replace shadows.
     88        $('div.dialog_container div.dialog_window').css('border', '1px solid #777');
     89    };
     90
     91    // Initialise the dialog controller
     92    dialog = new Dialog();
     93
     94    // Initialise the main Transmission controller
     95    transmission = new Transmission();
    7596});
    7697
     
    7899 * Checks to see if the content actually changed before poking the DOM.
    79100 */
    80 function setInnerHTML(e, html)
    81 {
    82         if (!e)
    83                 return;
    84 
    85         /* innerHTML is listed as a string, but the browser seems to change it.
    86          * For example, "&infin;" gets changed to "∞" somewhere down the line.
    87          * So, let's use an arbitrary  different field to test our state... */
    88         if (e.currentHTML != html)
    89         {
    90                 e.currentHTML = html;
    91                 e.innerHTML = html;
    92         }
    93 };
    94 
    95 function sanitizeText(text)
    96 {
    97         return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
     101function setInnerHTML(e, html) {
     102    if (!e) {
     103        return;
     104    };
     105
     106    /* innerHTML is listed as a string, but the browser seems to change it.
     107     * For example, "&infin;" gets changed to "∞" somewhere down the line.
     108     * So, let's use an arbitrary  different field to test our state... */
     109    if (e.currentHTML != html) {
     110        e.currentHTML = html;
     111        e.innerHTML = html;
     112    };
     113};
     114
     115function sanitizeText(text) {
     116    return text.replace(/</g, "&lt;").replace(/>/g, "&gt;");
    98117};
    99118
     
    103122 * so see if the text actually changed before poking the DOM.
    104123 */
    105 function setTextContent(e, text)
    106 {
    107         if (e && (e.textContent != text))
    108                 e.textContent = text;
     124function setTextContent(e, text) {
     125    if (e && (e.textContent != text)) {
     126        e.textContent = text;
     127    };
    109128};
    110129
     
    112131 *   Given a numerator and denominator, return a ratio string
    113132 */
    114 Math.ratio = function(numerator, denominator) {
    115         var result = Math.floor(100 * numerator / denominator) / 100;
    116 
    117         // check for special cases
    118         if (result==Number.POSITIVE_INFINITY || result==Number.NEGATIVE_INFINITY) result = -2;
    119         else if (isNaN(result)) result = -1;
    120 
    121         return result;
     133Math.ratio = function (numerator, denominator) {
     134    var result = Math.floor(100 * numerator / denominator) / 100;
     135
     136    // check for special cases
     137    if (result == Number.POSITIVE_INFINITY || result == Number.NEGATIVE_INFINITY) {
     138        result = -2;
     139    } else if (isNaN(result)) {
     140        result = -1;
     141    };
     142
     143    return result;
    122144};
    123145
     
    125147 * Round a string of a number to a specified number of decimal places
    126148 */
    127 Number.prototype.toTruncFixed = function(place) {
    128         var ret = Math.floor(this * Math.pow (10, place)) / Math.pow(10, place);
    129         return ret.toFixed(place);
    130 }
    131 
    132 Number.prototype.toStringWithCommas = function() {
     149Number.prototype.toTruncFixed = function (place) {
     150    var ret = Math.floor(this * Math.pow(10, place)) / Math.pow(10, place);
     151    return ret.toFixed(place);
     152};
     153
     154Number.prototype.toStringWithCommas = function () {
    133155    return this.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ",");
    134 }
    135 
     156};
    136157
    137158/*
     
    139160 */
    140161String.prototype.trim = function () {
    141         return this.replace(/^\s*/, "").replace(/\s*$/, "");
    142 }
     162    return this.replace(/^\s*/, "").replace(/\s*$/, "");
     163};
    143164
    144165/***
    145 ****  Preferences
    146 ***/
    147 
    148 function Prefs() { }
    149 Prefs.prototype = { };
    150 
    151 Prefs._RefreshRate        = 'refresh_rate';
    152 
    153 Prefs._FilterMode         = 'filter';
    154 Prefs._FilterAll          = 'all';
    155 Prefs._FilterActive       = 'active';
    156 Prefs._FilterSeeding      = 'seeding';
    157 Prefs._FilterDownloading  = 'downloading';
    158 Prefs._FilterPaused       = 'paused';
    159 Prefs._FilterFinished     = 'finished';
    160 
    161 Prefs._SortDirection      = 'sort_direction';
    162 Prefs._SortAscending      = 'ascending';
    163 Prefs._SortDescending     = 'descending';
    164 
    165 Prefs._SortMethod         = 'sort_method';
    166 Prefs._SortByAge          = 'age';
    167 Prefs._SortByActivity     = 'activity';
    168 Prefs._SortByName         = 'name';
    169 Prefs._SortByQueue        = 'queue_order';
    170 Prefs._SortBySize         = 'size';
    171 Prefs._SortByProgress     = 'percent_completed';
    172 Prefs._SortByRatio        = 'ratio';
    173 Prefs._SortByState        = 'state';
    174 
    175 Prefs._CompactDisplayState= 'compact_display_state';
    176 
    177 Prefs._Defaults =
    178 {
    179         'filter': 'all',
    180         'refresh_rate' : 5,
    181         'sort_direction': 'ascending',
    182         'sort_method': 'name',
    183         'turtle-state' : false,
    184         'compact_display_state' : false
     166 ****  Preferences
     167 ***/
     168
     169function Prefs() {};
     170Prefs.prototype = {};
     171
     172Prefs._RefreshRate = 'refresh_rate';
     173
     174Prefs._FilterMode = 'filter';
     175Prefs._FilterAll = 'all';
     176Prefs._FilterActive = 'active';
     177Prefs._FilterSeeding = 'seeding';
     178Prefs._FilterDownloading = 'downloading';
     179Prefs._FilterPaused = 'paused';
     180Prefs._FilterFinished = 'finished';
     181
     182Prefs._SortDirection = 'sort_direction';
     183Prefs._SortAscending = 'ascending';
     184Prefs._SortDescending = 'descending';
     185
     186Prefs._SortMethod = 'sort_method';
     187Prefs._SortByAge = 'age';
     188Prefs._SortByActivity = 'activity';
     189Prefs._SortByName = 'name';
     190Prefs._SortByQueue = 'queue_order';
     191Prefs._SortBySize = 'size';
     192Prefs._SortByProgress = 'percent_completed';
     193Prefs._SortByRatio = 'ratio';
     194Prefs._SortByState = 'state';
     195
     196Prefs._CompactDisplayState = 'compact_display_state';
     197
     198Prefs._Defaults = {
     199    'filter': 'all',
     200    'refresh_rate': 5,
     201    'sort_direction': 'ascending',
     202    'sort_method': 'name',
     203    'turtle-state': false,
     204    'compact_display_state': false
    185205};
    186206
     
    188208 * Set a preference option
    189209 */
    190 Prefs.setValue = function(key, val)
    191 {
    192         if (!(key in Prefs._Defaults))
    193                 console.warn("unrecognized preference key '%s'", key);
    194 
    195         var date = new Date();
    196         date.setFullYear (date.getFullYear() + 1);
    197         document.cookie = key+"="+val+"; expires="+date.toGMTString()+"; path=/";
     210Prefs.setValue = function (key, val) {
     211    if (!(key in Prefs._Defaults)) {
     212        console.warn("unrecognized preference key '%s'", key);
     213    };
     214
     215    var date = new Date();
     216    date.setFullYear(date.getFullYear() + 1);
     217    document.cookie = key + "=" + val + "; expires=" + date.toGMTString() + "; path=/";
    198218};
    199219
     
    204224 * @param fallback if the option isn't set, return this instead
    205225 */
    206 Prefs.getValue = function(key, fallback)
    207 {
    208         var val;
    209 
    210         if (!(key in Prefs._Defaults))
    211                 console.warn("unrecognized preference key '%s'", key);
    212 
    213         var lines = document.cookie.split(';');
    214         for (var i=0, len=lines.length; !val && i<len; ++i) {
    215                 var line = lines[i].trim();
    216                 var delim = line.indexOf('=');
    217                 if ((delim === key.length) && line.indexOf(key) === 0)
    218                         val = line.substring(delim + 1);
    219         }
    220 
    221         // FIXME: we support strings and booleans... add number support too?
    222         if (!val) val = fallback;
    223         else if (val === 'true') val = true;
    224         else if (val === 'false') val = false;
    225         return val;
     226Prefs.getValue = function (key, fallback) {
     227    var val;
     228
     229    if (!(key in Prefs._Defaults)) {
     230        console.warn("unrecognized preference key '%s'", key);
     231    };
     232
     233    var lines = document.cookie.split(';');
     234    for (var i = 0, len = lines.length; !val && i < len; ++i) {
     235        var line = lines[i].trim();
     236        var delim = line.indexOf('=');
     237        if ((delim === key.length) && line.indexOf(key) === 0) {
     238            val = line.substring(delim + 1);
     239        };
     240    };
     241
     242    // FIXME: we support strings and booleans... add number support too?
     243    if (!val) {
     244        val = fallback;
     245    } else if (val === 'true') {
     246        val = true;
     247    } else if (val === 'false') {
     248        val = false;
     249    };
     250    return val;
    226251};
    227252
     
    231256 * @pararm o object to be populated (optional)
    232257 */
    233 Prefs.getClutchPrefs = function(o)
    234 {
    235         if (!o)
    236                 o = { };
    237         for (var key in Prefs._Defaults)
    238                 o[key] = Prefs.getValue(key, Prefs._Defaults[key]);
    239         return o;
    240 };
    241 
     258Prefs.getClutchPrefs = function (o) {
     259    if (!o) {
     260        o = {};
     261    };
     262    for (var key in Prefs._Defaults) {
     263        o[key] = Prefs.getValue(key, Prefs._Defaults[key]);
     264    };
     265    return o;
     266};
    242267
    243268// forceNumeric() plug-in implementation
    244269jQuery.fn.forceNumeric = function () {
    245         return this.each(function () {
    246                 $(this).keydown(function (e) {
    247                         var key = e.which || e.keyCode;
    248                         return !e.shiftKey && !e.altKey && !e.ctrlKey &&
    249                                 // numbers
    250                                 key >= 48 && key <= 57 ||
    251                                 // Numeric keypad
    252                                 key >= 96 && key <= 105 ||
    253                                 // comma, period and minus, . on keypad
    254                                 key === 190 || key === 188 || key === 109 || key === 110 ||
    255                                 // Backspace and Tab and Enter
    256                                 key === 8 || key === 9 || key === 13 ||
    257                                 // Home and End
    258                                 key === 35 || key === 36 ||
    259                                 // left and right arrows
    260                                 key === 37 || key === 39 ||
    261                                 // Del and Ins
    262                                 key === 46 || key === 45;
    263                 });
    264         });
     270    return this.each(function () {
     271        $(this).keydown(function (e) {
     272            var key = e.which || e.keyCode;
     273            return !e.shiftKey && !e.altKey && !e.ctrlKey &&
     274                // numbers
     275                key >= 48 && key <= 57 ||
     276                // Numeric keypad
     277                key >= 96 && key <= 105 ||
     278                // comma, period and minus, . on keypad
     279                key === 190 || key === 188 || key === 109 || key === 110 ||
     280                // Backspace and Tab and Enter
     281                key === 8 || key === 9 || key === 13 ||
     282                // Home and End
     283                key === 35 || key === 36 ||
     284                // left and right arrows
     285                key === 37 || key === 39 ||
     286                // Del and Ins
     287                key === 46 || key === 45;
     288        });
     289    });
    265290}
    266 
    267291
    268292/**
     
    273297 * MIT License
    274298 */
    275 function parseUri (str) {
    276         var     o   = parseUri.options,
    277                 m   = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
    278                 uri = {},
    279                 i   = 14;
    280 
    281         while (i--) uri[o.key[i]] = m[i] || "";
    282 
    283         uri[o.q.name] = {};
    284         uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
    285                 if ($1) uri[o.q.name][$1] = $2;
    286         });
    287 
    288         return uri;
     299function parseUri(str) {
     300    var o = parseUri.options;
     301    var m = o.parser[o.strictMode ? "strict" : "loose"].exec(str);
     302    var uri = {};
     303    var i = 14;
     304
     305    while (i--) {
     306        uri[o.key[i]] = m[i] || "";
     307    };
     308
     309    uri[o.q.name] = {};
     310    uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
     311        if ($1) {
     312            uri[o.q.name][$1] = $2;
     313        };
     314    });
     315
     316    return uri;
    289317};
    290318
    291319parseUri.options = {
    292         strictMode: false,
    293         key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
    294         q:  {
    295                 name:  "queryKey",
    296                 parser: /(?:^|&)([^&=]*)=?([^&]*)/g
    297         },
    298         parser: {
    299                 strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
    300                 loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
    301         }
    302 };
     320    strictMode: false,
     321    key: ["source", "protocol", "authority", "userInfo", "user", "password", "host", "port", "relative", "path", "directory", "file", "query", "anchor"],
     322    q: {
     323        name: "queryKey",
     324        parser: /(?:^|&)([^&=]*)=?([^&]*)/g
     325    },
     326    parser: {
     327        strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
     328        loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
     329    }
     330};
  • trunk/web/javascript/dialog.js

    r14523 r14716  
    66 */
    77
    8 function Dialog(){
    9         this.initialize();
    10 }
     8function Dialog() {
     9    this.initialize();
     10};
    1111
    1212Dialog.prototype = {
    1313
    14         /*
    15         * Constructor
    16         */
    17         initialize: function() {
     14    /*
     15    * Constructor
     16    */
     17    initialize: function () {
    1818
    19                 /*
    20                 * Private Interface Variables
    21                 */
    22                 this._container = $('#dialog_container');
    23                 this._heading = $('#dialog_heading');
    24                 this._message = $('#dialog_message');
    25                 this._cancel_button = $('#dialog_cancel_button');
    26                 this._confirm_button = $('#dialog_confirm_button');
    27                 this._callback = null;
     19        /*
     20        * Private Interface Variables
     21        */
     22        this._container = $('#dialog_container');
     23        this._heading = $('#dialog_heading');
     24        this._message = $('#dialog_message');
     25        this._cancel_button = $('#dialog_cancel_button');
     26        this._confirm_button = $('#dialog_confirm_button');
     27        this._callback = null;
    2828
    29                 // Observe the buttons
    30                 this._cancel_button.bind('click', {dialog: this}, this.onCancelClicked);
    31                 this._confirm_button.bind('click', {dialog: this}, this.onConfirmClicked);
    32         },
     29        // Observe the buttons
     30        this._cancel_button.bind('click', {
     31            dialog: this
     32        }, this.onCancelClicked);
     33        this._confirm_button.bind('click', {
     34            dialog: this
     35        }, this.onConfirmClicked);
     36    },
    3337
     38    /*--------------------------------------------
     39     *
     40     *  E V E N T   F U N C T I O N S
     41     *
     42     *--------------------------------------------*/
    3443
     44    hideDialog: function () {
     45        $('body.dialog_showing').removeClass('dialog_showing');
     46        this._container.hide();
     47        transmission.hideMobileAddressbar();
     48        transmission.updateButtonStates();
     49    },
    3550
     51    onCancelClicked: function (event) {
     52        event.data.dialog.hideDialog();
     53    },
    3654
     55    onConfirmClicked: function (event) {
     56        var dialog = event.data.dialog;
     57        dialog._callback();
     58        dialog.hideDialog();
     59    },
    3760
    38         /*--------------------------------------------
    39         *
    40          *  E V E N T   F U N C T I O N S
    41         *
    42         *--------------------------------------------*/
     61    /*--------------------------------------------
     62    *
     63     *  I N T E R F A C E   F U N C T I O N S
     64    *
     65    *--------------------------------------------*/
    4366
    44         hideDialog: function()
    45         {
    46                 $('body.dialog_showing').removeClass('dialog_showing');
    47                 this._container.hide();
    48                 transmission.hideMobileAddressbar();
    49                 transmission.updateButtonStates();
    50         },
     67    /*
     68     * Display a confirm dialog
     69     */
     70    confirm: function (dialog_heading, dialog_message, confirm_button_label,
     71        callback, cancel_button_label) {
     72        if (!isMobileDevice) {
     73            $('.dialog_container').hide();
     74        };
     75        setTextContent(this._heading[0], dialog_heading);
     76        setTextContent(this._message[0], dialog_message);
     77        setTextContent(this._cancel_button[0], cancel_button_label || 'Cancel');
     78        setTextContent(this._confirm_button[0], confirm_button_label);
     79        this._confirm_button.show();
     80        this._callback = callback;
     81        $('body').addClass('dialog_showing');
     82        this._container.show();
     83        transmission.updateButtonStates();
     84        if (isMobileDevice) {
     85            transmission.hideMobileAddressbar();
     86        };
     87    },
    5188
    52         onCancelClicked: function(event)
    53         {
    54                 event.data.dialog.hideDialog();
    55         },
    56 
    57         onConfirmClicked: function(event)
    58         {
    59                 var dialog = event.data.dialog;
    60                 dialog._callback();
    61                 dialog.hideDialog();
    62         },
    63 
    64         /*--------------------------------------------
    65          *
    66          *  I N T E R F A C E   F U N C T I O N S
    67          *
    68          *--------------------------------------------*/
    69 
    70         /*
    71          * Display a confirm dialog
    72          */
    73         confirm: function(dialog_heading, dialog_message, confirm_button_label,
    74                           callback, cancel_button_label)
    75         {
    76                 if (!isMobileDevice)
    77                         $('.dialog_container').hide();
    78                 setTextContent(this._heading[0], dialog_heading);
    79                 setTextContent(this._message[0], dialog_message);
    80                 setTextContent(this._cancel_button[0], cancel_button_label || 'Cancel');
    81                 setTextContent(this._confirm_button[0], confirm_button_label);
    82                 this._confirm_button.show();
    83                 this._callback = callback;
    84                 $('body').addClass('dialog_showing');
    85                 this._container.show();
    86                 transmission.updateButtonStates();
    87                 if (isMobileDevice)
    88                         transmission.hideMobileAddressbar();
    89         },
    90 
    91         /*
    92          * Display an alert dialog
    93          */
    94         alert: function(dialog_heading, dialog_message, cancel_button_label) {
    95                 if (!isMobileDevice)
    96                         $('.dialog_container').hide();
    97                 setTextContent(this._heading[0], dialog_heading);
    98                 setTextContent(this._message[0], dialog_message);
    99                 // jquery::hide() doesn't work here in Safari for some odd reason
    100                 this._confirm_button.css('display', 'none');
    101                 setTextContent(this._cancel_button[0], cancel_button_label);
    102                 // Just in case
    103                 $('#upload_container').hide();
    104                 $('#move_container').hide();
    105                 $('body').addClass('dialog_showing');
    106                 transmission.updateButtonStates();
    107                 if (isMobileDevice)
    108                         transmission.hideMobileAddressbar();
    109                 this._container.show();
    110         }
    111 
    112 
    113 }
     89    /*
     90     * Display an alert dialog
     91     */
     92    alert: function (dialog_heading, dialog_message, cancel_button_label) {
     93        if (!isMobileDevice) {
     94            $('.dialog_container').hide();
     95        };
     96        setTextContent(this._heading[0], dialog_heading);
     97        setTextContent(this._message[0], dialog_message);
     98        // jquery::hide() doesn't work here in Safari for some odd reason
     99        this._confirm_button.css('display', 'none');
     100        setTextContent(this._cancel_button[0], cancel_button_label);
     101        // Just in case
     102        $('#upload_container').hide();
     103        $('#move_container').hide();
     104        $('body').addClass('dialog_showing');
     105        transmission.updateButtonStates();
     106        if (isMobileDevice) {
     107            transmission.hideMobileAddressbar();
     108        };
     109        this._container.show();
     110    }
     111};
  • trunk/web/javascript/file-row.js

    r14523 r14716  
    66 */
    77
    8 function FileRow(torrent, depth, name, indices, even)
    9 {
    10         var fields = {
    11                 have: 0,
    12                 indices: [],
    13                 isWanted: true,
    14                 priorityLow: false,
    15                 priorityNormal: false,
    16                 priorityHigh: false,
    17                 me: this,
    18                 size: 0,
    19                 torrent: null
    20         },
    21 
    22         elements = {
    23                 priority_low_button: null,
    24                 priority_normal_button: null,
    25                 priority_high_button: null,
    26                 progress: null,
    27                 root: null
    28         },
    29 
    30         initialize = function(torrent, depth, name, indices, even) {
    31                 fields.torrent = torrent;
    32                 fields.indices = indices;
    33                 createRow(torrent, depth, name, even);
    34         },
    35 
    36         refreshWantedHTML = function()
    37         {
    38                 var e = $(elements.root);
    39                 e.toggleClass('skip', !fields.isWanted);
    40                 e.toggleClass('complete', isDone());
    41                 $(e[0].checkbox).prop('disabled', !isEditable());
    42                 $(e[0].checkbox).prop('checked', fields.isWanted);
    43         },
    44         refreshProgressHTML = function()
    45         {
    46                 var pct = 100 * (fields.size ? (fields.have / fields.size) : 1.0),
    47                     c = [ Transmission.fmt.size(fields.have),
    48                           ' of ',
    49                           Transmission.fmt.size(fields.size),
    50                           ' (',
    51                           Transmission.fmt.percentString(pct),
    52                           '%)' ].join('');
    53                 setTextContent(elements.progress, c);
    54         },
    55         refreshImpl = function() {
    56                 var i,
    57                     file,
    58                     have = 0,
    59                     size = 0,
    60                     wanted = false,
    61                     low = false,
    62                     normal = false,
    63                     high = false;
    64 
    65                 // loop through the file_indices that affect this row
    66                 for (i=0; i<fields.indices.length; ++i) {
    67                         file = fields.torrent.getFile (fields.indices[i]);
    68                         have += file.bytesCompleted;
    69                         size += file.length;
    70                         wanted |= file.wanted;
    71                         switch (file.priority) {
    72                                 case -1: low = true; break;
    73                                 case  0: normal = true; break;
    74                                 case  1: high = true; break;
    75                         }
    76                 }
    77 
    78                 if ((fields.have != have) || (fields.size != size)) {
    79                         fields.have = have;
    80                         fields.size = size;
    81                         refreshProgressHTML();
    82                 }
    83 
    84                 if (fields.isWanted !== wanted) {
    85                         fields.isWanted = wanted;
    86                         refreshWantedHTML();
    87                 }
    88 
    89                 if (fields.priorityLow !== low) {
    90                         fields.priorityLow = low;
    91                         $(elements.priority_low_button).toggleClass('selected', low);
    92                 }
    93 
    94                 if (fields.priorityNormal !== normal) {
    95                         fields.priorityNormal = normal;
    96                         $(elements.priority_normal_button).toggleClass('selected', normal);
    97                 }
    98 
    99                 if (fields.priorityHigh !== high) {
    100                         fields.priorityHigh = high;
    101                         $(elements.priority_high_button).toggleClass('selected', high);
    102                 }
    103         },
    104 
    105         isDone = function () {
    106                 return fields.have >= fields.size;
    107         },
    108         isEditable = function () {
    109                 return (fields.torrent.getFileCount()>1) && !isDone();
    110         },
    111 
    112         createRow = function(torrent, depth, name, even) {
    113                 var e, root, box;
    114 
    115                 root = document.createElement('li');
    116                 root.className = 'inspector_torrent_file_list_entry' + (even?'even':'odd');
    117                 elements.root = root;
    118 
    119                 e = document.createElement('input');
    120                 e.type = 'checkbox';
    121                 e.className = "file_wanted_control";
    122                 e.title = 'Download file';
    123                 $(e).change(function(ev){ fireWantedChanged( $(ev.currentTarget).prop('checked')); });
    124                 root.checkbox = e;
    125                 root.appendChild(e);
    126 
    127                 e = document.createElement('div');
    128                 e.className = 'file-priority-radiobox';
    129                 box = e;
    130 
    131                         e = document.createElement('div');
    132                         e.className = 'low';
    133                         e.title = 'Low Priority';
    134                         $(e).click(function(){ firePriorityChanged(-1); });
    135                         elements.priority_low_button = e;
    136                         box.appendChild(e);
    137 
    138                         e = document.createElement('div');
    139                         e.className = 'normal';
    140                         e.title = 'Normal Priority';
    141                         $(e).click(function(){ firePriorityChanged(0); });
    142                         elements.priority_normal_button = e;
    143                         box.appendChild(e);
    144 
    145                         e = document.createElement('div');
    146                         e.title = 'High Priority';
    147                         e.className = 'high';
    148                         $(e).click(function(){ firePriorityChanged(1); });
    149                         elements.priority_high_button = e;
    150                         box.appendChild(e);
    151 
    152                 root.appendChild(box);
    153 
    154                 e = document.createElement('div');
    155                 e.className = "inspector_torrent_file_list_entry_name";
    156                 setTextContent(e, name);
    157                 $(e).click(function(){ fireNameClicked(-1); });
    158                 root.appendChild(e);
    159 
    160                 e = document.createElement('div');
    161                 e.className = "inspector_torrent_file_list_entry_progress";
    162                 root.appendChild(e);
    163                 $(e).click(function(){ fireNameClicked(-1); });
    164                 elements.progress = e;
    165 
    166                 $(root).css('margin-left', '' + (depth*16) + 'px');
    167 
    168                 refreshImpl();
    169                 return root;
    170         },
    171 
    172         fireWantedChanged = function(do_want) {
    173                 $(fields.me).trigger('wantedToggled',[ fields.indices, do_want ]);
    174         },
    175         firePriorityChanged = function(priority) {
    176                 $(fields.me).trigger('priorityToggled',[ fields.indices, priority ]);
    177         },
    178         fireNameClicked = function() {
    179                 $(fields.me).trigger('nameClicked',[ fields.me, fields.indices ]);
    180         };
    181 
    182         /***
    183         ****  PUBLIC
    184         ***/
    185 
    186         this.getElement = function() {
    187                 return elements.root;
    188         };
    189         this.refresh = function() {
    190                 refreshImpl();
    191         };
    192 
    193         initialize(torrent, depth, name, indices, even);
     8function FileRow(torrent, depth, name, indices, even) {
     9    var fields = {
     10        have: 0,
     11        indices: [],
     12        isWanted: true,
     13        priorityLow: false,
     14        priorityNormal: false,
     15        priorityHigh: false,
     16        me: this,
     17        size: 0,
     18        torrent: null
     19    };
     20
     21    var elements = {
     22        priority_low_button: null,
     23        priority_normal_button: null,
     24        priority_high_button: null,
     25        progress: null,
     26        root: null
     27    };
     28
     29    var initialize = function (torrent, depth, name, indices, even) {
     30        fields.torrent = torrent;
     31        fields.indices = indices;
     32        createRow(torrent, depth, name, even);
     33    };
     34
     35    var refreshWantedHTML = function () {
     36        var e = $(elements.root);
     37        e.toggleClass('skip', !fields.isWanted);
     38        e.toggleClass('complete', isDone());
     39        $(e[0].checkbox).prop('disabled', !isEditable());
     40        $(e[0].checkbox).prop('checked', fields.isWanted);
     41    };
     42
     43    var refreshProgressHTML = function () {
     44        var pct = 100 * (fields.size ? (fields.have / fields.size) : 1.0)
     45        var c = [Transmission.fmt.size(fields.have), ' of ', Transmission.fmt.size(fields.size), ' (', Transmission.fmt.percentString(pct), '%)'].join('');
     46        setTextContent(elements.progress, c);
     47    };
     48
     49    var refreshImpl = function () {
     50        var i,
     51            file,
     52            have = 0,
     53            size = 0,
     54            wanted = false,
     55            low = false,
     56            normal = false,
     57            high = false;
     58
     59        // loop through the file_indices that affect this row
     60        for (i = 0; i < fields.indices.length; ++i) {
     61            file = fields.torrent.getFile(fields.indices[i]);
     62            have += file.bytesCompleted;
     63            size += file.length;
     64            wanted |= file.wanted;
     65            switch (file.priority) {
     66            case -1:
     67                low = true;
     68                break;
     69            case 0:
     70                normal = true;
     71                break;
     72            case 1:
     73                high = true;
     74                break;
     75            }
     76        }
     77
     78        if ((fields.have != have) || (fields.size != size)) {
     79            fields.have = have;
     80            fields.size = size;
     81            refreshProgressHTML();
     82        }
     83
     84        if (fields.isWanted !== wanted) {
     85            fields.isWanted = wanted;
     86            refreshWantedHTML();
     87        }
     88
     89        if (fields.priorityLow !== low) {
     90            fields.priorityLow = low;
     91            $(elements.priority_low_button).toggleClass('selected', low);
     92        }
     93
     94        if (fields.priorityNormal !== normal) {
     95            fields.priorityNormal = normal;
     96            $(elements.priority_normal_button).toggleClass('selected', normal);
     97        }
     98
     99        if (fields.priorityHigh !== high) {
     100            fields.priorityHigh = high;
     101            $(elements.priority_high_button).toggleClass('selected', high);
     102        }
     103    };
     104
     105    var isDone = function () {
     106        return fields.have >= fields.size;
     107    };
     108
     109    var isEditable = function () {
     110        return (fields.torrent.getFileCount() > 1) && !isDone();
     111    };
     112
     113    var createRow = function (torrent, depth, name, even) {
     114        var e, root, box;
     115
     116        root = document.createElement('li');
     117        root.className = 'inspector_torrent_file_list_entry' + (even ? 'even' : 'odd');
     118        elements.root = root;
     119
     120        e = document.createElement('input');
     121        e.type = 'checkbox';
     122        e.className = "file_wanted_control";
     123        e.title = 'Download file';
     124        $(e).change(function (ev) {
     125            fireWantedChanged($(ev.currentTarget).prop('checked'));
     126        });
     127        root.checkbox = e;
     128        root.appendChild(e);
     129
     130        e = document.createElement('div');
     131        e.className = 'file-priority-radiobox';
     132        box = e;
     133
     134        e = document.createElement('div');
     135        e.className = 'low';
     136        e.title = 'Low Priority';
     137        $(e).click(function () {
     138            firePriorityChanged(-1);
     139        });
     140        elements.priority_low_button = e;
     141        box.appendChild(e);
     142
     143        e = document.createElement('div');
     144        e.className = 'normal';
     145        e.title = 'Normal Priority';
     146        $(e).click(function () {
     147            firePriorityChanged(0);
     148        });
     149        elements.priority_normal_button = e;
     150        box.appendChild(e);
     151
     152        e = document.createElement('div');
     153        e.title = 'High Priority';
     154        e.className = 'high';
     155        $(e).click(function () {
     156            firePriorityChanged(1);
     157        });
     158        elements.priority_high_button = e;
     159        box.appendChild(e);
     160
     161        root.appendChild(box);
     162
     163        e = document.createElement('div');
     164        e.className = "inspector_torrent_file_list_entry_name";
     165        setTextContent(e, name);
     166        $(e).click(function () {
     167            fireNameClicked(-1);
     168        });
     169        root.appendChild(e);
     170
     171        e = document.createElement('div');
     172        e.className = "inspector_torrent_file_list_entry_progress";
     173        root.appendChild(e);
     174        $(e).click(function () {
     175            fireNameClicked(-1);
     176        });
     177        elements.progress = e;
     178
     179        $(root).css('margin-left', '' + (depth * 16) + 'px');
     180
     181        refreshImpl();
     182        return root;
     183    };
     184
     185    var fireWantedChanged = function (do_want) {
     186        $(fields.me).trigger('wantedToggled', [fields.indices, do_want]);
     187    };
     188
     189    var firePriorityChanged = function (priority) {
     190        $(fields.me).trigger('priorityToggled', [fields.indices, priority]);
     191    };
     192
     193    var fireNameClicked = function () {
     194        $(fields.me).trigger('nameClicked', [fields.me, fields.indices]);
     195    };
     196
     197    /***
     198     ****  PUBLIC
     199     ***/
     200
     201    this.getElement = function () {
     202        return elements.root;
     203    };
     204    this.refresh = function () {
     205        refreshImpl();
     206    };
     207
     208    initialize(torrent, depth, name, indices, even);
    194209};
  • trunk/web/javascript/formatter.js

    r14523 r14716  
    66 */
    77
    8 Transmission.fmt = (function()
    9 {
    10         var speed_K = 1000;
    11         var speed_B_str =  'B/s';
    12         var speed_K_str = 'kB/s';
    13         var speed_M_str = 'MB/s';
    14         var speed_G_str = 'GB/s';
    15         var speed_T_str = 'TB/s';
    16 
    17         var size_K = 1000;
    18         var size_B_str =  'B';
    19         var size_K_str = 'kB';
    20         var size_M_str = 'MB';
    21         var size_G_str = 'GB';
    22         var size_T_str = 'TB';
    23 
    24         var mem_K = 1024;
    25         var mem_B_str =   'B';
    26         var mem_K_str = 'KiB';
    27         var mem_M_str = 'MiB';
    28         var mem_G_str = 'GiB';
    29         var mem_T_str = 'TiB';
    30 
    31         return {
    32 
    33                 updateUnits: function(u)
    34                 {
    35 /*
    36                         speed_K     = u['speed-bytes'];
    37                         speed_K_str = u['speed-units'][0];
    38                         speed_M_str = u['speed-units'][1];
    39                         speed_G_str = u['speed-units'][2];
    40                         speed_T_str = u['speed-units'][3];
    41 
    42                         size_K     = u['size-bytes'];
    43                         size_K_str = u['size-units'][0];
    44                         size_M_str = u['size-units'][1];
    45                         size_G_str = u['size-units'][2];
    46                         size_T_str = u['size-units'][3];
    47 
    48                         mem_K     = u['memory-bytes'];
    49                         mem_K_str = u['memory-units'][0];
    50                         mem_M_str = u['memory-units'][1];
    51                         mem_G_str = u['memory-units'][2];
    52                         mem_T_str = u['memory-units'][3];
    53 */
    54                 },
    55 
    56                 /*
    57                  *   Format a percentage to a string
    58                  */
    59                 percentString: function(x) {
    60                         if (x < 10.0)
    61                                 return x.toTruncFixed(2);
    62                         else if (x < 100.0)
    63                                 return x.toTruncFixed(1);
    64                         else
    65                                 return x.toTruncFixed(0);
    66                 },
    67 
    68                 /*
    69                  *   Format a ratio to a string
    70                  */
    71                 ratioString: function(x) {
    72                         if (x === -1)
    73                                 return "None";
    74                         if (x === -2)
    75                                 return '&infin;';
    76                         return this.percentString(x);
    77                 },
    78 
    79                 /**
    80                  * Formats the a memory size into a human-readable string
    81                  * @param {Number} bytes the filesize in bytes
    82                  * @return {String} human-readable string
    83                  */
    84                 mem: function(bytes)
    85                 {
    86                         if (bytes < mem_K)
    87                                 return [ bytes, mem_B_str ].join(' ');
    88 
    89                         var convertedSize;
    90                         var unit;
    91 
    92                         if (bytes < Math.pow(mem_K, 2))
    93                         {
    94                                 convertedSize = bytes / mem_K;
    95                                 unit = mem_K_str;
    96                         }
    97                         else if (bytes < Math.pow(mem_K, 3))
    98                         {
    99                                 convertedSize = bytes / Math.pow(mem_K, 2);
    100                                 unit = mem_M_str;
    101                         }
    102                         else if (bytes < Math.pow(mem_K, 4))
    103                         {
    104                                 convertedSize = bytes / Math.pow(mem_K, 3);
    105                                 unit = mem_G_str;
    106                         }
    107                         else
    108                         {
    109                                 convertedSize = bytes / Math.pow(mem_K, 4);
    110                                 unit = mem_T_str;
    111                         }
    112 
    113                         // try to have at least 3 digits and at least 1 decimal
    114                         return convertedSize <= 9.995 ? [ convertedSize.toTruncFixed(2), unit ].join(' ')
    115                                                       : [ convertedSize.toTruncFixed(1), unit ].join(' ');
    116                 },
    117 
    118                 /**
    119                  * Formats the a disk capacity or file size into a human-readable string
    120                  * @param {Number} bytes the filesize in bytes
    121                  * @return {String} human-readable string
    122                  */
    123                 size: function(bytes)
    124                 {
    125                         if (bytes < size_K)
    126                                 return [ bytes, size_B_str ].join(' ');
    127 
    128                         var convertedSize;
    129                         var unit;
    130 
    131                         if (bytes < Math.pow(size_K, 2))
    132                         {
    133                                 convertedSize = bytes / size_K;
    134                                 unit = size_K_str;
    135                         }
    136                         else if (bytes < Math.pow(size_K, 3))
    137                         {
    138                                 convertedSize = bytes / Math.pow(size_K, 2);
    139                                 unit = size_M_str;
    140                         }
    141                         else if (bytes < Math.pow(size_K, 4))
    142                         {
    143                                 convertedSize = bytes / Math.pow(size_K, 3);
    144                                 unit = size_G_str;
    145                         }
    146                         else
    147                         {
    148                                 convertedSize = bytes / Math.pow(size_K, 4);
    149                                 unit = size_T_str;
    150                         }
    151 
    152                         // try to have at least 3 digits and at least 1 decimal
    153                         return convertedSize <= 9.995 ? [ convertedSize.toTruncFixed(2), unit ].join(' ')
    154                                                       : [ convertedSize.toTruncFixed(1), unit ].join(' ');
    155                 },
    156 
    157                 speedBps: function(Bps)
    158                 {
    159                         return this.speed(this.toKBps(Bps));
    160                 },
    161 
    162                 toKBps: function(Bps)
    163                 {
    164                         return Math.floor(Bps / speed_K);
    165                 },
    166 
    167                 speed: function(KBps)
    168                 {
    169                         var speed = KBps;
    170 
    171                         if (speed <= 999.95) // 0 KBps to 999 K
    172                                 return [ speed.toTruncFixed(0), speed_K_str ].join(' ');
    173 
    174                         speed /= speed_K;
    175 
    176                         if (speed <= 99.995) // 1 M to 99.99 M
    177                                 return [ speed.toTruncFixed(2), speed_M_str ].join(' ');
    178                         if (speed <= 999.95) // 100 M to 999.9 M
    179                                 return [ speed.toTruncFixed(1), speed_M_str ].join(' ');
    180 
    181                         // insane speeds
    182                         speed /= speed_K;
    183                         return [ speed.toTruncFixed(2), speed_G_str ].join(' ');
    184                 },
    185 
    186                 timeInterval: function(seconds)
    187                 {
    188                         var days    = Math.floor (seconds / 86400),
    189                             hours   = Math.floor ((seconds % 86400) / 3600),
    190                             minutes = Math.floor ((seconds % 3600) / 60),
    191                             seconds = Math.floor (seconds % 60),
    192                             d = days    + ' ' + (days    > 1 ? 'days'    : 'day'),
    193                             h = hours   + ' ' + (hours   > 1 ? 'hours'   : 'hour'),
    194                             m = minutes + ' ' + (minutes > 1 ? 'minutes' : 'minute'),
    195                             s = seconds + ' ' + (seconds > 1 ? 'seconds' : 'second');
    196 
    197                         if (days) {
    198                                 if (days >= 4 || !hours)
    199                                         return d;
    200                                 return d + ', ' + h;
    201                         }
    202                         if (hours) {
    203                                 if (hours >= 4 || !minutes)
    204                                         return h;
    205                                 return h + ', ' + m;
    206                         }
    207                         if (minutes) {
    208                                 if (minutes >= 4 || !seconds)
    209                                         return m;
    210                                 return m + ', ' + s;
    211                         }
    212                         return s;
    213                 },
    214 
    215                 timestamp: function(seconds)
    216                 {
    217                         if (!seconds)
    218                                 return 'N/A';
    219 
    220                         var myDate = new Date(seconds*1000);
    221                         var now = new Date();
    222 
    223                         var date = "";
    224                         var time = "";
    225 
    226                         var sameYear = now.getFullYear() === myDate.getFullYear();
    227                         var sameMonth = now.getMonth() === myDate.getMonth();
    228 
    229                         var dateDiff = now.getDate() - myDate.getDate();
    230                         if (sameYear && sameMonth && Math.abs(dateDiff) <= 1){
    231                                 if (dateDiff === 0){
    232                                         date = "Today";
    233                                 }
    234                                 else if (dateDiff === 1){
    235                                         date = "Yesterday";
    236                                 }
    237                                 else{
    238                                         date = "Tomorrow";
    239                                 }
    240                         }
    241                         else{
    242                                 date = myDate.toDateString();
    243                         }
    244 
    245                         var hours = myDate.getHours();
    246                         var period = "AM";
    247                         if (hours > 12){
    248                                 hours = hours - 12;
    249                                 period = "PM";
    250                         }
    251                         if (hours === 0){
    252                                 hours = 12;
    253                         }
    254                         if (hours < 10){
    255                                 hours = "0" + hours;
    256                         }
    257                         var minutes = myDate.getMinutes();
    258                         if (minutes < 10){
    259                                 minutes = "0" + minutes;
    260                         }
    261                         var seconds = myDate.getSeconds();
    262                                 if (seconds < 10){
    263                                         seconds = "0" + seconds;
    264                         }
    265 
    266                         time = [hours, minutes, seconds].join(':');
    267 
    268                         return [date, time, period].join(' ');
    269                 },
    270 
    271                 ngettext: function(msgid, msgid_plural, n)
    272                 {
    273                         // TODO(i18n): http://doc.qt.digia.com/4.6/i18n-plural-rules.html
    274                         return n === 1 ? msgid : msgid_plural;
    275                 },
    276 
    277                 countString: function(msgid, msgid_plural, n)
    278                 {
    279                         return [ n.toStringWithCommas(), this.ngettext(msgid,msgid_plural,n) ].join(' ');
    280                 },
    281 
    282                 peerStatus: function( flagStr )
    283                 {
    284                         var formattedFlags = [];
    285                         for (var i=0, flag; flag=flagStr[i]; ++i)
    286                         {
    287                                 var explanation = null;
    288                                 switch (flag)
    289                                 {
    290                                         case "O": explanation = "Optimistic unchoke"; break;
    291                                         case "D": explanation = "Downloading from this peer"; break;
    292                                         case "d": explanation = "We would download from this peer if they'd let us"; break;
    293                                         case "U": explanation = "Uploading to peer"; break;
    294                                         case "u": explanation = "We would upload to this peer if they'd ask"; break;
    295                                         case "K": explanation = "Peer has unchoked us, but we're not interested"; break;
    296                                         case "?": explanation = "We unchoked this peer, but they're not interested"; break;
    297                                         case "E": explanation = "Encrypted Connection"; break;
    298                                         case "H": explanation = "Peer was discovered through Distributed Hash Table (DHT)"; break;
    299                                         case "X": explanation = "Peer was discovered through Peer Exchange (PEX)"; break;
    300                                         case "I": explanation = "Peer is an incoming connection"; break;
    301                                         case "T": explanation = "Peer is connected via uTP"; break;
    302                                 }
    303 
    304                                 if (!explanation) {
    305                                         formattedFlags.push(flag);
    306                                 } else {
    307                                         formattedFlags.push("<span title=\"" + flag + ': ' + explanation + "\">" + flag + "</span>");
    308                                 }
    309                         }
    310                         return formattedFlags.join('');
    311                 }
    312         }
     8Transmission.fmt = (function () {
     9    var speed_K = 1000;
     10    var speed_B_str = 'B/s';
     11    var speed_K_str = 'kB/s';
     12    var speed_M_str = 'MB/s';
     13    var speed_G_str = 'GB/s';
     14    var speed_T_str = 'TB/s';
     15
     16    var size_K = 1000;
     17    var size_B_str = 'B';
     18    var size_K_str = 'kB';
     19    var size_M_str = 'MB';
     20    var size_G_str = 'GB';
     21    var size_T_str = 'TB';
     22
     23    var mem_K = 1024;
     24    var mem_B_str = 'B';
     25    var mem_K_str = 'KiB';
     26    var mem_M_str = 'MiB';
     27    var mem_G_str = 'GiB';
     28    var mem_T_str = 'TiB';
     29
     30    return {
     31
     32        /*
     33         *   Format a percentage to a string
     34         */
     35        percentString: function (x) {
     36            if (x < 10.0) {
     37                return x.toTruncFixed(2);
     38            } else if (x < 100.0) {
     39                return x.toTruncFixed(1);
     40            } else {
     41                return x.toTruncFixed(0);
     42            }
     43        },
     44
     45        /*
     46         *   Format a ratio to a string
     47         */
     48        ratioString: function (x) {
     49            if (x === -1) {
     50                return "None";
     51            }
     52            if (x === -2) {
     53                return '&infin;';
     54            }
     55            return this.percentString(x);
     56        },
     57
     58        /**
     59         * Formats the a memory size into a human-readable string
     60         * @param {Number} bytes the filesize in bytes
     61         * @return {String} human-readable string
     62         */
     63        mem: function (bytes) {
     64            if (bytes < mem_K)
     65                return [bytes, mem_B_str].join(' ');
     66
     67            var convertedSize;
     68            var unit;
     69
     70            if (bytes < Math.pow(mem_K, 2)) {
     71                convertedSize = bytes / mem_K;
     72                unit = mem_K_str;
     73            } else if (bytes < Math.pow(mem_K, 3)) {
     74                convertedSize = bytes / Math.pow(mem_K, 2);
     75                unit = mem_M_str;
     76            } else if (bytes < Math.pow(mem_K, 4)) {
     77                convertedSize = bytes / Math.pow(mem_K, 3);
     78                unit = mem_G_str;
     79            } else {
     80                convertedSize = bytes / Math.pow(mem_K, 4);
     81                unit = mem_T_str;
     82            }
     83
     84            // try to have at least 3 digits and at least 1 decimal
     85            return convertedSize <= 9.995 ? [convertedSize.toTruncFixed(2), unit].join(' ') : [convertedSize.toTruncFixed(1), unit].join(' ');
     86        },
     87
     88        /**
     89         * Formats the a disk capacity or file size into a human-readable string
     90         * @param {Number} bytes the filesize in bytes
     91         * @return {String} human-readable string
     92         */
     93        size: function (bytes) {
     94            if (bytes < size_K) {
     95                return [bytes, size_B_str].join(' ');
     96            }
     97
     98            var convertedSize;
     99            var unit;
     100
     101            if (bytes < Math.pow(size_K, 2)) {
     102                convertedSize = bytes / size_K;
     103                unit = size_K_str;
     104            } else if (bytes < Math.pow(size_K, 3)) {
     105                convertedSize = bytes / Math.pow(size_K, 2);
     106                unit = size_M_str;
     107            } else if (bytes < Math.pow(size_K, 4)) {
     108                convertedSize = bytes / Math.pow(size_K, 3);
     109                unit = size_G_str;
     110            } else {
     111                convertedSize = bytes / Math.pow(size_K, 4);
     112                unit = size_T_str;
     113            }
     114
     115            // try to have at least 3 digits and at least 1 decimal
     116            return convertedSize <= 9.995 ? [convertedSize.toTruncFixed(2), unit].join(' ') : [convertedSize.toTruncFixed(1), unit].join(' ');
     117        },
     118
     119        speedBps: function (Bps) {
     120            return this.speed(this.toKBps(Bps));
     121        },
     122
     123        toKBps: function (Bps) {
     124            return Math.floor(Bps / speed_K);
     125        },
     126
     127        speed: function (KBps) {
     128            var speed = KBps;
     129
     130            if (speed <= 999.95) { // 0 KBps to 999 K
     131                return [speed.toTruncFixed(0), speed_K_str].join(' ');
     132            }
     133
     134            speed /= speed_K;
     135
     136            if (speed <= 99.995) { // 1 M to 99.99 M
     137                return [speed.toTruncFixed(2), speed_M_str].join(' ');
     138            }
     139            if (speed <= 999.95) { // 100 M to 999.9 M
     140                return [speed.toTruncFixed(1), speed_M_str].join(' ');
     141            }
     142
     143            // insane speeds
     144            speed /= speed_K;
     145            return [speed.toTruncFixed(2), speed_G_str].join(' ');
     146        },
     147
     148        timeInterval: function (seconds) {
     149            var days = Math.floor(seconds / 86400),
     150                hours = Math.floor((seconds % 86400) / 3600),
     151                minutes = Math.floor((seconds % 3600) / 60),
     152                seconds = Math.floor(seconds % 60),
     153                d = days + ' ' + (days > 1 ? 'days' : 'day'),
     154                h = hours + ' ' + (hours > 1 ? 'hours' : 'hour'),
     155                m = minutes + ' ' + (minutes > 1 ? 'minutes' : 'minute'),
     156                s = seconds + ' ' + (seconds > 1 ? 'seconds' : 'second');
     157
     158            if (days) {
     159                if (days >= 4 || !hours) {
     160                    return d;
     161                }
     162                return d + ', ' + h;
     163            }
     164            if (hours) {
     165                if (hours >= 4 || !minutes) {
     166                    return h;
     167                }
     168                return h + ', ' + m;
     169            }
     170            if (minutes) {
     171                if (minutes >= 4 || !seconds) {
     172                    return m;
     173                }
     174                return m + ', ' + s;
     175            }
     176            return s;
     177        },
     178
     179        timestamp: function (seconds) {
     180            if (!seconds) {
     181                return 'N/A';
     182            }
     183
     184            var myDate = new Date(seconds * 1000);
     185            var now = new Date();
     186
     187            var date = "";
     188            var time = "";
     189
     190            var sameYear = now.getFullYear() === myDate.getFullYear();
     191            var sameMonth = now.getMonth() === myDate.getMonth();
     192
     193            var dateDiff = now.getDate() - myDate.getDate();
     194            if (sameYear && sameMonth && Math.abs(dateDiff) <= 1) {
     195                if (dateDiff === 0) {
     196                    date = "Today";
     197                } else if (dateDiff === 1) {
     198                    date = "Yesterday";
     199                } else {
     200                    date = "Tomorrow";
     201                }
     202            } else {
     203                date = myDate.toDateString();
     204            }
     205
     206            var hours = myDate.getHours();
     207            var period = "AM";
     208            if (hours > 12) {
     209                hours = hours - 12;
     210                period = "PM";
     211            }
     212            if (hours === 0) {
     213                hours = 12;
     214            }
     215            if (hours < 10) {
     216                hours = "0" + hours;
     217            }
     218            var minutes = myDate.getMinutes();
     219            if (minutes < 10) {
     220                minutes = "0" + minutes;
     221            }
     222            var seconds = myDate.getSeconds();
     223            if (seconds < 10) {
     224                seconds = "0" + seconds;
     225            }
     226
     227            time = [hours, minutes, seconds].join(':');
     228
     229            return [date, time, period].join(' ');
     230        },
     231
     232        ngettext: function (msgid, msgid_plural, n) {
     233            // TODO(i18n): http://doc.qt.digia.com/4.6/i18n-plural-rules.html
     234            return n === 1 ? msgid : msgid_plural;
     235        },
     236
     237        countString: function (msgid, msgid_plural, n) {
     238            return [n.toStringWithCommas(), this.ngettext(msgid, msgid_plural, n)].join(' ');
     239        },
     240
     241        peerStatus: function (flagStr) {
     242            var formattedFlags = [];
     243            for (var i = 0, flag; flag = flagStr[i]; ++i) {
     244                var explanation = null;
     245                switch (flag) {
     246                case "O":
     247                    explanation = "Optimistic unchoke";
     248                    break;
     249                case "D":
     250                    explanation = "Downloading from this peer";
     251                    break;
     252                case "d":
     253                    explanation = "We would download from this peer if they'd let us";
     254                    break;
     255                case "U":
     256                    explanation = "Uploading to peer";
     257                    break;
     258                case "u":
     259                    explanation = "We would upload to this peer if they'd ask";
     260                    break;
     261                case "K":
     262                    explanation = "Peer has unchoked us, but we're not interested";
     263                    break;
     264                case "?":
     265                    explanation = "We unchoked this peer, but they're not interested";
     266                    break;
     267                case "E":
     268                    explanation = "Encrypted Connection";
     269                    break;
     270                case "H":
     271                    explanation = "Peer was discovered through Distributed Hash Table (DHT)";
     272                    break;
     273                case "X":
     274                    explanation = "Peer was discovered through Peer Exchange (PEX)";
     275                    break;
     276                case "I":
     277                    explanation = "Peer is an incoming connection";
     278                    break;
     279                case "T":
     280                    explanation = "Peer is connected via uTP";
     281                    break;
     282                };
     283
     284                if (!explanation) {
     285                    formattedFlags.push(flag);
     286                } else {
     287                    formattedFlags.push("<span title=\"" + flag + ': ' + explanation + "\">" + flag + "</span>");
     288                };
     289            };
     290
     291            return formattedFlags.join('');
     292        }
     293    }
    313294})();
  • trunk/web/javascript/inspector.js

    r14523 r14716  
    88function Inspector(controller) {
    99
    10         var data = {
    11                 controller: null,
    12                 elements: { },
    13                 torrents: [ ]
    14         },
    15 
    16         needsExtraInfo = function (torrents) {
    17                 var i, id, tor;
    18 
    19                 for (i = 0; tor = torrents[i]; i++)
    20                         if (!tor.hasExtraInfo())
    21                                 return true;
    22 
    23                 return false;
    24         },
    25 
    26         refreshTorrents = function () {
    27                 var fields,
    28                     ids = $.map(data.torrents.slice(0), function (t) {return t.getId();});
    29 
    30                 if (ids && ids.length)
    31                 {
    32                         fields = ['id'].concat(Torrent.Fields.StatsExtra);
    33 
    34                         if (needsExtraInfo(data.torrents))
    35                                 $.merge(fields, Torrent.Fields.InfoExtra);
    36 
    37                         data.controller.updateTorrents(ids, fields);
    38                 }
    39         },
    40 
    41         onTabClicked = function (ev) {
    42                 var tab = ev.currentTarget;
    43 
    44                 if (isMobileDevice)
    45                         ev.stopPropagation();
    46 
    47                 // select this tab and deselect the others
    48                 $(tab).addClass('selected').siblings().removeClass('selected');
    49 
    50                 // show this tab and hide the others
    51                 $('#' + tab.id.replace('tab','page')).show().siblings('.inspector-page').hide();
    52 
    53                 updateInspector();
    54         },
    55 
    56         updateInspector = function () {
    57                 var e = data.elements,
    58                     torrents = data.torrents,
    59                     name;
    60 
    61                 // update the name, which is shown on all the pages
    62                 if (!torrents || !torrents.length)
    63                         name = 'No Selection';
    64                 else if(torrents.length === 1)
    65                         name = torrents[0].getName();
    66                 else
    67                         name = '' + torrents.length+' Transfers Selected';
    68                 setTextContent(e.name_lb, name || na);
    69 
    70                 // update the visible page
    71                 if ($(e.info_page).is(':visible'))
    72                         updateInfoPage();
    73                 else if ($(e.peers_page).is(':visible'))
    74                         updatePeersPage();
    75                 else if ($(e.trackers_page).is(':visible'))
    76                         updateTrackersPage();
    77                 else if ($(e.files_page).is(':visible'))
    78                         updateFilesPage();
    79         },
    80 
    81         /****
    82         *****  GENERAL INFO PAGE
    83         ****/
    84 
    85         updateInfoPage = function () {
    86                 var torrents = data.torrents,
    87                     e = data.elements,
    88                     fmt = Transmission.fmt,
    89                     none = 'None',
    90                     mixed = 'Mixed',
    91                     unknown = 'Unknown',
    92                     isMixed, allPaused, allFinished,
    93                     str,
    94                     baseline, it, s, i, t,
    95                     sizeWhenDone = 0,
    96                     leftUntilDone = 0,
    97                     available = 0,
    98                     haveVerified = 0,
    99                     haveUnverified = 0,
    100                     verifiedPieces = 0,
    101                     stateString,
    102                     latest,
    103                     pieces,
    104                     size,
    105                     pieceSize,
    106                     creator, mixed_creator,
    107                     date, mixed_date,
    108                     v, u, f, d, pct,
    109                     uri,
    110                     now = Date.now();
    111 
    112                 //
    113                 //  state_lb
    114                 //
    115 
    116                 if(torrents.length <1)
    117                         str = none;
    118                 else {
    119                         isMixed = false;
    120                         allPaused = true;
    121                         allFinished = true;
    122 
    123                         baseline = torrents[0].getStatus();
    124                         for(i=0; t=torrents[i]; ++i) {
    125                                 it = t.getStatus();
    126                                 if(it != baseline)
    127                                         isMixed = true;
    128                                 if(!t.isStopped())
    129                                         allPaused = allFinished = false;
    130                                 if(!t.isFinished())
    131                                         allFinished = false;
    132                         }
    133                         if( isMixed )
    134                                 str = mixed;
    135                         else if( allFinished )
    136                                 str = 'Finished';
    137                         else if( allPaused )
    138                                 str = 'Paused';
    139                         else
    140                                 str = torrents[0].getStateString();
    141                 }
    142                 setTextContent(e.state_lb, str);
    143                 stateString = str;
    144 
    145                 //
    146                 //  have_lb
    147                 //
    148 
    149                 if(torrents.length < 1)
    150                         str = none;
    151                 else {
    152                         baseline = torrents[0].getStatus();
    153                         for(i=0; t=torrents[i]; ++i) {
    154                                 if(!t.needsMetaData()) {
    155                                         haveUnverified += t.getHaveUnchecked();
    156                                         v = t.getHaveValid();
    157                                         haveVerified += v;
    158                                         if(t.getPieceSize())
    159                                                 verifiedPieces += v / t.getPieceSize();
    160                                         sizeWhenDone += t.getSizeWhenDone();
    161                                         leftUntilDone += t.getLeftUntilDone();
    162                                         available += (t.getHave()) + t.getDesiredAvailable();
    163                                 }
    164                         }
    165 
    166                         d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 );
    167                         str = fmt.percentString( d );
    168 
    169                         if( !haveUnverified && !leftUntilDone )
    170                                 str = fmt.size(haveVerified) + ' (100%)';
    171                         else if( !haveUnverified )
    172                                 str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%)';
    173                         else
    174                                 str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%), ' + fmt.size(haveUnverified) + ' Unverified';
    175                 }
    176                 setTextContent(e.have_lb, str);
    177 
    178                 //
    179                 //  availability_lb
    180                 //
    181 
    182                 if(torrents.length < 1)
    183                         str = none;
    184                 else if( sizeWhenDone == 0 )
    185                         str = none;
    186                 else
    187                         str = '' + fmt.percentString( ( 100.0 * available ) / sizeWhenDone ) +  '%';
    188                 setTextContent(e.availability_lb, str);
    189 
    190                 //
    191                 //  downloaded_lb
    192                 //
    193 
    194                 if(torrents.length < 1)
    195                         str = none;
    196                 else {
    197                         d = f = 0;
    198                         for(i=0; t=torrents[i]; ++i) {
    199                                 d += t.getDownloadedEver();
    200                                 f += t.getFailedEver();
    201                         }
    202                         if(f)
    203                                 str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
    204                         else
    205                                 str = fmt.size(d);
    206                 }
    207                 setTextContent(e.downloaded_lb, str);
    208 
    209                 //
    210                 //  uploaded_lb
    211                 //
    212 
    213                 if(torrents.length < 1)
    214                         str = none;
    215                 else {
    216                         d = u = 0;
    217                         if(torrents.length == 1) {
    218                                 d = torrents[0].getDownloadedEver();
    219                                 u = torrents[0].getUploadedEver();
    220 
    221                                 if (d == 0)
    222                                         d = torrents[0].getHaveValid();
    223                         }
    224                         else {
    225                                 for(i=0; t=torrents[i]; ++i) {
    226                                         d += t.getDownloadedEver();
    227                                         u += t.getUploadedEver();
    228                                 }
    229                         }
    230                         str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString( Math.ratio(u,d))+')';
    231                 }
    232                 setTextContent(e.uploaded_lb, str);
    233 
    234                 //
    235                 // running time
    236                 //
    237 
    238                 if(torrents.length < 1)
    239                         str = none;
    240                 else {
    241                         allPaused = true;
    242                         baseline = torrents[0].getStartDate();
    243                         for(i=0; t=torrents[i]; ++i) {
    244                                 if(baseline != t.getStartDate())
    245                                         baseline = 0;
    246                                 if(!t.isStopped())
    247                                         allPaused = false;
    248                         }
    249                         if(allPaused)
    250                                 str = stateString; // paused || finished
    251                         else if(!baseline)
    252                                 str = mixed;
    253                         else
    254                                 str = fmt.timeInterval(now/1000 - baseline);
    255                 }
    256                 setTextContent(e.running_time_lb, str);
    257 
    258                 //
    259                 // remaining time
    260                 //
    261 
    262                 str = '';
    263                 if(torrents.length < 1)
    264                         str = none;
    265                 else {
    266                         baseline = torrents[0].getETA();
    267                         for(i=0; t=torrents[i]; ++i) {
    268                                 if(baseline != t.getETA()) {
    269                                         str = mixed;
    270                                         break;
    271                                 }
    272                         }
    273                 }
    274                 if(!str.length) {
    275                         if(baseline < 0)
    276                                 str = unknown;
    277                         else
    278                                 str = fmt.timeInterval(baseline);
    279                 }
    280                 setTextContent(e.remaining_time_lb, str);
    281 
    282                 //
    283                 // last activity
    284                 //
    285 
    286                 latest = -1;
    287                 if(torrents.length < 1)
    288                         str = none;
    289                 else {
    290                         baseline = torrents[0].getLastActivity();
    291                         for(i=0; t=torrents[i]; ++i) {
    292                                 d = t.getLastActivity();
    293                                 if(latest < d)
    294                                         latest = d;
    295                         }
    296                         d = now/1000 - latest; // seconds since last activity
    297                         if(d < 0)
    298                                 str = none;
    299                         else if(d < 5)
    300                                 str = 'Active now';
    301                         else
    302                                 str = fmt.timeInterval(d) + ' ago';
    303                 }
    304                 setTextContent(e.last_activity_lb, str);
    305 
    306                 //
    307                 // error
    308                 //
    309 
    310                 if(torrents.length < 1)
    311                         str = none;
    312                 else {
    313                         str = torrents[0].getErrorString();
    314                         for(i=0; t=torrents[i]; ++i) {
    315                                 if(str != t.getErrorString()) {
    316                                         str = mixed;
    317                                         break;
    318                                 }
    319                         }
    320                 }
    321                 setTextContent(e.error_lb, str || none);
    322 
    323                 //
    324                 // size
    325                 //
    326 
    327                 if(torrents.length < 1)
    328                         str = none;
    329                 else {
    330                         pieces = 0;
    331                         size = 0;
    332                         pieceSize = torrents[0].getPieceSize();
    333                         for(i=0; t=torrents[i]; ++i) {
    334                                 pieces += t.getPieceCount();
    335                                 size += t.getTotalSize();
    336                                 if(pieceSize != t.getPieceSize())
    337                                         pieceSize = 0;
    338                         }
    339                         if(!size)
    340                                 str = none;
    341                         else if(pieceSize > 0)
    342                                 str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
    343                         else
    344                                 str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
    345                 }
    346                 setTextContent(e.size_lb, str);
    347 
    348                 //
    349                 //  hash
    350                 //
    351 
    352                 if(torrents.length < 1)
    353                         str = none;
    354                 else {
    355                         str = torrents[0].getHashString();
    356                         for(i=0; t=torrents[i]; ++i) {
    357                                 if(str != t.getHashString()) {
    358                                         str = mixed;
    359                                         break;
    360                                 }
    361                         }
    362                 }
    363                 setTextContent(e.hash_lb, str);
    364 
    365                 //
    366                 //  privacy
    367                 //
    368 
    369                 if(torrents.length < 1)
    370                         str = none;
    371                 else {
    372                         baseline = torrents[0].getPrivateFlag();
    373                         str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
    374                         for(i=0; t=torrents[i]; ++i) {
    375                                 if(baseline != t.getPrivateFlag()) {
    376                                         str = mixed;
    377                                         break;
    378                                 }
    379                         }
    380                 }
    381                 setTextContent(e.privacy_lb, str);
    382 
    383                 //
    384                 //  comment
    385                 //
    386 
    387                 if(torrents.length < 1)
    388                         str = none;
    389                 else {
    390                         str = torrents[0].getComment();
    391                         for(i=0; t=torrents[i]; ++i) {
    392                                 if(str != t.getComment()) {
    393                                         str = mixed;
    394                                         break;
    395                                 }
    396                         }
    397                 }
    398                 if(!str)
    399                         str = none;
    400                 uri = parseUri(str);
    401                 if (uri.protocol == 'http' || uri.parseUri == 'https') {
    402                         str = encodeURI(str);
    403                         setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
    404                 }
    405                 else
    406                         setTextContent(e.comment_lb, str);
    407 
    408                 //
    409                 //  origin
    410                 //
    411 
    412                 if(torrents.length < 1)
    413                         str = none;
    414                 else {
    415                         mixed_creator = false;
    416                         mixed_date = false;
    417                         creator = torrents[0].getCreator();
    418                         date = torrents[0].getDateCreated();
    419                         for(i=0; t=torrents[i]; ++i) {
    420                                 if(creator != t.getCreator())
    421                                         mixed_creator = true;
    422                                 if(date != t.getDateCreated())
    423                                         mixed_date = true;
    424                         }
    425                         var empty_creator = !creator || !creator.length,
    426                             empty_date = !date;
    427                         if(mixed_creator || mixed_date)
    428                                 str = mixed;
    429                         else if(empty_creator && empty_date)
    430                                 str = unknown;
    431                         else if(empty_date && !empty_creator)
    432                                 str = 'Created by ' + creator;
    433                         else if(empty_creator && !empty_date)
    434                                 str = 'Created on ' + (new Date(date*1000)).toDateString();
    435                         else
    436                                 str = 'Created by ' + creator + ' on ' + (new Date(date*1000)).toDateString();
    437                 }
    438                 setTextContent(e.origin_lb, str);
    439 
    440                 //
    441                 //  foldername
    442                 //
    443 
    444                 if(torrents.length < 1)
    445                         str = none;
    446                 else {
    447                         str = torrents[0].getDownloadDir();
    448                         for(i=0; t=torrents[i]; ++i) {
    449                                 if(str != t.getDownloadDir()) {
    450                                         str = mixed;
    451                                         break;
    452                                 }
    453                         }
    454                 }
    455                 setTextContent(e.foldername_lb, str);
    456         },
    457 
    458         /****
    459         *****  FILES PAGE
    460         ****/
    461 
    462         changeFileCommand = function(fileIndices, command) {
    463                 var torrentId = data.file_torrent.getId();
    464                 data.controller.changeFileCommand(torrentId, fileIndices, command);
    465         },
    466 
    467         onFileWantedToggled = function(ev, fileIndices, want) {
    468                 changeFileCommand(fileIndices, want?'files-wanted':'files-unwanted');
    469         },
    470 
    471         onFilePriorityToggled = function(ev, fileIndices, priority) {
    472                 var command;
    473                 switch(priority) {
    474                         case -1: command = 'priority-low'; break;
    475                         case  1: command = 'priority-high'; break;
    476                         default: command = 'priority-normal'; break;
    477                 }
    478                 changeFileCommand(fileIndices, command);
    479         },
    480 
    481         onNameClicked = function(ev, fileRow, fileIndices) {
    482                 $(fileRow.getElement()).siblings().slideToggle();
    483         },
    484 
    485         clearFileList = function() {
    486                 $(data.elements.file_list).empty();
    487                 delete data.file_torrent;
    488                 delete data.file_torrent_n;
    489                 delete data.file_rows;
    490         },
    491 
    492         createFileTreeModel = function (tor) {
    493                 var i, j, n, name, tokens, walk, tree, token, sub,
    494                     leaves = [ ],
    495                     tree = { children: { }, file_indices: [ ] };
    496 
    497                 n = tor.getFileCount();
    498                 for (i=0; i<n; ++i) {
    499                         name = tor.getFile(i).name;
    500                         tokens = name.split('/');
    501                         walk = tree;
    502                         for (j=0; j<tokens.length; ++j) {
    503                                 token = tokens[j];
    504                                 sub = walk.children[token];
    505                                 if (!sub) {
    506                                         walk.children[token] = sub = {
    507                                                 name: token,
    508                                                 parent: walk,
    509                                                 children: { },
    510                                                 file_indices: [ ],
    511                                                 depth: j
    512                                         };
    513                                 }
    514                                 walk = sub;
    515                         }
    516                         walk.file_index = i;
    517                         delete walk.children;
    518                         leaves.push (walk);
    519                 }
    520 
    521                 for (i=0; i<leaves.length; ++i) {
    522                         walk = leaves[i];
    523                         j = walk.file_index;
    524                         do {
    525                                 walk.file_indices.push (j);
    526                                 walk = walk.parent;
    527                         } while (walk);
    528                 }
    529 
    530                 return tree;
    531         },
    532 
    533         addNodeToView = function (tor, parent, sub, i) {
    534                 var row;
    535                 row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i%2);
    536                 data.file_rows.push(row);
    537                 parent.appendChild(row.getElement());
    538                 $(row).bind('wantedToggled',onFileWantedToggled);
    539                 $(row).bind('priorityToggled',onFilePriorityToggled);
    540                 $(row).bind('nameClicked',onNameClicked);
    541         },
    542 
    543         addSubtreeToView = function (tor, parent, sub, i) {
    544                 var key, div;
    545                 div = document.createElement('div');
    546                 if (sub.parent)
    547                         addNodeToView (tor, div, sub, i++);
    548                 if (sub.children)
    549                         for (key in sub.children)
    550                                 i = addSubtreeToView (tor, div, sub.children[key]);
    551                 parent.appendChild(div);
    552                 return i;
    553         },
    554 
    555         updateFilesPage = function() {
    556                 var i, n, tor, fragment, tree,
    557                     file_list = data.elements.file_list,
    558                     torrents = data.torrents;
    559 
    560                 // only show one torrent at a time
    561                 if (torrents.length !== 1) {
    562                         clearFileList();
    563                         return;
    564                 }
    565 
    566                 tor = torrents[0];
    567                 n = tor ? tor.getFileCount() : 0;
    568                 if (tor!=data.file_torrent || n!=data.file_torrent_n) {
    569                         // rebuild the file list...
    570                         clearFileList();
    571                         data.file_torrent = tor;
    572                         data.file_torrent_n = n;
    573                         data.file_rows = [ ];
    574                         fragment = document.createDocumentFragment();
    575                         tree = createFileTreeModel (tor);
    576                         addSubtreeToView (tor, fragment, tree, 0);
    577                         file_list.appendChild (fragment);
    578                 } else {
    579                         // ...refresh the already-existing file list
    580                         for (i=0, n=data.file_rows.length; i<n; ++i)
    581                                 data.file_rows[i].refresh();
    582                 }
    583         },
    584 
    585         /****
    586         *****  PEERS PAGE
    587         ****/
    588 
    589         updatePeersPage = function() {
    590                 var i, k, tor, peers, peer, parity,
    591                     html = [],
    592                     fmt = Transmission.fmt,
    593                     peers_list = data.elements.peers_list,
    594                     torrents = data.torrents;
    595 
    596                 for (k=0; tor=torrents[k]; ++k)
    597                 {
    598                         peers = tor.getPeers();
    599                         html.push('<div class="inspector_group">');
    600                         if (torrents.length > 1) {
    601                                 html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
    602                         }
    603                         if (!peers || !peers.length) {
    604                                 html.push('<br></div>'); // firefox won't paint the top border if the div is empty
    605                                 continue;
    606                         }
    607                         html.push('<table class="peer_list">',
    608                                   '<tr class="inspector_peer_entry even">',
    609                                   '<th class="encryptedCol"></th>',
    610                                   '<th class="upCol">Up</th>',
    611                                   '<th class="downCol">Down</th>',
    612                                   '<th class="percentCol">%</th>',
    613                                   '<th class="statusCol">Status</th>',
    614                                   '<th class="addressCol">Address</th>',
    615                                   '<th class="clientCol">Client</th>',
    616                                   '</tr>');
    617                         for (i=0; peer=peers[i]; ++i) {
    618                                 parity = (i%2) ? 'odd' : 'even';
    619                                 html.push('<tr class="inspector_peer_entry ', parity, '">',
    620                                           '<td>', (peer.isEncrypted ? '<div class="encrypted-peer-cell" title="Encrypted Connection">'
    621                                                                     : '<div class="unencrypted-peer-cell">'), '</div>', '</td>',
    622                                           '<td>', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '</td>',
    623                                           '<td>', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '</td>',
    624                                           '<td class="percentCol">', Math.floor(peer.progress*100), '%', '</td>',
    625                                           '<td>', fmt.peerStatus(peer.flagStr), '</td>',
    626                                           '<td>', sanitizeText(peer.address), '</td>',
    627                                           '<td class="clientCol">', sanitizeText(peer.clientName), '</td>',
    628                                           '</tr>');
    629                         }
    630                         html.push('</table></div>');
    631                 }
    632 
    633                 setInnerHTML(peers_list, html.join(''));
    634         },
    635 
    636         /****
    637         *****  TRACKERS PAGE
    638         ****/
    639 
    640         getAnnounceState = function(tracker) {
    641                 var timeUntilAnnounce, s = '';
    642                 switch (tracker.announceState) {
    643                         case Torrent._TrackerActive:
    644                                 s = 'Announce in progress';
    645                                 break;
    646                         case Torrent._TrackerWaiting:
    647                                 timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
    648                                 if (timeUntilAnnounce < 0) {
    649                                     timeUntilAnnounce = 0;
    650                                 }
    651                                 s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
    652                                 break;
    653                         case Torrent._TrackerQueued:
    654                                 s = 'Announce is queued';
    655                                 break;
    656                         case Torrent._TrackerInactive:
    657                                 s = tracker.isBackup ?
    658                                     'Tracker will be used as a backup' :
    659                                     'Announce not scheduled';
    660                                 break;
    661                         default:
    662                                 s = 'unknown announce state: ' + tracker.announceState;
    663                 }
    664                 return s;
    665         },
    666 
    667         lastAnnounceStatus = function(tracker) {
    668 
    669                 var lastAnnounceLabel = 'Last Announce',
    670                     lastAnnounce = [ 'N/A' ],
    671                 lastAnnounceTime;
    672 
    673                 if (tracker.hasAnnounced) {
    674                         lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
    675                         if (tracker.lastAnnounceSucceeded) {
    676                                 lastAnnounce = [ lastAnnounceTime, ' (got ',  Transmission.fmt.countString('peer','peers',tracker.lastAnnouncePeerCount), ')' ];
    677                         } else {
    678                                 lastAnnounceLabel = 'Announce error';
    679                                 lastAnnounce = [ (tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime ];
    680                         }
    681                 }
    682                 return { 'label':lastAnnounceLabel, 'value':lastAnnounce.join('') };
    683         },
    684 
    685         lastScrapeStatus = function(tracker) {
    686 
    687                 var lastScrapeLabel = 'Last Scrape',
    688                     lastScrape = 'N/A',
    689                 lastScrapeTime;
    690 
    691                 if (tracker.hasScraped) {
    692                         lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
    693                         if (tracker.lastScrapeSucceeded) {
    694                                 lastScrape = lastScrapeTime;
    695                         } else {
    696                                 lastScrapeLabel = 'Scrape error';
    697                                 lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
    698                         }
    699                 }
    700                 return {'label':lastScrapeLabel, 'value':lastScrape};
    701         },
    702 
    703         updateTrackersPage = function() {
    704                 var i, j, tier, tracker, trackers, tor,
    705                     html, parity, lastAnnounceStatusHash,
    706                     announceState, lastScrapeStatusHash,
    707                     na = 'N/A',
    708                     trackers_list = data.elements.trackers_list,
    709                     torrents = data.torrents;
    710 
    711                 // By building up the HTML as as string, then have the browser
    712                 // turn this into a DOM tree, this is a fast operation.
    713                 html = [];
    714                 for (i=0; tor=torrents[i]; ++i)
    715                 {
    716                         html.push ('<div class="inspector_group">');
    717 
    718                         if (torrents.length > 1)
    719                                 html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
    720 
    721                         tier = -1;
    722                         trackers = tor.getTrackers();
    723                         for (j=0; tracker=trackers[j]; ++j)
    724                         {
    725                                 if (tier != tracker.tier)
    726                                 {
    727                                         if (tier !== -1) // close previous tier
    728                                                 html.push('</ul></div>');
    729 
    730                                         tier = tracker.tier;
    731 
    732                                         html.push('<div class="inspector_group_label">',
    733                                                   'Tier ', tier+1, '</div>',
    734                                                   '<ul class="tier_list">');
    735                                 }
    736 
    737                                 // Display construction
    738                                 lastAnnounceStatusHash = lastAnnounceStatus(tracker);
    739                                 announceState = getAnnounceState(tracker);
    740                                 lastScrapeStatusHash = lastScrapeStatus(tracker);
    741                                 parity = (j%2) ? 'odd' : 'even';
    742                                 html.push('<li class="inspector_tracker_entry ', parity, '"><div class="tracker_host" title="', sanitizeText(tracker.announce), '">',
    743                                           sanitizeText(tracker.host || tracker.announce), '</div>',
    744                                           '<div class="tracker_activity">',
    745                                           '<div>', lastAnnounceStatusHash['label'], ': ', lastAnnounceStatusHash['value'], '</div>',
    746                                           '<div>', announceState, '</div>',
    747                                           '<div>', lastScrapeStatusHash['label'], ': ', lastScrapeStatusHash['value'], '</div>',
    748                                           '</div><table class="tracker_stats">',
    749                                           '<tr><th>Seeders:</th><td>', (tracker.seederCount > -1 ? tracker.seederCount : na), '</td></tr>',
    750                                           '<tr><th>Leechers:</th><td>', (tracker.leecherCount > -1 ? tracker.leecherCount : na), '</td></tr>',
    751                                           '<tr><th>Downloads:</th><td>', (tracker.downloadCount > -1 ? tracker.downloadCount : na), '</td></tr>',
    752                                           '</table></li>');
    753                         }
    754                         if (tier !== -1) // close last tier
    755                                 html.push('</ul></div>');
    756 
    757                         html.push('</div>'); // inspector_group
    758                 }
    759 
    760                 setInnerHTML (trackers_list, html.join(''));
    761         },
    762 
    763         initialize = function (controller) {
    764 
    765                 var ti = '#torrent_inspector_';
    766 
    767                 data.controller = controller;
    768 
    769                 $('.inspector-tab').click(onTabClicked);
    770 
    771                 data.elements.info_page      = $('#inspector-page-info')[0];
    772                 data.elements.files_page     = $('#inspector-page-files')[0];
    773                 data.elements.peers_page     = $('#inspector-page-peers')[0];
    774                 data.elements.trackers_page  = $('#inspector-page-trackers')[0];
    775 
    776                 data.elements.file_list      = $('#inspector_file_list')[0];
    777                 data.elements.peers_list     = $('#inspector_peers_list')[0];
    778                 data.elements.trackers_list  = $('#inspector_trackers_list')[0];
    779 
    780                 data.elements.have_lb           = $('#inspector-info-have')[0];
    781                 data.elements.availability_lb   = $('#inspector-info-availability')[0];
    782                 data.elements.downloaded_lb     = $('#inspector-info-downloaded')[0];
    783                 data.elements.uploaded_lb       = $('#inspector-info-uploaded')[0];
    784                 data.elements.state_lb          = $('#inspector-info-state')[0];
    785                 data.elements.running_time_lb   = $('#inspector-info-running-time')[0];
    786                 data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
    787                 data.elements.last_activity_lb  = $('#inspector-info-last-activity')[0];
    788                 data.elements.error_lb          = $('#inspector-info-error')[0];
    789                 data.elements.size_lb           = $('#inspector-info-size')[0];
    790                 data.elements.foldername_lb     = $('#inspector-info-location')[0];
    791                 data.elements.hash_lb           = $('#inspector-info-hash')[0];
    792                 data.elements.privacy_lb        = $('#inspector-info-privacy')[0];
    793                 data.elements.origin_lb         = $('#inspector-info-origin')[0];
    794                 data.elements.comment_lb        = $('#inspector-info-comment')[0];
    795                 data.elements.name_lb           = $('#torrent_inspector_name')[0];
    796 
    797                 // force initial 'N/A' updates on all the pages
    798                 updateInspector();
    799                 updateInfoPage();
    800                 updatePeersPage();
    801                 updateTrackersPage();
    802                 updateFilesPage();
    803         };
    804 
    805         /****
    806         *****  PUBLIC FUNCTIONS
    807         ****/
    808 
    809         this.setTorrents = function (torrents) {
    810                 var d = data;
    811 
    812                 // update the inspector when a selected torrent's data changes.
    813                 $(d.torrents).unbind('dataChanged.inspector');
    814                 $(torrents).bind('dataChanged.inspector', $.proxy(updateInspector,this));
    815                 d.torrents = torrents;
    816 
    817                 // periodically ask for updates to the inspector's torrents
    818                 clearInterval(d.refreshInterval);
    819                 d.refreshInterval = setInterval($.proxy(refreshTorrents,this), 2000);
    820                 refreshTorrents();
    821 
    822                 // refresh the inspector's UI
    823                 updateInspector();
    824         };
    825 
    826         initialize (controller);
     10    var data = {
     11            controller: null,
     12            elements: {},
     13            torrents: []
     14        },
     15
     16        needsExtraInfo = function (torrents) {
     17            var i, id, tor;
     18
     19            for (i = 0; tor = torrents[i]; i++)
     20                if (!tor.hasExtraInfo())
     21                    return true;
     22
     23            return false;
     24        },
     25
     26        refreshTorrents = function () {
     27            var fields,
     28                ids = $.map(data.torrents.slice(0), function (t) {
     29                    return t.getId();
     30                });
     31
     32            if (ids && ids.length) {
     33                fields = ['id'].concat(Torrent.Fields.StatsExtra);
     34
     35                if (needsExtraInfo(data.torrents)) {
     36                    $.merge(fields, Torrent.Fields.InfoExtra);
     37                }
     38
     39                data.controller.updateTorrents(ids, fields);
     40            }
     41        },
     42
     43        onTabClicked = function (ev) {
     44            var tab = ev.currentTarget;
     45
     46            if (isMobileDevice) {
     47                ev.stopPropagation();
     48            }
     49
     50            // select this tab and deselect the others
     51            $(tab).addClass('selected').siblings().removeClass('selected');
     52
     53            // show this tab and hide the others
     54            $('#' + tab.id.replace('tab', 'page')).show().siblings('.inspector-page').hide();
     55
     56            updateInspector();
     57        },
     58
     59        updateInspector = function () {
     60            var e = data.elements,
     61                torrents = data.torrents,
     62                name;
     63
     64            // update the name, which is shown on all the pages
     65            if (!torrents || !torrents.length) {
     66                name = 'No Selection';
     67            } else if (torrents.length === 1) {
     68                name = torrents[0].getName();
     69            } else {
     70                name = '' + torrents.length + ' Transfers Selected';
     71            }
     72            setTextContent(e.name_lb, name || na);
     73
     74            // update the visible page
     75            if ($(e.info_page).is(':visible')) {
     76                updateInfoPage();
     77            } else if ($(e.peers_page).is(':visible')) {
     78                updatePeersPage();
     79            } else if ($(e.trackers_page).is(':visible')) {
     80                updateTrackersPage();
     81            } else if ($(e.files_page).is(':visible')) {
     82                updateFilesPage();
     83            }
     84        },
     85
     86        /****
     87         *****  GENERAL INFO PAGE
     88         ****/
     89
     90        updateInfoPage = function () {
     91            var torrents = data.torrents,
     92                e = data.elements,
     93                fmt = Transmission.fmt,
     94                none = 'None',
     95                mixed = 'Mixed',
     96                unknown = 'Unknown',
     97                isMixed, allPaused, allFinished,
     98                str,
     99                baseline, it, s, i, t,
     100                sizeWhenDone = 0,
     101                leftUntilDone = 0,
     102                available = 0,
     103                haveVerified = 0,
     104                haveUnverified = 0,
     105                verifiedPieces = 0,
     106                stateString,
     107                latest,
     108                pieces,
     109                size,
     110                pieceSize,
     111                creator, mixed_creator,
     112                date, mixed_date,
     113                v, u, f, d, pct,
     114                uri,
     115                now = Date.now();
     116
     117            //
     118            //  state_lb
     119            //
     120
     121            if (torrents.length < 1) {
     122                str = none;
     123            } else {
     124                isMixed = false;
     125                allPaused = true;
     126                allFinished = true;
     127
     128                baseline = torrents[0].getStatus();
     129                for (i = 0; t = torrents[i]; ++i) {
     130                    it = t.getStatus();
     131                    if (it != baseline) {
     132                        isMixed = true;
     133                    }
     134                    if (!t.isStopped()) {
     135                        allPaused = allFinished = false;
     136                    }
     137                    if (!t.isFinished()) {
     138                        allFinished = false;
     139                    }
     140                }
     141                if (isMixed) {
     142                    str = mixed;
     143                } else if (allFinished) {
     144                    str = 'Finished';
     145                } else if (allPaused) {
     146                    str = 'Paused';
     147                } else {
     148                    str = torrents[0].getStateString();
     149                }
     150            }
     151            setTextContent(e.state_lb, str);
     152            stateString = str;
     153
     154            //
     155            //  have_lb
     156            //
     157
     158            if (torrents.length < 1)
     159                str = none;
     160            else {
     161                baseline = torrents[0].getStatus();
     162                for (i = 0; t = torrents[i]; ++i) {
     163                    if (!t.needsMetaData()) {
     164                        haveUnverified += t.getHaveUnchecked();
     165                        v = t.getHaveValid();
     166                        haveVerified += v;
     167                        if (t.getPieceSize()) {
     168                            verifiedPieces += v / t.getPieceSize();
     169                        }
     170                        sizeWhenDone += t.getSizeWhenDone();
     171                        leftUntilDone += t.getLeftUntilDone();
     172                        available += (t.getHave()) + t.getDesiredAvailable();
     173                    }
     174                }
     175
     176                d = 100.0 * (sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
     177                str = fmt.percentString(d);
     178
     179                if (!haveUnverified && !leftUntilDone) {
     180                    str = fmt.size(haveVerified) + ' (100%)';
     181                } else if (!haveUnverified) {
     182                    str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%)';
     183                } else {
     184                    str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%), ' + fmt.size(haveUnverified) + ' Unverified';
     185                }
     186            }
     187            setTextContent(e.have_lb, str);
     188
     189            //
     190            //  availability_lb
     191            //
     192
     193            if (torrents.length < 1) {
     194                str = none;
     195            } else if (sizeWhenDone == 0) {
     196                str = none;
     197            } else {
     198                str = '' + fmt.percentString((100.0 * available) / sizeWhenDone) + '%';
     199            };
     200            setTextContent(e.availability_lb, str);
     201
     202            //
     203            //  downloaded_lb
     204            //
     205
     206            if (torrents.length < 1) {
     207                str = none;
     208            } else {
     209                d = f = 0;
     210                for (i = 0; t = torrents[i]; ++i) {
     211                    d += t.getDownloadedEver();
     212                    f += t.getFailedEver();
     213                };
     214                if (f) {
     215                    str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
     216                } else {
     217                    str = fmt.size(d);
     218                };
     219            };
     220            setTextContent(e.downloaded_lb, str);
     221
     222            //
     223            //  uploaded_lb
     224            //
     225
     226            if (torrents.length < 1) {
     227                str = none;
     228            } else {
     229                d = u = 0;
     230                if (torrents.length == 1) {
     231                    d = torrents[0].getDownloadedEver();
     232                    u = torrents[0].getUploadedEver();
     233
     234                    if (d == 0) {
     235                        d = torrents[0].getHaveValid();
     236                    };
     237                } else {
     238                    for (i = 0; t = torrents[i]; ++i) {
     239                        d += t.getDownloadedEver();
     240                        u += t.getUploadedEver();
     241                    };
     242                };
     243                str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString(Math.ratio(u, d)) + ')';
     244            };
     245            setTextContent(e.uploaded_lb, str);
     246
     247            //
     248            // running time
     249            //
     250
     251            if (torrents.length < 1) {
     252                str = none;
     253            } else {
     254                allPaused = true;
     255                baseline = torrents[0].getStartDate();
     256                for (i = 0; t = torrents[i]; ++i) {
     257                    if (baseline != t.getStartDate()) {
     258                        baseline = 0;
     259                    }
     260                    if (!t.isStopped()) {
     261                        allPaused = false;
     262                    }
     263                }
     264                if (allPaused) {
     265                    str = stateString; // paused || finished}
     266                } else if (!baseline) {
     267                    str = mixed;
     268                } else {
     269                    str = fmt.timeInterval(now / 1000 - baseline);
     270                }
     271            };
     272
     273            setTextContent(e.running_time_lb, str);
     274
     275            //
     276            // remaining time
     277            //
     278
     279            str = '';
     280            if (torrents.length < 1) {
     281                str = none;
     282            } else {
     283                baseline = torrents[0].getETA();
     284                for (i = 0; t = torrents[i]; ++i) {
     285                    if (baseline != t.getETA()) {
     286                        str = mixed;
     287                        break;
     288                    }
     289                }
     290            }
     291            if (!str.length) {
     292                if (baseline < 0) {
     293                    str = unknown;
     294                } else {
     295                    str = fmt.timeInterval(baseline);
     296                }
     297            }
     298            setTextContent(e.remaining_time_lb, str);
     299
     300            //
     301            // last activity
     302            //
     303
     304            latest = -1;
     305            if (torrents.length < 1) {
     306                str = none;
     307            } else {
     308                baseline = torrents[0].getLastActivity();
     309                for (i = 0; t = torrents[i]; ++i) {
     310                    d = t.getLastActivity();
     311                    if (latest < d) {
     312                        latest = d;
     313                    };
     314                };
     315                d = now / 1000 - latest; // seconds since last activity
     316                if (d < 0) {
     317                    str = none;
     318                } else if (d < 5) {
     319                    str = 'Active now';
     320                } else {
     321                    str = fmt.timeInterval(d) + ' ago';
     322                };
     323            };
     324            setTextContent(e.last_activity_lb, str);
     325
     326            //
     327            // error
     328            //
     329
     330            if (torrents.length < 1) {
     331                str = none;
     332            } else {
     333                str = torrents[0].getErrorString();
     334                for (i = 0; t = torrents[i]; ++i) {
     335                    if (str != t.getErrorString()) {
     336                        str = mixed;
     337                        break;
     338                    };
     339                };
     340            };
     341            setTextContent(e.error_lb, str || none);
     342
     343            //
     344            // size
     345            //
     346
     347            if (torrents.length < 1) {
     348                {
     349                    str = none;
     350                };
     351            } else {
     352                pieces = 0;
     353                size = 0;
     354                pieceSize = torrents[0].getPieceSize();
     355                for (i = 0; t = torrents[i]; ++i) {
     356                    pieces += t.getPieceCount();
     357                    size += t.getTotalSize();
     358                    if (pieceSize != t.getPieceSize()) {
     359                        pieceSize = 0;
     360                    }
     361                };
     362                if (!size) {
     363                    str = none;
     364                } else if (pieceSize > 0) {
     365                    str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
     366                } else {
     367                    str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
     368                };
     369            };
     370            setTextContent(e.size_lb, str);
     371
     372            //
     373            //  hash
     374            //
     375
     376            if (torrents.length < 1) {
     377                str = none;
     378            } else {
     379                str = torrents[0].getHashString();
     380                for (i = 0; t = torrents[i]; ++i) {
     381                    if (str != t.getHashString()) {
     382                        str = mixed;
     383                        break;
     384                    };
     385                };
     386            };
     387            setTextContent(e.hash_lb, str);
     388
     389            //
     390            //  privacy
     391            //
     392
     393            if (torrents.length < 1) {
     394                str = none;
     395            } else {
     396                baseline = torrents[0].getPrivateFlag();
     397                str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
     398                for (i = 0; t = torrents[i]; ++i) {
     399                    if (baseline != t.getPrivateFlag()) {
     400                        str = mixed;
     401                        break;
     402                    };
     403                };
     404            };
     405            setTextContent(e.privacy_lb, str);
     406
     407            //
     408            //  comment
     409            //
     410
     411            if (torrents.length < 1) {
     412                str = none;
     413            } else {
     414                str = torrents[0].getComment();
     415                for (i = 0; t = torrents[i]; ++i) {
     416                    if (str != t.getComment()) {
     417                        str = mixed;
     418                        break;
     419                    };
     420                };
     421            };
     422            if (!str) {
     423                str = none;
     424            }
     425            uri = parseUri(str);
     426            if (uri.protocol == 'http' || uri.parseUri == 'https') {
     427                str = encodeURI(str);
     428                setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
     429            } else {
     430                setTextContent(e.comment_lb, str);
     431            };
     432
     433            //
     434            //  origin
     435            //
     436
     437            if (torrents.length < 1) {
     438                str = none;
     439            } else {
     440                mixed_creator = false;
     441                mixed_date = false;
     442                creator = torrents[0].getCreator();
     443                date = torrents[0].getDateCreated();
     444                for (i = 0; t = torrents[i]; ++i) {
     445                    if (creator != t.getCreator()) {
     446                        mixed_creator = true;
     447                    };
     448                    if (date != t.getDateCreated()) {
     449                        mixed_date = true;
     450                    };
     451                };
     452                var empty_creator = !creator || !creator.length;
     453                var empty_date = !date;
     454                if (mixed_creator || mixed_date) {
     455                    str = mixed;
     456                } else if (empty_creator && empty_date) {
     457                    str = unknown;
     458                } else if (empty_date && !empty_creator) {
     459                    str = 'Created by ' + creator;
     460                } else if (empty_creator && !empty_date) {
     461                    str = 'Created on ' + (new Date(date * 1000)).toDateString();
     462                } else {
     463                    str = 'Created by ' + creator + ' on ' + (new Date(date * 1000)).toDateString();
     464                };
     465            };
     466            setTextContent(e.origin_lb, str);
     467
     468            //
     469            //  foldername
     470            //
     471
     472            if (torrents.length < 1) {
     473                str = none;
     474            } else {
     475                str = torrents[0].getDownloadDir();
     476                for (i = 0; t = torrents[i]; ++i) {
     477                    if (str != t.getDownloadDir()) {
     478                        str = mixed;
     479                        break;
     480                    };
     481                };
     482            };
     483            setTextContent(e.foldername_lb, str);
     484        },
     485
     486        /****
     487         *****  FILES PAGE
     488         ****/
     489
     490        changeFileCommand = function (fileIndices, command) {
     491            var torrentId = data.file_torrent.getId();
     492            data.controller.changeFileCommand(torrentId, fileIndices, command);
     493        },
     494
     495        onFileWantedToggled = function (ev, fileIndices, want) {
     496            changeFileCommand(fileIndices, want ? 'files-wanted' : 'files-unwanted');
     497        },
     498
     499        onFilePriorityToggled = function (ev, fileIndices, priority) {
     500            var command;
     501            switch (priority) {
     502            case -1:
     503                command = 'priority-low';
     504                break;
     505            case 1:
     506                command = 'priority-high';
     507                break;
     508            default:
     509                command = 'priority-normal';
     510                break;
     511            }
     512            changeFileCommand(fileIndices, command);
     513        },
     514
     515        onNameClicked = function (ev, fileRow, fileIndices) {
     516            $(fileRow.getElement()).siblings().slideToggle();
     517        },
     518
     519        clearFileList = function () {
     520            $(data.elements.file_list).empty();
     521            delete data.file_torrent;
     522            delete data.file_torrent_n;
     523            delete data.file_rows;
     524        },
     525
     526        createFileTreeModel = function (tor) {
     527            var i, j, n, name, tokens, walk, tree, token, sub,
     528                leaves = [],
     529                tree = {
     530                    children: {},
     531                    file_indices: []
     532                };
     533
     534            n = tor.getFileCount();
     535            for (i = 0; i < n; ++i) {
     536                name = tor.getFile(i).name;
     537                tokens = name.split('/');
     538                walk = tree;
     539                for (j = 0; j < tokens.length; ++j) {
     540                    token = tokens[j];
     541                    sub = walk.children[token];
     542                    if (!sub) {
     543                        walk.children[token] = sub = {
     544                            name: token,
     545                            parent: walk,
     546                            children: {},
     547                            file_indices: [],
     548                            depth: j
     549                        };
     550                    }
     551                    walk = sub;
     552                }
     553                walk.file_index = i;
     554                delete walk.children;
     555                leaves.push(walk);
     556            }
     557
     558            for (i = 0; i < leaves.length; ++i) {
     559                walk = leaves[i];
     560                j = walk.file_index;
     561                do {
     562                    walk.file_indices.push(j);
     563                    walk = walk.parent;
     564                } while (walk);
     565            }
     566
     567            return tree;
     568        },
     569
     570        addNodeToView = function (tor, parent, sub, i) {
     571            var row;
     572            row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i % 2);
     573            data.file_rows.push(row);
     574            parent.appendChild(row.getElement());
     575            $(row).bind('wantedToggled', onFileWantedToggled);
     576            $(row).bind('priorityToggled', onFilePriorityToggled);
     577            $(row).bind('nameClicked', onNameClicked);
     578        },
     579
     580        addSubtreeToView = function (tor, parent, sub, i) {
     581            var key, div;
     582            div = document.createElement('div');
     583            if (sub.parent) {
     584                addNodeToView(tor, div, sub, i++);
     585            }
     586            if (sub.children) {
     587                for (key in sub.children) {
     588                    i = addSubtreeToView(tor, div, sub.children[key]);
     589                }
     590            }
     591            parent.appendChild(div);
     592            return i;
     593        },
     594
     595        updateFilesPage = function () {
     596            var i, n, tor, fragment, tree,
     597                file_list = data.elements.file_list,
     598                torrents = data.torrents;
     599
     600            // only show one torrent at a time
     601            if (torrents.length !== 1) {
     602                clearFileList();
     603                return;
     604            }
     605
     606            tor = torrents[0];
     607            n = tor ? tor.getFileCount() : 0;
     608            if (tor != data.file_torrent || n != data.file_torrent_n) {
     609                // rebuild the file list...
     610                clearFileList();
     611                data.file_torrent = tor;
     612                data.file_torrent_n = n;
     613                data.file_rows = [];
     614                fragment = document.createDocumentFragment();
     615                tree = createFileTreeModel(tor);
     616                addSubtreeToView(tor, fragment, tree, 0);
     617                file_list.appendChild(fragment);
     618            } else {
     619                // ...refresh the already-existing file list
     620                for (i = 0, n = data.file_rows.length; i < n; ++i)
     621                    data.file_rows[i].refresh();
     622            }
     623        },
     624
     625        /****
     626         *****  PEERS PAGE
     627         ****/
     628
     629        updatePeersPage = function () {
     630            var i, k, tor, peers, peer, parity,
     631                html = [],
     632                fmt = Transmission.fmt,
     633                peers_list = data.elements.peers_list,
     634                torrents = data.torrents;
     635
     636            for (k = 0; tor = torrents[k]; ++k) {
     637                peers = tor.getPeers();
     638                html.push('<div class="inspector_group">');
     639                if (torrents.length > 1) {
     640                    html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
     641                }
     642                if (!peers || !peers.length) {
     643                    html.push('<br></div>'); // firefox won't paint the top border if the div is empty
     644                    continue;
     645                }
     646                html.push('<table class="peer_list">',
     647                    '<tr class="inspector_peer_entry even">',
     648                    '<th class="encryptedCol"></th>',
     649                    '<th class="upCol">Up</th>',
     650                    '<th class="downCol">Down</th>',
     651                    '<th class="percentCol">%</th>',
     652                    '<th class="statusCol">Status</th>',
     653                    '<th class="addressCol">Address</th>',
     654                    '<th class="clientCol">Client</th>',
     655                    '</tr>');
     656                for (i = 0; peer = peers[i]; ++i) {
     657                    parity = (i % 2) ? 'odd' : 'even';
     658                    html.push('<tr class="inspector_peer_entry ', parity, '">',
     659                        '<td>', (peer.isEncrypted ? '<div class="encrypted-peer-cell" title="Encrypted Connection">' : '<div class="unencrypted-peer-cell">'), '</div>', '</td>',
     660                        '<td>', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '</td>',
     661                        '<td>', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '</td>',
     662                        '<td class="percentCol">', Math.floor(peer.progress * 100), '%', '</td>',
     663                        '<td>', fmt.peerStatus(peer.flagStr), '</td>',
     664                        '<td>', sanitizeText(peer.address), '</td>',
     665                        '<td class="clientCol">', sanitizeText(peer.clientName), '</td>',
     666                        '</tr>');
     667                }
     668                html.push('</table></div>');
     669            }
     670
     671            setInnerHTML(peers_list, html.join(''));
     672        },
     673
     674        /****
     675         *****  TRACKERS PAGE
     676         ****/
     677
     678        getAnnounceState = function (tracker) {
     679            var timeUntilAnnounce, s = '';
     680            switch (tracker.announceState) {
     681            case Torrent._TrackerActive:
     682                s = 'Announce in progress';
     683                break;
     684            case Torrent._TrackerWaiting:
     685                timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
     686                if (timeUntilAnnounce < 0) {
     687                    timeUntilAnnounce = 0;
     688                }
     689                s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
     690                break;
     691            case Torrent._TrackerQueued:
     692                s = 'Announce is queued';
     693                break;
     694            case Torrent._TrackerInactive:
     695                s = tracker.isBackup ?
     696                    'Tracker will be used as a backup' :
     697                    'Announce not scheduled';
     698                break;
     699            default:
     700                s = 'unknown announce state: ' + tracker.announceState;
     701            }
     702            return s;
     703        },
     704
     705        lastAnnounceStatus = function (tracker) {
     706
     707            var lastAnnounceLabel = 'Last Announce',
     708                lastAnnounce = ['N/A'],
     709                lastAnnounceTime;
     710
     711            if (tracker.hasAnnounced) {
     712                lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
     713                if (tracker.lastAnnounceSucceeded) {
     714                    lastAnnounce = [lastAnnounceTime, ' (got ', Transmission.fmt.countString('peer', 'peers', tracker.lastAnnouncePeerCount), ')'];
     715                } else {
     716                    lastAnnounceLabel = 'Announce error';
     717                    lastAnnounce = [(tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime];
     718                }
     719            }
     720            return {
     721                'label': lastAnnounceLabel,
     722                'value': lastAnnounce.join('')
     723            };
     724        },
     725
     726        lastScrapeStatus = function (tracker) {
     727
     728            var lastScrapeLabel = 'Last Scrape',
     729                lastScrape = 'N/A',
     730                lastScrapeTime;
     731
     732            if (tracker.hasScraped) {
     733                lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
     734                if (tracker.lastScrapeSucceeded) {
     735                    lastScrape = lastScrapeTime;
     736                } else {
     737                    lastScrapeLabel = 'Scrape error';
     738                    lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
     739                }
     740            }
     741            return {
     742                'label': lastScrapeLabel,
     743                'value': lastScrape
     744            };
     745        },
     746
     747        updateTrackersPage = function () {
     748            var i, j, tier, tracker, trackers, tor,
     749                html, parity, lastAnnounceStatusHash,
     750                announceState, lastScrapeStatusHash,
     751                na = 'N/A',
     752                trackers_list = data.elements.trackers_list,
     753                torrents = data.torrents;
     754
     755            // By building up the HTML as as string, then have the browser
     756            // turn this into a DOM tree, this is a fast operation.
     757            html = [];
     758            for (i = 0; tor = torrents[i]; ++i) {
     759                html.push('<div class="inspector_group">');
     760
     761                if (torrents.length > 1) {
     762                    html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
     763                }
     764
     765                tier = -1;
     766                trackers = tor.getTrackers();
     767                for (j = 0; tracker = trackers[j]; ++j) {
     768                    if (tier != tracker.tier) {
     769                        if (tier !== -1) { // close previous tier
     770                            html.push('</ul></div>');
     771                        }
     772
     773                        tier = tracker.tier;
     774
     775                        html.push('<div class="inspector_group_label">',
     776                            'Tier ', tier + 1, '</div>',
     777                            '<ul class="tier_list">');
     778                    }
     779
     780                    // Display construction
     781                    lastAnnounceStatusHash = lastAnnounceStatus(tracker);
     782                    announceState = getAnnounceState(tracker);
     783                    lastScrapeStatusHash = lastScrapeStatus(tracker);
     784                    parity = (j % 2) ? 'odd' : 'even';
     785                    html.push('<li class="inspector_tracker_entry ', parity, '"><div class="tracker_host" title="', sanitizeText(tracker.announce), '">',
     786                        sanitizeText(tracker.host || tracker.announce), '</div>',
     787                        '<div class="tracker_activity">',
     788                        '<div>', lastAnnounceStatusHash['label'], ': ', lastAnnounceStatusHash['value'], '</div>',
     789                        '<div>', announceState, '</div>',
     790                        '<div>', lastScrapeStatusHash['label'], ': ', lastScrapeStatusHash['value'], '</div>',
     791                        '</div><table class="tracker_stats">',
     792                        '<tr><th>Seeders:</th><td>', (tracker.seederCount > -1 ? tracker.seederCount : na), '</td></tr>',
     793                        '<tr><th>Leechers:</th><td>', (tracker.leecherCount > -1 ? tracker.leecherCount : na), '</td></tr>',
     794                        '<tr><th>Downloads:</th><td>', (tracker.downloadCount > -1 ? tracker.downloadCount : na), '</td></tr>',
     795                        '</table></li>');
     796                }
     797                if (tier !== -1) { // close last tier
     798                    html.push('</ul></div>');
     799                }
     800
     801                html.push('</div>'); // inspector_group
     802            }
     803
     804            setInnerHTML(trackers_list, html.join(''));
     805        },
     806
     807        initialize = function (controller) {
     808
     809            var ti = '#torrent_inspector_';
     810
     811            data.controller = controller;
     812
     813            $('.inspector-tab').click(onTabClicked);
     814
     815            data.elements.info_page = $('#inspector-page-info')[0];
     816            data.elements.files_page = $('#inspector-page-files')[0];
     817            data.elements.peers_page = $('#inspector-page-peers')[0];
     818            data.elements.trackers_page = $('#inspector-page-trackers')[0];
     819
     820            data.elements.file_list = $('#inspector_file_list')[0];
     821            data.elements.peers_list = $('#inspector_peers_list')[0];
     822            data.elements.trackers_list = $('#inspector_trackers_list')[0];
     823
     824            data.elements.have_lb = $('#inspector-info-have')[0];
     825            data.elements.availability_lb = $('#inspector-info-availability')[0];
     826            data.elements.downloaded_lb = $('#inspector-info-downloaded')[0];
     827            data.elements.uploaded_lb = $('#inspector-info-uploaded')[0];
     828            data.elements.state_lb = $('#inspector-info-state')[0];
     829            data.elements.running_time_lb = $('#inspector-info-running-time')[0];
     830            data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
     831            data.elements.last_activity_lb = $('#inspector-info-last-activity')[0];
     832            data.elements.error_lb = $('#inspector-info-error')[0];
     833            data.elements.size_lb = $('#inspector-info-size')[0];
     834            data.elements.foldername_lb = $('#inspector-info-location')[0];
     835            data.elements.hash_lb = $('#inspector-info-hash')[0];
     836            data.elements.privacy_lb = $('#inspector-info-privacy')[0];
     837            data.elements.origin_lb = $('#inspector-info-origin')[0];
     838            data.elements.comment_lb = $('#inspector-info-comment')[0];
     839            data.elements.name_lb = $('#torrent_inspector_name')[0];
     840
     841            // force initial 'N/A' updates on all the pages
     842            updateInspector();
     843            updateInfoPage();
     844            updatePeersPage();
     845            updateTrackersPage();
     846            updateFilesPage();
     847        };
     848
     849    /****
     850     *****  PUBLIC FUNCTIONS
     851     ****/
     852
     853    this.setTorrents = function (torrents) {
     854        var d = data;
     855
     856        // update the inspector when a selected torrent's data changes.
     857        $(d.torrents).unbind('dataChanged.inspector');
     858        $(torrents).bind('dataChanged.inspector', $.proxy(updateInspector, this));
     859        d.torrents = torrents;
     860
     861        // periodically ask for updates to the inspector's torrents
     862        clearInterval(d.refreshInterval);
     863        d.refreshInterval = setInterval($.proxy(refreshTorrents, this), 2000);
     864        refreshTorrents();
     865
     866        // refresh the inspector's UI
     867        updateInspector();
     868    };
     869
     870    initialize(controller);
    827871};
  • trunk/web/javascript/notifications.js

    r14523 r14716  
    22
    33$(document).ready(function () {
    4         if (!window.webkitNotifications) {
    5                 return;
    6         }
     4    if (!window.webkitNotifications) {
     5        return;
     6    };
    77
    8         var notificationsEnabled = (window.webkitNotifications.checkPermission() === 0),
    9             toggle = $('#toggle_notifications');
     8    var notificationsEnabled = (window.webkitNotifications.checkPermission() === 0)
     9    var toggle = $('#toggle_notifications');
    1010
    11         toggle.show();
    12         updateMenuTitle();
    13         $(transmission).bind('downloadComplete seedingComplete', function (event, torrent) {
    14                 if (notificationsEnabled) {
    15                 var title = (event.type == 'downloadComplete' ? 'Download' : 'Seeding') + ' complete',
    16                         content = torrent.getName(),
    17                         notification;
     11    toggle.show();
     12    updateMenuTitle();
     13    $(transmission).bind('downloadComplete seedingComplete', function (event, torrent) {
     14        if (notificationsEnabled) {
     15            var title = (event.type == 'downloadComplete' ? 'Download' : 'Seeding') + ' complete',
     16                content = torrent.getName(),
     17                notification;
    1818
    19                 notification = window.webkitNotifications.createNotification('style/transmission/images/logo.png', title, content);
    20                 notification.show();
    21                 setTimeout(function () {
    22                   notification.cancel();
    23                 }, 5000);
    24         };
    25         });
     19            notification = window.webkitNotifications.createNotification('style/transmission/images/logo.png', title, content);
     20            notification.show();
     21            setTimeout(function () {
     22                notification.cancel();
     23            }, 5000);
     24        };
     25    });
    2626
    27         function updateMenuTitle() {
    28                 toggle.html((notificationsEnabled ? 'Disable' : 'Enable') + ' Notifications');
    29         }
     27    function updateMenuTitle() {
     28        toggle.html((notificationsEnabled ? 'Disable' : 'Enable') + ' Notifications');
     29    };
    3030
    31         Notifications.toggle = function () {
    32                 if (window.webkitNotifications.checkPermission() !== 0) {
    33                         window.webkitNotifications.requestPermission(function () {
    34                                 notificationsEnabled = (window.webkitNotifications.checkPermission() === 0);
    35                                 updateMenuTitle();
    36                         });
    37                 } else {
    38                         notificationsEnabled = !notificationsEnabled;
    39                         updateMenuTitle();
    40                 }
    41         };
     31    Notifications.toggle = function () {
     32        if (window.webkitNotifications.checkPermission() !== 0) {
     33            window.webkitNotifications.requestPermission(function () {
     34                notificationsEnabled = (window.webkitNotifications.checkPermission() === 0);
     35                updateMenuTitle();
     36            });
     37        } else {
     38            notificationsEnabled = !notificationsEnabled;
     39            updateMenuTitle();
     40        };
     41    };
    4242});
  • trunk/web/javascript/prefs-dialog.js

    r14523 r14716  
    88function PrefsDialog(remote) {
    99
    10         var data = {
    11             dialog: null,
    12             remote: null,
    13             elements: { },
    14 
    15             // all the RPC session keys that we have gui controls for
    16             keys: [
    17                 'alt-speed-down',
    18                 'alt-speed-time-begin',
    19                 'alt-speed-time-day',
    20                 'alt-speed-time-enabled',
    21                 'alt-speed-time-end',
    22                 'alt-speed-up',
    23                 'blocklist-enabled',
    24                 'blocklist-size',
    25                 'blocklist-url',
    26                 'dht-enabled',
    27                 'download-dir',
    28                 'encryption',
    29                 'idle-seeding-limit',
    30                 'idle-seeding-limit-enabled',
    31                 'lpd-enabled',
    32                 'peer-limit-global',
    33                 'peer-limit-per-torrent',
    34                 'peer-port',
    35                 'peer-port-random-on-start',
    36                 'pex-enabled',
    37                 'port-forwarding-enabled',
    38                 'rename-partial-files',
    39                 'seedRatioLimit',
    40                 'seedRatioLimited',
    41                 'speed-limit-down',
    42                 'speed-limit-down-enabled',
    43                 'speed-limit-up',
    44                 'speed-limit-up-enabled',
    45                 'start-added-torrents',
    46                 'utp-enabled'
    47             ],
    48 
    49             // map of keys that are enabled only if a 'parent' key is enabled
    50             groups: {
    51                 'alt-speed-time-enabled': ['alt-speed-time-begin',
    52                                            'alt-speed-time-day',
    53                                            'alt-speed-time-end' ],
    54                 'blocklist-enabled': ['blocklist-url',
    55                                       'blocklist-update-button' ],
    56                 'idle-seeding-limit-enabled': [ 'idle-seeding-limit' ],
    57                 'seedRatioLimited': [ 'seedRatioLimit' ],
    58                 'speed-limit-down-enabled': [ 'speed-limit-down' ],
    59                 'speed-limit-up-enabled': [ 'speed-limit-up' ]
    60             }
    61         },
    62 
    63         initTimeDropDown = function(e)
    64         {
    65                 var i, hour, mins, value, content;
    66 
    67                 for (i=0; i<24*4; ++i) {
    68                         hour = parseInt(i/4, 10);
    69                         mins = ((i%4) * 15);
    70                         value = i * 15;
    71                         content = hour + ':' + (mins || '00');
    72                         e.options[i] = new Option(content, value);
    73                 }
    74         },
    75 
    76         onPortChecked = function(response)
    77         {
    78                 var is_open = response['arguments']['port-is-open'],
    79                     text = 'Port is <b>' + (is_open ? 'Open' : 'Closed') + '</b>',
    80                     e = data.elements.root.find('#port-label');
    81                 setInnerHTML(e[0],text);
    82         },
    83 
    84         setGroupEnabled = function(parent_key, enabled)
    85         {
    86                 var i, key, keys, root;
    87 
    88                 if (parent_key in data.groups)
    89                 {
    90                         root = data.elements.root,
    91                         keys = data.groups[parent_key];
    92 
    93                         for (i=0; key=keys[i]; ++i)
    94                                 root.find('#'+key).attr('disabled',!enabled);
    95                 }
    96         },
    97 
    98         onBlocklistUpdateClicked = function ()
    99         {
    100                 data.remote.updateBlocklist();
    101                 setBlocklistButtonEnabled(false);
    102         },
    103         setBlocklistButtonEnabled = function(b)
    104         {
    105                 var e = data.elements.blocklist_button;
    106                 e.attr('disabled',!b);
    107                 e.val(b ? 'Update' : 'Updating...');
    108         },
    109 
    110         getValue = function(e)
    111         {
    112                 var str;
    113 
    114                 switch (e[0].type)
    115                 {
    116                         case 'checkbox':
    117                         case 'radio':
    118                                 return e.prop('checked');
    119 
    120                         case 'text':
    121                         case 'url':
    122                         case 'email':
    123                         case 'number':
    124                         case 'search':
    125                         case 'select-one':
    126                                 str = e.val();
    127                                 if( parseInt(str,10).toString() === str)
    128                                         return parseInt(str,10);
    129                                 if( parseFloat(str).toString() === str)
    130                                         return parseFloat(str);
    131                                 return str;
    132 
    133                         default:
    134                                 return null;
    135                 }
    136         },
    137 
    138         /* this callback is for controls whose changes can be applied
    139            immediately, like checkboxs, radioboxes, and selects */
    140         onControlChanged = function(ev)
    141         {
    142                 var o = {};
    143                 o[ev.target.id] = getValue($(ev.target));
    144                 data.remote.savePrefs(o);
    145         },
    146 
    147         /* these two callbacks are for controls whose changes can't be applied
    148            immediately -- like a text entry field -- because it takes many
    149            change events for the user to get to the desired result */
    150         onControlFocused  = function(ev)
    151         {
    152                 data.oldValue = getValue($(ev.target));
    153         },
    154         onControlBlurred  = function(ev)
    155         {
    156                 var newValue = getValue($(ev.target));
    157                 if (newValue !== data.oldValue)
    158                 {
    159                         var o = {};
    160                         o[ev.target.id] = newValue;
    161                         data.remote.savePrefs(o);
    162                         delete data.oldValue;
    163                 }
    164         },
    165 
    166         getDefaultMobileOptions = function()
    167         {
    168                 return {
    169                         width: $(window).width(),
    170                         height: $(window).height(),
    171                         position: [ 'left', 'top' ]
    172                 };
    173         },
    174 
    175         initialize = function (remote)
    176         {
    177                 var i, key, e, o;
    178 
    179                 data.remote = remote;
    180 
    181                 e = $('#prefs-dialog');
    182                 data.elements.root = e;
    183 
    184                 initTimeDropDown(e.find('#alt-speed-time-begin')[0]);
    185                 initTimeDropDown(e.find('#alt-speed-time-end')[0]);
    186 
    187                 o = isMobileDevice
    188                   ? getDefaultMobileOptions()
    189                   : { width: 350, height: 400 };
    190                 o.autoOpen = false;
    191                 o.show = o.hide = 'fade';
    192                 o.close = onDialogClosed;
    193                 e.tabbedDialog(o);
    194 
    195                 e = e.find('#blocklist-update-button');
    196                 data.elements.blocklist_button = e;
    197                 e.click(onBlocklistUpdateClicked);
    198 
    199                 // listen for user input
    200                 for (i=0; key=data.keys[i]; ++i)
    201                 {
    202                         e = data.elements.root.find('#'+key);
    203                         switch (e[0].type)
    204                         {
    205                                 case 'checkbox':
    206                                 case 'radio':
    207                                 case 'select-one':
    208                                         e.change(onControlChanged);
    209                                         break;
    210 
    211                                 case 'text':
    212                                 case 'url':
    213                                 case 'email':
    214                                 case 'number':
    215                                 case 'search':
    216                                         e.focus(onControlFocused);
    217                                         e.blur(onControlBlurred);
    218 
    219                                 default:
    220                                         break;
    221                         }
    222                 }
    223         },
    224 
    225         getValues = function()
    226         {
    227                 var i, key, val, o={},
    228                     keys = data.keys,
    229                     root = data.elements.root;
    230 
    231                 for (i=0; key=keys[i]; ++i) {
    232                         val = getValue(root.find('#'+key));
    233                         if (val !== null)
    234                                 o[key] = val;
    235                 }
    236 
    237                 return o;
    238         },
    239 
    240         onDialogClosed = function()
    241         {
    242                 transmission.hideMobileAddressbar();
    243 
    244                 $(data.dialog).trigger('closed', getValues());
    245         };
    246 
    247         /****
    248         *****  PUBLIC FUNCTIONS
    249         ****/
    250 
    251         // update the dialog's controls
    252         this.set = function (o)
    253         {
    254                 var e, i, key, val, option,
    255                     keys = data.keys,
    256                     root = data.elements.root;
    257 
    258                 setBlocklistButtonEnabled(true);
    259 
    260                 for (i=0; key=keys[i]; ++i)
    261                 {
    262                         val = o[key];
    263                         e = root.find('#'+key);
    264 
    265                         if (key === 'blocklist-size')
    266                         {
    267                                 // special case -- regular text area
    268                                 e.text('' + val.toStringWithCommas());
    269                         }
    270                         else switch (e[0].type)
    271                         {
    272                                 case 'checkbox':
    273                                 case 'radio':
    274                                         e.prop('checked', val);
    275                                         setGroupEnabled(key, val);
    276                                         break;
    277                                 case 'text':
    278                                 case 'url':
    279                                 case 'email':
    280                                 case 'number':
    281                                 case 'search':
    282                                         // don't change the text if the user's editing it.
    283                                         // it's very annoying when that happens!
    284                                         if (e[0] !== document.activeElement)
    285                                                 e.val(val);
    286                                         break;
    287                                 case 'select-one':
    288                                         e.val(val);
    289                                         break;
    290                                 default:
    291                                         break;
    292                         }
    293                 }
    294         };
    295 
    296         this.show = function ()
    297         {
    298                 transmission.hideMobileAddressbar();
    299 
    300                 setBlocklistButtonEnabled(true);
    301                 data.remote.checkPort(onPortChecked,this);
    302                 data.elements.root.dialog('open');
    303         };
    304 
    305         this.close = function ()
    306         {
    307                 transmission.hideMobileAddressbar();
    308                 data.elements.root.dialog('close');
    309         },
    310 
    311         this.shouldAddedTorrentsStart = function()
    312         {
    313                 return data.elements.root.find('#start-added-torrents')[0].checked;
    314         };
    315 
    316         data.dialog = this;
    317         initialize (remote);
     10    var data = {
     11        dialog: null,
     12        remote: null,
     13        elements: {},
     14
     15        // all the RPC session keys that we have gui controls for
     16        keys: [
     17            'alt-speed-down',
     18            'alt-speed-time-begin',
     19            'alt-speed-time-day',
     20            'alt-speed-time-enabled',
     21            'alt-speed-time-end',
     22            'alt-speed-up',
     23            'blocklist-enabled',
     24            'blocklist-size',
     25            'blocklist-url',
     26            'dht-enabled',
     27            'download-dir',
     28            'encryption',
     29            'idle-seeding-limit',
     30            'idle-seeding-limit-enabled',
     31            'lpd-enabled',
     32            'peer-limit-global',
     33            'peer-limit-per-torrent',
     34            'peer-port',
     35            'peer-port-random-on-start',
     36            'pex-enabled',
     37            'port-forwarding-enabled',
     38            'rename-partial-files',
     39            'seedRatioLimit',
     40            'seedRatioLimited',
     41            'speed-limit-down',
     42            'speed-limit-down-enabled',
     43            'speed-limit-up',
     44            'speed-limit-up-enabled',
     45            'start-added-torrents',
     46            'utp-enabled'
     47        ],
     48
     49        // map of keys that are enabled only if a 'parent' key is enabled
     50        groups: {
     51            'alt-speed-time-enabled': ['alt-speed-time-begin',
     52                'alt-speed-time-day',
     53                'alt-speed-time-end'
     54            ],
     55            'blocklist-enabled': ['blocklist-url',
     56                'blocklist-update-button'
     57            ],
     58            'idle-seeding-limit-enabled': ['idle-seeding-limit'],
     59            'seedRatioLimited': ['seedRatioLimit'],
     60            'speed-limit-down-enabled': ['speed-limit-down'],
     61            'speed-limit-up-enabled': ['speed-limit-up']
     62        }
     63    };
     64
     65    var initTimeDropDown = function (e) {
     66        var i, hour, mins, value, content;
     67
     68        for (i = 0; i < 24 * 4; ++i) {
     69            hour = parseInt(i / 4, 10);
     70            mins = ((i % 4) * 15);
     71            value = i * 15;
     72            content = hour + ':' + (mins || '00');
     73            e.options[i] = new Option(content, value);
     74        }
     75    };
     76
     77    var onPortChecked = function (response) {
     78        var is_open = response['arguments']['port-is-open'];
     79        var text = 'Port is <b>' + (is_open ? 'Open' : 'Closed') + '</b>';
     80        var e = data.elements.root.find('#port-label');
     81        setInnerHTML(e[0], text);
     82    };
     83
     84    var setGroupEnabled = function (parent_key, enabled) {
     85        var i, key, keys, root;
     86
     87        if (parent_key in data.groups) {
     88            root = data.elements.root;
     89            keys = data.groups[parent_key];
     90
     91            for (i = 0; key = keys[i]; ++i) {
     92                root.find('#' + key).attr('disabled', !enabled);
     93            };
     94        };
     95    };
     96
     97    var onBlocklistUpdateClicked = function () {
     98        data.remote.updateBlocklist();
     99        setBlocklistButtonEnabled(false);
     100    };
     101
     102    var setBlocklistButtonEnabled = function (b) {
     103        var e = data.elements.blocklist_button;
     104        e.attr('disabled', !b);
     105        e.val(b ? 'Update' : 'Updating...');
     106    };
     107
     108    var getValue = function (e) {
     109        var str;
     110
     111        switch (e[0].type) {
     112        case 'checkbox':
     113        case 'radio':
     114            return e.prop('checked');
     115
     116        case 'text':
     117        case 'url':
     118        case 'email':
     119        case 'number':
     120        case 'search':
     121        case 'select-one':
     122            str = e.val();
     123            if (parseInt(str, 10).toString() === str) {
     124                return parseInt(str, 10);
     125            };
     126            if (parseFloat(str).toString() === str) {
     127                return parseFloat(str);
     128            };
     129            return str;
     130
     131        default:
     132            return null;
     133        }
     134    };
     135
     136    /* this callback is for controls whose changes can be applied
     137       immediately, like checkboxs, radioboxes, and selects */
     138    var onControlChanged = function (ev) {
     139        var o = {};
     140        o[ev.target.id] = getValue($(ev.target));
     141        data.remote.savePrefs(o);
     142    };
     143
     144    /* these two callbacks are for controls whose changes can't be applied
     145       immediately -- like a text entry field -- because it takes many
     146       change events for the user to get to the desired result */
     147    var onControlFocused = function (ev) {
     148        data.oldValue = getValue($(ev.target));
     149    };
     150
     151    var onControlBlurred = function (ev) {
     152        var newValue = getValue($(ev.target));
     153        if (newValue !== data.oldValue) {
     154            var o = {};
     155            o[ev.target.id] = newValue;
     156            data.remote.savePrefs(o);
     157            delete data.oldValue;
     158        }
     159    };
     160
     161    var getDefaultMobileOptions = function () {
     162        return {
     163            width: $(window).width(),
     164            height: $(window).height(),
     165            position: ['left', 'top']
     166        };
     167    };
     168
     169    var initialize = function (remote) {
     170        var i, key, e, o;
     171
     172        data.remote = remote;
     173
     174        e = $('#prefs-dialog');
     175        data.elements.root = e;
     176
     177        initTimeDropDown(e.find('#alt-speed-time-begin')[0]);
     178        initTimeDropDown(e.find('#alt-speed-time-end')[0]);
     179
     180        o = isMobileDevice ? getDefaultMobileOptions() : {
     181            width: 350,
     182            height: 400
     183        };
     184        o.autoOpen = false;
     185        o.show = o.hide = 'fade';
     186        o.close = onDialogClosed;
     187        e.tabbedDialog(o);
     188
     189        e = e.find('#blocklist-update-button');
     190        data.elements.blocklist_button = e;
     191        e.click(onBlocklistUpdateClicked);
     192
     193        // listen for user input
     194        for (i = 0; key = data.keys[i]; ++i) {
     195            e = data.elements.root.find('#' + key);
     196            switch (e[0].type) {
     197            case 'checkbox':
     198            case 'radio':
     199            case 'select-one':
     200                e.change(onControlChanged);
     201                break;
     202
     203            case 'text':
     204            case 'url':
     205            case 'email':
     206            case 'number':
     207            case 'search':
     208                e.focus(onControlFocused);
     209                e.blur(onControlBlurred);
     210
     211            default:
     212                break;
     213            };
     214        };
     215    };
     216
     217    var getValues = function () {
     218        var i, key, val, o = {},
     219            keys = data.keys,
     220            root = data.elements.root;
     221
     222        for (i = 0; key = keys[i]; ++i) {
     223            val = getValue(root.find('#' + key));
     224            if (val !== null) {
     225                o[key] = val;
     226            };
     227        };
     228
     229        return o;
     230    };
     231
     232    var onDialogClosed = function () {
     233        transmission.hideMobileAddressbar();
     234
     235        $(data.dialog).trigger('closed', getValues());
     236    };
     237
     238    /****
     239     *****  PUBLIC FUNCTIONS
     240     ****/
     241
     242    // update the dialog's controls
     243    this.set = function (o) {
     244        var e, i, key, val, option;
     245        var keys = data.keys;
     246        var root = data.elements.root;
     247
     248        setBlocklistButtonEnabled(true);
     249
     250        for (i = 0; key = keys[i]; ++i) {
     251            val = o[key];
     252            e = root.find('#' + key);
     253
     254            if (key === 'blocklist-size') {
     255                // special case -- regular text area
     256                e.text('' + val.toStringWithCommas());
     257            } else switch (e[0].type) {
     258            case 'checkbox':
     259            case 'radio':
     260                e.prop('checked', val);
     261                setGroupEnabled(key, val);
     262                break;
     263            case 'text':
     264            case 'url':
     265            case 'email':
     266            case 'number':
     267            case 'search':
     268                // don't change the text if the user's editing it.
     269                // it's very annoying when that happens!
     270                if (e[0] !== document.activeElement) {
     271                    e.val(val);
     272                };
     273                break;
     274            case 'select-one':
     275                e.val(val);
     276                break;
     277            default:
     278                break;
     279            };
     280        };
     281    };
     282
     283    this.show = function () {
     284        transmission.hideMobileAddressbar();
     285
     286        setBlocklistButtonEnabled(true);
     287        data.remote.checkPort(onPortChecked, this);
     288        data.elements.root.dialog('open');
     289    };
     290
     291    this.close = function () {
     292        transmission.hideMobileAddressbar();
     293        data.elements.root.dialog('close');
     294    };
     295
     296    this.shouldAddedTorrentsStart = function () {
     297        return data.elements.root.find('#start-added-torrents')[0].checked;
     298    };
     299
     300    data.dialog = this;
     301    initialize(remote);
    318302};
  • trunk/web/javascript/remote.js

    r14523 r14716  
    77
    88var RPC = {
    9         _DaemonVersion          : 'version',
    10         _DownSpeedLimit         : 'speed-limit-down',
    11         _DownSpeedLimited       : 'speed-limit-down-enabled',
    12         _QueueMoveTop           : 'queue-move-top',
    13         _QueueMoveBottom        : 'queue-move-bottom',
    14         _QueueMoveUp            : 'queue-move-up',
    15         _QueueMoveDown          : 'queue-move-down',
    16         _Root                   : '../rpc',
    17         _TurtleDownSpeedLimit   : 'alt-speed-down',
    18         _TurtleState            : 'alt-speed-enabled',
    19         _TurtleUpSpeedLimit     : 'alt-speed-up',
    20         _UpSpeedLimit           : 'speed-limit-up',
    21         _UpSpeedLimited         : 'speed-limit-up-enabled'
     9    _DaemonVersion: 'version',
     10    _DownSpeedLimit: 'speed-limit-down',
     11    _DownSpeedLimited: 'speed-limit-down-enabled',
     12    _QueueMoveTop: 'queue-move-top',
     13    _QueueMoveBottom: 'queue-move-bottom',
     14    _QueueMoveUp: 'queue-move-up',
     15    _QueueMoveDown: 'queue-move-down',
     16    _Root: '../rpc',
     17    _TurtleDownSpeedLimit: 'alt-speed-down',
     18    _TurtleState: 'alt-speed-enabled',
     19    _TurtleUpSpeedLimit: 'alt-speed-up',
     20    _UpSpeedLimit: 'speed-limit-up',
     21    _UpSpeedLimited: 'speed-limit-up-enabled'
    2222};
    2323
    24 function TransmissionRemote(controller)
    25 {
    26         this.initialize(controller);
    27         return this;
     24function TransmissionRemote(controller) {
     25    this.initialize(controller);
     26    return this;
    2827}
    2928
    30 TransmissionRemote.prototype =
    31 {
    32         /*
    33          * Constructor
    34          */
    35         initialize: function(controller) {
    36                 this._controller = controller;
    37                 this._error = '';
    38                 this._token = '';
    39         },
    40 
    41         /*
    42          * Display an error if an ajax request fails, and stop sending requests
    43          * or on a 409, globally set the X-Transmission-Session-Id and resend
    44          */
    45         ajaxError: function(request, error_string, exception, ajaxObject) {
    46                 var token,
    47                    remote = this;
    48 
    49                 // set the Transmission-Session-Id on a 409
    50                 if (request.status === 409 && (token = request.getResponseHeader('X-Transmission-Session-Id'))){
    51                         remote._token = token;
    52                         $.ajax(ajaxObject);
    53                         return;
    54                 }
    55 
    56                 remote._error = request.responseText
    57                               ? request.responseText.trim().replace(/(<([^>]+)>)/ig,"")
    58                               : "";
    59                 if (!remote._error.length)
    60                         remote._error = 'Server not responding';
    61 
    62                 dialog.confirm('Connection Failed',
    63                         'Could not connect to the server. You may need to reload the page to reconnect.',
    64                         'Details',
    65                         function() {
    66                                 alert(remote._error);
    67                         },
    68                         'Dismiss');
    69                 remote._controller.togglePeriodicSessionRefresh(false);
    70         },
    71 
    72         appendSessionId: function(XHR) {
    73                 if (this._token) {
    74                         XHR.setRequestHeader('X-Transmission-Session-Id', this._token);
    75                 }
    76         },
    77 
    78         sendRequest: function(data, callback, context, async) {
    79                 var remote = this;
    80                 if (typeof async != 'boolean')
    81                         async = true;
    82 
    83                 var ajaxSettings = {
    84                         url: RPC._Root,
    85                         type: 'POST',
    86                         contentType: 'json',
    87                         dataType: 'json',
    88                         cache: false,
    89                         data: JSON.stringify(data),
    90                         beforeSend: function(XHR){ remote.appendSessionId(XHR); },
    91                         error: function(request, error_string, exception){ remote.ajaxError(request, error_string, exception, ajaxSettings); },
    92                         success: callback,
    93                         context: context,
    94                         async: async
    95                 };
    96 
    97                 $.ajax(ajaxSettings);
    98         },
    99 
    100         loadDaemonPrefs: function(callback, context, async) {
    101                 var o = { method: 'session-get' };
    102                 this.sendRequest(o, callback, context, async);
    103         },
    104 
    105         checkPort: function(callback, context, async) {
    106                 var o = { method: 'port-test' };
    107                 this.sendRequest(o, callback, context, async);
    108         },
    109 
    110         renameTorrent: function(torrentIds, oldpath, newname, callback, context) {
    111                 var o = {
    112                         method: 'torrent-rename-path',
    113                         arguments: {
    114                                 'ids': torrentIds,
    115                                 'path': oldpath,
    116                                 'name': newname
    117                         }
    118                 };
    119                 this.sendRequest(o, callback, context);
    120         },
    121 
    122         loadDaemonStats: function(callback, context, async) {
    123                 var o = { method: 'session-stats' };
    124                 this.sendRequest(o, callback, context, async);
    125         },
    126 
    127         updateTorrents: function(torrentIds, fields, callback, context) {
    128                 var o = {
    129                         method: 'torrent-get',
    130                         arguments: {
    131                                 'fields': fields
    132                         }
    133                 };
    134                 if (torrentIds)
    135                         o['arguments'].ids = torrentIds;
    136                 this.sendRequest(o, function(response) {
    137                         var args = response['arguments'];
    138                         callback.call(context,args.torrents,args.removed);
    139                 });
    140         },
    141 
    142         getFreeSpace: function(dir, callback, context) {
    143                 var remote = this;
    144                 var o = {
    145                         method: 'free-space',
    146                         arguments: { path: dir }
    147                 };
    148                 this.sendRequest(o, function(response) {
    149                         var args = response['arguments'];
    150                         callback.call (context, args.path, args['size-bytes']);
    151                 });
    152         },
    153 
    154         changeFileCommand: function(torrentId, fileIndices, command) {
    155                 var remote = this,
    156                     args = { ids: [torrentId] };
    157                 args[command] = fileIndices;
    158                 this.sendRequest({
    159                         arguments: args,
    160                         method: 'torrent-set'
    161                 }, function() {
    162                         remote._controller.refreshTorrents([torrentId]);
    163                 });
    164         },
    165 
    166         sendTorrentSetRequests: function(method, torrent_ids, args, callback, context) {
    167                 if (!args) args = { };
    168                 args['ids'] = torrent_ids;
    169                 var o = {
    170                         method: method,
    171                         arguments: args
    172                 };
    173                 this.sendRequest(o, callback, context);
    174         },
    175 
    176         sendTorrentActionRequests: function(method, torrent_ids, callback, context) {
    177                 this.sendTorrentSetRequests(method, torrent_ids, null, callback, context);
    178         },
    179 
    180         startTorrents: function(torrent_ids, noqueue, callback, context) {
    181                 var name = noqueue ? 'torrent-start-now' : 'torrent-start';
    182                 this.sendTorrentActionRequests(name, torrent_ids, callback, context);
    183         },
    184         stopTorrents: function(torrent_ids, callback, context) {
    185                 this.sendTorrentActionRequests('torrent-stop', torrent_ids, callback, context);
    186         },
    187 
    188         moveTorrents: function(torrent_ids, new_location, callback, context) {
    189                 var remote = this;
    190                 this.sendTorrentSetRequests( 'torrent-set-location', torrent_ids,
    191                         {"move": true, "location": new_location}, callback, context);
    192         },
    193 
    194         removeTorrents: function(torrent_ids, callback, context) {
    195                 this.sendTorrentActionRequests('torrent-remove', torrent_ids, callback, context);
    196         },
    197         removeTorrentsAndData: function(torrents) {
    198                 var remote = this;
    199                 var o = {
    200                         method: 'torrent-remove',
    201                         arguments: {
    202                                 'delete-local-data': true,
    203                                 ids: [ ]
    204                         }
    205                 };
    206 
    207                 if (torrents) {
    208                         for (var i=0, len=torrents.length; i<len; ++i) {
    209                                 o.arguments.ids.push(torrents[i].getId());
    210                         }
    211                 }
    212                 this.sendRequest(o, function() {
    213                         remote._controller.refreshTorrents();
    214                 });
    215         },
    216         verifyTorrents: function(torrent_ids, callback, context) {
    217                 this.sendTorrentActionRequests('torrent-verify', torrent_ids, callback, context);
    218         },
    219         reannounceTorrents: function(torrent_ids, callback, context) {
    220                 this.sendTorrentActionRequests('torrent-reannounce', torrent_ids, callback, context);
    221         },
    222         addTorrentByUrl: function(url, options) {
    223                 var remote = this;
    224                 if (url.match(/^[0-9a-f]{40}$/i)) {
    225                         url = 'magnet:?xt=urn:btih:'+url;
    226                 }
    227                 var o = {
    228                         method: 'torrent-add',
    229                         arguments: {
    230                                 paused: (options.paused),
    231                                 filename: url
    232                         }
    233                 };
    234                 this.sendRequest(o, function() {
    235                         remote._controller.refreshTorrents();
    236                 });
    237         },
    238         savePrefs: function(args) {
    239                 var remote = this;
    240                 var o = {
    241                         method: 'session-set',
    242                         arguments: args
    243                 };
    244                 this.sendRequest(o, function() {
    245                         remote._controller.loadDaemonPrefs();
    246                 });
    247         },
    248         updateBlocklist: function() {
    249                 var remote = this;
    250                 var o = {
    251                         method: 'blocklist-update'
    252                 };
    253                 this.sendRequest(o, function() {
    254                         remote._controller.loadDaemonPrefs();
    255                 });
    256         },
    257 
    258         // Added queue calls
    259         moveTorrentsToTop: function(torrent_ids, callback, context) {
    260                 this.sendTorrentActionRequests(RPC._QueueMoveTop, torrent_ids, callback, context);
    261         },
    262         moveTorrentsToBottom: function(torrent_ids, callback, context) {
    263                 this.sendTorrentActionRequests(RPC._QueueMoveBottom, torrent_ids, callback, context);
    264         },
    265         moveTorrentsUp: function(torrent_ids, callback, context) {
    266                 this.sendTorrentActionRequests(RPC._QueueMoveUp, torrent_ids, callback, context);
    267         },
    268         moveTorrentsDown: function(torrent_ids, callback, context) {
    269                 this.sendTorrentActionRequests(RPC._QueueMoveDown, torrent_ids, callback, context);
    270         }
     29TransmissionRemote.prototype = {
     30    /*
     31     * Constructor
     32     */
     33    initialize: function (controller) {
     34        this._controller = controller;
     35        this._error = '';
     36        this._token = '';
     37    },
     38
     39    /*
     40     * Display an error if an ajax request fails, and stop sending requests
     41     * or on a 409, globally set the X-Transmission-Session-Id and resend
     42     */
     43    ajaxError: function (request, error_string, exception, ajaxObject) {
     44        var token;
     45        var remote = this;
     46
     47        // set the Transmission-Session-Id on a 409
     48        if (request.status === 409 && (token = request.getResponseHeader('X-Transmission-Session-Id'))) {
     49            remote._token = token;
     50            $.ajax(ajaxObject);
     51            return;
     52        };
     53
     54        remote._error = request.responseText ? request.responseText.trim().replace(/(<([^>]+)>)/ig, "") : "";
     55        if (!remote._error.length) {
     56            remote._error = 'Server not responding';
     57        };
     58
     59        dialog.confirm('Connection Failed',
     60            'Could not connect to the server. You may need to reload the page to reconnect.',
     61            'Details',
     62            function () {
     63                alert(remote._error);
     64            },
     65            'Dismiss');
     66        remote._controller.togglePeriodicSessionRefresh(false);
     67    },
     68
     69    appendSessionId: function (XHR) {
     70        if (this._token) {
     71            XHR.setRequestHeader('X-Transmission-Session-Id', this._token);
     72        };
     73    },
     74
     75    sendRequest: function (data, callback, context, async) {
     76        var remote = this;
     77        if (typeof async != 'boolean') {
     78            async = true;
     79        };
     80
     81        var ajaxSettings = {
     82            url: RPC._Root,
     83            type: 'POST',
     84            contentType: 'json',
     85            dataType: 'json',
     86            cache: false,
     87            data: JSON.stringify(data),
     88            beforeSend: function (XHR) {
     89                remote.appendSessionId(XHR);
     90            },
     91            error: function (request, error_string, exception) {
     92                remote.ajaxError(request, error_string, exception, ajaxSettings);
     93            },
     94            success: callback,
     95            context: context,
     96            async: async
     97        };
     98
     99        $.ajax(ajaxSettings);
     100    },
     101
     102    loadDaemonPrefs: function (callback, context, async) {
     103        var o = {
     104            method: 'session-get'
     105        };
     106        this.sendRequest(o, callback, context, async);
     107    },
     108
     109    checkPort: function (callback, context, async) {
     110        var o = {
     111            method: 'port-test'
     112        };
     113        this.sendRequest(o, callback, context, async);
     114    },
     115
     116    renameTorrent: function (torrentIds, oldpath, newname, callback, context) {
     117        var o = {
     118            method: 'torrent-rename-path',
     119            arguments: {
     120                'ids': torrentIds,
     121                'path': oldpath,
     122                'name': newname
     123            }
     124        };
     125        this.sendRequest(o, callback, context);
     126    },
     127
     128    loadDaemonStats: function (callback, context, async) {
     129        var o = {
     130            method: 'session-stats'
     131        };
     132        this.sendRequest(o, callback, context, async);
     133    },
     134
     135    updateTorrents: function (torrentIds, fields, callback, context) {
     136        var o = {
     137            method: 'torrent-get',
     138            arguments: {
     139                'fields': fields
     140            }
     141        };
     142        if (torrentIds) {
     143            o['arguments'].ids = torrentIds;
     144        };
     145        this.sendRequest(o, function (response) {
     146            var args = response['arguments'];
     147            callback.call(context, args.torrents, args.removed);
     148        });
     149    },
     150
     151    getFreeSpace: function (dir, callback, context) {
     152        var remote = this;
     153        var o = {
     154            method: 'free-space',
     155            arguments: {
     156                path: dir
     157            }
     158        };
     159        this.sendRequest(o, function (response) {
     160            var args = response['arguments'];
     161            callback.call(context, args.path, args['size-bytes']);
     162        });
     163    },
     164
     165    changeFileCommand: function (torrentId, fileIndices, command) {
     166        var remote = this,
     167            args = {
     168                ids: [torrentId]
     169            };
     170        args[command] = fileIndices;
     171        this.sendRequest({
     172            arguments: args,
     173            method: 'torrent-set'
     174        }, function () {
     175            remote._controller.refreshTorrents([torrentId]);
     176        });
     177    },
     178
     179    sendTorrentSetRequests: function (method, torrent_ids, args, callback, context) {
     180        if (!args) {
     181            args = {};
     182        };
     183        args['ids'] = torrent_ids;
     184        var o = {
     185            method: method,
     186            arguments: args
     187        };
     188        this.sendRequest(o, callback, context);
     189    },
     190
     191    sendTorrentActionRequests: function (method, torrent_ids, callback, context) {
     192        this.sendTorrentSetRequests(method, torrent_ids, null, callback, context);
     193    },
     194
     195    startTorrents: function (torrent_ids, noqueue, callback, context) {
     196        var name = noqueue ? 'torrent-start-now' : 'torrent-start';
     197        this.sendTorrentActionRequests(name, torrent_ids, callback, context);
     198    },
     199    stopTorrents: function (torrent_ids, callback, context) {
     200        this.sendTorrentActionRequests('torrent-stop', torrent_ids, callback, context);
     201    },
     202
     203    moveTorrents: function (torrent_ids, new_location, callback, context) {
     204        var remote = this;
     205        this.sendTorrentSetRequests('torrent-set-location', torrent_ids, {
     206            "move": true,
     207            "location": new_location
     208        }, callback, context);
     209    },
     210
     211    removeTorrents: function (torrent_ids, callback, context) {
     212        this.sendTorrentActionRequests('torrent-remove', torrent_ids, callback, context);
     213    },
     214    removeTorrentsAndData: function (torrents) {
     215        var remote = this;
     216        var o = {
     217            method: 'torrent-remove',
     218            arguments: {
     219                'delete-local-data': true,
     220                ids: []
     221            }
     222        };
     223
     224        if (torrents) {
     225            for (var i = 0, len = torrents.length; i < len; ++i) {
     226                o.arguments.ids.push(torrents[i].getId());
     227            };
     228        };
     229        this.sendRequest(o, function () {
     230            remote._controller.refreshTorrents();
     231        });
     232    },
     233    verifyTorrents: function (torrent_ids, callback, context) {
     234        this.sendTorrentActionRequests('torrent-verify', torrent_ids, callback, context);
     235    },
     236    reannounceTorrents: function (torrent_ids, callback, context) {
     237        this.sendTorrentActionRequests('torrent-reannounce', torrent_ids, callback, context);
     238    },
     239    addTorrentByUrl: function (url, options) {
     240        var remote = this;
     241        if (url.match(/^[0-9a-f]{40}$/i)) {
     242            url = 'magnet:?xt=urn:btih:' + url;
     243        }
     244        var o = {
     245            method: 'torrent-add',
     246            arguments: {
     247                paused: (options.paused),
     248                filename: url
     249            }
     250        };
     251        this.sendRequest(o, function () {
     252            remote._controller.refreshTorrents();
     253        });
     254    },
     255    savePrefs: function (args) {
     256        var remote = this;
     257        var o = {
     258            method: 'session-set',
     259            arguments: args
     260        };
     261        this.sendRequest(o, function () {
     262            remote._controller.loadDaemonPrefs();
     263        });
     264    },
     265    updateBlocklist: function () {
     266        var remote = this;
     267        var o = {
     268            method: 'blocklist-update'
     269        };
     270        this.sendRequest(o, function () {
     271            remote._controller.loadDaemonPrefs();
     272        });
     273    },
     274
     275    // Added queue calls
     276    moveTorrentsToTop: function (torrent_ids, callback, context) {
     277        this.sendTorrentActionRequests(RPC._QueueMoveTop, torrent_ids, callback, context);
     278    },
     279    moveTorrentsToBottom: function (torrent_ids, callback, context) {
     280        this.sendTorrentActionRequests(RPC._QueueMoveBottom, torrent_ids, callback, context);
     281    },
     282    moveTorrentsUp: function (torrent_ids, callback, context) {
     283        this.sendTorrentActionRequests(RPC._QueueMoveUp, torrent_ids, callback, context);
     284    },
     285    moveTorrentsDown: function (torrent_ids, callback, context) {
     286        this.sendTorrentActionRequests(RPC._QueueMoveDown, torrent_ids, callback, context);
     287    }
    271288};
  • trunk/web/javascript/torrent-row.js

    r14523 r14716  
    66 */
    77
    8 function TorrentRendererHelper()
    9 {
    10 }
    11 
    12 TorrentRendererHelper.getProgressInfo = function(controller, t)
    13 {
    14         var pct, extra,
    15             s = t.getStatus(),
    16             seed_ratio_limit = t.seedRatioLimit(controller);
    17 
    18         if (t.needsMetaData())
    19                 pct = t.getMetadataPercentComplete() * 100;
    20         else if (!t.isDone())
    21                 pct = Math.round(t.getPercentDone() * 100);
    22         else if (seed_ratio_limit > 0 && t.isSeeding()) // don't split up the bar if paused or queued
    23                 pct = Math.round(t.getUploadRatio() * 100 / seed_ratio_limit);
    24         else
    25                 pct = 100;
    26 
    27         if (s === Torrent._StatusStopped)
    28                 extra = 'paused';
    29         else if (s === Torrent._StatusDownloadWait)
    30                 extra = 'leeching queued';
    31         else if (t.needsMetaData())
    32                 extra = 'magnet';
    33         else if (s === Torrent._StatusDownload)
    34                 extra = 'leeching';
    35         else if (s === Torrent._StatusSeedWait)
    36                 extra = 'seeding queued';
    37         else if (s === Torrent._StatusSeed)
    38                 extra = 'seeding';
    39         else
    40                 extra = '';
    41 
    42         return {
    43                 percent: pct,
    44                 complete: [ 'torrent_progress_bar', 'complete', extra ].join(' '),
    45                 incomplete: [ 'torrent_progress_bar', 'incomplete', extra ].join(' ')
    46         };
    47 };
    48 
    49 TorrentRendererHelper.createProgressbar = function(classes)
    50 {
    51         var complete, incomplete, progressbar;
    52 
    53         complete = document.createElement('div');
    54         complete.className = 'torrent_progress_bar complete';
    55 
    56         incomplete = document.createElement('div');
    57         incomplete.className = 'torrent_progress_bar incomplete';
    58 
    59         progressbar = document.createElement('div');
    60         progressbar.className = 'torrent_progress_bar_container ' + classes;
    61         progressbar.appendChild(complete);
    62         progressbar.appendChild(incomplete);
    63 
    64         return { 'element': progressbar, 'complete': complete, 'incomplete': incomplete };
    65 };
    66 
    67 TorrentRendererHelper.renderProgressbar = function(controller, t, progressbar)
    68 {
    69         var e, style, width, display,
    70             info = TorrentRendererHelper.getProgressInfo(controller, t);
    71 
    72         // update the complete progressbar
    73         e = progressbar.complete;
    74         style = e.style;
    75         width = '' + info.percent + '%';
    76         display = info.percent > 0 ? 'block' : 'none';
    77         if (style.width!==width || style.display!==display)
    78                 $(e).css({ width: ''+info.percent+'%', display: display });
    79         if (e.className !== info.complete)
    80                 e.className = info.complete;
    81 
    82         // update the incomplete progressbar
    83         e = progressbar.incomplete;
    84         display = (info.percent < 100) ? 'block' : 'none';
    85         if (e.style.display !== display)
    86                 e.style.display = display;
    87         if (e.className !== info.incomplete)
    88                 e.className = info.incomplete;
    89 };
    90 
    91 TorrentRendererHelper.formatUL = function(t)
    92 {
    93         return '↑ ' + Transmission.fmt.speedBps(t.getUploadSpeed());
    94 };
    95 
    96 TorrentRendererHelper.formatDL = function(t)
    97 {
    98         return '↓ ' + Transmission.fmt.speedBps(t.getDownloadSpeed());
     8function TorrentRendererHelper() {}
     9
     10TorrentRendererHelper.getProgressInfo = function (controller, t) {
     11    var pct, extra;
     12    var s = t.getStatus();
     13    var seed_ratio_limit = t.seedRatioLimit(controller);
     14
     15    if (t.needsMetaData()) {
     16        pct = t.getMetadataPercentComplete() * 100;
     17    } else if (!t.isDone()) {
     18        pct = Math.round(t.getPercentDone() * 100);
     19    } else if (seed_ratio_limit > 0 && t.isSeeding()) { // don't split up the bar if paused or queued
     20        pct = Math.round(t.getUploadRatio() * 100 / seed_ratio_limit);
     21    } else {
     22        pct = 100;
     23    };
     24
     25    if (s === Torrent._StatusStopped) {
     26        extra = 'paused';
     27    } else if (s === Torrent._StatusDownloadWait) {
     28        extra = 'leeching queued';
     29    } else if (t.needsMetaData()) {
     30        extra = 'magnet';
     31    } else if (s === Torrent._StatusDownload) {
     32        extra = 'leeching';
     33    } else if (s === Torrent._StatusSeedWait) {
     34        extra = 'seeding queued';
     35    } else if (s === Torrent._StatusSeed) {
     36        extra = 'seeding';
     37    } else {
     38        extra = '';
     39    };
     40
     41    return {
     42        percent: pct,
     43        complete: ['torrent_progress_bar', 'complete', extra].join(' '),
     44        incomplete: ['torrent_progress_bar', 'incomplete', extra].join(' ')
     45    };
     46};
     47
     48TorrentRendererHelper.createProgressbar = function (classes) {
     49    var complete, incomplete, progressbar;
     50
     51    complete = document.createElement('div');
     52    complete.className = 'torrent_progress_bar complete';
     53
     54    incomplete = document.createElement('div');
     55    incomplete.className = 'torrent_progress_bar incomplete';
     56
     57    progressbar = document.createElement('div');
     58    progressbar.className = 'torrent_progress_bar_container ' + classes;
     59    progressbar.appendChild(complete);
     60    progressbar.appendChild(incomplete);
     61
     62    return {
     63        'element': progressbar,
     64        'complete': complete,
     65        'incomplete': incomplete
     66    };
     67};
     68
     69TorrentRendererHelper.renderProgressbar = function (controller, t, progressbar) {
     70    var e, style, width, display
     71    var info = TorrentRendererHelper.getProgressInfo(controller, t);
     72
     73    // update the complete progressbar
     74    e = progressbar.complete;
     75    style = e.style;
     76    width = '' + info.percent + '%';
     77    display = info.percent > 0 ? 'block' : 'none';
     78    if (style.width !== width || style.display !== display) {
     79        $(e).css({
     80            width: '' + info.percent + '%',
     81            display: display
     82        });
     83    };
     84
     85    if (e.className !== info.complete) {
     86        e.className = info.complete;
     87    };
     88
     89    // update the incomplete progressbar
     90    e = progressbar.incomplete;
     91    display = (info.percent < 100) ? 'block' : 'none';
     92
     93    if (e.style.display !== display) {
     94        e.style.display = display;
     95    };
     96
     97    if (e.className !== info.incomplete) {
     98        e.className = info.incomplete;
     99    };
     100};
     101
     102TorrentRendererHelper.formatUL = function (t) {
     103    return '↑ ' + Transmission.fmt.speedBps(t.getUploadSpeed());
     104};
     105
     106TorrentRendererHelper.formatDL = function (t) {
     107    return '↓ ' + Transmission.fmt.speedBps(t.getDownloadSpeed());
    99108};
    100109
    101110/****
    102 *****
    103 *****
    104 ****/
    105 
    106 function TorrentRendererFull()
    107 {
    108 }
    109 TorrentRendererFull.prototype =
    110 {
    111         createRow: function()
    112         {
    113                 var root, name, peers, progressbar, details, image, button;
    114 
    115                 root = document.createElement('li');
    116                 root.className = 'torrent';
    117 
    118                 name = document.createElement('div');
    119                 name.className = 'torrent_name';
    120 
    121                 peers = document.createElement('div');
    122                 peers.className = 'torrent_peer_details';
    123 
    124                 progressbar = TorrentRendererHelper.createProgressbar('full');
    125 
    126                 details = document.createElement('div');
    127                 details.className = 'torrent_progress_details';
    128 
    129                 image = document.createElement('div');
    130                 button = document.createElement('a');
    131                 button.appendChild(image);
    132 
    133                 root.appendChild(name);
    134                 root.appendChild(peers);
    135                 root.appendChild(button);
    136                 root.appendChild(progressbar.element);
    137                 root.appendChild(details);
    138 
    139                 root._name_container = name;
    140                 root._peer_details_container = peers;
    141                 root._progress_details_container = details;
    142                 root._progressbar = progressbar;
    143                 root._pause_resume_button_image = image;
    144                 root._toggle_running_button = button;
    145 
    146                 return root;
    147         },
    148 
    149         getPeerDetails: function(t)
    150         {
    151                 var err,
    152                     peer_count,
    153                     webseed_count,
    154                     fmt = Transmission.fmt;
    155 
    156                 if ((err = t.getErrorMessage()))
    157                         return err;
    158 
    159                 if (t.isDownloading())
    160                 {
    161                         peer_count = t.getPeersConnected();
    162                         webseed_count = t.getWebseedsSendingToUs();
    163 
    164                         if (webseed_count && peer_count)
    165                         {
    166                                 // Downloading from 2 of 3 peer(s) and 2 webseed(s)
    167                                 return [ 'Downloading from',
    168                                          t.getPeersSendingToUs(),
    169                                          'of',
    170                                          fmt.countString('peer','peers',peer_count),
    171                                          'and',
    172                                          fmt.countString('web seed','web seeds',webseed_count),
    173                                          '-',
    174                                          TorrentRendererHelper.formatDL(t),
    175                                          TorrentRendererHelper.formatUL(t) ].join(' ');
    176                         }
    177                         else if (webseed_count)
    178                         {
    179                                 // Downloading from 2 webseed(s)
    180                                 return [ 'Downloading from',
    181                                          fmt.countString('web seed','web seeds',webseed_count),
    182                                          '-',
    183                                          TorrentRendererHelper.formatDL(t),
    184                                          TorrentRendererHelper.formatUL(t) ].join(' ');
    185                         }
    186                         else
    187                         {
    188                                 // Downloading from 2 of 3 peer(s)
    189                                 return [ 'Downloading from',
    190                                          t.getPeersSendingToUs(),
    191                                          'of',
    192                                          fmt.countString('peer','peers',peer_count),
    193                                          '-',
    194                                          TorrentRendererHelper.formatDL(t),
    195                                          TorrentRendererHelper.formatUL(t) ].join(' ');
    196                         }
    197                 }
    198 
    199                 if (t.isSeeding())
    200                         return [ 'Seeding to',
    201                                  t.getPeersGettingFromUs(),
    202                                  'of',
    203                                  fmt.countString ('peer','peers',t.getPeersConnected()),
    204                                  '-',
    205                                  TorrentRendererHelper.formatUL(t) ].join(' ');
    206 
    207                 if (t.isChecking())
    208                         return [ 'Verifying local data (',
    209                                  Transmission.fmt.percentString(100.0 * t.getRecheckProgress()),
    210                                  '% tested)' ].join('');
    211 
    212                 return t.getStateString();
    213         },
    214 
    215         getProgressDetails: function(controller, t)
    216         {
    217                 if (t.needsMetaData()) {
    218                         var MetaDataStatus = "retrieving";
    219                         if (t.isStopped())
    220                                 MetaDataStatus = "needs";
    221                         var percent = 100 * t.getMetadataPercentComplete();
    222                         return [ "Magnetized transfer - " + MetaDataStatus + " metadata (",
    223                                  Transmission.fmt.percentString(percent),
    224                                  "%)" ].join('');
    225                 }
    226 
    227                 var c,
    228                     sizeWhenDone = t.getSizeWhenDone(),
    229                     totalSize = t.getTotalSize(),
    230                     is_done = t.isDone() || t.isSeeding();
    231 
    232                 if (is_done) {
    233                         if (totalSize === sizeWhenDone) // seed: '698.05 MiB'
    234                                 c = [ Transmission.fmt.size(totalSize) ];
    235                         else // partial seed: '127.21 MiB of 698.05 MiB (18.2%)'
    236                                 c = [ Transmission.fmt.size(sizeWhenDone),
    237                                       ' of ',
    238                                       Transmission.fmt.size(t.getTotalSize()),
    239                                       ' (', t.getPercentDoneStr(), '%)' ];
    240                         // append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)'
    241                         c.push(', uploaded ',
    242                                Transmission.fmt.size(t.getUploadedEver()),
    243                                ' (Ratio ',
    244                                Transmission.fmt.ratioString(t.getUploadRatio()),
    245                                ')');
    246                 } else { // not done yet
    247                         c = [ Transmission.fmt.size(sizeWhenDone - t.getLeftUntilDone()),
    248                               ' of ', Transmission.fmt.size(sizeWhenDone),
    249                               ' (', t.getPercentDoneStr(), '%)' ];
    250                 }
    251 
    252                 // maybe append eta
    253                 if (!t.isStopped() && (!is_done || t.seedRatioLimit(controller)>0)) {
    254                         c.push(' - ');
    255                         var eta = t.getETA();
    256                         if (eta < 0 || eta >= (999*60*60) /* arbitrary */)
    257                                 c.push('remaining time unknown');
    258                         else
    259                                 c.push(Transmission.fmt.timeInterval(t.getETA()),
    260                                        ' remaining');
    261                 }
    262 
    263                 return c.join('');
    264         },
    265 
    266         render: function(controller, t, root)
    267         {
    268                 // name
    269                 setTextContent(root._name_container, t.getName());
    270 
    271                 // progressbar
    272                 TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
    273 
    274                 // peer details
    275                 var has_error = t.getError() !== Torrent._ErrNone;
    276                 var e = root._peer_details_container;
    277                 $(e).toggleClass('error',has_error);
    278                 setTextContent(e, this.getPeerDetails(t));
    279 
    280                 // progress details
    281                 e = root._progress_details_container;
    282                 setTextContent(e, this.getProgressDetails(controller, t));
    283 
    284                 // pause/resume button
    285                 var is_stopped = t.isStopped();
    286                 e = root._pause_resume_button_image;
    287                 e.alt = is_stopped ? 'Resume' : 'Pause';
    288                 e.className = is_stopped ? 'torrent_resume' : 'torrent_pause';
    289         }
     111 *****
     112 *****
     113 ****/
     114
     115function TorrentRendererFull() {};
     116TorrentRendererFull.prototype = {
     117    createRow: function () {
     118        var root, name, peers, progressbar, details, image, button;
     119
     120        root = document.createElement('li');
     121        root.className = 'torrent';
     122
     123        name = document.createElement('div');
     124        name.className = 'torrent_name';
     125
     126        peers = document.createElement('div');
     127        peers.className = 'torrent_peer_details';
     128
     129        progressbar = TorrentRendererHelper.createProgressbar('full');
     130
     131        details = document.createElement('div');
     132        details.className = 'torrent_progress_details';
     133
     134        image = document.createElement('div');
     135        button = document.createElement('a');
     136        button.appendChild(image);
     137
     138        root.appendChild(name);
     139        root.appendChild(peers);
     140        root.appendChild(button);
     141        root.appendChild(progressbar.element);
     142        root.appendChild(details);
     143
     144        root._name_container = name;
     145        root._peer_details_container = peers;
     146        root._progress_details_container = details;
     147        root._progressbar = progressbar;
     148        root._pause_resume_button_image = image;
     149        root._toggle_running_button = button;
     150
     151        return root;
     152    },
     153
     154    getPeerDetails: function (t) {
     155        var err,
     156            peer_count,
     157            webseed_count,
     158            fmt = Transmission.fmt;
     159
     160        if ((err = t.getErrorMessage())) {
     161            return err;
     162        };
     163
     164        if (t.isDownloading()) {
     165            peer_count = t.getPeersConnected();
     166            webseed_count = t.getWebseedsSendingToUs();
     167
     168            if (webseed_count && peer_count) {
     169                // Downloading from 2 of 3 peer(s) and 2 webseed(s)
     170                return ['Downloading from',
     171                    t.getPeersSendingToUs(),
     172                    'of',
     173                    fmt.countString('peer', 'peers', peer_count),
     174                    'and',
     175                    fmt.countString('web seed', 'web seeds', webseed_count),
     176                    '-',
     177                    TorrentRendererHelper.formatDL(t),
     178                    TorrentRendererHelper.formatUL(t)
     179                ].join(' ');
     180            } else if (webseed_count) {
     181                // Downloading from 2 webseed(s)
     182                return ['Downloading from',
     183                    fmt.countString('web seed', 'web seeds', webseed_count),
     184                    '-',
     185                    TorrentRendererHelper.formatDL(t),
     186                    TorrentRendererHelper.formatUL(t)
     187                ].join(' ');
     188            } else {
     189                // Downloading from 2 of 3 peer(s)
     190                return ['Downloading from',
     191                    t.getPeersSendingToUs(),
     192                    'of',
     193                    fmt.countString('peer', 'peers', peer_count),
     194                    '-',
     195                    TorrentRendererHelper.formatDL(t),
     196                    TorrentRendererHelper.formatUL(t)
     197                ].join(' ');
     198            };
     199        };
     200
     201        if (t.isSeeding()) {
     202            return ['Seeding to', t.getPeersGettingFromUs(), 'of', fmt.countString('peer', 'peers', t.getPeersConnected()), '-', TorrentRendererHelper.formatUL(t)].join(' ');
     203        };
     204
     205        if (t.isChecking()) {
     206            return ['Verifying local data (', Transmission.fmt.percentString(100.0 * t.getRecheckProgress()), '% tested)'].join('');
     207        }
     208
     209        return t.getStateString();
     210    },
     211
     212    getProgressDetails: function (controller, t) {
     213        if (t.needsMetaData()) {
     214            var MetaDataStatus = "retrieving";
     215            if (t.isStopped()) {
     216                MetaDataStatus = "needs";
     217            };
     218            var percent = 100 * t.getMetadataPercentComplete();
     219            return ["Magnetized transfer - " + MetaDataStatus + " metadata (",
     220                Transmission.fmt.percentString(percent),
     221                "%)"
     222            ].join('');
     223        }
     224
     225        var c;
     226        var sizeWhenDone = t.getSizeWhenDone();
     227        var totalSize = t.getTotalSize();
     228        var is_done = t.isDone() || t.isSeeding();
     229
     230        if (is_done) {
     231            if (totalSize === sizeWhenDone) {
     232                // seed: '698.05 MiB'
     233                c = [Transmission.fmt.size(totalSize)];
     234            } else { // partial seed: '127.21 MiB of 698.05 MiB (18.2%)'
     235                c = [Transmission.fmt.size(sizeWhenDone), ' of ', Transmission.fmt.size(t.getTotalSize()), ' (', t.getPercentDoneStr(), '%)'];
     236            };
     237            // append UL stats: ', uploaded 8.59 GiB (Ratio: 12.3)'
     238            c.push(', uploaded ',
     239                Transmission.fmt.size(t.getUploadedEver()),
     240                ' (Ratio ',
     241                Transmission.fmt.ratioString(t.getUploadRatio()),
     242                ')');
     243        } else { // not done yet
     244            c = [Transmission.fmt.size(sizeWhenDone - t.getLeftUntilDone()),
     245                ' of ', Transmission.fmt.size(sizeWhenDone),
     246                ' (', t.getPercentDoneStr(), '%)'
     247            ];
     248        };
     249
     250        // maybe append eta
     251        if (!t.isStopped() && (!is_done || t.seedRatioLimit(controller) > 0)) {
     252            c.push(' - ');
     253            var eta = t.getETA();
     254            if (eta < 0 || eta >= (999 * 60 * 60) /* arbitrary */ ) {
     255                c.push('remaining time unknown');
     256            } else {
     257                c.push(Transmission.fmt.timeInterval(t.getETA()), ' remaining');
     258            };
     259        };
     260
     261        return c.join('');
     262    },
     263
     264    render: function (controller, t, root) {
     265        // name
     266        setTextContent(root._name_container, t.getName());
     267
     268        // progressbar
     269        TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
     270
     271        // peer details
     272        var has_error = t.getError() !== Torrent._ErrNone;
     273        var e = root._peer_details_container;
     274        $(e).toggleClass('error', has_error);
     275        setTextContent(e, this.getPeerDetails(t));
     276
     277        // progress details
     278        e = root._progress_details_container;
     279        setTextContent(e, this.getProgressDetails(controller, t));
     280
     281        // pause/resume button
     282        var is_stopped = t.isStopped();
     283        e = root._pause_resume_button_image;
     284        e.alt = is_stopped ? 'Resume' : 'Pause';
     285        e.className = is_stopped ? 'torrent_resume' : 'torrent_pause';
     286    }
    290287};
    291288
    292289/****
    293 *****
    294 *****
    295 ****/
    296 
    297 function TorrentRendererCompact()
    298 {
    299 }
    300 TorrentRendererCompact.prototype =
    301 {
    302         createRow: function()
    303         {
    304                 var progressbar, details, name, root;
    305 
    306                 progressbar = TorrentRendererHelper.createProgressbar('compact');
    307 
    308                 details = document.createElement('div');
    309                 details.className = 'torrent_peer_details compact';
    310 
    311                 name = document.createElement('div');
    312                 name.className = 'torrent_name compact';
    313 
    314                 root = document.createElement('li');
    315                 root.appendChild(progressbar.element);
    316                 root.appendChild(details);
    317                 root.appendChild(name);
    318                 root.className = 'torrent compact';
    319                 root._progressbar = progressbar;
    320                 root._details_container = details;
    321                 root._name_container = name;
    322                 return root;
    323         },
    324 
    325         getPeerDetails: function(t)
    326         {
    327                 var c;
    328                 if ((c = t.getErrorMessage()))
    329                         return c;
    330                 if (t.isDownloading()) {
    331                         var have_dn = t.getDownloadSpeed() > 0,
    332                             have_up = t.getUploadSpeed() > 0;
    333                         if (!have_up && !have_dn)
    334                                 return 'Idle';
    335                         var s = '';
    336                         if (have_dn)
    337                                 s += TorrentRendererHelper.formatDL(t);
    338                         if (have_dn && have_up)
    339                                 s += ' '
    340                         if (have_up)
    341                                 s += TorrentRendererHelper.formatUL(t);
    342                         return s;
    343                 }
    344                 if (t.isSeeding())
    345                         return [ 'Ratio: ',
    346                                  Transmission.fmt.ratioString(t.getUploadRatio()),
    347                                  ', ',
    348                                  TorrentRendererHelper.formatUL(t) ].join('');
    349                 return t.getStateString();
    350         },
    351 
    352         render: function(controller, t, root)
    353         {
    354                 // name
    355                 var is_stopped = t.isStopped();
    356                 var e = root._name_container;
    357                 $(e).toggleClass('paused', is_stopped);
    358                 setTextContent(e, t.getName());
    359 
    360                 // peer details
    361                 var has_error = t.getError() !== Torrent._ErrNone;
    362                 e = root._details_container;
    363                 $(e).toggleClass('error', has_error);
    364                 setTextContent(e, this.getPeerDetails(t));
    365 
    366                 // progressbar
    367                 TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
    368         }
     290 *****
     291 *****
     292 ****/
     293
     294function TorrentRendererCompact() {};
     295TorrentRendererCompact.prototype = {
     296    createRow: function () {
     297        var progressbar, details, name, root;
     298
     299        progressbar = TorrentRendererHelper.createProgressbar('compact');
     300
     301        details = document.createElement('div');
     302        details.className = 'torrent_peer_details compact';
     303
     304        name = document.createElement('div');
     305        name.className = 'torrent_name compact';
     306
     307        root = document.createElement('li');
     308        root.appendChild(progressbar.element);
     309        root.appendChild(details);
     310        root.appendChild(name);
     311        root.className = 'torrent compact';
     312        root._progressbar = progressbar;
     313        root._details_container = details;
     314        root._name_container = name;
     315        return root;
     316    },
     317
     318    getPeerDetails: function (t) {
     319        var c;
     320        if ((c = t.getErrorMessage())) {
     321            return c;
     322        };
     323        if (t.isDownloading()) {
     324            var have_dn = t.getDownloadSpeed() > 0;
     325            var have_up = t.getUploadSpeed() > 0;
     326
     327            if (!have_up && !have_dn) {
     328                return 'Idle';
     329            };
     330            var s = '';
     331            if (have_dn) {
     332                s += TorrentRendererHelper.formatDL(t);
     333            };
     334            if (have_dn && have_up) {
     335                s += ' ';
     336            };
     337            if (have_up) {
     338                s += TorrentRendererHelper.formatUL(t);
     339            };
     340            return s;
     341        };
     342        if (t.isSeeding()) {
     343            return ['Ratio: ', Transmission.fmt.ratioString(t.getUploadRatio()), ', ', TorrentRendererHelper.formatUL(t)].join('');
     344        };
     345        return t.getStateString();
     346    },
     347
     348    render: function (controller, t, root) {
     349        // name
     350        var is_stopped = t.isStopped();
     351        var e = root._name_container;
     352        $(e).toggleClass('paused', is_stopped);
     353        setTextContent(e, t.getName());
     354
     355        // peer details
     356        var has_error = t.getError() !== Torrent._ErrNone;
     357        e = root._details_container;
     358        $(e).toggleClass('error', has_error);
     359        setTextContent(e, this.getPeerDetails(t));
     360
     361        // progressbar
     362        TorrentRendererHelper.renderProgressbar(controller, t, root._progressbar);
     363    }
    369364};
    370365
    371366/****
    372 *****
    373 *****
    374 ****/
    375 
    376 function TorrentRow(view, controller, torrent)
    377 {
    378         this.initialize(view, controller, torrent);
    379 }
    380 TorrentRow.prototype =
    381 {
    382         initialize: function(view, controller, torrent) {
    383                 var row = this;
    384                 this._view = view;
    385                 this._torrent = torrent;
    386                 this._element = view.createRow();
    387                 this.render(controller);
    388                 $(this._torrent).bind('dataChanged.torrentRowListener',function(){row.render(controller);});
    389 
    390         },
    391         getElement: function() {
    392                 return this._element;
    393         },
    394         render: function(controller) {
    395                 var tor = this.getTorrent();
    396                 if (tor)
    397                         this._view.render(controller, tor, this.getElement());
    398         },
    399         isSelected: function() {
    400                 return this.getElement().className.indexOf('selected') !== -1;
    401         },
    402 
    403         getTorrent: function() {
    404                 return this._torrent;
    405         },
    406         getTorrentId: function() {
    407                 return this.getTorrent().getId();
    408         }
    409 };
     367 *****
     368 *****
     369 ****/
     370
     371function TorrentRow(view, controller, torrent) {
     372    this.initialize(view, controller, torrent);
     373};
     374TorrentRow.prototype = {
     375    initialize: function (view, controller, torrent) {
     376        var row = this;
     377        this._view = view;
     378        this._torrent = torrent;
     379        this._element = view.createRow();
     380        this.render(controller);
     381        $(this._torrent).bind('dataChanged.torrentRowListener', function () {
     382            row.render(controller);
     383        });
     384
     385    },
     386    getElement: function () {
     387        return this._element;
     388    },
     389    render: function (controller) {
     390        var tor = this.getTorrent();
     391        if (tor) {
     392            this._view.render(controller, tor, this.getElement());
     393        };
     394    },
     395    isSelected: function () {
     396        return this.getElement().className.indexOf('selected') !== -1;
     397    },
     398
     399    getTorrent: function () {
     400        return this._torrent;
     401    },
     402    getTorrentId: function () {
     403        return this.getTorrent().getId();
     404    }
     405};
  • trunk/web/javascript/torrent.js

    r14656 r14716  
    66 */
    77
    8 function Torrent(data)
    9 {
    10         this.initialize(data);
    11 }
     8function Torrent(data) {
     9    this.initialize(data);
     10};
    1211
    1312/***
    14 ****
    15 ****  Constants
    16 ****
    17 ***/
     13 ****
     14 ****  Constants
     15 ****
     16 ***/
    1817
    1918// Torrent.fields.status
    20 Torrent._StatusStopped         = 0;
    21 Torrent._StatusCheckWait       = 1;
    22 Torrent._StatusCheck           = 2;
    23 Torrent._StatusDownloadWait    = 3;
    24 Torrent._StatusDownload        = 4;
    25 Torrent._StatusSeedWait        = 5;
    26 Torrent._StatusSeed            = 6;
     19Torrent._StatusStopped = 0;
     20Torrent._StatusCheckWait = 1;
     21Torrent._StatusCheck = 2;
     22Torrent._StatusDownloadWait = 3;
     23Torrent._StatusDownload = 4;
     24Torrent._StatusSeedWait = 5;
     25Torrent._StatusSeed = 6;
    2726
    2827// Torrent.fields.seedRatioMode
    29 Torrent._RatioUseGlobal        = 0;
    30 Torrent._RatioUseLocal         = 1;
    31 Torrent._RatioUnlimited        = 2;
     28Torrent._RatioUseGlobal = 0;
     29Torrent._RatioUseLocal = 1;
     30Torrent._RatioUnlimited = 2;
    3231
    3332// Torrent.fields.error
    34 Torrent._ErrNone               = 0;
    35 Torrent._ErrTrackerWarning     = 1;
    36 Torrent._ErrTrackerError       = 2;
    37 Torrent._ErrLocalError         = 3;
     33Torrent._ErrNone = 0;
     34Torrent._ErrTrackerWarning = 1;
     35Torrent._ErrTrackerError = 2;
     36Torrent._ErrLocalError = 3;
    3837
    3938// TrackerStats' announceState
    40 Torrent._TrackerInactive       = 0;
    41 Torrent._TrackerWaiting        = 1;
    42 Torrent._TrackerQueued         = 2;
    43 Torrent._TrackerActive         = 3;
    44 
    45 
    46 Torrent.Fields = { };
     39Torrent._TrackerInactive = 0;
     40Torrent._TrackerWaiting = 1;
     41Torrent._TrackerQueued = 2;
     42Torrent._TrackerActive = 3;
     43
     44Torrent.Fields = {};
    4745
    4846// commonly used fields which only need to be loaded once,
     
    5048// finishes downloading its metadata
    5149Torrent.Fields.Metadata = [
    52         'addedDate',
    53         'name',
    54         'totalSize'
     50    'addedDate',
     51    'name',
     52    'totalSize'
    5553];
    5654
    5755// commonly used fields which need to be periodically refreshed
    5856Torrent.Fields.Stats = [
    59         'error',
    60         'errorString',
    61         'eta',
    62         'isFinished',
    63         'isStalled',
    64         'leftUntilDone',
    65         'metadataPercentComplete',
    66         'peersConnected',
    67         'peersGettingFromUs',
    68         'peersSendingToUs',
    69         'percentDone',
    70         'queuePosition',
    71         'rateDownload',
    72         'rateUpload',
    73         'recheckProgress',
    74         'seedRatioMode',
    75         'seedRatioLimit',
    76         'sizeWhenDone',
    77         'status',
    78         'trackers',
    79         'downloadDir',
    80         'uploadedEver',
    81         'uploadRatio',
    82         'webseedsSendingToUs'
     57    'error',
     58    'errorString',
     59    'eta',
     60    'isFinished',
     61    'isStalled',
     62    'leftUntilDone',
     63    'metadataPercentComplete',
     64    'peersConnected',
     65    'peersGettingFromUs',
     66    'peersSendingToUs',
     67    'percentDone',
     68    'queuePosition',
     69    'rateDownload',
     70    'rateUpload',
     71    'recheckProgress',
     72    'seedRatioMode',
     73    'seedRatioLimit',
     74    'sizeWhenDone',
     75    'status',
     76    'trackers',
     77    'downloadDir',
     78    'uploadedEver',
     79    'uploadRatio',
     80    'webseedsSendingToUs'
    8381];
    8482
    8583// fields used by the inspector which only need to be loaded once
    8684Torrent.Fields.InfoExtra = [
    87         'comment',
    88         'creator',
    89         'dateCreated',
    90         'files',
    91         'hashString',
    92         'isPrivate',
    93         'pieceCount',
    94         'pieceSize'
     85    'comment',
     86    'creator',
     87    'dateCreated',
     88    'files',
     89    'hashString',
     90    'isPrivate',
     91    'pieceCount',
     92    'pieceSize'
    9593];
    9694
    9795// fields used in the inspector which need to be periodically refreshed
    9896Torrent.Fields.StatsExtra = [
    99         'activityDate',
    100         'corruptEver',
    101         'desiredAvailable',
    102         'downloadedEver',
    103         'fileStats',
    104         'haveUnchecked',
    105         'haveValid',
    106         'peers',
    107         'startDate',
    108         'trackerStats'
     97    'activityDate',
     98    'corruptEver',
     99    'desiredAvailable',
     100    'downloadedEver',
     101    'fileStats',
     102    'haveUnchecked',
     103    'haveValid',
     104    'peers',
     105    'startDate',
     106    'trackerStats'
    109107];
    110108
    111109/***
    112 ****
    113 ****  Methods
    114 ****
    115 ***/
    116 
    117 Torrent.prototype =
    118 {
    119         initialize: function(data)
    120         {
    121                 this.fields = {};
    122                 this.fieldObservers = {};
    123                 this.refresh (data);
    124         },
    125 
    126         notifyOnFieldChange: function(field, callback) {
    127                 this.fieldObservers[field] = this.fieldObservers[field] || [];
    128                 this.fieldObservers[field].push(callback);
    129         },
    130 
    131         setField: function(o, name, value)
    132         {
    133                 var i, observer;
    134 
    135                 if (o[name] === value)
    136                         return false;
    137                 if (o == this.fields && this.fieldObservers[name] && this.fieldObservers[name].length) {
    138                         for (i=0; observer=this.fieldObservers[name][i]; ++i) {
    139                                 observer.call(this, value, o[name], name);
    140                         }
    141                 }
    142                 o[name] = value;
    143                 return true;
    144         },
    145 
    146         // fields.files is an array of unions of RPC's "files" and "fileStats" objects.
    147         updateFiles: function(files)
    148         {
    149                 var changed = false,
    150                     myfiles = this.fields.files || [],
    151                     keys = [ 'length', 'name', 'bytesCompleted', 'wanted', 'priority' ],
    152                     i, f, j, key, myfile;
    153 
    154                 for (i=0; f=files[i]; ++i) {
    155                         myfile = myfiles[i] || {};
    156                         for (j=0; key=keys[j]; ++j)
    157                                 if(key in f)
    158                                         changed |= this.setField(myfile,key,f[key]);
    159                         myfiles[i] = myfile;
    160                 }
    161                 this.fields.files = myfiles;
    162                 return changed;
    163         },
    164 
    165         collateTrackers: function(trackers)
    166         {
    167                 var i, t, announces = [];
    168 
    169                 for (i=0; t=trackers[i]; ++i)
    170                         announces.push(t.announce.toLowerCase());
    171                 return announces.join('\t');
    172         },
    173 
    174         refreshFields: function(data)
    175         {
    176                 var key,
    177                     changed = false;
    178 
    179                 for (key in data) {
    180                         switch (key) {
    181                                 case 'files':
    182                                 case 'fileStats': // merge files and fileStats together
    183                                         changed |= this.updateFiles(data[key]);
    184                                         break;
    185                                 case 'trackerStats': // 'trackerStats' is a superset of 'trackers'...
    186                                         changed |= this.setField(this.fields,'trackers',data[key]);
    187                                         break;
    188                                 case 'trackers': // ...so only save 'trackers' if we don't have it already
    189                                         if (!(key in this.fields))
    190                                               &nb