]> granicus.if.org Git - transmission/commitdiff
Merge branch '2.9x'
authorMike Gelfand <mikedld@mikedld.com>
Tue, 1 May 2018 19:44:51 +0000 (22:44 +0300)
committerMike Gelfand <mikedld@mikedld.com>
Tue, 1 May 2018 19:44:51 +0000 (22:44 +0300)
1  2 
CMakeLists.txt
NEWS
Transmission.xcodeproj/project.pbxproj
configure.ac
macosx/AboutWindowController.m
macosx/Info.plist
update-version-h.sh
web/javascript/inspector.js

diff --cc CMakeLists.txt
index 0bded128199c7f9cddb896e11c353d3f7486354e,da28e3196c218a9aaaeaa6d663a523dfb2a916d7..beb82c62ba1901c7291977aba01cad3e1222b20b
@@@ -44,8 -41,8 +44,8 @@@ set(TR_NAME ${PROJECT_NAME}
  #         "Z" for unsupported trunk builds,
  #         "0" for stable, supported releases
  # these should be the only two lines you need to change
- set(TR_USER_AGENT_PREFIX "2.93+")
- set(TR_PEER_ID_PREFIX "-TR293Z-")
 -set(TR_USER_AGENT_PREFIX "2.94")
 -set(TR_PEER_ID_PREFIX "-TR2940-")
++set(TR_USER_AGENT_PREFIX "2.94+")
++set(TR_PEER_ID_PREFIX "-TR294Z-")
  
  string(REGEX MATCH "^([0-9]+)\\.([0-9]+).*" TR_VERSION "${TR_USER_AGENT_PREFIX}")
  set(TR_VERSION_MAJOR "${CMAKE_MATCH_1}")
diff --cc NEWS
Simple merge
diff --cc configure.ac
index 0d3b7897fd259e5664ee9d124de93bc5943c94c0,df8cb5007b705f751db4713b0ec0405361e5a2e9..b0e053e915c21a7176005c6e84585bb4884e5803
@@@ -3,8 -3,8 +3,8 @@@ dnl STATUS: "X" for prerelease beta bui
  dnl         "Z" for unsupported trunk builds,
  dnl         "0" for stable, supported releases
  dnl these should be the only two lines you need to change
- m4_define([user_agent_prefix],[2.93+])
- m4_define([peer_id_prefix],[-TR293Z-])
 -m4_define([user_agent_prefix],[2.94])
 -m4_define([peer_id_prefix],[-TR2940-])
++m4_define([user_agent_prefix],[2.94+])
++m4_define([peer_id_prefix],[-TR294Z-])
  
  AC_INIT([transmission],[user_agent_prefix],[https://github.com/transmission/transmission])
  AC_SUBST(USERAGENT_PREFIX,[user_agent_prefix])
index e2787ac595243f32af1a992a2e4b4ddbedbbbe81,8a30c514cb3e270c76c116355e6c2e4675465953..06b509e5f0eab188fd8a6dcbfd74dcfd72017cd1
@@@ -34,22 -38,20 +36,20 @@@ AboutWindowController * fAboutBoxInstan
  
  - (void) awakeFromNib
  {
-     NSDictionary * info = [[NSBundle mainBundle] infoDictionary];
-     [fVersionField setStringValue: [NSString stringWithFormat: @"%@ (%@)",
-         info[@"CFBundleShortVersionString"], info[(NSString *)kCFBundleVersionKey]]];
+     [fVersionField setStringValue: @(LONG_VERSION_STRING)];
 -    
 +
      [fCopyrightField setStringValue: [[NSBundle mainBundle] localizedStringForKey: @"NSHumanReadableCopyright"
                                          value: nil table: @"InfoPlist"]];
 -    
 -    [[fTextView textStorage] setAttributedString: [[[NSAttributedString alloc] initWithPath:
 -            [[NSBundle mainBundle] pathForResource: @"Credits" ofType: @"rtf"] documentAttributes: nil] autorelease]];
 -    
 +
 +    [[fTextView textStorage] setAttributedString: [[NSAttributedString alloc] initWithPath:
 +            [[NSBundle mainBundle] pathForResource: @"Credits" ofType: @"rtf"] documentAttributes: nil]];
 +
      //size license button
      const CGFloat oldButtonWidth = NSWidth([fLicenseButton frame]);
 -    
 +
      [fLicenseButton setTitle: NSLocalizedString(@"License", "About window -> license button")];
      [fLicenseButton sizeToFit];
 -    
 +
      NSRect buttonFrame = [fLicenseButton frame];
      buttonFrame.size.width += 10.0;
      buttonFrame.origin.x -= NSWidth(buttonFrame) - oldButtonWidth;
Simple merge
Simple merge
index 0da1a73851ad609474bbacbd0e9ee4c2e3a4f8ba,419ad263b7b534c629a7a5329cae31f9d4f3668c..20ac853fe9ba192195f5a90c2779289ff4d50d45
  
  function Inspector(controller) {
  
 -      var data = {
 -              controller: null,
 -              elements: { },
 -              torrents: [ ]
 -      },
 -
 -      needsExtraInfo = function (torrents) {
 -              var i, id, tor;
 -
 -              for (i = 0; tor = torrents[i]; i++)
 -                      if (!tor.hasExtraInfo())
 -                              return true;
 -
 -              return false;
 -      },
 -
 -      refreshTorrents = function () {
 -              var fields,
 -                  ids = $.map(data.torrents.slice(0), function (t) {return t.getId();});
 -
 -              if (ids && ids.length)
 -              {
 -                      fields = ['id'].concat(Torrent.Fields.StatsExtra);
 -
 -                      if (needsExtraInfo(data.torrents))
 -                              $.merge(fields, Torrent.Fields.InfoExtra);
 -
 -                      data.controller.updateTorrents(ids, fields);
 -              }
 -      },
 -
 -      onTabClicked = function (ev) {
 -              var tab = ev.currentTarget;
 -
 -              if (isMobileDevice)
 -                      ev.stopPropagation();
 -
 -              // select this tab and deselect the others
 -              $(tab).addClass('selected').siblings().removeClass('selected');
 -
 -              // show this tab and hide the others
 -              $('#' + tab.id.replace('tab','page')).show().siblings('.inspector-page').hide();
 -
 -              updateInspector();
 -      },
 -
 -      updateInspector = function () {
 -              var e = data.elements,
 -                  torrents = data.torrents,
 -                  name;
 -
 -              // update the name, which is shown on all the pages
 -              if (!torrents || !torrents.length)
 -                      name = 'No Selection';
 -              else if(torrents.length === 1)
 -                      name = torrents[0].getName();
 -              else
 -                      name = '' + torrents.length+' Transfers Selected';
 -              setTextContent(e.name_lb, name || na);
 -
 -              // update the visible page
 -              if ($(e.info_page).is(':visible'))
 -                      updateInfoPage();
 -              else if ($(e.peers_page).is(':visible'))
 -                      updatePeersPage();
 -              else if ($(e.trackers_page).is(':visible'))
 -                      updateTrackersPage();
 -              else if ($(e.files_page).is(':visible'))
 -                      updateFilesPage();
 -      },
 -
 -      /****
 -      *****  GENERAL INFO PAGE
 -      ****/
 -
 -      updateInfoPage = function () {
 -              var torrents = data.torrents,
 -                  e = data.elements,
 -                  fmt = Transmission.fmt,
 -                  none = 'None',
 -                  mixed = 'Mixed',
 -                  unknown = 'Unknown',
 -                  isMixed, allPaused, allFinished,
 -                  str,
 -                  baseline, it, s, i, t,
 -                  sizeWhenDone = 0,
 -                  leftUntilDone = 0,
 -                  available = 0,
 -                  haveVerified = 0,
 -                  haveUnverified = 0,
 -                  verifiedPieces = 0,
 -                  stateString,
 -                  latest,
 -                  pieces,
 -                  size,
 -                  pieceSize,
 -                  creator, mixed_creator,
 -                  date, mixed_date,
 -                  v, u, f, d, pct,
 -                  uri,
 -                  now = Date.now();
 -
 -              //
 -              //  state_lb
 -              //
 -
 -              if(torrents.length <1)
 -                      str = none;
 -              else {
 -                      isMixed = false;
 -                      allPaused = true;
 -                      allFinished = true;
 -
 -                      baseline = torrents[0].getStatus();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              it = t.getStatus();
 -                              if(it != baseline)
 -                                      isMixed = true;
 -                              if(!t.isStopped())
 -                                      allPaused = allFinished = false;
 -                              if(!t.isFinished())
 -                                      allFinished = false;
 -                      }
 -                      if( isMixed )
 -                              str = mixed;
 -                      else if( allFinished )
 -                              str = 'Finished';
 -                      else if( allPaused )
 -                              str = 'Paused';
 -                      else
 -                              str = torrents[0].getStateString();
 -              }
 -              setTextContent(e.state_lb, str);
 -              stateString = str;
 -
 -              //
 -              //  have_lb
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      baseline = torrents[0].getStatus();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(!t.needsMetaData()) {
 -                                      haveUnverified += t.getHaveUnchecked();
 -                                      v = t.getHaveValid();
 -                                      haveVerified += v;
 -                                      if(t.getPieceSize())
 -                                              verifiedPieces += v / t.getPieceSize();
 -                                      sizeWhenDone += t.getSizeWhenDone();
 -                                      leftUntilDone += t.getLeftUntilDone();
 -                                      available += (t.getHave()) + t.getDesiredAvailable();
 -                              }
 -                      }
 -
 -                      d = 100.0 * ( sizeWhenDone ? ( sizeWhenDone - leftUntilDone ) / sizeWhenDone : 1 );
 -                      str = fmt.percentString( d );
 -
 -                      if( !haveUnverified && !leftUntilDone )
 -                              str = fmt.size(haveVerified) + ' (100%)';
 -                      else if( !haveUnverified )
 -                              str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%)';
 -                      else
 -                              str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str +'%), ' + fmt.size(haveUnverified) + ' Unverified';
 -              }
 -              setTextContent(e.have_lb, str);
 -
 -              //
 -              //  availability_lb
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else if( sizeWhenDone == 0 )
 -                      str = none;
 -              else
 -                      str = '' + fmt.percentString( ( 100.0 * available ) / sizeWhenDone ) +  '%';
 -              setTextContent(e.availability_lb, str);
 -
 -              //
 -              //  downloaded_lb
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      d = f = 0;
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              d += t.getDownloadedEver();
 -                              f += t.getFailedEver();
 -                      }
 -                      if(f)
 -                              str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
 -                      else
 -                              str = fmt.size(d);
 -              }
 -              setTextContent(e.downloaded_lb, str);
 -
 -              //
 -              //  uploaded_lb
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      d = u = 0;
 -                      if(torrents.length == 1) {
 -                              d = torrents[0].getDownloadedEver();
 -                              u = torrents[0].getUploadedEver();
 -
 -                              if (d == 0)
 -                                      d = torrents[0].getHaveValid();
 -                      }
 -                      else {
 -                              for(i=0; t=torrents[i]; ++i) {
 -                                      d += t.getDownloadedEver();
 -                                      u += t.getUploadedEver();
 -                              }
 -                      }
 -                      str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString( Math.ratio(u,d))+')';
 -              }
 -              setTextContent(e.uploaded_lb, str);
 -
 -              //
 -              // running time
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      allPaused = true;
 -                      baseline = torrents[0].getStartDate();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(baseline != t.getStartDate())
 -                                      baseline = 0;
 -                              if(!t.isStopped())
 -                                      allPaused = false;
 -                      }
 -                      if(allPaused)
 -                              str = stateString; // paused || finished
 -                      else if(!baseline)
 -                              str = mixed;
 -                      else
 -                              str = fmt.timeInterval(now/1000 - baseline);
 -              }
 -              setTextContent(e.running_time_lb, str);
 -
 -              //
 -              // remaining time
 -              //
 -
 -              str = '';
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      baseline = torrents[0].getETA();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(baseline != t.getETA()) {
 -                                      str = mixed;
 -                                      break;
 -                              }
 -                      }
 -              }
 -              if(!str.length) {
 -                      if(baseline < 0)
 -                              str = unknown;
 -                      else
 -                              str = fmt.timeInterval(baseline);
 -              }
 -              setTextContent(e.remaining_time_lb, str);
 -
 -              //
 -              // last activity
 -              //
 -
 -              latest = -1;
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      baseline = torrents[0].getLastActivity();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              d = t.getLastActivity();
 -                              if(latest < d)
 -                                      latest = d;
 -                      }
 -                      d = now/1000 - latest; // seconds since last activity
 -                      if(d < 0)
 -                              str = none;
 -                      else if(d < 5)
 -                              str = 'Active now';
 -                      else
 -                              str = fmt.timeInterval(d) + ' ago';
 -              }
 -              setTextContent(e.last_activity_lb, str);
 -
 -              //
 -              // error
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      str = torrents[0].getErrorString();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(str != t.getErrorString()) {
 -                                      str = mixed;
 -                                      break;
 -                              }
 -                      }
 -              }
 -              setTextContent(e.error_lb, str || none);
 -
 -              //
 -              // size
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      pieces = 0;
 -                      size = 0;
 -                      pieceSize = torrents[0].getPieceSize();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              pieces += t.getPieceCount();
 -                              size += t.getTotalSize();
 -                              if(pieceSize != t.getPieceSize())
 -                                      pieceSize = 0;
 -                      }
 -                      if(!size)
 -                              str = none;
 -                      else if(pieceSize > 0)
 -                              str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
 -                      else
 -                              str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
 -              }
 -              setTextContent(e.size_lb, str);
 -
 -              //
 -              //  hash
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      str = torrents[0].getHashString();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(str != t.getHashString()) {
 -                                      str = mixed;
 -                                      break;
 -                              }
 -                      }
 -              }
 -              setTextContent(e.hash_lb, str);
 -
 -              //
 -              //  privacy
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      baseline = torrents[0].getPrivateFlag();
 -                      str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(baseline != t.getPrivateFlag()) {
 -                                      str = mixed;
 -                                      break;
 -                              }
 -                      }
 -              }
 -              setTextContent(e.privacy_lb, str);
 -
 -              //
 -              //  comment
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      str = torrents[0].getComment();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(str != t.getComment()) {
 -                                      str = mixed;
 -                                      break;
 -                              }
 -                      }
 -              }
 -              if(!str)
 -                      str = none;
 -              uri = parseUri(str);
 -              if (uri.protocol == 'http' || uri.parseUri == 'https') {
 -                      str = encodeURI(str);
 -                      setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
 -              }
 -              else
 -                      setTextContent(e.comment_lb, str);
 -
 -              //
 -              //  origin
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      mixed_creator = false;
 -                      mixed_date = false;
 -                      creator = torrents[0].getCreator();
 -                      date = torrents[0].getDateCreated();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(creator != t.getCreator())
 -                                      mixed_creator = true;
 -                              if(date != t.getDateCreated())
 -                                      mixed_date = true;
 -                      }
 -                      var empty_creator = !creator || !creator.length,
 -                          empty_date = !date;
 -                      if(mixed_creator || mixed_date)
 -                              str = mixed;
 -                      else if(empty_creator && empty_date)
 -                              str = unknown;
 -                      else if(empty_date && !empty_creator)
 -                              str = 'Created by ' + creator;
 -                      else if(empty_creator && !empty_date)
 -                              str = 'Created on ' + (new Date(date*1000)).toDateString();
 -                      else
 -                              str = 'Created by ' + creator + ' on ' + (new Date(date*1000)).toDateString();
 -              }
 -              setTextContent(e.origin_lb, str);
 -
 -              //
 -              //  foldername
 -              //
 -
 -              if(torrents.length < 1)
 -                      str = none;
 -              else {
 -                      str = torrents[0].getDownloadDir();
 -                      for(i=0; t=torrents[i]; ++i) {
 -                              if(str != t.getDownloadDir()) {
 -                                      str = mixed;
 -                                      break;
 -                              }
 -                      }
 -              }
 -              setTextContent(e.foldername_lb, str);
 -      },
 -
 -      /****
 -      *****  FILES PAGE
 -      ****/
 -
 -      changeFileCommand = function(fileIndices, command) {
 -              var torrentId = data.file_torrent.getId();
 -              data.controller.changeFileCommand(torrentId, fileIndices, command);
 -      },
 -
 -      onFileWantedToggled = function(ev, fileIndices, want) {
 -              changeFileCommand(fileIndices, want?'files-wanted':'files-unwanted');
 -      },
 -
 -      onFilePriorityToggled = function(ev, fileIndices, priority) {
 -              var command;
 -              switch(priority) {
 -                      case -1: command = 'priority-low'; break;
 -                      case  1: command = 'priority-high'; break;
 -                      default: command = 'priority-normal'; break;
 -              }
 -              changeFileCommand(fileIndices, command);
 -      },
 -
 -      onNameClicked = function(ev, fileRow, fileIndices) {
 -              $(fileRow.getElement()).siblings().slideToggle();
 -      },
 -
 -      clearFileList = function() {
 -              $(data.elements.file_list).empty();
 -              delete data.file_torrent;
 -              delete data.file_torrent_n;
 -              delete data.file_rows;
 -      },
 -
 -      createFileTreeModel = function (tor) {
 -              var i, j, n, name, tokens, walk, tree, token, sub,
 -                  leaves = [ ],
 -                  tree = { children: { }, file_indices: [ ] };
 -
 -              n = tor.getFileCount();
 -              for (i=0; i<n; ++i) {
 -                      name = tor.getFile(i).name;
 -                      tokens = name.split('/');
 -                      walk = tree;
 -                      for (j=0; j<tokens.length; ++j) {
 -                              token = tokens[j];
 -                              sub = walk.children[token];
 -                              if (!sub) {
 -                                      walk.children[token] = sub = {
 -                                              name: token,
 -                                              parent: walk,
 -                                              children: { },
 -                                              file_indices: [ ],
 -                                              depth: j
 -                                      };
 -                              }
 -                              walk = sub;
 -                      }
 -                      walk.file_index = i;
 -                      delete walk.children;
 -                      leaves.push (walk);
 -              }
 -
 -              for (i=0; i<leaves.length; ++i) {
 -                      walk = leaves[i];
 -                      j = walk.file_index;
 -                      do {
 -                              walk.file_indices.push (j);
 -                              walk = walk.parent;
 -                      } while (walk);
 -              }
 -
 -              return tree;
 -      },
 -
 -      addNodeToView = function (tor, parent, sub, i) {
 -              var row;
 -              row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i%2);
 -              data.file_rows.push(row);
 -              parent.appendChild(row.getElement());
 -              $(row).bind('wantedToggled',onFileWantedToggled);
 -              $(row).bind('priorityToggled',onFilePriorityToggled);
 -              $(row).bind('nameClicked',onNameClicked);
 -      },
 -
 -      addSubtreeToView = function (tor, parent, sub, i) {
 -              var key, div;
 -              div = document.createElement('div');
 -              if (sub.parent)
 -                      addNodeToView (tor, div, sub, i++);
 -              if (sub.children)
 -                      for (key in sub.children)
 -                              i = addSubtreeToView (tor, div, sub.children[key]);
 -              parent.appendChild(div);
 -              return i;
 -      },
 -
 -      updateFilesPage = function() {
 -              var i, n, tor, fragment, tree,
 -                  file_list = data.elements.file_list,
 -                  torrents = data.torrents;
 -
 -              // only show one torrent at a time
 -              if (torrents.length !== 1) {
 -                      clearFileList();
 -                      return;
 -              }
 -
 -              tor = torrents[0];
 -              n = tor ? tor.getFileCount() : 0;
 -              if (tor!=data.file_torrent || n!=data.file_torrent_n) {
 -                      // rebuild the file list...
 -                      clearFileList();
 -                      data.file_torrent = tor;
 -                      data.file_torrent_n = n;
 -                      data.file_rows = [ ];
 -                      fragment = document.createDocumentFragment();
 -                      tree = createFileTreeModel (tor);
 -                      addSubtreeToView (tor, fragment, tree, 0);
 -                      file_list.appendChild (fragment);
 -              } else {
 -                      // ...refresh the already-existing file list
 -                      for (i=0, n=data.file_rows.length; i<n; ++i)
 -                              data.file_rows[i].refresh();
 -              }
 -      },
 -
 -      /****
 -      *****  PEERS PAGE
 -      ****/
 -
 -      updatePeersPage = function() {
 -              var i, k, tor, peers, peer, parity,
 -                  html = [],
 -                  fmt = Transmission.fmt,
 -                  peers_list = data.elements.peers_list,
 -                  torrents = data.torrents;
 -
 -              for (k=0; tor=torrents[k]; ++k)
 -              {
 -                      peers = tor.getPeers();
 -                      html.push('<div class="inspector_group">');
 -                      if (torrents.length > 1) {
 -                              html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
 -                      }
 -                      if (!peers || !peers.length) {
 -                              html.push('<br></div>'); // firefox won't paint the top border if the div is empty
 -                              continue;
 -                      }
 -                      html.push('<table class="peer_list">',
 -                                '<tr class="inspector_peer_entry even">',
 -                                '<th class="encryptedCol"></th>',
 -                                '<th class="upCol">Up</th>',
 -                                '<th class="downCol">Down</th>',
 -                                '<th class="percentCol">%</th>',
 -                                '<th class="statusCol">Status</th>',
 -                                '<th class="addressCol">Address</th>',
 -                                '<th class="clientCol">Client</th>',
 -                                '</tr>');
 -                      for (i=0; peer=peers[i]; ++i) {
 -                              parity = (i%2) ? 'odd' : 'even';
 -                              html.push('<tr class="inspector_peer_entry ', parity, '">',
 -                                        '<td>', (peer.isEncrypted ? '<div class="encrypted-peer-cell" title="Encrypted Connection">'
 -                                                                  : '<div class="unencrypted-peer-cell">'), '</div>', '</td>',
 -                                        '<td>', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '</td>',
 -                                        '<td>', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '</td>',
 -                                        '<td class="percentCol">', Math.floor(peer.progress*100), '%', '</td>',
 -                                        '<td>', fmt.peerStatus(peer.flagStr), '</td>',
 -                                        '<td>', sanitizeText(peer.address), '</td>',
 -                                        '<td class="clientCol">', sanitizeText(peer.clientName), '</td>',
 -                                        '</tr>');
 -                      }
 -                      html.push('</table></div>');
 -              }
 -
 -              setInnerHTML(peers_list, html.join(''));
 -      },
 -
 -      /****
 -      *****  TRACKERS PAGE
 -      ****/
 -
 -      getAnnounceState = function(tracker) {
 -              var timeUntilAnnounce, s = '';
 -              switch (tracker.announceState) {
 -                      case Torrent._TrackerActive:
 -                              s = 'Announce in progress';
 -                              break;
 -                      case Torrent._TrackerWaiting:
 -                              timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
 -                              if (timeUntilAnnounce < 0) {
 -                                  timeUntilAnnounce = 0;
 -                              }
 -                              s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
 -                              break;
 -                      case Torrent._TrackerQueued:
 -                              s = 'Announce is queued';
 -                              break;
 -                      case Torrent._TrackerInactive:
 -                              s = tracker.isBackup ?
 -                                  'Tracker will be used as a backup' :
 -                                  'Announce not scheduled';
 -                              break;
 -                      default:
 -                              s = 'unknown announce state: ' + tracker.announceState;
 -              }
 -              return s;
 -      },
 -
 -      lastAnnounceStatus = function(tracker) {
 -
 -              var lastAnnounceLabel = 'Last Announce',
 -                  lastAnnounce = [ 'N/A' ],
 -              lastAnnounceTime;
 -
 -              if (tracker.hasAnnounced) {
 -                      lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
 -                      if (tracker.lastAnnounceSucceeded) {
 -                              lastAnnounce = [ lastAnnounceTime, ' (got ',  Transmission.fmt.countString('peer','peers',tracker.lastAnnouncePeerCount), ')' ];
 -                      } else {
 -                              lastAnnounceLabel = 'Announce error';
 -                              lastAnnounce = [ (tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime ];
 -                      }
 -              }
 -              return { 'label':lastAnnounceLabel, 'value':lastAnnounce.join('') };
 -      },
 -
 -      lastScrapeStatus = function(tracker) {
 -
 -              var lastScrapeLabel = 'Last Scrape',
 -                  lastScrape = 'N/A',
 -              lastScrapeTime;
 -
 -              if (tracker.hasScraped) {
 -                      lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
 -                      if (tracker.lastScrapeSucceeded) {
 -                              lastScrape = lastScrapeTime;
 -                      } else {
 -                              lastScrapeLabel = 'Scrape error';
 -                              lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
 -                      }
 -              }
 -              return {'label':lastScrapeLabel, 'value':lastScrape};
 -      },
 -
 -      updateTrackersPage = function() {
 -              var i, j, tier, tracker, trackers, tor,
 -                  html, parity, lastAnnounceStatusHash,
 -                  announceState, lastScrapeStatusHash,
 -                  na = 'N/A',
 -                  trackers_list = data.elements.trackers_list,
 -                  torrents = data.torrents;
 -
 -              // By building up the HTML as as string, then have the browser
 -              // turn this into a DOM tree, this is a fast operation.
 -              html = [];
 -              for (i=0; tor=torrents[i]; ++i)
 -              {
 -                      html.push ('<div class="inspector_group">');
 -
 -                      if (torrents.length > 1)
 -                              html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
 -
 -                      tier = -1;
 -                      trackers = tor.getTrackers();
 -                      for (j=0; tracker=trackers[j]; ++j)
 -                      {
 -                              if (tier != tracker.tier)
 -                              {
 -                                      if (tier !== -1) // close previous tier
 -                                              html.push('</ul></div>');
 -
 -                                      tier = tracker.tier;
 -
 -                                      html.push('<div class="inspector_group_label">',
 -                                                'Tier ', tier+1, '</div>',
 -                                                '<ul class="tier_list">');
 -                              }
 -
 -                              // Display construction
 -                              lastAnnounceStatusHash = lastAnnounceStatus(tracker);
 -                              announceState = getAnnounceState(tracker);
 -                              lastScrapeStatusHash = lastScrapeStatus(tracker);
 -                              parity = (j%2) ? 'odd' : 'even';
 -                              html.push('<li class="inspector_tracker_entry ', parity, '"><div class="tracker_host" title="', sanitizeText(tracker.announce), '">',
 -                                        sanitizeText(tracker.host || tracker.announce), '</div>',
 -                                        '<div class="tracker_activity">',
 -                                        '<div>', lastAnnounceStatusHash['label'], ': ', sanitizeText(lastAnnounceStatusHash['value']), '</div>',
 -                                        '<div>', announceState, '</div>',
 -                                        '<div>', lastScrapeStatusHash['label'], ': ', sanitizeText(lastScrapeStatusHash['value']), '</div>',
 -                                        '</div><table class="tracker_stats">',
 -                                        '<tr><th>Seeders:</th><td>', (tracker.seederCount > -1 ? tracker.seederCount : na), '</td></tr>',
 -                                        '<tr><th>Leechers:</th><td>', (tracker.leecherCount > -1 ? tracker.leecherCount : na), '</td></tr>',
 -                                        '<tr><th>Downloads:</th><td>', (tracker.downloadCount > -1 ? tracker.downloadCount : na), '</td></tr>',
 -                                        '</table></li>');
 -                      }
 -                      if (tier !== -1) // close last tier
 -                              html.push('</ul></div>');
 -
 -                      html.push('</div>'); // inspector_group
 -              }
 -
 -              setInnerHTML (trackers_list, html.join(''));
 -      },
 -
 -      initialize = function (controller) {
 -
 -              var ti = '#torrent_inspector_';
 -
 -              data.controller = controller;
 -
 -              $('.inspector-tab').click(onTabClicked);
 -
 -              data.elements.info_page      = $('#inspector-page-info')[0];
 -              data.elements.files_page     = $('#inspector-page-files')[0];
 -              data.elements.peers_page     = $('#inspector-page-peers')[0];
 -              data.elements.trackers_page  = $('#inspector-page-trackers')[0];
 -
 -              data.elements.file_list      = $('#inspector_file_list')[0];
 -              data.elements.peers_list     = $('#inspector_peers_list')[0];
 -              data.elements.trackers_list  = $('#inspector_trackers_list')[0];
 -
 -              data.elements.have_lb           = $('#inspector-info-have')[0];
 -              data.elements.availability_lb   = $('#inspector-info-availability')[0];
 -              data.elements.downloaded_lb     = $('#inspector-info-downloaded')[0];
 -              data.elements.uploaded_lb       = $('#inspector-info-uploaded')[0];
 -              data.elements.state_lb          = $('#inspector-info-state')[0];
 -              data.elements.running_time_lb   = $('#inspector-info-running-time')[0];
 -              data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
 -              data.elements.last_activity_lb  = $('#inspector-info-last-activity')[0];
 -              data.elements.error_lb          = $('#inspector-info-error')[0];
 -              data.elements.size_lb           = $('#inspector-info-size')[0];
 -              data.elements.foldername_lb     = $('#inspector-info-location')[0];
 -              data.elements.hash_lb           = $('#inspector-info-hash')[0];
 -              data.elements.privacy_lb        = $('#inspector-info-privacy')[0];
 -              data.elements.origin_lb         = $('#inspector-info-origin')[0];
 -              data.elements.comment_lb        = $('#inspector-info-comment')[0];
 -              data.elements.name_lb           = $('#torrent_inspector_name')[0];
 -
 -              // force initial 'N/A' updates on all the pages
 -              updateInspector();
 -              updateInfoPage();
 -              updatePeersPage();
 -              updateTrackersPage();
 -              updateFilesPage();
 -      };
 -
 -      /****
 -      *****  PUBLIC FUNCTIONS
 -      ****/
 -
 -      this.setTorrents = function (torrents) {
 -              var d = data;
 -
 -              // update the inspector when a selected torrent's data changes.
 -              $(d.torrents).unbind('dataChanged.inspector');
 -              $(torrents).bind('dataChanged.inspector', $.proxy(updateInspector,this));
 -              d.torrents = torrents;
 -
 -              // periodically ask for updates to the inspector's torrents
 -              clearInterval(d.refreshInterval);
 -              d.refreshInterval = setInterval($.proxy(refreshTorrents,this), 2000);
 -              refreshTorrents();
 -
 -              // refresh the inspector's UI
 -              updateInspector();
 -      };
 -
 -      initialize (controller);
 +    var data = {
 +            controller: null,
 +            elements: {},
 +            torrents: []
 +        },
 +
 +        needsExtraInfo = function (torrents) {
 +            var i, id, tor;
 +
 +            for (i = 0; tor = torrents[i]; i++)
 +                if (!tor.hasExtraInfo())
 +                    return true;
 +
 +            return false;
 +        },
 +
 +        refreshTorrents = function (callback) {
 +            var fields,
 +                ids = $.map(data.torrents.slice(0), function (t) {
 +                    return t.getId();
 +                });
 +
 +            if (ids && ids.length) {
 +                fields = ['id'].concat(Torrent.Fields.StatsExtra);
 +
 +                if (needsExtraInfo(data.torrents)) {
 +                    $.merge(fields, Torrent.Fields.InfoExtra);
 +                }
 +
 +                data.controller.updateTorrents(ids, fields, callback);
 +            }
 +        },
 +
 +        onTabClicked = function (ev) {
 +            var tab = ev.currentTarget;
 +
 +            if (isMobileDevice) {
 +                ev.stopPropagation();
 +            }
 +
 +            // select this tab and deselect the others
 +            $(tab).addClass('selected').siblings().removeClass('selected');
 +
 +            // show this tab and hide the others
 +            $('#' + tab.id.replace('tab', 'page')).show().siblings('.inspector-page').hide();
 +
 +            updateInspector();
 +        },
 +
 +        updateInspector = function () {
 +            var e = data.elements,
 +                torrents = data.torrents,
 +                name;
 +
 +            // update the name, which is shown on all the pages
 +            if (!torrents || !torrents.length) {
 +                name = 'No Selection';
 +            } else if (torrents.length === 1) {
 +                name = torrents[0].getName();
 +            } else {
 +                name = '' + torrents.length + ' Transfers Selected';
 +            }
 +            setTextContent(e.name_lb, name || na);
 +
 +            // update the visible page
 +            if ($(e.info_page).is(':visible')) {
 +                updateInfoPage();
 +            } else if ($(e.peers_page).is(':visible')) {
 +                updatePeersPage();
 +            } else if ($(e.trackers_page).is(':visible')) {
 +                updateTrackersPage();
 +            } else if ($(e.files_page).is(':visible')) {
 +                updateFilesPage();
 +            }
 +        },
 +
 +        /****
 +         *****  GENERAL INFO PAGE
 +         ****/
 +
 +        updateInfoPage = function () {
 +            var torrents = data.torrents,
 +                e = data.elements,
 +                fmt = Transmission.fmt,
 +                none = 'None',
 +                mixed = 'Mixed',
 +                unknown = 'Unknown',
 +                isMixed, allPaused, allFinished,
 +                str,
 +                baseline, it, s, i, t,
 +                sizeWhenDone = 0,
 +                leftUntilDone = 0,
 +                available = 0,
 +                haveVerified = 0,
 +                haveUnverified = 0,
 +                verifiedPieces = 0,
 +                stateString,
 +                latest,
 +                pieces,
 +                size,
 +                pieceSize,
 +                creator, mixed_creator,
 +                date, mixed_date,
 +                v, u, f, d, pct,
 +                uri,
 +                now = Date.now();
 +
 +            //
 +            //  state_lb
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                isMixed = false;
 +                allPaused = true;
 +                allFinished = true;
 +
 +                baseline = torrents[0].getStatus();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    it = t.getStatus();
 +                    if (it != baseline) {
 +                        isMixed = true;
 +                    }
 +                    if (!t.isStopped()) {
 +                        allPaused = allFinished = false;
 +                    }
 +                    if (!t.isFinished()) {
 +                        allFinished = false;
 +                    }
 +                }
 +                if (isMixed) {
 +                    str = mixed;
 +                } else if (allFinished) {
 +                    str = 'Finished';
 +                } else if (allPaused) {
 +                    str = 'Paused';
 +                } else {
 +                    str = torrents[0].getStateString();
 +                }
 +            }
 +            setTextContent(e.state_lb, str);
 +            stateString = str;
 +
 +            //
 +            //  have_lb
 +            //
 +
 +            if (torrents.length < 1)
 +                str = none;
 +            else {
 +                baseline = torrents[0].getStatus();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (!t.needsMetaData()) {
 +                        haveUnverified += t.getHaveUnchecked();
 +                        v = t.getHaveValid();
 +                        haveVerified += v;
 +                        if (t.getPieceSize()) {
 +                            verifiedPieces += v / t.getPieceSize();
 +                        }
 +                        sizeWhenDone += t.getSizeWhenDone();
 +                        leftUntilDone += t.getLeftUntilDone();
 +                        available += (t.getHave()) + t.getDesiredAvailable();
 +                    }
 +                }
 +
 +                d = 100.0 * (sizeWhenDone ? (sizeWhenDone - leftUntilDone) / sizeWhenDone : 1);
 +                str = fmt.percentString(d);
 +
 +                if (!haveUnverified && !leftUntilDone) {
 +                    str = fmt.size(haveVerified) + ' (100%)';
 +                } else if (!haveUnverified) {
 +                    str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%)';
 +                } else {
 +                    str = fmt.size(haveVerified) + ' of ' + fmt.size(sizeWhenDone) + ' (' + str + '%), ' + fmt.size(haveUnverified) + ' Unverified';
 +                }
 +            }
 +            setTextContent(e.have_lb, str);
 +
 +            //
 +            //  availability_lb
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else if (sizeWhenDone == 0) {
 +                str = none;
 +            } else {
 +                str = '' + fmt.percentString((100.0 * available) / sizeWhenDone) + '%';
 +            };
 +            setTextContent(e.availability_lb, str);
 +
 +            //
 +            //  downloaded_lb
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                d = f = 0;
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    d += t.getDownloadedEver();
 +                    f += t.getFailedEver();
 +                };
 +                if (f) {
 +                    str = fmt.size(d) + ' (' + fmt.size(f) + ' corrupt)';
 +                } else {
 +                    str = fmt.size(d);
 +                };
 +            };
 +            setTextContent(e.downloaded_lb, str);
 +
 +            //
 +            //  uploaded_lb
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                d = u = 0;
 +                if (torrents.length == 1) {
 +                    d = torrents[0].getDownloadedEver();
 +                    u = torrents[0].getUploadedEver();
 +
 +                    if (d == 0) {
 +                        d = torrents[0].getHaveValid();
 +                    };
 +                } else {
 +                    for (i = 0; t = torrents[i]; ++i) {
 +                        d += t.getDownloadedEver();
 +                        u += t.getUploadedEver();
 +                    };
 +                };
 +                str = fmt.size(u) + ' (Ratio: ' + fmt.ratioString(Math.ratio(u, d)) + ')';
 +            };
 +            setTextContent(e.uploaded_lb, str);
 +
 +            //
 +            // running time
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                allPaused = true;
 +                baseline = torrents[0].getStartDate();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (baseline != t.getStartDate()) {
 +                        baseline = 0;
 +                    }
 +                    if (!t.isStopped()) {
 +                        allPaused = false;
 +                    }
 +                }
 +                if (allPaused) {
 +                    str = stateString; // paused || finished}
 +                } else if (!baseline) {
 +                    str = mixed;
 +                } else {
 +                    str = fmt.timeInterval(now / 1000 - baseline);
 +                }
 +            };
 +
 +            setTextContent(e.running_time_lb, str);
 +
 +            //
 +            // remaining time
 +            //
 +
 +            str = '';
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                baseline = torrents[0].getETA();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (baseline != t.getETA()) {
 +                        str = mixed;
 +                        break;
 +                    }
 +                }
 +            }
 +            if (!str.length) {
 +                if (baseline < 0) {
 +                    str = unknown;
 +                } else {
 +                    str = fmt.timeInterval(baseline);
 +                }
 +            }
 +            setTextContent(e.remaining_time_lb, str);
 +
 +            //
 +            // last activity
 +            //
 +
 +            latest = -1;
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                baseline = torrents[0].getLastActivity();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    d = t.getLastActivity();
 +                    if (latest < d) {
 +                        latest = d;
 +                    };
 +                };
 +                d = now / 1000 - latest; // seconds since last activity
 +                if (d < 0) {
 +                    str = none;
 +                } else if (d < 5) {
 +                    str = 'Active now';
 +                } else {
 +                    str = fmt.timeInterval(d) + ' ago';
 +                };
 +            };
 +            setTextContent(e.last_activity_lb, str);
 +
 +            //
 +            // error
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                str = torrents[0].getErrorString();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (str != t.getErrorString()) {
 +                        str = mixed;
 +                        break;
 +                    };
 +                };
 +            };
 +            setTextContent(e.error_lb, str || none);
 +
 +            //
 +            // size
 +            //
 +
 +            if (torrents.length < 1) {
 +                {
 +                    str = none;
 +                };
 +            } else {
 +                pieces = 0;
 +                size = 0;
 +                pieceSize = torrents[0].getPieceSize();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    pieces += t.getPieceCount();
 +                    size += t.getTotalSize();
 +                    if (pieceSize != t.getPieceSize()) {
 +                        pieceSize = 0;
 +                    }
 +                };
 +                if (!size) {
 +                    str = none;
 +                } else if (pieceSize > 0) {
 +                    str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces @ ' + fmt.mem(pieceSize) + ')';
 +                } else {
 +                    str = fmt.size(size) + ' (' + pieces.toStringWithCommas() + ' pieces)';
 +                };
 +            };
 +            setTextContent(e.size_lb, str);
 +
 +            //
 +            //  hash
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                str = torrents[0].getHashString();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (str != t.getHashString()) {
 +                        str = mixed;
 +                        break;
 +                    };
 +                };
 +            };
 +            setTextContent(e.hash_lb, str);
 +
 +            //
 +            //  privacy
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                baseline = torrents[0].getPrivateFlag();
 +                str = baseline ? 'Private to this tracker -- DHT and PEX disabled' : 'Public torrent';
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (baseline != t.getPrivateFlag()) {
 +                        str = mixed;
 +                        break;
 +                    };
 +                };
 +            };
 +            setTextContent(e.privacy_lb, str);
 +
 +            //
 +            //  comment
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                str = torrents[0].getComment();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (str != t.getComment()) {
 +                        str = mixed;
 +                        break;
 +                    };
 +                };
 +            };
 +            if (!str) {
 +                str = none;
 +            }
 +            uri = parseUri(str);
 +            if (uri.protocol == 'http' || uri.protocol == 'https') {
 +                str = encodeURI(str);
 +                setInnerHTML(e.comment_lb, '<a href="' + str + '" target="_blank" >' + str + '</a>');
 +            } else {
 +                setTextContent(e.comment_lb, str);
 +            };
 +
 +            //
 +            //  origin
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                mixed_creator = false;
 +                mixed_date = false;
 +                creator = torrents[0].getCreator();
 +                date = torrents[0].getDateCreated();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (creator != t.getCreator()) {
 +                        mixed_creator = true;
 +                    };
 +                    if (date != t.getDateCreated()) {
 +                        mixed_date = true;
 +                    };
 +                };
 +                var empty_creator = !creator || !creator.length;
 +                var empty_date = !date;
 +                if (mixed_creator || mixed_date) {
 +                    str = mixed;
 +                } else if (empty_creator && empty_date) {
 +                    str = unknown;
 +                } else if (empty_date && !empty_creator) {
 +                    str = 'Created by ' + creator;
 +                } else if (empty_creator && !empty_date) {
 +                    str = 'Created on ' + (new Date(date * 1000)).toDateString();
 +                } else {
 +                    str = 'Created by ' + creator + ' on ' + (new Date(date * 1000)).toDateString();
 +                };
 +            };
 +            setTextContent(e.origin_lb, str);
 +
 +            //
 +            //  foldername
 +            //
 +
 +            if (torrents.length < 1) {
 +                str = none;
 +            } else {
 +                str = torrents[0].getDownloadDir();
 +                for (i = 0; t = torrents[i]; ++i) {
 +                    if (str != t.getDownloadDir()) {
 +                        str = mixed;
 +                        break;
 +                    };
 +                };
 +            };
 +            setTextContent(e.foldername_lb, str);
 +        },
 +
 +        /****
 +         *****  FILES PAGE
 +         ****/
 +
 +        changeFileCommand = function (fileIndices, command) {
 +            var torrentId = data.file_torrent.getId();
 +            data.controller.changeFileCommand(torrentId, fileIndices, command);
 +        },
 +
 +        onFileWantedToggled = function (ev, fileIndices, want) {
 +            changeFileCommand(fileIndices, want ? 'files-wanted' : 'files-unwanted');
 +        },
 +
 +        onFilePriorityToggled = function (ev, fileIndices, priority) {
 +            var command;
 +            switch (priority) {
 +            case -1:
 +                command = 'priority-low';
 +                break;
 +            case 1:
 +                command = 'priority-high';
 +                break;
 +            default:
 +                command = 'priority-normal';
 +                break;
 +            }
 +            changeFileCommand(fileIndices, command);
 +        },
 +
 +        onNameClicked = function (ev, fileRow, fileIndices) {
 +            $(fileRow.getElement()).siblings().slideToggle();
 +        },
 +
 +        clearFileList = function () {
 +            $(data.elements.file_list).empty();
 +            delete data.file_torrent;
 +            delete data.file_torrent_n;
 +            delete data.file_rows;
 +        },
 +
 +        createFileTreeModel = function (tor) {
 +            var i, j, n, name, tokens, walk, tree, token, sub,
 +                leaves = [],
 +                tree = {
 +                    children: {},
 +                    file_indices: []
 +                };
 +
 +            n = tor.getFileCount();
 +            for (i = 0; i < n; ++i) {
 +                name = tor.getFile(i).name;
 +                tokens = name.split('/');
 +                walk = tree;
 +                for (j = 0; j < tokens.length; ++j) {
 +                    token = tokens[j];
 +                    sub = walk.children[token];
 +                    if (!sub) {
 +                        walk.children[token] = sub = {
 +                            name: token,
 +                            parent: walk,
 +                            children: {},
 +                            file_indices: [],
 +                            depth: j
 +                        };
 +                    }
 +                    walk = sub;
 +                }
 +                walk.file_index = i;
 +                delete walk.children;
 +                leaves.push(walk);
 +            }
 +
 +            for (i = 0; i < leaves.length; ++i) {
 +                walk = leaves[i];
 +                j = walk.file_index;
 +                do {
 +                    walk.file_indices.push(j);
 +                    walk = walk.parent;
 +                } while (walk);
 +            }
 +
 +            return tree;
 +        },
 +
 +        addNodeToView = function (tor, parent, sub, i) {
 +            var row;
 +            row = new FileRow(tor, sub.depth, sub.name, sub.file_indices, i % 2);
 +            data.file_rows.push(row);
 +            parent.appendChild(row.getElement());
 +            $(row).bind('wantedToggled', onFileWantedToggled);
 +            $(row).bind('priorityToggled', onFilePriorityToggled);
 +            $(row).bind('nameClicked', onNameClicked);
 +        },
 +
 +        addSubtreeToView = function (tor, parent, sub, i) {
 +            var key, div;
 +            div = document.createElement('div');
 +            if (sub.parent) {
 +                addNodeToView(tor, div, sub, i++);
 +            }
 +            if (sub.children) {
 +                for (key in sub.children) {
 +                    i = addSubtreeToView(tor, div, sub.children[key]);
 +                }
 +            }
 +            parent.appendChild(div);
 +            return i;
 +        },
 +
 +        updateFilesPage = function () {
 +            var i, n, tor, fragment, tree,
 +                file_list = data.elements.file_list,
 +                torrents = data.torrents;
 +
 +            // only show one torrent at a time
 +            if (torrents.length !== 1) {
 +                clearFileList();
 +                return;
 +            }
 +
 +            tor = torrents[0];
 +            n = tor ? tor.getFileCount() : 0;
 +            if (tor != data.file_torrent || n != data.file_torrent_n) {
 +                // rebuild the file list...
 +                clearFileList();
 +                data.file_torrent = tor;
 +                data.file_torrent_n = n;
 +                data.file_rows = [];
 +                fragment = document.createDocumentFragment();
 +                tree = createFileTreeModel(tor);
 +                addSubtreeToView(tor, fragment, tree, 0);
 +                file_list.appendChild(fragment);
 +            } else {
 +                // ...refresh the already-existing file list
 +                for (i = 0, n = data.file_rows.length; i < n; ++i)
 +                    data.file_rows[i].refresh();
 +            }
 +        },
 +
 +        /****
 +         *****  PEERS PAGE
 +         ****/
 +
 +        updatePeersPage = function () {
 +            var i, k, tor, peers, peer, parity,
 +                html = [],
 +                fmt = Transmission.fmt,
 +                peers_list = data.elements.peers_list,
 +                torrents = data.torrents;
 +
 +            for (k = 0; tor = torrents[k]; ++k) {
 +                peers = tor.getPeers();
 +                html.push('<div class="inspector_group">');
 +                if (torrents.length > 1) {
 +                    html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
 +                }
 +                if (!peers || !peers.length) {
 +                    html.push('<br></div>'); // firefox won't paint the top border if the div is empty
 +                    continue;
 +                }
 +                html.push('<table class="peer_list">',
 +                    '<tr class="inspector_peer_entry even">',
 +                    '<th class="encryptedCol"></th>',
 +                    '<th class="upCol">Up</th>',
 +                    '<th class="downCol">Down</th>',
 +                    '<th class="percentCol">%</th>',
 +                    '<th class="statusCol">Status</th>',
 +                    '<th class="addressCol">Address</th>',
 +                    '<th class="clientCol">Client</th>',
 +                    '</tr>');
 +                for (i = 0; peer = peers[i]; ++i) {
 +                    parity = (i % 2) ? 'odd' : 'even';
 +                    html.push('<tr class="inspector_peer_entry ', parity, '">',
 +                        '<td>', (peer.isEncrypted ? '<div class="encrypted-peer-cell" title="Encrypted Connection">' : '<div class="unencrypted-peer-cell">'), '</div>', '</td>',
 +                        '<td>', (peer.rateToPeer ? fmt.speedBps(peer.rateToPeer) : ''), '</td>',
 +                        '<td>', (peer.rateToClient ? fmt.speedBps(peer.rateToClient) : ''), '</td>',
 +                        '<td class="percentCol">', Math.floor(peer.progress * 100), '%', '</td>',
 +                        '<td>', fmt.peerStatus(peer.flagStr), '</td>',
 +                        '<td>', sanitizeText(peer.address), '</td>',
 +                        '<td class="clientCol">', sanitizeText(peer.clientName), '</td>',
 +                        '</tr>');
 +                }
 +                html.push('</table></div>');
 +            }
 +
 +            setInnerHTML(peers_list, html.join(''));
 +        },
 +
 +        /****
 +         *****  TRACKERS PAGE
 +         ****/
 +
 +        getAnnounceState = function (tracker) {
 +            var timeUntilAnnounce, s = '';
 +            switch (tracker.announceState) {
 +            case Torrent._TrackerActive:
 +                s = 'Announce in progress';
 +                break;
 +            case Torrent._TrackerWaiting:
 +                timeUntilAnnounce = tracker.nextAnnounceTime - ((new Date()).getTime() / 1000);
 +                if (timeUntilAnnounce < 0) {
 +                    timeUntilAnnounce = 0;
 +                }
 +                s = 'Next announce in ' + Transmission.fmt.timeInterval(timeUntilAnnounce);
 +                break;
 +            case Torrent._TrackerQueued:
 +                s = 'Announce is queued';
 +                break;
 +            case Torrent._TrackerInactive:
 +                s = tracker.isBackup ?
 +                    'Tracker will be used as a backup' :
 +                    'Announce not scheduled';
 +                break;
 +            default:
 +                s = 'unknown announce state: ' + tracker.announceState;
 +            }
 +            return s;
 +        },
 +
 +        lastAnnounceStatus = function (tracker) {
 +
 +            var lastAnnounceLabel = 'Last Announce',
 +                lastAnnounce = ['N/A'],
 +                lastAnnounceTime;
 +
 +            if (tracker.hasAnnounced) {
 +                lastAnnounceTime = Transmission.fmt.timestamp(tracker.lastAnnounceTime);
 +                if (tracker.lastAnnounceSucceeded) {
 +                    lastAnnounce = [lastAnnounceTime, ' (got ', Transmission.fmt.countString('peer', 'peers', tracker.lastAnnouncePeerCount), ')'];
 +                } else {
 +                    lastAnnounceLabel = 'Announce error';
 +                    lastAnnounce = [(tracker.lastAnnounceResult ? (tracker.lastAnnounceResult + ' - ') : ''), lastAnnounceTime];
 +                }
 +            }
 +            return {
 +                'label': lastAnnounceLabel,
 +                'value': lastAnnounce.join('')
 +            };
 +        },
 +
 +        lastScrapeStatus = function (tracker) {
 +
 +            var lastScrapeLabel = 'Last Scrape',
 +                lastScrape = 'N/A',
 +                lastScrapeTime;
 +
 +            if (tracker.hasScraped) {
 +                lastScrapeTime = Transmission.fmt.timestamp(tracker.lastScrapeTime);
 +                if (tracker.lastScrapeSucceeded) {
 +                    lastScrape = lastScrapeTime;
 +                } else {
 +                    lastScrapeLabel = 'Scrape error';
 +                    lastScrape = (tracker.lastScrapeResult ? tracker.lastScrapeResult + ' - ' : '') + lastScrapeTime;
 +                }
 +            }
 +            return {
 +                'label': lastScrapeLabel,
 +                'value': lastScrape
 +            };
 +        },
 +
 +        updateTrackersPage = function () {
 +            var i, j, tier, tracker, trackers, tor,
 +                html, parity, lastAnnounceStatusHash,
 +                announceState, lastScrapeStatusHash,
 +                na = 'N/A',
 +                trackers_list = data.elements.trackers_list,
 +                torrents = data.torrents;
 +
 +            // By building up the HTML as as string, then have the browser
 +            // turn this into a DOM tree, this is a fast operation.
 +            html = [];
 +            for (i = 0; tor = torrents[i]; ++i) {
 +                html.push('<div class="inspector_group">');
 +
 +                if (torrents.length > 1) {
-                     html.push('<div class="inspector_torrent_label">', tor.getName(), '</div>');
++                    html.push('<div class="inspector_torrent_label">', sanitizeText(tor.getName()), '</div>');
 +                }
 +
 +                tier = -1;
 +                trackers = tor.getTrackers();
 +                for (j = 0; tracker = trackers[j]; ++j) {
 +                    if (tier != tracker.tier) {
 +                        if (tier !== -1) { // close previous tier
 +                            html.push('</ul></div>');
 +                        }
 +
 +                        tier = tracker.tier;
 +
 +                        html.push('<div class="inspector_group_label">',
 +                            'Tier ', tier + 1, '</div>',
 +                            '<ul class="tier_list">');
 +                    }
 +
 +                    // Display construction
 +                    lastAnnounceStatusHash = lastAnnounceStatus(tracker);
 +                    announceState = getAnnounceState(tracker);
 +                    lastScrapeStatusHash = lastScrapeStatus(tracker);
 +                    parity = (j % 2) ? 'odd' : 'even';
 +                    html.push('<li class="inspector_tracker_entry ', parity, '"><div class="tracker_host" title="', sanitizeText(tracker.announce), '">',
 +                        sanitizeText(tracker.host || tracker.announce), '</div>',
 +                        '<div class="tracker_activity">',
-                         '<div>', lastAnnounceStatusHash['label'], ': ', lastAnnounceStatusHash['value'], '</div>',
++                        '<div>', lastAnnounceStatusHash['label'], ': ', sanitizeText(lastAnnounceStatusHash['value']), '</div>',
 +                        '<div>', announceState, '</div>',
-                         '<div>', lastScrapeStatusHash['label'], ': ', lastScrapeStatusHash['value'], '</div>',
++                        '<div>', lastScrapeStatusHash['label'], ': ', sanitizeText(lastScrapeStatusHash['value']), '</div>',
 +                        '</div><table class="tracker_stats">',
 +                        '<tr><th>Seeders:</th><td>', (tracker.seederCount > -1 ? tracker.seederCount : na), '</td></tr>',
 +                        '<tr><th>Leechers:</th><td>', (tracker.leecherCount > -1 ? tracker.leecherCount : na), '</td></tr>',
 +                        '<tr><th>Downloads:</th><td>', (tracker.downloadCount > -1 ? tracker.downloadCount : na), '</td></tr>',
 +                        '</table></li>');
 +                }
 +                if (tier !== -1) { // close last tier
 +                    html.push('</ul></div>');
 +                }
 +
 +                html.push('</div>'); // inspector_group
 +            }
 +
 +            setInnerHTML(trackers_list, html.join(''));
 +        },
 +
 +        initialize = function (controller) {
 +
 +            var ti = '#torrent_inspector_';
 +
 +            data.controller = controller;
 +
 +            $('.inspector-tab').click(onTabClicked);
 +
 +            data.elements.info_page = $('#inspector-page-info')[0];
 +            data.elements.files_page = $('#inspector-page-files')[0];
 +            data.elements.peers_page = $('#inspector-page-peers')[0];
 +            data.elements.trackers_page = $('#inspector-page-trackers')[0];
 +
 +            data.elements.file_list = $('#inspector_file_list')[0];
 +            data.elements.peers_list = $('#inspector_peers_list')[0];
 +            data.elements.trackers_list = $('#inspector_trackers_list')[0];
 +
 +            data.elements.have_lb = $('#inspector-info-have')[0];
 +            data.elements.availability_lb = $('#inspector-info-availability')[0];
 +            data.elements.downloaded_lb = $('#inspector-info-downloaded')[0];
 +            data.elements.uploaded_lb = $('#inspector-info-uploaded')[0];
 +            data.elements.state_lb = $('#inspector-info-state')[0];
 +            data.elements.running_time_lb = $('#inspector-info-running-time')[0];
 +            data.elements.remaining_time_lb = $('#inspector-info-remaining-time')[0];
 +            data.elements.last_activity_lb = $('#inspector-info-last-activity')[0];
 +            data.elements.error_lb = $('#inspector-info-error')[0];
 +            data.elements.size_lb = $('#inspector-info-size')[0];
 +            data.elements.foldername_lb = $('#inspector-info-location')[0];
 +            data.elements.hash_lb = $('#inspector-info-hash')[0];
 +            data.elements.privacy_lb = $('#inspector-info-privacy')[0];
 +            data.elements.origin_lb = $('#inspector-info-origin')[0];
 +            data.elements.comment_lb = $('#inspector-info-comment')[0];
 +            data.elements.name_lb = $('#torrent_inspector_name')[0];
 +
 +            // force initial 'N/A' updates on all the pages
 +            updateInspector();
 +            updateInfoPage();
 +            updatePeersPage();
 +            updateTrackersPage();
 +            updateFilesPage();
 +        };
 +
 +    /****
 +     *****  PUBLIC FUNCTIONS
 +     ****/
 +
 +    this.setTorrents = function (torrents) {
 +        var d = data,
 +            that = this;
 +
 +        // update the inspector when a selected torrent's data changes.
 +        $(d.torrents).unbind('dataChanged.inspector');
 +        $(torrents).bind('dataChanged.inspector', $.proxy(updateInspector, this));
 +        d.torrents = torrents;
 +
 +        // periodically ask for updates to the inspector's torrents
 +        clearTimeout(d.refreshTimeout);
 +
 +        function callback() {
 +            refreshTorrents(rescheduleTimeout);
 +        }
 +
 +        function rescheduleTimeout() {
 +            d.refreshTimeout = setTimeout(callback, 2000);
 +        }
 +
 +        rescheduleTimeout();
 +        refreshTorrents();
 +
 +        // refresh the inspector's UI
 +        updateInspector();
 +    };
 +
 +    initialize(controller);
  };