(trunk web) in Transmission.refilter(), only refilter/resort the torrents that have changed since the last update. This makes the web client scale reasonably well even up to 1000s of torrents.

This commit is contained in:
Jordan Lee
2011-08-27 21:35:19 +00:00
parent ee34f2a137
commit 4c1f32836f
4 changed files with 198 additions and 76 deletions

View File

@@ -208,7 +208,7 @@
</div> </div>
<div id="torrent_container"> <div id="torrent_container">
<ul class="torrent_list" id="torrent_list"><li style="display: none;"></li></ul> <ul class="torrent_list" id="torrent_list"></ul>
</div> </div>
<div class="dialog_container" id="dialog_container" style="display:none;"> <div class="dialog_container" id="dialog_container" style="display:none;">

View File

@@ -203,7 +203,6 @@ Prefs._SortByQueue = 'queue_order';
Prefs._SortByProgress = 'percent_completed'; Prefs._SortByProgress = 'percent_completed';
Prefs._SortByRatio = 'ratio'; Prefs._SortByRatio = 'ratio';
Prefs._SortByState = 'state'; Prefs._SortByState = 'state';
Prefs._SortByTracker = 'tracker';
Prefs._TurtleState = 'turtle-state'; Prefs._TurtleState = 'turtle-state';
Prefs._CompactDisplayState= 'compact_display_state'; Prefs._CompactDisplayState= 'compact_display_state';

View File

