• Home
  • History
  • Annotate
  • Line#
  • Navigate
  • Raw
  • Download
  • only in /netgear-R7000-V1.0.7.12_1.2.5/ap/gpl/transmission/transmission-2.73/web/javascript/
1/**
2 * Copyright �� Jordan Lee, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer
3 *
4 * This file is licensed under the GPLv2.
5 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6 */
7
8function Transmission()
9{
10	this.initialize();
11}
12
13Transmission.prototype =
14{
15	/****
16	*****
17	*****  STARTUP
18	*****
19	****/
20
21	initialize: function()
22	{
23		var e;
24
25		// Initialize the helper classes
26		this.remote = new TransmissionRemote(this);
27		this.inspector = new Inspector(this, this.remote);
28		this.prefsDialog = new PrefsDialog(this.remote);
29		$(this.prefsDialog).bind('closed', $.proxy(this.onPrefsDialogClosed,this));
30
31		this.isMenuEnabled = !isMobileDevice;
32
33		// Initialize the implementation fields
34		this.filterText    = '';
35		this._torrents     = {};
36		this._rows         = [];
37		this.dirtyTorrents = {};
38		this.uriCache      = {};
39
40		// Initialize the clutch preferences
41		Prefs.getClutchPrefs(this);
42
43		// Set up user events
44		$(".numberinput").forceNumeric();
45		$('#toolbar-pause').click($.proxy(this.stopSelectedClicked,this));
46		$('#toolbar-start').click($.proxy(this.startSelectedClicked,this));
47		$('#toolbar-pause-all').click($.proxy(this.stopAllClicked,this));
48		$('#toolbar-start-all').click($.proxy(this.startAllClicked,this));
49		$('#toolbar-remove').click($.proxy(this.removeClicked,this));
50		$('#toolbar-open').click($.proxy(this.openTorrentClicked,this));
51
52		$('#prefs-button').click($.proxy(this.togglePrefsDialogClicked,this));
53
54		$('#upload_confirm_button').click($.proxy(this.confirmUploadClicked,this));
55		$('#upload_cancel_button').click($.proxy(this.hideUploadDialog,this));
56
57		$('#move_confirm_button').click($.proxy(this.confirmMoveClicked,this));
58		$('#move_cancel_button').click($.proxy(this.hideMoveDialog,this));
59
60		$('#turtle-button').click($.proxy(this.toggleTurtleClicked,this));
61		$('#compact-button').click($.proxy(this.toggleCompactClicked,this));
62
63		// tell jQuery to copy the dataTransfer property from events over if it exists
64		jQuery.event.props.push("dataTransfer");
65
66		$('#torrent_upload_form').submit(function() { $('#upload_confirm_button').click(); return false; });
67
68		$('#toolbar-inspector').click($.proxy(this.toggleInspector,this));
69
70		e = $('#filter-mode');
71		e.val(this[Prefs._FilterMode]);
72		e.change($.proxy(this.onFilterModeClicked,this));
73		$('#filter-tracker').change($.proxy(this.onFilterTrackerClicked,this));
74
75		if (!isMobileDevice) {
76			$(document).bind('keydown', $.proxy(this.keyDown,this) );
77			$(document).bind('keyup', $.proxy(this.keyUp, this) );
78			$('#torrent_container').click( $.proxy(this.deselectAll,this) );
79			$('#torrent_container').bind('dragover', $.proxy(this.dragenter,this));
80			$('#torrent_container').bind('dragenter', $.proxy(this.dragenter,this));
81			$('#torrent_container').bind('drop', $.proxy(this.drop,this));
82			$('#inspector_link').click( $.proxy(this.toggleInspector,this) );
83
84			this.setupSearchBox();
85			this.createContextMenu();
86		}
87
88		if (this.isMenuEnabled)
89			this.createSettingsMenu();
90
91		e = {};
92		e.torrent_list              = $('#torrent_list')[0];
93		e.toolbar_buttons           = $('#toolbar ul li');
94		e.toolbar_pause_button      = $('#toolbar-pause')[0];
95		e.toolbar_start_button      = $('#toolbar-start')[0];
96		e.toolbar_remove_button     = $('#toolbar-remove')[0];
97		this.elements = e;
98
99		// Apply the prefs settings to the gui
100		this.initializeSettings();
101
102		// Get preferences & torrents from the daemon
103		var async = false;
104		this.loadDaemonPrefs(async);
105		this.loadDaemonStats(async);
106		this.initializeTorrents();
107		this.refreshTorrents();
108		this.togglePeriodicSessionRefresh(true);
109
110		this.updateButtonsSoon();
111	},
112
113	loadDaemonPrefs: function(async) {
114		this.remote.loadDaemonPrefs(function(data) {
115			var o = data['arguments'];
116			Prefs.getClutchPrefs(o);
117			this.updateGuiFromSession(o);
118			this.sessionProperties = o;
119		}, this, async);
120	},
121
122	loadImages: function() {
123		for (var i=0, row; row=arguments[i]; ++i)
124			jQuery("<img>").attr("src", row);
125	},
126
127	/*
128	 * Load the clutch prefs and init the GUI according to those prefs
129	 */
130	initializeSettings: function()
131	{
132		Prefs.getClutchPrefs(this);
133
134		if (this.isMenuEnabled)
135		{
136			$('#sort_by_' + this[Prefs._SortMethod]).selectMenuItem();
137
138			if (this[Prefs._SortDirection] === Prefs._SortDescending)
139				$('#reverse_sort_order').selectMenuItem();
140		}
141
142		this.initCompactMode();
143	},
144
145	/*
146	 * Set up the search box
147	 */
148	setupSearchBox: function()
149	{
150		var tr = this;
151		var search_box = $('#torrent_search');
152		search_box.bind('keyup click', function() {
153			tr.setFilterText(this.value);
154		});
155		if (!$.browser.safari)
156		{
157			search_box.addClass('blur');
158			search_box[0].value = 'Filter';
159			search_box.bind('blur', function() {
160				if (this.value === '') {
161					$(this).addClass('blur');
162					this.value = 'Filter';
163					tr.setFilterText(null);
164				}
165			}).bind('focus', function() {
166				if ($(this).is('.blur')) {
167					this.value = '';
168					$(this).removeClass('blur');
169				}
170			});
171		}
172	},
173
174	/**
175	 * Create the torrent right-click menu
176	 */
177	createContextMenu: function() {
178		var tr = this;
179		var bindings = {
180			context_pause_selected:       function() { tr.stopSelectedTorrents(); },
181			context_resume_selected:      function() { tr.startSelectedTorrents(false); },
182			context_resume_now_selected:  function() { tr.startSelectedTorrents(true); },
183			context_move:                 function() { tr.moveSelectedTorrents(false); },
184			context_remove:               function() { tr.removeSelectedTorrents(); },
185			context_removedata:           function() { tr.removeSelectedTorrentsAndData(); },
186			context_verify:               function() { tr.verifySelectedTorrents(); },
187			context_reannounce:           function() { tr.reannounceSelectedTorrents(); },
188			context_move_top:             function() { tr.moveTop(); },
189			context_move_up:              function() { tr.moveUp(); },
190			context_move_down:            function() { tr.moveDown(); },
191			context_move_bottom:          function() { tr.moveBottom(); },
192			context_select_all:           function() { tr.selectAll(); },
193			context_deselect_all:         function() { tr.deselectAll(); }
194		};
195
196		// Set up the context menu
197		$('ul#torrent_list').contextMenu('torrent_context_menu', {
198			bindings:          bindings,
199			menuStyle:         { width: '310px', backgroundColor: '#fff', border: 'none', padding: '5px 0', textAlign: 'left' },
200			itemStyle:         { backgroundColor: 'transparent', margin: '0', padding: '3px 10px 3px 20px', color: '#000', cursor: 'default', border: 'none'},
201			itemHoverStyle:    { backgroundColor: '#24e', color: '#fff', border: 'none'},
202			shadow:            false,
203			boundingElement:   $('div#torrent_container'),
204			boundingRightPad:  20,
205			boundingBottomPad: 5,
206			onContextMenu: function(ev) {
207				var element = $(ev.target).closest('.torrent')[0];
208				var i = $('#torrent_list > li').index(element);
209				if ((i!==-1) && !tr._rows[i].isSelected())
210					tr.setSelectedRow(tr._rows[i]);
211				return true;
212			}
213		});
214	},
215
216	createSettingsMenu: function() {
217		$('#settings_menu').transMenu({
218			selected_char: '&#x2714;',
219			direction: 'up',
220			onClick: $.proxy(this.onMenuClicked,this)
221		});
222
223		$('#unlimited_download_rate').selectMenuItem();
224		$('#unlimited_upload_rate').selectMenuItem();
225	},
226
227
228	/****
229	*****
230	*****  UTILITIES
231	*****
232	****/
233
234	getAllTorrents: function()
235	{
236		var torrents = [];
237		for (var key in this._torrents)
238			torrents.push(this._torrents[key]);
239		return torrents;
240	},
241
242	getTorrentIds: function(torrents)
243	{
244		return $.map(torrents.slice(0), function(t) {return t.getId();});
245	},
246
247	scrollToRow: function(row)
248	{
249		if (isMobileDevice) // FIXME: why?
250			return;
251
252		var list = $('#torrent_container'),
253		    scrollTop = list.scrollTop(),
254		    innerHeight = list.innerHeight(),
255		    offsetTop = row.getElement().offsetTop,
256		    offsetHeight = $(row.getElement()).outerHeight();
257
258		if (offsetTop < scrollTop)
259			list.scrollTop(offsetTop);
260		else if (innerHeight + scrollTop < offsetTop + offsetHeight)
261			list.scrollTop(offsetTop + offsetHeight - innerHeight);
262	},
263
264	seedRatioLimit: function() {
265		var p = this.sessionProperties;
266		if (p && p.seedRatioLimited)
267			return p.seedRatioLimit;
268		return -1;
269	},
270
271	setPref: function(key, val)
272	{
273		this[key] = val;
274		Prefs.setValue(key, val);
275	},
276
277	/****
278	*****
279	*****  SELECTION
280	*****
281	****/
282
283	getSelectedRows: function() {
284		return $.grep(this._rows, function(r) {return r.isSelected();});
285	},
286
287	getSelectedTorrents: function() {
288		return $.map(this.getSelectedRows(),function(r) {
289			return r.getTorrent();
290		});
291	},
292
293	getSelectedTorrentIds: function() {
294		return this.getTorrentIds(this.getSelectedTorrents());
295	},
296
297	setSelectedRow: function(row) {
298		$(this.elements.torrent_list).children('.selected').removeClass('selected');
299		this.selectRow(row);
300	},
301
302	selectRow: function(row) {
303		$(row.getElement()).addClass('selected');
304		this.callSelectionChangedSoon();
305	},
306
307	deselectRow: function(row) {
308		$(row.getElement()).removeClass('selected');
309		this.callSelectionChangedSoon();
310	},
311
312	selectAll: function() {
313		$(this.elements.torrent_list).children().addClass('selected');
314		this.callSelectionChangedSoon();
315	},
316	deselectAll: function() {
317		$(this.elements.torrent_list).children('.selected').removeClass('selected');
318		this.callSelectionChangedSoon();
319		delete this._last_torrent_clicked;
320	},
321
322	indexOfLastTorrent: function() {
323		for (var i=0, r; r=this._rows[i]; ++i)
324			if (r.getTorrentId() === this._last_torrent_clicked)
325				return i;
326		return -1;
327	},
328
329	// Select a range from this row to the last clicked torrent
330	selectRange: function(row)
331	{
332		var last = this.indexOfLastTorrent();
333
334		if (last === -1)
335		{
336			this.selectRow(row);
337		}
338		else // select the range between the prevous & current
339		{
340			var next = this._rows.indexOf(row);
341			var min = Math.min(last, next);
342			var max = Math.max(last, next);
343			for (var i=min; i<=max; ++i)
344				this.selectRow(this._rows[i]);
345		}
346
347		this.callSelectionChangedSoon();
348	},
349
350	selectionChanged: function()
351	{
352		this.updateButtonStates();
353
354		this.inspector.setTorrents(this.inspectorIsVisible() ? this.getSelectedTorrents() : []);
355
356		clearTimeout(this.selectionChangedTimer);
357		delete this.selectionChangedTimer;
358
359	},
360
361	callSelectionChangedSoon: function()
362	{
363		if (!this.selectionChangedTimer)
364		{
365			var callback = $.proxy(this.selectionChanged,this),
366			    msec = 200;
367			this.selectionChangedTimer = setTimeout(callback, msec);
368		}
369	},
370
371	/*--------------------------------------------
372	 *
373	 *  E V E N T   F U N C T I O N S
374	 *
375	 *--------------------------------------------*/
376
377	/*
378	 * Process key event
379	 */
380	keyDown: function(ev)
381	{
382		var handled = false,
383		    rows = this._rows,
384		    up = ev.keyCode === 38, // up key pressed
385		    dn = ev.keyCode === 40, // down key pressed
386		    shift = ev.keyCode === 16; // shift key pressed
387
388		if ((up || dn) && rows.length)
389		{
390			var last = this.indexOfLastTorrent(),
391			    i = last,
392			    anchor = this._shift_index,
393			    r,
394			    min = 0,
395			    max = rows.length - 1;
396
397			if (dn && (i+1 <= max))
398				++i;
399			else if (up && (i-1 >= min))
400				--i;
401
402			var r = rows[i];
403
404			if (anchor >= 0)
405			{
406				// user is extending the selection
407				// with the shift + arrow keys...
408				if (   ((anchor <= last) && (last < i))
409				    || ((anchor >= last) && (last > i)))
410				{
411					this.selectRow(r);
412				}
413				else if (((anchor >= last) && (i > last))
414				      || ((anchor <= last) && (last > i)))
415				{
416					this.deselectRow(rows[last]);
417				}
418			}
419			else
420			{
421				if (ev.shiftKey)
422					this.selectRange(r);
423				else
424					this.setSelectedRow(r);
425			}
426			this._last_torrent_clicked = r.getTorrentId();
427			this.scrollToRow(r);
428			handled = true;
429		}
430		else if (shift)
431		{
432			this._shift_index = this.indexOfLastTorrent();
433		}
434
435		return !handled;
436	},
437
438	keyUp: function(ev) {
439		if (ev.keyCode === 16) // shift key pressed
440			delete this._shift_index;
441	},
442
443	isButtonEnabled: function(ev) {
444		var p = (ev.target || ev.srcElement).parentNode;
445		return p.className!=='disabled'
446		    && p.parentNode.className!=='disabled';
447	},
448
449	stopSelectedClicked: function(ev) {
450		if (this.isButtonEnabled(ev)) {
451			this.stopSelectedTorrents();
452			this.hideMobileAddressbar();
453		}
454	},
455
456	startSelectedClicked: function(ev) {
457		if (this.isButtonEnabled(ev)) {
458			this.startSelectedTorrents(false);
459			this.hideMobileAddressbar();
460		}
461	},
462
463	stopAllClicked: function(ev) {
464		if (this.isButtonEnabled(ev)) {
465			this.stopAllTorrents();
466			this.hideMobileAddressbar();
467		}
468	},
469
470	startAllClicked: function(ev) {
471		if (this.isButtonEnabled(ev)) {
472			this.startAllTorrents(false);
473			this.hideMobileAddressbar();
474		}
475	},
476
477	openTorrentClicked: function(ev) {
478		if (this.isButtonEnabled(ev)) {
479			$('body').addClass('open_showing');
480			this.uploadTorrentFile();
481			this.updateButtonStates();
482		}
483	},
484
485	dragenter: function(ev) {
486		if (ev.dataTransfer && ev.dataTransfer.types) {
487			var types = ["text/uri-list", "text/plain"];
488			for (var i = 0; i < types.length; ++i) {
489				// it would be better to look at the links here;
490				// sadly, with Firefox, trying would throw.
491				if (ev.dataTransfer.types.contains(types[i])) {
492					ev.stopPropagation();
493					ev.preventDefault();
494					ev.dropEffect = "copy";
495					return false;
496				}
497			}
498		}
499		else if (ev.dataTransfer) {
500			ev.dataTransfer.dropEffect = "none";
501		}
502		return true;
503	},
504
505	drop: function(ev)
506	{
507		var i, uri, uris=null,
508		    types = ["text/uri-list", "text/plain"],
509		    paused = this.shouldAddedTorrentsStart();
510
511		if (!ev.dataTransfer || !ev.dataTransfer.types)
512			return true;
513
514		for (i=0; !uris && i<types.length; ++i)
515			if (ev.dataTransfer.types.contains(types[i]))
516				uris = ev.dataTransfer.getData(types[i]).split("\n");
517
518		for (i=0; uri=uris[i]; ++i) {
519			if (/^#/.test(uri)) // lines which start with "#" are comments
520				continue;
521			if (/^[a-z-]+:/i.test(uri)) // close enough to a url
522				this.remote.addTorrentByUrl(uri, paused);
523		}
524
525		ev.preventDefault();
526		return false;
527	},
528
529	hideUploadDialog: function() {
530		$('body.open_showing').removeClass('open_showing');
531		$('#upload_container').hide();
532		this.updateButtonStates();
533	},
534
535	confirmUploadClicked: function() {
536		this.uploadTorrentFile(true);
537		this.hideUploadDialog();
538	},
539
540	hideMoveDialog: function() {
541		$('#move_container').hide();
542		this.updateButtonStates();
543	},
544
545	confirmMoveClicked: function() {
546		this.moveSelectedTorrents(true);
547		this.hideUploadDialog();
548	},
549
550	removeClicked: function(ev) {
551		if (this.isButtonEnabled(ev)) {
552			this.removeSelectedTorrents();
553			this.hideMobileAddressbar();
554		}
555	},
556
557	// turn the periodic ajax session refresh on & off
558	togglePeriodicSessionRefresh: function(enabled) {
559		clearInterval(this.sessionInterval);
560		delete this.sessionInterval;
561		if (enabled) {
562		        var callback = $.proxy(this.loadDaemonPrefs,this),
563			    msec = 8000;
564			this.sessionInterval = setInterval(callback, msec);
565		}
566	},
567
568	toggleTurtleClicked: function()
569	{
570		var o = {};
571		o[RPC._TurtleState] = !$('#turtle-button').hasClass('selected');
572		this.remote.savePrefs(o);
573	},
574
575	/*--------------------------------------------
576	 *
577	 *  I N T E R F A C E   F U N C T I O N S
578	 *
579	 *--------------------------------------------*/
580
581	onPrefsDialogClosed: function() {
582		$('#prefs-button').removeClass('selected');
583	},
584
585	togglePrefsDialogClicked: function(ev)
586	{
587		var e = $('#prefs-button');
588
589		if (e.hasClass('selected'))
590			this.prefsDialog.close();
591		else {
592			e.addClass('selected');
593			this.prefsDialog.show();
594		}
595	},
596
597	setFilterText: function(search) {
598		this.filterText = search ? search.trim() : null;
599		this.refilter(true);
600	},
601
602	setSortMethod: function(sort_method) {
603		this.setPref(Prefs._SortMethod, sort_method);
604		this.refilter(true);
605	},
606
607	setSortDirection: function(direction) {
608		this.setPref(Prefs._SortDirection, direction);
609		this.refilter(true);
610	},
611
612	onMenuClicked: function(ev)
613	{
614		var o, dir,
615		    id = ev.target.id,
616		    remote = this.remote,
617		    element = $(ev.target);
618
619		if (element.hasClass('sort-mode'))
620		{
621			element.parent().find('.sort-mode').each(function() {
622				element.parent().deselectMenuItem();
623			});
624			element.selectMenuItem();
625			this.setSortMethod(id.replace(/sort_by_/, ''));
626		}
627		else if (element.hasClass('upload-speed'))
628		{
629			o = {};
630			o[RPC._UpSpeedLimit] = parseInt(ev.target.innerHTML);
631			o[RPC._UpSpeedLimited] = true;
632			remote.savePrefs(o);
633		}
634		else if (element.hasClass('download-speed'))
635		{
636			o = {};
637			o[RPC._DownSpeedLimit] = parseInt(ev.target.innerHTML);
638			o[RPC._DownSpeedLimited] = true;
639			remote.savePrefs(o);
640		}
641		else switch (id)
642		{
643			case 'statistics':
644				this.showStatsDialog();
645				break;
646
647			case 'about-button':
648				o = 'Transmission ' + this.serverVersion;
649				$('#about-dialog #about-title').html(o);
650				$('#about-dialog').dialog({
651					title: 'About',
652					show: 'fade',
653					hide: 'fade'
654				});
655				break;
656
657			case 'homepage':
658				window.open('http://www.transmissionbt.com/');
659				break;
660
661			case 'tipjar':
662				window.open('http://www.transmissionbt.com/donate.php');
663				break;
664
665			case 'unlimited_download_rate':
666				o = {};
667				o[RPC._DownSpeedLimited] = false;
668				remote.savePrefs(o);
669				break;
670
671			case 'limited_download_rate':
672				o = {};
673				o[RPC._DownSpeedLimited] = true;
674				remote.savePrefs(o);
675				break;
676
677			case 'unlimited_upload_rate':
678				o = {};
679				o[RPC._UpSpeedLimited] = false;
680				remote.savePrefs(o);
681				break;
682
683			case 'limited_upload_rate':
684				o = {};
685				o[RPC._UpSpeedLimited] = true;
686				remote.savePrefs(o);
687				break;
688
689			case 'reverse_sort_order':
690				if (element.menuItemIsSelected()) {
691					dir = Prefs._SortAscending;
692					element.deselectMenuItem();
693				} else {
694					dir = Prefs._SortDescending;
695					element.selectMenuItem();
696				}
697				this.setSortDirection(dir);
698				break;
699
700			case 'toggle_notifications':
701				Notifications && Notifications.toggle();
702				break;
703
704			default:
705				console.log('unhandled: ' + id);
706				break;
707
708		}
709		$('#settings_menu').trigger('closemenu');
710		ev.stopImmediatePropagation();
711	},
712
713
714	onTorrentChanged: function(ev, tor)
715	{
716		// update our dirty fields
717		this.dirtyTorrents[ tor.getId() ] = true;
718
719		// enqueue ui refreshes
720		this.refilterSoon();
721		this.updateButtonsSoon();
722	},
723
724	updateFromTorrentGet: function(updates, removed_ids)
725	{
726		var i, o, t, id, needed, needinfo = [],
727		    callback, fields;
728
729		for (i=0; o=updates[i]; ++i)
730		{
731			id = o.id;
732			if ((t = this._torrents[id]))
733			{
734				needed = t.needsMetaData();
735				t.refresh(o);
736				if (needed && !t.needsMetaData())
737					needinfo.push(id);
738			}
739			else {
740				t = this._torrents[id] = new Torrent(o);
741				this.dirtyTorrents[id] = true;
742				callback = $.proxy(this.onTorrentChanged,this);
743				$(t).bind('dataChanged',callback);
744				// do we need more info for this torrent?
745				if(!('name' in t.fields) || !('status' in t.fields))
746					needinfo.push(id);
747
748				t.notifyOnFieldChange('status', $.proxy(function (newValue, oldValue) {
749					if (oldValue === Torrent._StatusDownload && (newValue == Torrent._StatusSeed || newValue == Torrent._StatusSeedWait)) {
750						$(this).trigger('downloadComplete', [t]);
751					} else if (oldValue === Torrent._StatusSeed && newValue === Torrent._StatusStopped && t.isFinished()) {
752						$(this).trigger('seedingComplete', [t]);
753					} else {
754						$(this).trigger('statusChange', [t]);
755					}
756				}, this));
757			}
758		}
759
760		if (needinfo.length) {
761			// whee, new torrents! get their initial information.
762			fields = ['id'].concat(Torrent.Fields.Metadata,
763			                       Torrent.Fields.Stats);
764			this.updateTorrents(needinfo, fields);
765			this.refilterSoon();
766		}
767
768		if (removed_ids) {
769			this.deleteTorrents(removed_ids);
770			this.refilterSoon();
771		}
772	},
773
774	updateTorrents: function(ids, fields)
775	{
776		this.remote.updateTorrents(ids, fields,
777		                           this.updateFromTorrentGet, this);
778	},
779
780	refreshTorrents: function()
781	{
782		var callback = $.proxy(this.refreshTorrents,this),
783		    msec = this[Prefs._RefreshRate] * 1000,
784		    fields = ['id'].concat(Torrent.Fields.Stats);
785
786		// send a request right now
787		this.updateTorrents('recently-active', fields);
788
789		// schedule the next request
790		clearTimeout(this.refreshTorrentsTimeout);
791		this.refreshTorrentsTimeout = setTimeout(callback, msec);
792	},
793
794	initializeTorrents: function()
795	{
796		var fields = ['id'].concat(Torrent.Fields.Metadata,
797		                           Torrent.Fields.Stats);
798		this.updateTorrents(null, fields);
799	},
800
801	onRowClicked: function(ev)
802	{
803		var meta_key = ev.metaKey || ev.ctrlKey,
804		    row = ev.currentTarget.row;
805
806		// handle the per-row "torrent_resume" button
807		if (ev.target.className === 'torrent_resume') {
808			this.startTorrent(row.getTorrent());
809			return;
810		}
811
812		// handle the per-row "torrent_pause" button
813		if (ev.target.className === 'torrent_pause') {
814			this.stopTorrent(row.getTorrent());
815			return;
816		}
817
818		// Prevents click carrying to parent element
819		// which deselects all on click
820		ev.stopPropagation();
821		// but still hide the context menu if it is showing
822		$('#jqContextMenu').hide();
823
824		if (isMobileDevice) {
825			if (row.isSelected())
826				this.setInspectorVisible(true);
827			this.setSelectedRow(row);
828
829		} else if (ev.shiftKey) {
830			this.selectRange(row);
831			// Need to deselect any selected text
832			window.focus();
833
834		// Apple-Click, not selected
835		} else if (!row.isSelected() && meta_key) {
836			this.selectRow(row);
837
838		// Regular Click, not selected
839		} else if (!row.isSelected()) {
840			this.setSelectedRow(row);
841
842		// Apple-Click, selected
843		} else if (row.isSelected() && meta_key) {
844			this.deselectRow(row);
845
846		// Regular Click, selected
847		} else if (row.isSelected()) {
848			this.setSelectedRow(row);
849		}
850
851		this._last_torrent_clicked = row.getTorrentId();
852	},
853
854	deleteTorrents: function(ids)
855	{
856		var i, id;
857
858		if (ids && ids.length)
859		{
860			for (i=0; id=ids[i]; ++i) {
861				this.dirtyTorrents[id] = true;
862				delete this._torrents[id];
863			}
864			this.refilter();
865		}
866	},
867
868	shouldAddedTorrentsStart: function()
869	{
870		return this.prefsDialog.shouldAddedTorrentsStart();
871	},
872
873	/*
874	 * Select a torrent file to upload
875	 * FIXME
876	 */
877	uploadTorrentFile: function(confirmed)
878	{
879		// Display the upload dialog
880		if (! confirmed) {
881			$('input#torrent_upload_file').attr('value', '');
882			$('input#torrent_upload_url').attr('value', '');
883			$('input#torrent_auto_start').attr('checked', this.shouldAddedTorrentsStart());
884			$('#upload_container').show();
885			$('#torrent_upload_url').focus();
886
887		// Submit the upload form
888		} else {
889			var args = {};
890			var remote = this.remote;
891			var paused = !$('#torrent_auto_start').is(':checked');
892			if ('' != $('#torrent_upload_url').val()) {
893				remote.addTorrentByUrl($('#torrent_upload_url').val(), { paused: paused });
894			} else {
895				args.url = '../upload?paused=' + paused;
896				args.type = 'POST';
897				args.data = { 'X-Transmission-Session-Id' : remote._token };
898				args.dataType = 'xml';
899				args.iframe = true;
900				$('#torrent_upload_form').ajaxSubmit(args);
901			}
902		}
903	},
904
905	promptSetLocation: function(confirmed, torrents) {
906		if (! confirmed) {
907			var path;
908			if (torrents.length === 1) {
909				path = torrents[0].getDownloadDir();
910			} else {
911				path = $("#download-dir").val();
912			}
913			$('input#torrent_path').attr('value', path);
914			$('#move_container').show();
915			$('#torrent_path').focus();
916		} else {
917			var ids = this.getTorrentIds(torrents);
918			this.remote.moveTorrents(
919				ids,
920				$("input#torrent_path").val(),
921				this.refreshTorrents,
922				this);
923			$('#move_container').hide();
924		}
925	},
926
927	moveSelectedTorrents: function(confirmed) {
928		var torrents = this.getSelectedTorrents();
929		if (torrents.length)
930			this.promptSetLocation(confirmed, torrents);
931	},
932
933	removeSelectedTorrents: function() {
934		var torrents = this.getSelectedTorrents();
935		if (torrents.length)
936			this.promptToRemoveTorrents(torrents);
937	},
938
939	removeSelectedTorrentsAndData: function() {
940		var torrents = this.getSelectedTorrents();
941		if (torrents.length)
942			this.promptToRemoveTorrentsAndData(torrents);
943	},
944
945	promptToRemoveTorrents: function(torrents) {
946		if (torrents.length === 1)
947		{
948			var torrent = torrents[0],
949			    header = 'Remove ' + torrent.getName() + '?',
950			    message = 'Once removed, continuing the transfer will require the torrent file. Are you sure you want to remove it?';
951			dialog.confirm(header, message, 'Remove', 'transmission.removeTorrents', torrents);
952		}
953		else
954		{
955			var header = 'Remove ' + torrents.length + ' transfers?',
956			    message = 'Once removed, continuing the transfers will require the torrent files. Are you sure you want to remove them?';
957			dialog.confirm(header, message, 'Remove', 'transmission.removeTorrents', torrents);
958		}
959	},
960
961	promptToRemoveTorrentsAndData:function(torrents)
962	{
963		if (torrents.length === 1)
964		{
965			var torrent = torrents[0],
966			    header = 'Remove ' + torrent.getName() + ' and delete data?',
967			    message = 'All data downloaded for this torrent will be deleted. Are you sure you want to remove it?';
968			dialog.confirm(header, message, 'Remove', 'transmission.removeTorrentsAndData', torrents);
969		}
970		else
971		{
972			var header = 'Remove ' + torrents.length + ' transfers and delete data?',
973			    message = 'All data downloaded for these torrents will be deleted. Are you sure you want to remove them?';
974			dialog.confirm(header, message, 'Remove', 'transmission.removeTorrentsAndData', torrents);
975		}
976	},
977
978	removeTorrents: function(torrents) {
979		var ids = this.getTorrentIds(torrents);
980		this.remote.removeTorrents(ids, this.refreshTorrents, this);
981	},
982
983	removeTorrentsAndData: function(torrents) {
984		this.remote.removeTorrentsAndData(torrents);
985	},
986
987	verifySelectedTorrents: function() {
988		this.verifyTorrents(this.getSelectedTorrents());
989	},
990
991	reannounceSelectedTorrents: function() {
992		this.reannounceTorrents(this.getSelectedTorrents());
993	},
994
995	startAllTorrents: function(force) {
996		this.startTorrents(this.getAllTorrents(), force);
997	},
998	startSelectedTorrents: function(force) {
999		this.startTorrents(this.getSelectedTorrents(), force);
1000	},
1001	startTorrent: function(torrent) {
1002		this.startTorrents([ torrent ], false);
1003	},
1004
1005	startTorrents: function(torrents, force) {
1006		this.remote.startTorrents(this.getTorrentIds(torrents), force,
1007		                          this.refreshTorrents, this);
1008	},
1009	verifyTorrent: function(torrent) {
1010		this.verifyTorrents([ torrent ]);
1011	},
1012	verifyTorrents: function(torrents) {
1013		this.remote.verifyTorrents(this.getTorrentIds(torrents),
1014		                           this.refreshTorrents, this);
1015	},
1016
1017	reannounceTorrent: function(torrent) {
1018		this.reannounceTorrents([ torrent ]);
1019	},
1020	reannounceTorrents: function(torrents) {
1021		this.remote.reannounceTorrents(this.getTorrentIds(torrents),
1022		                               this.refreshTorrents, this);
1023	},
1024
1025	stopAllTorrents: function() {
1026		this.stopTorrents(this.getAllTorrents());
1027	},
1028	stopSelectedTorrents: function() {
1029		this.stopTorrents(this.getSelectedTorrents());
1030	},
1031	stopTorrent: function(torrent) {
1032		this.stopTorrents([ torrent ]);
1033	},
1034	stopTorrents: function(torrents) {
1035		this.remote.stopTorrents(this.getTorrentIds(torrents),
1036		                         this.refreshTorrents, this);
1037	},
1038	changeFileCommand: function(torrentId, rowIndices, command) {
1039		this.remote.changeFileCommand(torrentId, rowIndices, command);
1040	},
1041
1042	hideMobileAddressbar: function(delaySecs) {
1043		if (isMobileDevice && !scroll_timeout) {
1044			var callback = $.proxy(this.doToolbarHide,this),
1045			    msec = delaySecs*1000 || 150;
1046			scroll_timeout = setTimeout(callback,msec);
1047		}
1048	},
1049	doToolbarHide: function() {
1050		window.scrollTo(0,1);
1051		scroll_timeout=null;
1052	},
1053
1054	// Queue
1055	moveTop: function() {
1056		this.remote.moveTorrentsToTop(this.getSelectedTorrentIds(),
1057		                              this.refreshTorrents, this);
1058	},
1059	moveUp: function() {
1060		this.remote.moveTorrentsUp(this.getSelectedTorrentIds(),
1061		                           this.refreshTorrents, this);
1062	},
1063	moveDown: function() {
1064		this.remote.moveTorrentsDown(this.getSelectedTorrentIds(),
1065		                             this.refreshTorrents, this);
1066	},
1067	moveBottom: function() {
1068		this.remote.moveTorrentsToBottom(this.getSelectedTorrentIds(),
1069		                                 this.refreshTorrents, this);
1070	},
1071
1072	/***
1073	****
1074	***/
1075
1076	updateGuiFromSession: function(o)
1077	{
1078		var limit, limited, e, b, text,
1079                    fmt = Transmission.fmt,
1080                    menu = $('#settings_menu');
1081
1082		this.serverVersion = o.version;
1083
1084		this.prefsDialog.set(o);
1085
1086		if (RPC._TurtleState in o)
1087		{
1088			b = o[RPC._TurtleState];
1089			e = $('#turtle-button');
1090			text = [ 'Click to ', (b?'disable':'enable'),
1091			         ' Temporary Speed Limits (',
1092			         fmt.speed(o[RPC._TurtleUpSpeedLimit]),
1093			         ' up,',
1094			         fmt.speed(o[RPC._TurtleDownSpeedLimit]),
1095			         ' down)' ].join('');
1096			e.toggleClass('selected', b);
1097			e.attr('title', text);
1098		}
1099
1100		if (this.isMenuEnabled && (RPC._DownSpeedLimited in o)
1101		                       && (RPC._DownSpeedLimit in o))
1102		{
1103			limit = o[RPC._DownSpeedLimit];
1104			limited = o[RPC._DownSpeedLimited];
1105
1106			e = menu.find('#limited_download_rate');
1107                        e.html('Limit (' + fmt.speed(limit) + ')');
1108
1109                        if (!limited)
1110                        	e = menu.find('#unlimited_download_rate');
1111                        e.deselectMenuSiblings().selectMenuItem();
1112		}
1113
1114		if (this.isMenuEnabled && (RPC._UpSpeedLimited in o)
1115		                       && (RPC._UpSpeedLimit in o))
1116		{
1117			limit = o[RPC._UpSpeedLimit];
1118			limited = o[RPC._UpSpeedLimited];
1119
1120			e = menu.find('#limited_upload_rate');
1121                        e.html('Limit (' + fmt.speed(limit) + ')');
1122
1123                        if (!limited)
1124                        	e = menu.find('#unlimited_upload_rate');
1125                        e.deselectMenuSiblings().selectMenuItem();
1126		}
1127	},
1128
1129	updateStatusbar: function()
1130	{
1131		var u=0, d=0,
1132		    i, row, text,
1133		    fmt = Transmission.fmt,
1134		    torrents = this.getAllTorrents();
1135
1136		// up/down speed
1137		for (i=0; row=torrents[i]; ++i) {
1138			u += row.getUploadSpeed();
1139			d += row.getDownloadSpeed();
1140		}
1141
1142		$('#speed-up-container').toggleClass('active', u>0 );
1143		$('#speed-up-label').text( fmt.speedBps( u ) );
1144
1145		$('#speed-dn-container').toggleClass('active', d>0 );
1146		$('#speed-dn-label').text( fmt.speedBps( d ) );
1147
1148		// visible torrents
1149		$('#filter-count').text( fmt.countString('Transfer','Transfers',this._rows.length ) );
1150	},
1151
1152	setEnabled: function(key, flag)
1153	{
1154		$(key).toggleClass('disabled', !flag);
1155	},
1156
1157	updateFilterSelect: function()
1158	{
1159		var i, names, name, str, o,
1160		    e = $('#filter-tracker'),
1161		    trackers = this.getTrackers();
1162
1163		// build a sorted list of names
1164		names = [];
1165		for (name in trackers)
1166			names.push (name);
1167		names.sort();
1168
1169		// build the new html
1170		if (!this.filterTracker)
1171			str = '<option value="all" selected="selected">All</option>';
1172		else
1173			str = '<option value="all">All</option>';
1174		for (i=0; name=names[i]; ++i) {
1175			o = trackers[name];
1176			str += '<option value="' + o.domain + '"';
1177			if (trackers[name].domain === this.filterTracker) str += ' selected="selected"';
1178			str += '>' + name + '</option>';
1179		}
1180
1181		if (!this.filterTrackersStr || (this.filterTrackersStr !== str)) {
1182			this.filterTrackersStr = str;
1183			$('#filter-tracker').html(str);
1184		}
1185	},
1186
1187	updateButtonsSoon: function()
1188	{
1189		if (!this.buttonRefreshTimer)
1190		{
1191			var callback = $.proxy(this.updateButtonStates,this),
1192			    msec = 100;
1193			this.buttonRefreshTimer = setTimeout(callback, msec);
1194		}
1195	},
1196
1197	updateButtonStates: function()
1198	{
1199		var e = this.elements,
1200		    haveActive = false,
1201		    havePaused = false,
1202		    haveSel = false,
1203		    haveActiveSel = false,
1204		    havePausedSel = false;
1205
1206		clearTimeout(this.buttonRefreshTimer);
1207		delete this.buttonRefreshTimer;
1208
1209		for (var i=0, row; row=this._rows[i]; ++i) {
1210			var isStopped = row.getTorrent().isStopped();
1211			var isSelected = row.isSelected();
1212			if (!isStopped) haveActive = true;
1213			if (isStopped) havePaused = true;
1214			if (isSelected) haveSel = true;
1215			if (isSelected && !isStopped) haveActiveSel = true;
1216			if (isSelected && isStopped) havePausedSel = true;
1217		}
1218
1219		this.setEnabled(e.toolbar_pause_button,  haveActiveSel);
1220		this.setEnabled(e.toolbar_start_button,  havePausedSel);
1221		this.setEnabled(e.toolbar_remove_button, haveSel);
1222	},
1223
1224	/****
1225	*****
1226	*****  INSPECTOR
1227	*****
1228	****/
1229
1230	inspectorIsVisible: function()
1231	{
1232		return $('#torrent_inspector').is(':visible');
1233	},
1234	toggleInspector: function()
1235	{
1236		this.setInspectorVisible(!this.inspectorIsVisible());
1237	},
1238	setInspectorVisible: function(visible)
1239	{
1240		if (visible)
1241			this.inspector.setTorrents(this.getSelectedTorrents());
1242
1243		// update the ui widgetry
1244		$('#torrent_inspector').toggle(visible);
1245		$('#toolbar-inspector').toggleClass('selected',visible);
1246		this.hideMobileAddressbar();
1247		if (isMobileDevice) {
1248			$('body').toggleClass('inspector_showing',visible);
1249		} else {
1250			var w = visible ? $('#torrent_inspector').outerWidth() + 1 + 'px' : '0px';
1251			$('#torrent_container')[0].style.right = w;
1252		}
1253	},
1254
1255	/****
1256	*****
1257	*****  FILTER
1258	*****
1259	****/
1260
1261	refilterSoon: function()
1262	{
1263		if (!this.refilterTimer) {
1264			var tr = this,
1265			    callback = function(){tr.refilter(false);},
1266			    msec = 100;
1267			this.refilterTimer = setTimeout(callback, msec);
1268		}
1269	},
1270
1271	sortRows: function(rows)
1272	{
1273		var i, tor, row,
1274		    id2row = {},
1275		    torrents = [];
1276
1277		for (i=0; row=rows[i]; ++i) {
1278			tor = row.getTorrent();
1279			torrents.push(tor);
1280			id2row[ tor.getId() ] = row;
1281		}
1282
1283		Torrent.sortTorrents(torrents, this[Prefs._SortMethod],
1284		                               this[Prefs._SortDirection]);
1285
1286		for (i=0; tor=torrents[i]; ++i)
1287			rows[i] = id2row[ tor.getId() ];
1288	},
1289
1290	refilter: function(rebuildEverything)
1291	{
1292		var i, e, id, t, row, tmp, rows, clean_rows, dirty_rows, frag,
1293		    sort_mode = this[Prefs._SortMethod],
1294		    sort_direction = this[Prefs._SortDirection],
1295		    filter_mode = this[Prefs._FilterMode],
1296		    filter_text = this.filterText,
1297		    filter_tracker = this.filterTracker,
1298		    renderer = this.torrentRenderer,
1299		    list = this.elements.torrent_list,
1300		    old_sel_count = $(list).children('.selected').length;
1301
1302		this.updateFilterSelect();
1303
1304		clearTimeout(this.refilterTimer);
1305		delete this.refilterTimer;
1306
1307		if (rebuildEverything) {
1308			$(list).empty();
1309			this._rows = [];
1310			for (id in this._torrents)
1311				this.dirtyTorrents[id] = true;
1312		}
1313
1314		// rows that overlap with dirtyTorrents need to be refiltered.
1315		// those that don't are 'clean' and don't need refiltering.
1316		clean_rows = [];
1317		dirty_rows = [];
1318		for (i=0; row=this._rows[i]; ++i) {
1319			if(row.getTorrentId() in this.dirtyTorrents)
1320				dirty_rows.push(row);
1321			else
1322				clean_rows.push(row);
1323		}
1324
1325		// remove the dirty rows from the dom
1326		e = [];
1327		for (i=0; row=dirty_rows[i]; ++i)
1328			e.push (row.getElement());
1329		$(e).detach();
1330
1331		// drop any dirty rows that don't pass the filter test
1332		tmp = [];
1333		for (i=0; row=dirty_rows[i]; ++i) {
1334			id = row.getTorrentId();
1335			t = this._torrents[ id ];
1336			if (t && t.test(filter_mode, filter_text, filter_tracker))
1337				tmp.push(row);
1338			delete this.dirtyTorrents[id];
1339		}
1340		dirty_rows = tmp;
1341
1342		// make new rows for dirty torrents that pass the filter test
1343		// but don't already have a row
1344		for (id in this.dirtyTorrents) {
1345			t = this._torrents[id];
1346			if (t && t.test(filter_mode, filter_text, filter_tracker)) {
1347				row = new TorrentRow(renderer, this, t);
1348				e = row.getElement();
1349				e.row = row;
1350				dirty_rows.push(row);
1351				$(e).click($.proxy(this.onRowClicked,this));
1352				$(e).dblclick($.proxy(this.toggleInspector,this));
1353			}
1354		}
1355
1356		// sort the dirty rows
1357		this.sortRows (dirty_rows);
1358
1359		// now we have two sorted arrays of rows
1360		// and can do a simple two-way sorted merge.
1361		rows = [];
1362		var ci=0, cmax=clean_rows.length;
1363		var di=0, dmax=dirty_rows.length;
1364		frag = document.createDocumentFragment();
1365		while (ci!=cmax || di!=dmax)
1366		{
1367			var push_clean;
1368
1369			if (ci==cmax)
1370				push_clean = false;
1371			else if (di==dmax)
1372				push_clean = true;
1373			else {
1374				var c = Torrent.compareTorrents(
1375				           clean_rows[ci].getTorrent(),
1376				           dirty_rows[di].getTorrent(),
1377				           sort_mode, sort_direction);
1378				push_clean = (c < 0);
1379			}
1380
1381			if (push_clean)
1382				rows.push(clean_rows[ci++]);
1383			else {
1384				row = dirty_rows[di++];
1385				e = row.getElement();
1386				if (ci !== cmax)
1387					list.insertBefore(e, clean_rows[ci].getElement());
1388				else
1389					frag.appendChild(e);
1390				rows.push(row);
1391			}
1392		}
1393		list.appendChild(frag);
1394
1395		// update our implementation fields
1396		this._rows = rows;
1397		this.dirtyTorrents = {};
1398
1399		// jquery's even/odd starts with 1 not 0, so invert its logic
1400		e = []
1401		for (i=0; row=rows[i]; ++i)
1402			e.push(row.getElement());
1403		$(e).filter(":odd").addClass('even');
1404		$(e).filter(":even").removeClass('even');
1405
1406		// sync gui
1407		this.updateStatusbar();
1408		if (old_sel_count !== $(list).children('.selected').length)
1409			this.selectionChanged();
1410	},
1411
1412	setFilterMode: function(mode)
1413	{
1414		// set the state
1415		this.setPref(Prefs._FilterMode, mode);
1416
1417		// refilter
1418		this.refilter(true);
1419	},
1420
1421	onFilterModeClicked: function(ev)
1422	{
1423		this.setFilterMode($('#filter-mode').val());
1424	},
1425
1426	onFilterTrackerClicked: function(ev)
1427	{
1428		var tracker = $('#filter-tracker').val();
1429		this.setFilterTracker(tracker==='all' ? null : tracker);
1430	},
1431
1432	setFilterTracker: function(domain)
1433	{
1434		// update which tracker is selected in the popup
1435		var key = domain ? this.getReadableDomain(domain) : 'all',
1436		    id = '#show-tracker-' + key;
1437		$(id).addClass('selected').siblings().removeClass('selected');
1438
1439		this.filterTracker = domain;
1440		this.refilter(true);
1441	},
1442
1443	// example: "tracker.ubuntu.com" returns "ubuntu.com"
1444	getDomainName: function(host)
1445	{
1446		var dot = host.indexOf('.');
1447		if (dot !== host.lastIndexOf('.'))
1448			host = host.slice(dot+1);
1449		return host;
1450	},
1451
1452	// example: "ubuntu.com" returns "Ubuntu"
1453	getReadableDomain: function(name)
1454	{
1455		if (name.length)
1456			name = name.charAt(0).toUpperCase() + name.slice(1);
1457		var dot = name.indexOf('.');
1458		if (dot !== -1)
1459			name = name.slice(0, dot);
1460		return name;
1461	},
1462
1463	getTrackers: function()
1464	{
1465		var ret = {};
1466
1467		var torrents = this.getAllTorrents();
1468		for (var i=0, torrent; torrent=torrents[i]; ++i)
1469		{
1470			var names = [];
1471			var trackers = torrent.getTrackers();
1472			for (var j=0, tracker; tracker=trackers[j]; ++j)
1473			{
1474				var uri, announce = tracker.announce;
1475
1476				if (announce in this.uriCache)
1477					uri = this.uriCache[announce];
1478				else {
1479					uri = this.uriCache[announce] = parseUri (announce);
1480					uri.domain = this.getDomainName (uri.host);
1481					uri.name = this.getReadableDomain (uri.domain);
1482				}
1483
1484				if (!(uri.name in ret))
1485					ret[uri.name] = { 'uri': uri,
1486					                  'domain': uri.domain,
1487					                  'count': 0 };
1488
1489				if (names.indexOf(uri.name) === -1)
1490					names.push(uri.name);
1491			}
1492			for (var j=0, name; name=names[j]; ++j)
1493				ret[name].count++;
1494		}
1495
1496		return ret;
1497	},
1498
1499	/***
1500	****
1501	****  Compact Mode
1502	****
1503	***/
1504
1505	toggleCompactClicked: function()
1506	{
1507		this.setCompactMode(!this[Prefs._CompactDisplayState]);
1508	},
1509	setCompactMode: function(is_compact)
1510	{
1511		var key = Prefs._CompactDisplayState,
1512		    was_compact = this[key];
1513
1514		if (was_compact !== is_compact) {
1515			this.setPref(key, is_compact);
1516			this.onCompactModeChanged();
1517		}
1518	},
1519	initCompactMode: function()
1520	{
1521		this.onCompactModeChanged();
1522	},
1523	onCompactModeChanged: function()
1524	{
1525		var compact = this[Prefs._CompactDisplayState];
1526
1527		// update the ui: footer button
1528		$("#compact-button").toggleClass('selected',compact);
1529
1530		// update the ui: torrent list
1531		this.torrentRenderer = compact ? new TorrentRendererCompact()
1532		                               : new TorrentRendererFull();
1533		this.refilter(true);
1534	},
1535
1536	/***
1537	****
1538	****  Statistics
1539	****
1540	***/
1541
1542	// turn the periodic ajax stats refresh on & off
1543	togglePeriodicStatsRefresh: function(enabled) {
1544		clearInterval(this.statsInterval);
1545		delete this.statsInterval;
1546		if (enabled) {
1547			var callback = $.proxy(this.loadDaemonStats,this),
1548                            msec = 5000;
1549			this.statsInterval = setInterval(callback, msec);
1550		}
1551	},
1552
1553	loadDaemonStats: function(async) {
1554		this.remote.loadDaemonStats(function(data) {
1555			this.updateStats(data['arguments']);
1556		}, this, async);
1557	},
1558
1559	// Process new session stats from the server
1560	updateStats: function(stats)
1561	{
1562		var s, ratio,
1563		    fmt = Transmission.fmt;
1564
1565		s = stats["current-stats"];
1566		ratio = Math.ratio(s.uploadedBytes,s.downloadedBytes);
1567		$('#stats-session-uploaded').html(fmt.size(s.uploadedBytes));
1568		$('#stats-session-downloaded').html(fmt.size(s.downloadedBytes));
1569		$('#stats-session-ratio').html(fmt.ratioString(ratio));
1570		$('#stats-session-duration').html(fmt.timeInterval(s.secondsActive));
1571
1572		s = stats["cumulative-stats"];
1573		ratio = Math.ratio(s.uploadedBytes,s.downloadedBytes);
1574		$('#stats-total-count').html(s.sessionCount + " times");
1575		$('#stats-total-uploaded').html(fmt.size(s.uploadedBytes));
1576		$('#stats-total-downloaded').html(fmt.size(s.downloadedBytes));
1577		$('#stats-total-ratio').html(fmt.ratioString(ratio));
1578		$('#stats-total-duration').html(fmt.timeInterval(s.secondsActive));
1579	},
1580
1581
1582	showStatsDialog: function() {
1583		this.loadDaemonStats();
1584		this.hideMobileAddressbar();
1585		this.togglePeriodicStatsRefresh(true);
1586		$('#stats-dialog').dialog({
1587			close: $.proxy(this.onStatsDialogClosed,this),
1588			show: 'fade',
1589			hide: 'fade',
1590			title: 'Statistics'
1591		});
1592	},
1593
1594	onStatsDialogClosed: function() {
1595		this.hideMobileAddressbar();
1596		this.togglePeriodicStatsRefresh(false);
1597	}
1598};
1599