/** * Copyright © Jordan Lee, Dave Perrett, Malcolm Jarvis and Bruno Bierbaumer * * This file is licensed under the GPLv2. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html */ function Transmission() { this.initialize(); } Transmission.prototype = { /**** ***** ***** STARTUP ***** ****/ initialize: function () { var e; // Initialize the helper classes this.remote = new TransmissionRemote(this); this.inspector = new Inspector(this, this.remote); this.prefsDialog = new PrefsDialog(this.remote); $(this.prefsDialog).bind('closed', $.proxy(this.onPrefsDialogClosed, this)); this.isMenuEnabled = !isMobileDevice; // Initialize the implementation fields this.filterText = ''; this._torrents = {}; this._rows = []; this.dirtyTorrents = {}; this.uriCache = {}; // Initialize the clutch preferences Prefs.getClutchPrefs(this); // Set up user events $('#toolbar-pause').click($.proxy(this.stopSelectedClicked, this)); $('#toolbar-start').click($.proxy(this.startSelectedClicked, this)); $('#toolbar-pause-all').click($.proxy(this.stopAllClicked, this)); $('#toolbar-start-all').click($.proxy(this.startAllClicked, this)); $('#toolbar-remove').click($.proxy(this.removeClicked, this)); $('#toolbar-open').click($.proxy(this.openTorrentClicked, this)); $('#prefs-button').click($.proxy(this.togglePrefsDialogClicked, this)); $('#upload_confirm_button').click($.proxy(this.confirmUploadClicked, this)); $('#upload_cancel_button').click($.proxy(this.hideUploadDialog, this)); $('#rename_confirm_button').click($.proxy(this.confirmRenameClicked, this)); $('#rename_cancel_button').click($.proxy(this.hideRenameDialog, this)); $('#move_confirm_button').click($.proxy(this.confirmMoveClicked, this)); $('#move_cancel_button').click($.proxy(this.hideMoveDialog, this)); $('#turtle-button').click($.proxy(this.toggleTurtleClicked, this)); $('#compact-button').click($.proxy(this.toggleCompactClicked, this)); // tell jQuery to copy the dataTransfer property from events over if it exists jQuery.event.props.push("dataTransfer"); $('#torrent_upload_form').submit(function () { $('#upload_confirm_button').click(); return false; }); $('#toolbar-inspector').click($.proxy(this.toggleInspector, this)); e = $('#filter-mode'); e.val(this[Prefs._FilterMode]); e.change($.proxy(this.onFilterModeClicked, this)); $('#filter-tracker').change($.proxy(this.onFilterTrackerClicked, this)); if (!isMobileDevice) { $(document).bind('keydown', $.proxy(this.keyDown, this)); $(document).bind('keyup', $.proxy(this.keyUp, this)); $('#torrent_container').click($.proxy(this.deselectAll, this)); $('#torrent_container').bind('dragover', $.proxy(this.dragenter, this)); $('#torrent_container').bind('dragenter', $.proxy(this.dragenter, this)); $('#torrent_container').bind('drop', $.proxy(this.drop, this)); $('#inspector_link').click($.proxy(this.toggleInspector, this)); this.setupSearchBox(); this.createContextMenu(); }; if (this.isMenuEnabled) { this.createSettingsMenu(); }; e = {}; e.torrent_list = $('#torrent_list')[0]; e.toolbar_buttons = $('#toolbar ul li'); e.toolbar_pause_button = $('#toolbar-pause')[0]; e.toolbar_start_button = $('#toolbar-start')[0]; e.toolbar_remove_button = $('#toolbar-remove')[0]; this.elements = e; // Apply the prefs settings to the gui this.initializeSettings(); // Get preferences & torrents from the daemon var async = false; this.loadDaemonPrefs(async); this.loadDaemonStats(async); this.initializeTorrents(); this.refreshTorrents(); this.togglePeriodicSessionRefresh(true); this.updateButtonsSoon(); }, loadDaemonPrefs: function (async, callback) { this.remote.loadDaemonPrefs(function (data) { var o = data['arguments']; Prefs.getClutchPrefs(o); this.updateGuiFromSession(o); this.sessionProperties = o; if (callback) { callback(); } }, this, async); }, loadImages: function () { for (var i = 0, row; row = arguments[i]; ++i) { jQuery("<img>").attr("src", row); }; }, /* * Load the clutch prefs and init the GUI according to those prefs */ initializeSettings: function () { Prefs.getClutchPrefs(this); if (this.isMenuEnabled) { $('#sort_by_' + this[Prefs._SortMethod]).selectMenuItem(); if (this[Prefs._SortDirection] === Prefs._SortDescending) { $('#reverse_sort_order').selectMenuItem(); }; } this.initCompactMode(); }, /* * Set up the search box */ setupSearchBox: function () { var tr = this; var search_box = $('#torrent_search'); search_box.bind('keyup click', function () { tr.setFilterText(this.value); }); if (!$.browser.safari) { search_box.addClass('blur'); search_box[0].value = 'Filter'; search_box.bind('blur', function () { if (this.value === '') { $(this).addClass('blur'); this.value = 'Filter'; tr.setFilterText(null); }; }).bind('focus', function () { if ($(this).is('.blur')) { this.value = ''; $(this).removeClass('blur'); } }); } }, /** * Create the torrent right-click menu */ createContextMenu: function () { var tr = this; var bindings = { pause_selected: function () { tr.stopSelectedTorrents(); }, resume_selected: function () { tr.startSelectedTorrents(false); }, resume_now_selected: function () { tr.startSelectedTorrents(true); }, move: function () { tr.moveSelectedTorrents(false); }, remove: function () { tr.removeSelectedTorrents(); }, remove_data: function () { tr.removeSelectedTorrentsAndData(); }, verify: function () { tr.verifySelectedTorrents(); }, rename: function () { tr.renameSelectedTorrents(); }, reannounce: function () { tr.reannounceSelectedTorrents(); }, move_top: function () { tr.moveTop(); }, move_up: function () { tr.moveUp(); }, move_down: function () { tr.moveDown(); }, move_bottom: function () { tr.moveBottom(); }, select_all: function () { tr.selectAll(); }, deselect_all: function () { tr.deselectAll(); } }; // Set up the context menu $("ul#torrent_list").contextmenu({ delegate: ".torrent", menu: "#torrent_context_menu", preventSelect: true, taphold: true, show: { effect: "none" }, hide: { effect: "none" }, select: function (event, ui) { bindings[ui.cmd](); }, beforeOpen: $.proxy(function (event, ui) { var element = $(event.currentTarget); var i = $('#torrent_list > li').index(element); if ((i !== -1) && !this._rows[i].isSelected()) { this.setSelectedRow(this._rows[i]); }; this.calculateTorrentStates(function (s) { var tl = $(event.target); tl.contextmenu("enableEntry", "pause_selected", s.activeSel > 0); tl.contextmenu("enableEntry", "resume_selected", s.pausedSel > 0); tl.contextmenu("enableEntry", "resume_now_selected", s.pausedSel > 0 || s.queuedSel > 0); tl.contextmenu("enableEntry", "rename", s.sel == 1); }); }, this) }); }, createSettingsMenu: function () { $("#footer_super_menu").transMenu({ open: function () { $("#settings_menu").addClass("selected"); }, close: function () { $("#settings_menu").removeClass("selected"); }, select: $.proxy(this.onMenuClicked, this) }); $("#settings_menu").click(function (event) { $("#footer_super_menu").transMenu("open"); }); }, /**** ***** ****/ updateFreeSpaceInAddDialog: function () { var formdir = $('input#add-dialog-folder-input').val(); this.remote.getFreeSpace(formdir, this.onFreeSpaceResponse, this); }, onFreeSpaceResponse: function (dir, bytes) { var e, str, formdir; formdir = $('input#add-dialog-folder-input').val(); if (formdir == dir) { e = $('label#add-dialog-folder-label'); if (bytes > 0) { str = ' <i>(' + Transmission.fmt.size(bytes) + ' Free)</i>'; } else { str = ''; }; e.html('Destination folder' + str + ':'); } }, /**** ***** ***** UTILITIES ***** ****/ getAllTorrents: function () { var torrents = []; for (var key in this._torrents) { torrents.push(this._torrents[key]); }; return torrents; }, getTorrentIds: function (torrents) { return $.map(torrents.slice(0), function (t) { return t.getId(); }); }, scrollToRow: function (row) { if (isMobileDevice) { // FIXME: why? return var list = $('#torrent_container'); var scrollTop = list.scrollTop(); var innerHeight = list.innerHeight(); var offsetTop = row.getElement().offsetTop; var offsetHeight = $(row.getElement()).outerHeight(); if (offsetTop < scrollTop) { list.scrollTop(offsetTop); } else if (innerHeight + scrollTop < offsetTop + offsetHeight) { list.scrollTop(offsetTop + offsetHeight - innerHeight); }; }; }, seedRatioLimit: function () { var p = this.sessionProperties; if (p && p.seedRatioLimited) { return p.seedRatioLimit; }; return -1; }, setPref: function (key, val) { this[key] = val; Prefs.setValue(key, val); }, /**** ***** ***** SELECTION ***** ****/ getSelectedRows: function () { return $.grep(this._rows, function (r) { return r.isSelected(); }); }, getSelectedTorrents: function () { return $.map(this.getSelectedRows(), function (r) { return r.getTorrent(); }); }, getSelectedTorrentIds: function () { return this.getTorrentIds(this.getSelectedTorrents()); }, setSelectedRow: function (row) { $(this.elements.torrent_list).children('.selected').removeClass('selected'); this.selectRow(row); }, selectRow: function (row) { $(row.getElement()).addClass('selected'); this.callSelectionChangedSoon(); }, deselectRow: function (row) { $(row.getElement()).removeClass('selected'); this.callSelectionChangedSoon(); }, selectAll: function () { $(this.elements.torrent_list).children().addClass('selected'); this.callSelectionChangedSoon(); }, deselectAll: function () { $(this.elements.torrent_list).children('.selected').removeClass('selected'); this.callSelectionChangedSoon(); delete this._last_torrent_clicked; }, indexOfLastTorrent: function () { for (var i = 0, r; r = this._rows[i]; ++i) { if (r.getTorrentId() === this._last_torrent_clicked) { return i; }; }; return -1; }, // Select a range from this row to the last clicked torrent selectRange: function (row) { var last = this.indexOfLastTorrent(); if (last === -1) { this.selectRow(row); } else { // select the range between the prevous & current var next = this._rows.indexOf(row); var min = Math.min(last, next); var max = Math.max(last, next); for (var i = min; i <= max; ++i) { this.selectRow(this._rows[i]); }; } this.callSelectionChangedSoon(); }, selectionChanged: function () { this.updateButtonStates(); this.inspector.setTorrents(this.inspectorIsVisible() ? this.getSelectedTorrents() : []); clearTimeout(this.selectionChangedTimer); delete this.selectionChangedTimer; }, callSelectionChangedSoon: function () { if (!this.selectionChangedTimer) { var callback = $.proxy(this.selectionChanged, this), msec = 200; this.selectionChangedTimer = setTimeout(callback, msec); } }, /*-------------------------------------------- * * E V E N T F U N C T I O N S * *--------------------------------------------*/ /* * Process key event */ keyDown: function (ev) { var handled = false; var rows = this._rows; var isInputFocused = $(ev.target).is('input'); var isDialogVisible = ($('.dialog_heading:visible').length > 0 || $('.ui-dialog:visible').length > 0); // hotkeys var up_key = ev.keyCode === 38; // up key pressed var dn_key = ev.keyCode === 40; // down key pressed var a_key = ev.keyCode === 65; // a key pressed var c_key = ev.keyCode === 67; // c key pressed var d_key = ev.keyCode === 68; // d key pressed var i_key = ev.keyCode === 73; // i key pressed var l_key = ev.keyCode === 76; // l key pressed var m_key = ev.keyCode === 77; // m key pressed var o_key = ev.keyCode === 79; // o key pressed var p_key = ev.keyCode === 80; // p key pressed var r_key = ev.keyCode === 82; // r key pressed var t_key = ev.keyCode === 84; // t key pressed var u_key = ev.keyCode === 85; // u key pressed var shift_key = ev.keyCode === 16; // shift key pressed var slash_key = ev.keyCode === 191; // slash (/) key pressed var backspace_key = ev.keyCode === 8; // backspace key pressed var del_key = ev.keyCode === 46; // delete key pressed var enter_key = ev.keyCode === 13; // enter key pressed var esc_key = ev.keyCode === 27; // esc key pressed var comma_key = ev.keyCode === 188; // comma key pressed if (enter_key) { // handle other dialogs if (dialog && dialog.isVisible()) { dialog.executeCallback(); handled = true; } // handle upload dialog if ($('#upload_container').is(':visible')) { this.confirmUploadClicked(); handled = true; } // handle move dialog if ($('#move_container').is(':visible')) { this.confirmMoveClicked(); handled = true; } // handle rename dialog if ($('#rename_container').is(':visible')) { this.confirmRenameClicked(); handled = true; } } if (esc_key) { // handle other dialogs if (dialog && dialog.isVisible()) { dialog.hideDialog(); handled = true; } // handle upload dialog if ($('#upload_container').is(':visible')) { this.hideUploadDialog(); handled = true; } // handle move dialog if ($('#move_container').is(':visible')) { this.hideMoveDialog(); handled = true; } // handle rename dialog if ($('#rename_container').is(':visible')) { this.hideRenameDialog(); handled = true; } } // Some hotkeys can only be used if the following conditions are met: // 1. when no input fields are focused // 2. when no other dialogs are visible // 3. when the meta or ctrl key isn't pressed (i.e. opening dev tools shouldn't trigger the info panel) if (!isInputFocused && !isDialogVisible && !ev.metaKey && !ev.ctrlKey) { if (comma_key) { this.togglePrefsDialogClicked(); handled = true; } if (slash_key) { this.showHotkeysDialog(); handled = true; } if (a_key) { if (ev.shiftKey) { this.deselectAll(); } else { this.selectAll(); } handled = true; } if (c_key) { this.toggleCompactClicked(); handled = true; } if ((backspace_key || del_key || d_key) && rows.length) { this.removeSelectedTorrents(); handled = true; } if (i_key) { this.toggleInspector(); handled = true; } if (m_key || l_key) { this.moveSelectedTorrents() handled = true; } if (o_key || u_key) { this.openTorrentClicked(ev); handled = true; } if (p_key) { this.stopSelectedTorrents(); handled = true; } if (r_key) { this.startSelectedTorrents(); handled = true; } if (t_key) { this.toggleTurtleClicked(); handled = true; } if ((up_key || dn_key) && rows.length) { var last = this.indexOfLastTorrent(), i = last, anchor = this._shift_index, r, min = 0, max = rows.length - 1; if (dn_key && (i + 1 <= max)) { ++i; } else if (up_key && (i - 1 >= min)) { --i; }; var r = rows[i]; if (anchor >= 0) { // user is extending the selection // with the shift + arrow keys... if (((anchor <= last) && (last < i)) || ((anchor >= last) && (last > i))) { this.selectRow(r); } else if (((anchor >= last) && (i > last)) || ((anchor <= last) && (last > i))) { this.deselectRow(rows[last]); } } else { if (ev.shiftKey) { this.selectRange(r); } else { this.setSelectedRow(r); }; } this._last_torrent_clicked = r.getTorrentId(); this.scrollToRow(r); handled = true; } else if (shift_key) { this._shift_index = this.indexOfLastTorrent(); } } return !handled; }, keyUp: function (ev) { if (ev.keyCode === 16) { // shift key pressed delete this._shift_index; }; }, isButtonEnabled: function (ev) { var p = (ev.target || ev.srcElement).parentNode; return p.className !== 'disabled' && p.parentNode.className !== 'disabled'; }, stopSelectedClicked: function (ev) { if (this.isButtonEnabled(ev)) { this.stopSelectedTorrents(); this.hideMobileAddressbar(); } }, startSelectedClicked: function (ev) { if (this.isButtonEnabled(ev)) { this.startSelectedTorrents(false); this.hideMobileAddressbar(); } }, stopAllClicked: function (ev) { if (this.isButtonEnabled(ev)) { this.stopAllTorrents(); this.hideMobileAddressbar(); } }, startAllClicked: function (ev) { if (this.isButtonEnabled(ev)) { this.startAllTorrents(false); this.hideMobileAddressbar(); } }, openTorrentClicked: function (ev) { if (this.isButtonEnabled(ev)) { $('body').addClass('open_showing'); this.uploadTorrentFile(); this.updateButtonStates(); } }, dragenter: function (ev) { if (ev.dataTransfer && ev.dataTransfer.types) { var types = ["text/uri-list", "text/plain"]; for (var i = 0; i < types.length; ++i) { // it would be better to look at the links here; // sadly, with Firefox, trying would throw. if (ev.dataTransfer.types.contains(types[i])) { ev.stopPropagation(); ev.preventDefault(); ev.dropEffect = "copy"; return false; } } } else if (ev.dataTransfer) { ev.dataTransfer.dropEffect = "none"; } return true; }, drop: function (ev) { var i, uri; var uris = null; var types = ["text/uri-list", "text/plain"]; var paused = this.shouldAddedTorrentsStart(); if (!ev.dataTransfer || !ev.dataTransfer.types) { return true; }; for (i = 0; !uris && i < types.length; ++i) { if (ev.dataTransfer.types.contains(types[i])) { uris = ev.dataTransfer.getData(types[i]).split("\n"); }; }; for (i = 0; uri = uris[i]; ++i) { if (/^#/.test(uri)) { // lines which start with "#" are comments continue; }; if (/^[a-z-]+:/i.test(uri)) { // close enough to a url this.remote.addTorrentByUrl(uri, paused); }; }; ev.preventDefault(); return false; }, hideUploadDialog: function () { $('body.open_showing').removeClass('open_showing'); $('#upload_container').hide(); this.updateButtonStates(); }, confirmUploadClicked: function () { this.uploadTorrentFile(true); this.hideUploadDialog(); }, hideMoveDialog: function () { $('#move_container').hide(); this.updateButtonStates(); }, confirmMoveClicked: function () { this.moveSelectedTorrents(true); this.hideUploadDialog(); }, hideRenameDialog: function () { $('body.open_showing').removeClass('open_showing'); $('#rename_container').hide(); }, confirmRenameClicked: function () { var torrents = this.getSelectedTorrents(); this.renameTorrent(torrents[0], $('input#torrent_rename_name').attr('value')); this.hideRenameDialog(); }, removeClicked: function (ev) { if (this.isButtonEnabled(ev)) { this.removeSelectedTorrents(); this.hideMobileAddressbar(); }; }, // turn the periodic ajax session refresh on & off togglePeriodicSessionRefresh: function (enabled) { var that = this, msec = 8000; function callback() { that.loadDaemonPrefs(undefined, rescheduleTimeout); } function rescheduleTimeout() { that.sessionTimeout = setTimeout(callback, msec); } clearTimeout(this.sessionTimeout); delete this.sessionTimeout; if (enabled) { rescheduleTimeout(); } }, toggleTurtleClicked: function () { var o = {}; o[RPC._TurtleState] = !$('#turtle-button').hasClass('selected'); this.remote.savePrefs(o); }, /*-------------------------------------------- * * I N T E R F A C E F U N C T I O N S * *--------------------------------------------*/ onPrefsDialogClosed: function () { $('#prefs-button').removeClass('selected'); }, togglePrefsDialogClicked: function (ev) { var e = $('#prefs-button'); if (e.hasClass('selected')) this.prefsDialog.close(); else { e.addClass('selected'); this.prefsDialog.show(); } }, setFilterText: function (search) { this.filterText = search ? search.trim() : null; this.refilter(true); }, setSortMethod: function (sort_method) { this.setPref(Prefs._SortMethod, sort_method); this.refilter(true); }, setSortDirection: function (direction) { this.setPref(Prefs._SortDirection, direction); this.refilter(true); }, onMenuClicked: function (event, ui) { var o, dir; var id = ui.id; var remote = this.remote; var element = ui.target; if (ui.group == 'sort-mode') { element.selectMenuItem(); this.setSortMethod(id.replace(/sort_by_/, '')); } else if (element.hasClass('upload-speed')) { o = {}; o[RPC._UpSpeedLimit] = parseInt(element.text()); o[RPC._UpSpeedLimited] = true; remote.savePrefs(o); } else if (element.hasClass('download-speed')) { o = {}; o[RPC._DownSpeedLimit] = parseInt(element.text()); o[RPC._DownSpeedLimited] = true; remote.savePrefs(o); } else { switch (id) { case 'statistics': this.showStatsDialog(); break; case 'hotkeys': this.showHotkeysDialog(); break; case 'about-button': o = 'Transmission ' + this.serverVersion; $('#about-dialog #about-title').html(o); $('#about-dialog').dialog({ title: 'About', show: 'fade', hide: 'fade' }); break; case 'homepage': window.open('https://transmissionbt.com/'); break; case 'tipjar': window.open('https://transmissionbt.com/donate/'); break; case 'unlimited_download_rate': o = {}; o[RPC._DownSpeedLimited] = false; remote.savePrefs(o); break; case 'limited_download_rate': o = {}; o[RPC._DownSpeedLimited] = true; remote.savePrefs(o); break; case 'unlimited_upload_rate': o = {}; o[RPC._UpSpeedLimited] = false; remote.savePrefs(o); break; case 'limited_upload_rate': o = {}; o[RPC._UpSpeedLimited] = true; remote.savePrefs(o); break; case 'reverse_sort_order': if (element.menuItemIsSelected()) { dir = Prefs._SortAscending; element.deselectMenuItem(); } else { dir = Prefs._SortDescending; element.selectMenuItem(); } this.setSortDirection(dir); break; case 'toggle_notifications': Notifications && Notifications.toggle(); break; default: console.log('unhandled: ' + id); break; }; }; }, onTorrentChanged: function (ev, tor) { // update our dirty fields this.dirtyTorrents[tor.getId()] = true; // enqueue ui refreshes this.refilterSoon(); this.updateButtonsSoon(); }, updateFromTorrentGet: function (updates, removed_ids) { var i, o, t, id, needed, callback, fields; var needinfo = []; for (i = 0; o = updates[i]; ++i) { id = o.id; if ((t = this._torrents[id])) { needed = t.needsMetaData(); t.refresh(o); if (needed && !t.needsMetaData()) { needinfo.push(id); }; } else { t = this._torrents[id] = new Torrent(o); this.dirtyTorrents[id] = true; callback = $.proxy(this.onTorrentChanged, this); $(t).bind('dataChanged', callback); // do we need more info for this torrent? if (!('name' in t.fields) || !('status' in t.fields)) needinfo.push(id); t.notifyOnFieldChange('status', $.proxy(function (newValue, oldValue) { if (oldValue === Torrent._StatusDownload && (newValue == Torrent._StatusSeed || newValue == Torrent._StatusSeedWait)) { $(this).trigger('downloadComplete', [t]); } else if (oldValue === Torrent._StatusSeed && newValue === Torrent._StatusStopped && t.isFinished()) { $(this).trigger('seedingComplete', [t]); } else { $(this).trigger('statusChange', [t]); } }, this)); } } if (needinfo.length) { // whee, new torrents! get their initial information. fields = ['id'].concat(Torrent.Fields.Metadata, Torrent.Fields.Stats); this.updateTorrents(needinfo, fields); this.refilterSoon(); } if (removed_ids) { this.deleteTorrents(removed_ids); this.refilterSoon(); } }, updateTorrents: function (ids, fields, callback) { var that = this; function f(updates, removedIds) { if (callback) { callback(); } that.updateFromTorrentGet(updates, removedIds); } this.remote.updateTorrents(ids, fields, f); }, refreshTorrents: function () { var callback = $.proxy(this.refreshTorrents, this); var msec = this[Prefs._RefreshRate] * 1000; var fields = ['id'].concat(Torrent.Fields.Stats); // send a request right now this.updateTorrents('recently-active', fields); // schedule the next request clearTimeout(this.refreshTorrentsTimeout); this.refreshTorrentsTimeout = setTimeout(callback, msec); }, initializeTorrents: function () { var fields = ['id'].concat(Torrent.Fields.Metadata, Torrent.Fields.Stats); this.updateTorrents(null, fields); }, onRowClicked: function (ev) { var meta_key = ev.metaKey || ev.ctrlKey, row = ev.currentTarget.row; // handle the per-row "torrent_resume" button if (ev.target.className === 'torrent_resume') { this.startTorrent(row.getTorrent()); return; } // handle the per-row "torrent_pause" button if (ev.target.className === 'torrent_pause') { this.stopTorrent(row.getTorrent()); return; } // Prevents click carrying to parent element // which deselects all on click ev.stopPropagation(); if (isMobileDevice) { if (row.isSelected()) this.setInspectorVisible(true); this.setSelectedRow(row); } else if (ev.shiftKey) { this.selectRange(row); // Need to deselect any selected text window.focus(); // Apple-Click, not selected } else if (!row.isSelected() && meta_key) { this.selectRow(row); // Regular Click, not selected } else if (!row.isSelected()) { this.setSelectedRow(row); // Apple-Click, selected } else if (row.isSelected() && meta_key) { this.deselectRow(row); // Regular Click, selected } else if (row.isSelected()) { this.setSelectedRow(row); } this._last_torrent_clicked = row.getTorrentId(); }, deleteTorrents: function (ids) { var i, id; if (ids && ids.length) { for (i = 0; id = ids[i]; ++i) { this.dirtyTorrents[id] = true; delete this._torrents[id]; }; this.refilter(); }; }, shouldAddedTorrentsStart: function () { return this.prefsDialog.shouldAddedTorrentsStart(); }, /* * Select a torrent file to upload */ uploadTorrentFile: function (confirmed) { var i, file, reader; var fileInput = $('input#torrent_upload_file'); var folderInput = $('input#add-dialog-folder-input'); var startInput = $('input#torrent_auto_start'); var urlInput = $('input#torrent_upload_url'); if (!confirmed) { // update the upload dialog's fields fileInput.attr('value', ''); urlInput.attr('value', ''); startInput.attr('checked', this.shouldAddedTorrentsStart()); folderInput.attr('value', $("#download-dir").val()); folderInput.change($.proxy(this.updateFreeSpaceInAddDialog, this)); this.updateFreeSpaceInAddDialog(); // show the dialog $('#upload_container').show(); urlInput.focus(); } else { var paused = !startInput.is(':checked'); var destination = folderInput.val(); var remote = this.remote; jQuery.each(fileInput[0].files, function (i, file) { var reader = new FileReader(); reader.onload = function (e) { var contents = e.target.result; var key = "base64," var index = contents.indexOf(key); if (index > -1) { var metainfo = contents.substring(index + key.length); var o = { method: 'torrent-add', arguments: { 'paused': paused, 'download-dir': destination, 'metainfo': metainfo } }; remote.sendRequest(o, function (response) { if (response.result != 'success') alert('Error adding "' + file.name + '": ' + response.result); }); } }; reader.readAsDataURL(file); }); var url = $('#torrent_upload_url').val(); if (url != '') { if (url.match(/^[0-9a-f]{40}$/i)) { url = 'magnet:?xt=urn:btih:' + url; }; var o = { 'method': 'torrent-add', arguments: { 'paused': paused, 'download-dir': destination, 'filename': url } }; remote.sendRequest(o, function (response) { if (response.result != 'success') { alert('Error adding "' + url + '": ' + response.result); }; }); } } }, promptSetLocation: function (confirmed, torrents) { if (!confirmed) { var path; if (torrents.length === 1) { path = torrents[0].getDownloadDir(); } else { path = $("#download-dir").val(); } $('input#torrent_path').attr('value', path); $('#move_container').show(); $('#torrent_path').focus(); } else { var ids = this.getTorrentIds(torrents); this.remote.moveTorrents(ids, $("input#torrent_path").val(), this.refreshTorrents, this); $('#move_container').hide(); } }, moveSelectedTorrents: function (confirmed) { var torrents = this.getSelectedTorrents(); if (torrents.length) { this.promptSetLocation(confirmed, torrents); }; }, removeSelectedTorrents: function () { var torrents = this.getSelectedTorrents(); if (torrents.length) { this.promptToRemoveTorrents(torrents); }; }, removeSelectedTorrentsAndData: function () { var torrents = this.getSelectedTorrents(); if (torrents.length) { this.promptToRemoveTorrentsAndData(torrents); }; }, promptToRemoveTorrents: function (torrents) { if (torrents.length === 1) { var torrent = torrents[0]; var header = 'Remove ' + torrent.getName() + '?'; var message = 'Once removed, continuing the transfer will require the torrent file. Are you sure you want to remove it?'; dialog.confirm(header, message, 'Remove', function () { transmission.removeTorrents(torrents); }); } else { var header = 'Remove ' + torrents.length + ' transfers?'; var message = 'Once removed, continuing the transfers will require the torrent files. Are you sure you want to remove them?'; dialog.confirm(header, message, 'Remove', function () { transmission.removeTorrents(torrents); }); } }, promptToRemoveTorrentsAndData: function (torrents) { if (torrents.length === 1) { var torrent = torrents[0]; var header = 'Remove ' + torrent.getName() + ' and delete data?'; var message = 'All data downloaded for this torrent will be deleted. Are you sure you want to remove it?'; dialog.confirm(header, message, 'Remove', function () { transmission.removeTorrentsAndData(torrents); }); } else { var header = 'Remove ' + torrents.length + ' transfers and delete data?'; var message = 'All data downloaded for these torrents will be deleted. Are you sure you want to remove them?'; dialog.confirm(header, message, 'Remove', function () { transmission.removeTorrentsAndData(torrents); }); } }, removeTorrents: function (torrents) { var ids = this.getTorrentIds(torrents); this.remote.removeTorrents(ids, this.refreshTorrents, this); }, removeTorrentsAndData: function (torrents) { this.remote.removeTorrentsAndData(torrents); }, promptToRenameTorrent: function (torrent) { $('body').addClass('open_showing'); $('input#torrent_rename_name').attr('value', torrent.getName()); $('#rename_container').show(); $('#torrent_rename_name').focus(); }, renameSelectedTorrents: function () { var torrents = this.getSelectedTorrents(); if (torrents.length != 1) { dialog.alert("Renaming", "You can rename only one torrent at a time.", "Ok"); } else { this.promptToRenameTorrent(torrents[0]); }; }, onTorrentRenamed: function (response) { var torrent; if ((response.result === 'success') && (response.arguments) && ((torrent = this._torrents[response.arguments.id]))) { torrent.refresh(response.arguments); } }, renameTorrent: function (torrent, newname) { var oldpath = torrent.getName(); this.remote.renameTorrent([torrent.getId()], oldpath, newname, this.onTorrentRenamed, this); }, verifySelectedTorrents: function () { this.verifyTorrents(this.getSelectedTorrents()); }, reannounceSelectedTorrents: function () { this.reannounceTorrents(this.getSelectedTorrents()); }, startAllTorrents: function (force) { this.startTorrents(this.getAllTorrents(), force); }, startSelectedTorrents: function (force) { this.startTorrents(this.getSelectedTorrents(), force); }, startTorrent: function (torrent) { this.startTorrents([torrent], false); }, startTorrents: function (torrents, force) { this.remote.startTorrents(this.getTorrentIds(torrents), force, this.refreshTorrents, this); }, verifyTorrent: function (torrent) { this.verifyTorrents([torrent]); }, verifyTorrents: function (torrents) { this.remote.verifyTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this); }, reannounceTorrent: function (torrent) { this.reannounceTorrents([torrent]); }, reannounceTorrents: function (torrents) { this.remote.reannounceTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this); }, stopAllTorrents: function () { this.stopTorrents(this.getAllTorrents()); }, stopSelectedTorrents: function () { this.stopTorrents(this.getSelectedTorrents()); }, stopTorrent: function (torrent) { this.stopTorrents([torrent]); }, stopTorrents: function (torrents) { this.remote.stopTorrents(this.getTorrentIds(torrents), this.refreshTorrents, this); }, changeFileCommand: function (torrentId, rowIndices, command) { this.remote.changeFileCommand(torrentId, rowIndices, command); }, hideMobileAddressbar: function (delaySecs) { if (isMobileDevice && !scroll_timeout) { var callback = $.proxy(this.doToolbarHide, this); var msec = delaySecs * 1000 || 150; scroll_timeout = setTimeout(callback, msec); }; }, doToolbarHide: function () { window.scrollTo(0, 1); scroll_timeout = null; }, // Queue moveTop: function () { this.remote.moveTorrentsToTop(this.getSelectedTorrentIds(), this.refreshTorrents, this); }, moveUp: function () { this.remote.moveTorrentsUp(this.getSelectedTorrentIds(), this.refreshTorrents, this); }, moveDown: function () { this.remote.moveTorrentsDown(this.getSelectedTorrentIds(), this.refreshTorrents, this); }, moveBottom: function () { this.remote.moveTorrentsToBottom(this.getSelectedTorrentIds(), this.refreshTorrents, this); }, /*** **** ***/ updateGuiFromSession: function (o) { var limit, limited, e, b, text; var fmt = Transmission.fmt; var menu = $('#footer_super_menu'); this.serverVersion = o.version; this.prefsDialog.set(o); if (RPC._TurtleState in o) { b = o[RPC._TurtleState]; e = $('#turtle-button'); text = ['Click to ', (b ? 'disable' : 'enable'), ' Temporary Speed Limits (', fmt.speed(o[RPC._TurtleUpSpeedLimit]), ' up,', fmt.speed(o[RPC._TurtleDownSpeedLimit]), ' down)'].join(''); e.toggleClass('selected', b); e.attr('title', text); } if (this.isMenuEnabled && (RPC._DownSpeedLimited in o) && (RPC._DownSpeedLimit in o)) { limit = o[RPC._DownSpeedLimit]; limited = o[RPC._DownSpeedLimited]; e = menu.find('#limited_download_rate'); e.html('Limit (' + fmt.speed(limit) + ')'); if (!limited) { e = menu.find('#unlimited_download_rate'); }; e.selectMenuItem(); } if (this.isMenuEnabled && (RPC._UpSpeedLimited in o) && (RPC._UpSpeedLimit in o)) { limit = o[RPC._UpSpeedLimit]; limited = o[RPC._UpSpeedLimited]; e = menu.find('#limited_upload_rate'); e.html('Limit (' + fmt.speed(limit) + ')'); if (!limited) { e = menu.find('#unlimited_upload_rate'); }; e.selectMenuItem(); } }, updateStatusbar: function () { var i, row; var u = 0; var d = 0; var fmt = Transmission.fmt; var torrents = this.getAllTorrents(); // up/down speed for (i = 0; row = torrents[i]; ++i) { u += row.getUploadSpeed(); d += row.getDownloadSpeed(); } $('#speed-up-container').toggleClass('active', u > 0); $('#speed-up-label').text(fmt.speedBps(u)); $('#speed-dn-container').toggleClass('active', d > 0); $('#speed-dn-label').text(fmt.speedBps(d)); // visible torrents $('#filter-count').text(fmt.countString('Transfer', 'Transfers', this._rows.length)); }, setEnabled: function (key, flag) { $(key).toggleClass('disabled', !flag); }, updateFilterSelect: function () { var i, names, name, str, o; var e = $('#filter-tracker'); var trackers = this.getTrackers(); // build a sorted list of names names = []; for (name in trackers) { names.push(name); }; names.sort(); // build the new html if (!this.filterTracker) { str = '<option value="all" selected="selected">All</option>'; } else { str = '<option value="all">All</option>'; }; for (i = 0; name = names[i]; ++i) { o = trackers[name]; str += '<option value="' + o.domain + '"'; if (trackers[name].domain === this.filterTracker) { str += ' selected="selected"'; }; str += '>' + name + '</option>'; } if (!this.filterTrackersStr || (this.filterTrackersStr !== str)) { this.filterTrackersStr = str; $('#filter-tracker').html(str); } }, updateButtonsSoon: function () { if (!this.buttonRefreshTimer) { var callback = $.proxy(this.updateButtonStates, this); var msec = 100; this.buttonRefreshTimer = setTimeout(callback, msec); } }, calculateTorrentStates: function (callback) { var stats = { total: 0, active: 0, paused: 0, sel: 0, activeSel: 0, pausedSel: 0, queuedSel: 0 }; clearTimeout(this.buttonRefreshTimer); delete this.buttonRefreshTimer; for (var i = 0, row; row = this._rows[i]; ++i) { var isStopped = row.getTorrent().isStopped(); var isSelected = row.isSelected(); var isQueued = row.getTorrent().isQueued(); ++stats.total; if (!isStopped) { ++stats.active; }; if (isStopped) { ++stats.paused; }; if (isSelected) { ++stats.sel; }; if (isSelected && !isStopped) { ++stats.activeSel; }; if (isSelected && isStopped) { ++stats.pausedSel; }; if (isSelected && isQueued) { ++stats.queuedSel; }; }; callback(stats); }, updateButtonStates: function () { var tr = this; var e = this.elements; this.calculateTorrentStates(function (s) { tr.setEnabled(e.toolbar_pause_button, s.activeSel > 0); tr.setEnabled(e.toolbar_start_button, s.pausedSel > 0); tr.setEnabled(e.toolbar_remove_button, s.sel > 0); }); }, /**** ***** ***** INSPECTOR ***** ****/ inspectorIsVisible: function () { return $('#torrent_inspector').is(':visible'); }, toggleInspector: function () { this.setInspectorVisible(!this.inspectorIsVisible()); }, setInspectorVisible: function (visible) { if (visible) { this.inspector.setTorrents(this.getSelectedTorrents()); }; // update the ui widgetry $('#torrent_inspector').toggle(visible); $('#toolbar-inspector').toggleClass('selected', visible); this.hideMobileAddressbar(); if (isMobileDevice) { $('body').toggleClass('inspector_showing', visible); } else { var w = visible ? $('#torrent_inspector').outerWidth() + 1 + 'px' : '0px'; $('#torrent_container')[0].style.right = w; } }, /**** ***** ***** FILTER ***** ****/ refilterSoon: function () { if (!this.refilterTimer) { var tr = this, callback = function () { tr.refilter(false); }, msec = 100; this.refilterTimer = setTimeout(callback, msec); } }, sortRows: function (rows) { var i, tor, row, id2row = {}, torrents = []; for (i = 0; row = rows[i]; ++i) { tor = row.getTorrent(); torrents.push(tor); id2row[tor.getId()] = row; } Torrent.sortTorrents(torrents, this[Prefs._SortMethod], this[Prefs._SortDirection]); for (i = 0; tor = torrents[i]; ++i) { rows[i] = id2row[tor.getId()]; }; }, refilter: function (rebuildEverything) { var i, e, id, t, row, tmp, rows, clean_rows, dirty_rows, frag; var sort_mode = this[Prefs._SortMethod]; var sort_direction = this[Prefs._SortDirection]; var filter_mode = this[Prefs._FilterMode]; var filter_text = this.filterText; var filter_tracker = this.filterTracker; var renderer = this.torrentRenderer; var list = this.elements.torrent_list; old_sel_count = $(list).children('.selected').length; this.updateFilterSelect(); clearTimeout(this.refilterTimer); delete this.refilterTimer; if (rebuildEverything) { $(list).empty(); this._rows = []; for (id in this._torrents) { this.dirtyTorrents[id] = true; }; } // rows that overlap with dirtyTorrents need to be refiltered. // those that don't are 'clean' and don't need refiltering. clean_rows = []; dirty_rows = []; for (i = 0; row = this._rows[i]; ++i) { if (row.getTorrentId() in this.dirtyTorrents) { dirty_rows.push(row); } else { clean_rows.push(row); }; } // remove the dirty rows from the dom e = []; for (i = 0; row = dirty_rows[i]; ++i) { e.push(row.getElement()); }; $(e).detach(); // drop any dirty rows that don't pass the filter test tmp = []; for (i = 0; row = dirty_rows[i]; ++i) { id = row.getTorrentId(); t = this._torrents[id]; if (t && t.test(filter_mode, filter_text, filter_tracker)) { tmp.push(row); }; delete this.dirtyTorrents[id]; } dirty_rows = tmp; // make new rows for dirty torrents that pass the filter test // but don't already have a row for (id in this.dirtyTorrents) { t = this._torrents[id]; if (t && t.test(filter_mode, filter_text, filter_tracker)) { row = new TorrentRow(renderer, this, t); e = row.getElement(); e.row = row; dirty_rows.push(row); $(e).click($.proxy(this.onRowClicked, this)); $(e).dblclick($.proxy(this.toggleInspector, this)); } } // sort the dirty rows this.sortRows(dirty_rows); // now we have two sorted arrays of rows // and can do a simple two-way sorted merge. rows = []; var ci = 0, cmax = clean_rows.length; var di = 0, dmax = dirty_rows.length; frag = document.createDocumentFragment(); while (ci != cmax || di != dmax) { var push_clean; if (ci == cmax) { push_clean = false; } else if (di == dmax) { push_clean = true; } else { var c = Torrent.compareTorrents(clean_rows[ci].getTorrent(), dirty_rows[di].getTorrent(), sort_mode, sort_direction); push_clean = (c < 0); } if (push_clean) { rows.push(clean_rows[ci++]); } else { row = dirty_rows[di++]; e = row.getElement(); if (ci !== cmax) { list.insertBefore(e, clean_rows[ci].getElement()); } else { frag.appendChild(e); }; rows.push(row); } } list.appendChild(frag); // update our implementation fields this._rows = rows; this.dirtyTorrents = {}; // jquery's even/odd starts with 1 not 0, so invert its logic e = [] for (i = 0; row = rows[i]; ++i) { e.push(row.getElement()); }; $(e).filter(":odd").addClass('even'); $(e).filter(":even").removeClass('even'); // sync gui this.updateStatusbar(); if (old_sel_count !== $(list).children('.selected').length) { this.selectionChanged(); }; }, setFilterMode: function (mode) { // set the state this.setPref(Prefs._FilterMode, mode); // refilter this.refilter(true); }, onFilterModeClicked: function (ev) { this.setFilterMode($('#filter-mode').val()); }, onFilterTrackerClicked: function (ev) { var tracker = $('#filter-tracker').val(); this.setFilterTracker(tracker === 'all' ? null : tracker); }, setFilterTracker: function (domain) { // update which tracker is selected in the popup var key = domain ? this.getReadableDomain(domain) : 'all'; var id = '#show-tracker-' + key; $(id).addClass('selected').siblings().removeClass('selected'); this.filterTracker = domain; this.refilter(true); }, // example: "tracker.ubuntu.com" returns "ubuntu.com" getDomainName: function (host) { var dot = host.indexOf('.'); if (dot !== host.lastIndexOf('.')) { host = host.slice(dot + 1); }; return host; }, // example: "ubuntu.com" returns "Ubuntu" getReadableDomain: function (name) { if (name.length) { name = name.charAt(0).toUpperCase() + name.slice(1); }; var dot = name.indexOf('.'); if (dot !== -1) { name = name.slice(0, dot); }; return name; }, getTrackers: function () { var ret = {}; var torrents = this.getAllTorrents(); for (var i = 0, torrent; torrent = torrents[i]; ++i) { var names = []; var trackers = torrent.getTrackers(); for (var j = 0, tracker; tracker = trackers[j]; ++j) { var uri, announce = tracker.announce; if (announce in this.uriCache) { uri = this.uriCache[announce]; } else { uri = this.uriCache[announce] = parseUri(announce); uri.domain = this.getDomainName(uri.host); uri.name = this.getReadableDomain(uri.domain); }; if (!(uri.name in ret)) { ret[uri.name] = { 'uri': uri, 'domain': uri.domain, 'count': 0 }; }; if (names.indexOf(uri.name) === -1) { names.push(uri.name); }; } for (var j = 0, name; name = names[j]; ++j) { ret[name].count++; }; } return ret; }, /*** **** **** Compact Mode **** ***/ toggleCompactClicked: function () { this.setCompactMode(!this[Prefs._CompactDisplayState]); }, setCompactMode: function (is_compact) { var key = Prefs._CompactDisplayState; var was_compact = this[key]; if (was_compact !== is_compact) { this.setPref(key, is_compact); this.onCompactModeChanged(); }; }, initCompactMode: function () { this.onCompactModeChanged(); }, onCompactModeChanged: function () { var compact = this[Prefs._CompactDisplayState]; // update the ui: footer button $("#compact-button").toggleClass('selected', compact); // update the ui: torrent list this.torrentRenderer = compact ? new TorrentRendererCompact() : new TorrentRendererFull(); this.refilter(true); }, /*** **** **** Statistics **** ***/ // turn the periodic ajax stats refresh on & off togglePeriodicStatsRefresh: function (enabled) { var that = this, msec = 5000; function callback() { that.loadDaemonStats(undefined, rescheduleTimeout); } function rescheduleTimeout() { that.statsTimeout = setTimeout(callback, msec); } clearTimeout(this.statsTimeout); delete this.statsTimeout; if (enabled) { rescheduleTimeout(); } }, loadDaemonStats: function (async, callback) { this.remote.loadDaemonStats(function (data) { this.updateStats(data['arguments']); if (callback) { callback(); } }, this, async); }, // Process new session stats from the server updateStats: function (stats) { var s, ratio; var fmt = Transmission.fmt; s = stats["current-stats"]; ratio = Math.ratio(s.uploadedBytes, s.downloadedBytes); $('#stats-session-uploaded').html(fmt.size(s.uploadedBytes)); $('#stats-session-downloaded').html(fmt.size(s.downloadedBytes)); $('#stats-session-ratio').html(fmt.ratioString(ratio)); $('#stats-session-duration').html(fmt.timeInterval(s.secondsActive)); s = stats["cumulative-stats"]; ratio = Math.ratio(s.uploadedBytes, s.downloadedBytes); $('#stats-total-count').html(s.sessionCount + " times"); $('#stats-total-uploaded').html(fmt.size(s.uploadedBytes)); $('#stats-total-downloaded').html(fmt.size(s.downloadedBytes)); $('#stats-total-ratio').html(fmt.ratioString(ratio)); $('#stats-total-duration').html(fmt.timeInterval(s.secondsActive)); }, showStatsDialog: function () { this.loadDaemonStats(); this.hideMobileAddressbar(); this.togglePeriodicStatsRefresh(true); $('#stats-dialog').dialog({ close: $.proxy(this.onStatsDialogClosed, this), show: 'fade', hide: 'fade', title: 'Statistics' }); }, onStatsDialogClosed: function () { this.hideMobileAddressbar(); this.togglePeriodicStatsRefresh(false); }, /*** **** **** Hotkeys **** ***/ showHotkeysDialog: function () { $('#hotkeys-dialog').dialog({ title: 'Hotkeys', show: 'fade', hide: 'fade' }); } };