@@ -95,7 +95,6 @@ Torrent.Fields.InfoExtra = [
// fields used in the inspector which need to be periodically refreshed // fields used in the inspector which need to be periodically refreshed
Torrent.Fields.StatsExtra = [ Torrent.Fields.StatsExtra = [
'activityDate',
'desiredAvailable', 'desiredAvailable',
'downloadDir', 'downloadDir',
'downloadedEver', 'downloadedEver',
@@ -124,10 +123,10 @@ Torrent.prototype =
setField: function(o, name, value) setField: function(o, name, value)
{ {
var changed = !(name in o) || (o[name] !== value); if (o[name] === value)
if (changed) return false;
o[name] = value; o[name] = value;
return changed; return true;
}, },
// fields.files is an array of unions of RPC's "files" and "fileStats" objects. // fields.files is an array of unions of RPC's "files" and "fileStats" objects.
@@ -181,7 +180,7 @@ Torrent.prototype =
refresh: function(data) refresh: function(data)
{ {
if (this.refreshFields(data)) if (this.refreshFields(data))
$(this).trigger('dataChanged'); $(this).trigger('dataChanged', this);
}, },
/**** /****
@@ -239,13 +238,7 @@ Torrent.prototype =
needsMetaData: function(){ return this.getMetadataPercentComplete() < 1; }, needsMetaData: function(){ return this.getMetadataPercentComplete() < 1; },
getActivity: function() { return this.getDownloadSpeed() + this.getUploadSpeed(); }, getActivity: function() { return this.getDownloadSpeed() + this.getUploadSpeed(); },
getPercentDoneStr: function() { return Transmission.fmt.percentString(100*this.getPercentDone()); }, getPercentDoneStr: function() { return Transmission.fmt.percentString(100*this.getPercentDone()); },
getPercentDone: function() { getPercentDone: function() { return this.fields.percentDone; },
var finalSize = this.getSizeWhenDone();
if (!finalSize) return 1.0;
var left = this.getLeftUntilDone();
if (!left) return 1.0;
return (finalSize - left) / finalSize;
},
getStateString: function() { getStateString: function() {
switch(this.getStatus()) { switch(this.getStatus()) {
case Torrent._StatusStopped: return this.isFinished() ? 'Seeding complete' : 'Paused'; case Torrent._StatusStopped: return this.isFinished() ? 'Seeding complete' : 'Paused';
@@ -398,6 +391,41 @@ Torrent.compareByProgress = function(ta, tb)
return (a - b) || Torrent.compareByRatio(ta, tb); return (a - b) || Torrent.compareByRatio(ta, tb);
}; };
Torrent.compareTorrents = function(a, b, sortMethod, sortDirection)
{
var i;
switch(sortMethod)
{
case Prefs._SortByActivity:
i = Torrent.compareByActivity(a,b);
break;
case Prefs._SortByAge:
i = Torrent.compareByAge(a,b);
break;
case Prefs._SortByQueue:
i = Torrent.compareByQueue(a,b);
break;
case Prefs._SortByProgress:
i = Torrent.compareByProgress(a,b);
break;
case Prefs._SortByState:
i = Torrent.compareByState(a,b);
break;
case Prefs._SortByRatio:
i = Torrent.compareByRatio(a,b);
break;
default:
i = Torrent.compareByName(a,b);
break;
}
if (sortDirection === Prefs._SortDescending)
i = -i;
return i;
};
/** /**
* @param torrents an array of Torrent objects * @param torrents an array of Torrent objects
* @param sortMethod one of Prefs._SortBy* * @param sortMethod one of Prefs._SortBy*

View File

@@ -25,9 +25,10 @@ Transmission.prototype =
this.remote = new TransmissionRemote(this); this.remote = new TransmissionRemote(this);
// Initialize the implementation fields // Initialize the implementation fields
this._current_search = ''; this.filterText = '';
this._torrents = { }; this._torrents = { };
this._rows = [ ]; this._rows = [ ];
this.dirtyTorrents = { };
// Initialize the clutch preferences // Initialize the clutch preferences
Prefs.getClutchPrefs(this); Prefs.getClutchPrefs(this);
@@ -65,6 +66,9 @@ Transmission.prototype =
// tell jQuery to copy the dataTransfer property from events over if it exists // tell jQuery to copy the dataTransfer property from events over if it exists
jQuery.event.props.push("dataTransfer"); jQuery.event.props.push("dataTransfer");
$(document).delegate('#torrent_list > li', 'click', function(ev) {tr.setSelectedRow(ev.currentTarget.row);});
$(document).delegate('#torrent_list > li', 'dblclick', function(e) {tr.toggleInspector();});
$('#torrent_upload_form').submit(function() { $('#upload_confirm_button').click(); return false; }); $('#torrent_upload_form').submit(function() { $('#upload_confirm_button').click(); return false; });
if (iPhone) { if (iPhone) {
@@ -233,7 +237,9 @@ Transmission.prototype =
{ {
var tr = this; var tr = this;
var search_box = $('#torrent_search'); var search_box = $('#torrent_search');
search_box.bind('keyup click', function() {tr.setSearch(this.value);}); search_box.bind('keyup click', function() {
tr.setFilterText(this.value);
});
if (!$.browser.safari) if (!$.browser.safari)
{ {
search_box.addClass('blur'); search_box.addClass('blur');
@@ -242,7 +248,7 @@ Transmission.prototype =
if (this.value == '') { if (this.value == '') {
$(this).addClass('blur'); $(this).addClass('blur');
this.value = 'Filter'; this.value = 'Filter';
tr.setSearch(null); tr.setFilterText(null);
} }
}).bind('focus', function() { }).bind('focus', function() {
if ($(this).is('.blur')) { if ($(this).is('.blur')) {
@@ -915,19 +921,19 @@ Transmission.prototype =
$('#stats_total_duration').html(fmt.timeInterval(t.secondsActive)); $('#stats_total_duration').html(fmt.timeInterval(t.secondsActive));
}, },
setSearch: function(search) { setFilterText: function(search) {
this._current_search = search ? search.trim() : null; this.filterText = search ? search.trim() : null;
this.refilter(); this.refilter(true);
}, },
setSortMethod: function(sort_method) { setSortMethod: function(sort_method) {
this.setPref(Prefs._SortMethod, sort_method); this.setPref(Prefs._SortMethod, sort_method);
this.refilter(); this.refilter(true);
}, },
setSortDirection: function(direction) { setSortDirection: function(direction) {
this.setPref(Prefs._SortDirection, direction); this.setPref(Prefs._SortDirection, direction);
this.refilter(); this.refilter(true);
}, },
/* /*
@@ -1035,8 +1041,14 @@ Transmission.prototype =
}, },
onTorrentChanged: function(ev) onTorrentChanged: function(tor)
{ {
var id = tor.getId();
// update our dirty fields
this.dirtyTorrents[id] = true;
// enqueue a filter refresh
this.refilterSoon(); this.refilterSoon();
// if this torrent is in the inspector, refresh the inspector // if this torrent is in the inspector, refresh the inspector
@@ -1057,7 +1069,8 @@ Transmission.prototype =
else { else {
var tr = this; var tr = this;
t = tr._torrents[id] = new Torrent(o); t = tr._torrents[id] = new Torrent(o);
$(t).bind('dataChanged',function(ev) {tr.onTorrentChanged(ev);}); this.dirtyTorrents[id] = true;
$(t).bind('dataChanged',function(ev,tor) {tr.onTorrentChanged(tor);});
if(!('name' in t.fields) || !('status' in t.fields)) // missing some fields... if(!('name' in t.fields) || !('status' in t.fields)) // missing some fields...
needinfo.push(id); needinfo.push(id);
} }
@@ -1952,71 +1965,145 @@ Transmission.prototype =
return true; return true;
}, },
refilter: function() sortRows: function(rows)
{ {
var i, tor, row,
id2row = {},
torrents = [];
for (i=0; row=rows[i]; ++i) {
tor = row.getTorrent();
torrents.push(tor);
id2row[ tor.getId() ] = row;
}
Torrent.sortTorrents(torrents, this[Prefs._SortMethod],
this[Prefs._SortDirection]);
for (i=0; tor=torrents[i]; ++i)
rows[i] = id2row[ tor.getId() ];
},
refilter: function(rebuildEverything)
{
var i, id, t, row, sel;
clearTimeout(this.refilterTimer); clearTimeout(this.refilterTimer);
delete this.refilterTimer; delete this.refilterTimer;
// make a filtered, sorted array of our torrents // get a temporary lookup table of selected torrent ids
var filter_mode = this[Prefs._FilterMode]; sel = { };
var filter_text = this._current_search; for (i=0; row=this._rows[i]; ++i)
var filter_tracker = this.filterTracker; if (row.isSelected())
var keep = $.grep(this.getAllTorrents(), function(t) { sel[row.getTorrentId()] = row;
return t.test(filter_mode, filter_text, filter_tracker);
if (rebuildEverything)
for (id in this._torrents)
this.dirtyTorrents[id] = ~0;
// rows that overlap with dirtyTorrents need to be refiltered.
// those that don't are 'clean' and don't need refiltering.
var dirty_rows = [];
var clean_rows = [];
for (i=0; row=this._rows[i]; ++i) {
if(row.getTorrentId() in this.dirtyTorrents)
dirty_rows.push(row);
else
clean_rows.push(row);
}
// remove the dirty rows from the dom
var elementsToRemove = $.map(dirty_rows.slice(0), function(r) {
return r.getElement();
}); });
Torrent.sortTorrents(keep, this[Prefs._SortMethod], this[Prefs._SortDirection]); $(elementsToRemove).remove();
// maybe rebuild the rows // drop any dirty rows that don't pass the filter test
if (this._force_refilter || !this.matchesTorrentList(keep)) var tmp = [];
var filter_mode = this[Prefs._FilterMode];
var filter_text = this.filterText;
var filter_tracker = this.filterTracker;
for (i=0; row=dirty_rows[i]; ++i) {
t = row.getTorrent();
if (t.test(filter_mode, filter_text, filter_tracker))
tmp.push(row);
delete this.dirtyTorrents[t.getId()];
}
dirty_rows = tmp;
// make new rows for dirty torrents that pass the filter test
// but don't already have a row
var renderer = this.torrentRenderer;
for (id in this.dirtyTorrents) {
t = this._torrents[id];
if (t.test(filter_mode, filter_text, filter_tracker)) {
var is_selected = t.getId() in sel;
row = new TorrentRow(renderer, this, t, is_selected);
row.getElement().row = row;
dirty_rows.push(row);
}
}
// sort the dirty rows
this.sortRows (dirty_rows);
// now we have two sorted arrays of rows
// and can do a simple two-way sorted merge.
var rows = []
var ci=0, cmax=clean_rows.length;
var di=0, dmax=dirty_rows.length;
var sort_method = this[Prefs._SortMethod];
var sort_direction = this[Prefs._SortDirection];
var list = this._torrent_list;
while (ci!=cmax || di!=dmax)
{ {
var old_sel = this.getSelectedTorrents(); var push_clean;
var new_sel_count = 0;
// make the new rows if (ci==cmax)
var tr = this; push_clean = false;
var rows = [ ]; else if (di==dmax)
var fragment = document.createDocumentFragment(); push_clean = true;
var renderer = this.torrentRenderer; else {
for (var i=0, tor; tor=keep[i]; ++i) var ctor = clean_rows[ci].getTorrent();
{ var dtor = dirty_rows[di].getTorrent();
var is_selected = old_sel.indexOf(tor) !== -1; var c = Torrent.compareTorrents(ctor, dtor, sort_method, sort_direction);
var row = new TorrentRow(renderer, this, tor, is_selected); push_clean = (c < 0);
row.setEven((i+1) % 2 == 0); }
if (is_selected)
new_sel_count++; if (push_clean)
if (!iPhone) { rows.push(clean_rows[ci++]);
var b = row.getToggleRunningButton(); else {
if (b) var row = dirty_rows[di++];
$(b).click({r:row}, function(ev) {tr.onToggleRunningClicked(ev);}); var e = row.getElement();
} if (ci !== cmax)
$(row.getElement()).click({r: row}, function(ev) {tr.onRowClicked(ev,ev.data.r);}) list.insertBefore(e, clean_rows[ci].getElement());
.dblclick(function() {tr.toggleInspector();}); else
fragment.appendChild(row.getElement()); list.appendChild(e);
rows.push(row); rows.push(row);
} }
$('ul.torrent_list').empty();
delete this._rows;
this._rows = rows;
this._torrent_list.appendChild(fragment);
if (old_sel.length !== new_sel_count)
this.selectionChanged();
delete this._force_refilter;
} }
// update our implementation fields
this._rows = rows;
this.dirtyTorrents = { };
// jquery's even/odd starts with 1 rather than 0, so invert the logic here
var elements = $.map(rows.slice(0), function(r){return r.getElement();});
$(elements).filter(":odd").addClass('even');
$(elements).filter(":even").removeClass('even');
// sync gui // sync gui
this.updateStatusbar(); this.updateStatusbar();
this.refreshFilterButton(); this.refreshFilterButton();
}, },
setFilter: function(mode) setFilterMode: function(mode)
{ {
// set the state // set the state
this.setPref(Prefs._FilterMode, mode); this.setPref(Prefs._FilterMode, mode);
// refilter // refilter
this.refilter(); this.refilter(true);
}, },
refreshFilterPopup: function() refreshFilterPopup: function()
@@ -2051,7 +2138,10 @@ Transmission.prototype =
div.innerHTML = '<span class="filter-img"></span>' div.innerHTML = '<span class="filter-img"></span>'
+ '<span class="filter-name">' + tr.getStateString(s) + '</span>' + '<span class="filter-name">' + tr.getStateString(s) + '</span>'
+ '<span class="count">' + counts[s] + '</span>'; + '<span class="count">' + counts[s] + '</span>';
$(div).click({'state':s}, function(ev) { tr.setFilter(ev.data.state); $('#filter-popup').dialog('close');}); $(div).click({'state':s}, function(ev) {
tr.setFilterMode(ev.data.state);
$('#filter-popup').dialog('close');
});
fragment.appendChild(div); fragment.appendChild(div);
} }
$('#filter-by-state .row').remove(); $('#filter-by-state .row').remove();
@@ -2071,7 +2161,10 @@ Transmission.prototype =
div.innerHTML = '<span class="filter-img"></span>' div.innerHTML = '<span class="filter-img"></span>'
+ '<span class="filter-name">All</span>' + '<span class="filter-name">All</span>'
+ '<span class="count">' + torrents.length + '</span>'; + '<span class="count">' + torrents.length + '</span>';
$(div).click(function() {tr.setFilterTracker(null); $('#filter-popup').dialog('close');}) $(div).click(function() {
tr.setFilterTracker(null);
$('#filter-popup').dialog('close');
});
fragment.appendChild(div); fragment.appendChild(div);
for (var i=0, name; name=names[i]; ++i) { for (var i=0, name; name=names[i]; ++i) {
var div = document.createElement('div'); var div = document.createElement('div');
@@ -2081,7 +2174,10 @@ Transmission.prototype =
div.innerHTML = '<img class="filter-img" src="http://'+o.domain+'/favicon.ico"/>' div.innerHTML = '<img class="filter-img" src="http://'+o.domain+'/favicon.ico"/>'
+ '<span class="filter-name">'+ name + '</span>' + '<span class="filter-name">'+ name + '</span>'
+ '<span class="count">'+ o.count + '</span>'; + '<span class="count">'+ o.count + '</span>';
$(div).click({domain:o.domain}, function(ev) { tr.setFilterTracker(ev.data.domain); $('#filter-popup').dialog('close');}); $(div).click({domain:o.domain}, function(ev) {
tr.setFilterTracker(ev.data.domain);
$('#filter-popup').dialog('close');
});
fragment.appendChild(div); fragment.appendChild(div);
} }
$('#filter-by-tracker .row').remove(); $('#filter-by-tracker .row').remove();
@@ -2110,7 +2206,7 @@ Transmission.prototype =
var id = '#show-tracker-' + key; var id = '#show-tracker-' + key;
$(id).addClass('selected').siblings().removeClass('selected'); $(id).addClass('selected').siblings().removeClass('selected');
this.refilterSoon(); this.refilter(true);
}, },
/* example: "tracker.ubuntu.com" returns "ubuntu.com" */ /* example: "tracker.ubuntu.com" returns "ubuntu.com" */
@@ -2200,7 +2296,6 @@ Transmission.prototype =
// update the ui: torrent list // update the ui: torrent list
this.torrentRenderer = compact ? new TorrentRendererCompact() this.torrentRenderer = compact ? new TorrentRendererCompact()
: new TorrentRendererFull(); : new TorrentRendererFull();
this._force_refilter = true; this.refilter(true);
this.refilter();
} }
}; };