/* * Copyright © Dave Perrett and Malcolm Jarvis * This code is licensed under the GPL version 2. * For details, see http://www.gnu.org/licenses/old-licenses/gpl-2.0.html * * Class Torrent */ function Torrent(controller,data) { this.initialize(controller,data); } // Constants Torrent._StatusWaitingToCheck = 1; Torrent._StatusChecking = 2; Torrent._StatusDownloading = 4; Torrent._StatusSeeding = 8; Torrent._StatusPaused = 16; Torrent._InfiniteTimeRemaining = 215784000; // 999 Hours - may as well be infinite Torrent.prototype = { /* * Constructor */ initialize: function(controller, data) { this._id = data.id; this._is_private = data.isPrivate; this._hashString = data.hashString; this._date = data.addedDate; this._size = data.totalSize; this._tracker = data.announceURL; this._comment = data.comment; this._creator = data.creator; this._creator_date = data.dateCreated; this._sizeWhenDone = data.sizeWhenDone; this._name = data.name; this._name_lc = this._name.toLowerCase( ); // Create a new
  • element var top_e = document.createElement( 'li' ); top_e.className = 'torrent'; top_e.id = 'torrent_' + data.id; var element = $(top_e); element._torrent = this; this._element = element; this._controller = controller; controller._rows.push( element ); // Create the 'name'
    var e = document.createElement( 'div' ); e.className = 'torrent_name'; top_e.appendChild( e ); element._name_container = e; // Create the 'progress details'
    e = document.createElement( 'div' ); e.className = 'torrent_progress_details'; top_e.appendChild( e ); element._progress_details_container = e; // Create the 'in progress' bar e = document.createElement( 'div' ); e.className = 'torrent_progress_bar incomplete'; e.style.width = '0%'; top_e.appendChild( e ); element._progress_complete_container = e; // Create the 'incomplete' bar (initially hidden) e = document.createElement( 'div' ); e.className = 'torrent_progress_bar incomplete'; e.style.display = 'none'; top_e.appendChild( e ); element._progress_incomplete_container = e; // Add the pause/resume button - don't specify the // image or alt text until the 'refresh()' function // (depends on torrent state) var image = document.createElement( 'div' ); image.className = 'torrent_pause'; e = document.createElement( 'a' ); e.appendChild( image ); top_e.appendChild( e ); element._pause_resume_button_image = image; //element._pause_resume_button = e; if (!iPhone) $(e).bind('click', {element: element}, this.clickPauseResumeButton); // Create the 'peer details'
    e = document.createElement( 'div' ); e.className = 'torrent_peer_details'; top_e.appendChild( e ); element._peer_details_container = e; // Set the torrent click observer element.bind('click', {element: element}, this.clickTorrent); if (!iPhone) element.bind('contextmenu', {element: element}, this.rightClickTorrent); // Safari hack - first torrent needs to be moved down for some reason. Seems to be ok when // using
  • 's in straight html, but adding through the DOM gets a bit odd. if ($.browser.safari) this._element.css('margin-top', '7px'); // insert the element this._controller._torrent_list.appendChild( top_e ); this._files = []; this.initializeTorrentFilesInspectorGroup(); if(data.files){ if(data.files.length == 1) this._fileList.className += ' single_file'; for (var i = 0; i < data.files.length; i++) { var file = data.files[i]; file.index = i; file.torrent = this; file.priority = data.fileStats[i].priority; file.wanted = data.fileStats[i].wanted; var torrentFile = new TorrentFile(file); this._files.push(torrentFile); var e = torrentFile.domElement(); e.className = (i % 2 ? 'even' : 'odd') + ' inspector_torrent_file_list_entry'; this._fileList.appendChild( e ); } } // Update all the labels etc this.refresh(data); }, initializeTorrentFilesInspectorGroup: function(length) { var e = document.createElement( 'ul' ); e.className = 'inspector_torrent_file_list inspector_group'; e.style.display = 'none'; this._controller._inspector_file_list.appendChild( e ); this._fileList = e; }, fileList: function() { return $(this._fileList); }, /*-------------------------------------------- * * S E T T E R S / G E T T E R S * *--------------------------------------------*/ /* Return the DOM element for this torrent (a
  • element) */ element: function() { return this._element; }, setElement: function( element ) { this._element = element; element._torrent = this; this.refreshHTML( ); }, activity: function() { return this._download_speed + this._upload_speed; }, comment: function() { return this._comment; }, completed: function() { return this._completed; }, creator: function() { return this._creator; }, dateAdded: function() { return this._date; }, downloadSpeed: function() { return this._download_speed; }, downloadTotal: function() { return this._download_total; }, errorMessage: function() { return this._error_message; }, hash: function() { return this._hashString; }, id: function() { return this._id; }, isActive: function() { return this.state() != Torrent._StatusPaused; }, isDownloading: function() { return this.state() == Torrent._StatusDownloading; }, isSeeding: function() { return this.state() == Torrent._StatusSeeding; }, name: function() { return this._name; }, peersSendingToUs: function() { return this._peers_sending_to_us; }, peersGettingFromUs: function() { return this._peers_getting_from_us; }, getPercentDone: function() { if( !this._sizeWhenDone ) return 1.0; if( !this._leftUntilDone ) return 1.0; return ( this._sizeWhenDone - this._leftUntilDone ) / this._sizeWhenDone; }, getPercentDoneStr: function() { return Math.floor(100 * Math.ratio( 100 * ( this._sizeWhenDone - this._leftUntilDone ), this._sizeWhenDone )) / 100; }, size: function() { return this._size; }, state: function() { return this._state; }, stateStr: function() { switch( this.state() ) { case Torrent._StatusSeeding: return 'Seeding'; case Torrent._StatusDownloading: return 'Downloading'; case Torrent._StatusPaused: return 'Paused'; case Torrent._StatusChecking: return 'Verifying local data'; case Torrent._StatusWaitingToCheck: return 'Waiting to verify'; default: return 'error'; } }, swarmSpeed: function() { return this._swarm_speed; }, totalLeechers: function() { return this._total_leechers; }, totalSeeders: function() { return this._total_seeders; }, uploadSpeed: function() { return this._upload_speed; }, uploadTotal: function() { return this._upload_total; }, showFileList: function() { this.refreshFiles(); this.fileList().show(); }, hideFileList: function() { this.fileList().hide(); }, /*-------------------------------------------- * * E V E N T F U N C T I O N S * *--------------------------------------------*/ /* * Process a right-click event on this torrent */ rightClickTorrent: function(event) { // don't stop the event! need it for the right-click menu var t = event.data.element._torrent; if ( !t.isSelected( ) ) t._controller.setSelectedTorrent( t ); }, /* * Process a click event on this torrent */ clickTorrent: function( event ) { // Prevents click carrying to parent element // which deselects all on click event.stopPropagation(); var torrent = event.data.element._torrent; // 'Apple' button emulation on PC : // Need settable meta-key and ctrl-key variables for mac emulation var meta_key = event.metaKey var ctrl_key = event.ctrlKey if (event.ctrlKey && navigator.appVersion.toLowerCase().indexOf("mac") == -1) { meta_key = true; ctrl_key = false; } // Shift-Click - Highlight a range between this torrent and the last-clicked torrent if (iPhone) { torrent._controller.setSelectedTorrent( torrent, true ); } else if (event.shiftKey) { torrent._controller.selectRange( torrent, true ); // Need to deselect any selected text window.focus(); // Apple-Click, not selected } else if (!torrent.isSelected() && meta_key) { torrent._controller.selectTorrent( torrent, true ); // Regular Click, not selected } else if (!torrent.isSelected()) { torrent._controller.setSelectedTorrent( torrent, true ); // Apple-Click, selected } else if (torrent.isSelected() && meta_key) { torrent._controller.deselectTorrent( torrent, true ); // Regular Click, selected } else if (torrent.isSelected()) { torrent._controller.setSelectedTorrent( torrent, true ); } torrent._controller.setLastTorrentClicked(torrent); }, /* * Process a click event on the pause/resume button */ clickPauseResumeButton: function( event ) { // prevent click event resulting in selection of torrent event.stopPropagation(); // either stop or start the torrent var torrent = event.data.element._torrent; if( torrent.isActive( ) ) torrent._controller.stopTorrent( torrent ); else torrent._controller.startTorrent( torrent ); }, /*-------------------------------------------- * * I N T E R F A C E F U N C T I O N S * *--------------------------------------------*/ refresh: function(data) { this.refreshData( data ); this.refreshHTML( ); }, /* * Refresh display */ refreshData: function(data) { this._completed = data.haveUnchecked + data.haveValid; this._verified = data.haveValid; this._leftUntilDone = data.leftUntilDone; this._download_total = data.downloadedEver; this._upload_total = data.uploadedEver; this._download_speed = data.rateDownload; this._upload_speed = data.rateUpload; this._peers_connected = data.peersConnected; this._peers_getting_from_us = data.peersGettingFromUs; this._peers_sending_to_us = data.peersSendingToUs; this._error = data.error; this._error_message = data.errorString; this._eta = data.eta; this._swarm_speed = data.swarmSpeed; this._total_leechers = Math.max( 0, data.leechers ); this._total_seeders = Math.max( 0, data.seeders ); this._state = data.status; if (data.fileStats) { for (var i = 0; i < data.fileStats.length; i++) { var file_data = {}; file_data.priority = data.fileStats[i].priority; file_data.wanted = data.fileStats[i].wanted; file_data.bytesCompleted = data.fileStats[i].bytesCompleted; this._files[i].readAttributes(file_data); } } }, refreshFileData: function(data) { for (var i = 0; i < data.fileStats.length; i++) { var file_data = {}; file_data.priority = data.fileStats[i].priority; file_data.wanted = data.fileStats[i].wanted; file_data.bytesCompleted = data.fileStats[i].bytesCompleted; this._files[i].readAttributes(file_data); this._files[i].refreshHTML(); } }, refreshHTML: function() { var progress_details; var peer_details; var root = this._element; var MaxBarWidth = 100; // reduce this to make the progress bar shorter (%) setInnerHTML( root._name_container, this._name ); // Add the progress bar var notDone = this._leftUntilDone > 0; // Fix for situation // when a verifying/downloading torrent gets state seeding if( this._state == Torrent._StatusSeeding ) notDone = false ; if( notDone ) { var eta = ''; if( this.isActive( ) ) { eta = '-'; if (this._eta < 0 || this._eta >= Torrent._InfiniteTimeRemaining ) eta += 'remaining time unknown'; else eta += Math.formatSeconds(this._eta) + ' remaining'; } // Create the 'progress details' label // Eg: '101 MB of 631 MB (16.02%) - 2 hr remaining' progress_details = Math.formatBytes( this._sizeWhenDone - this._leftUntilDone ) + ' of ' + Math.formatBytes( this._sizeWhenDone ) + ' (' + this.getPercentDoneStr() + '%)' + eta; // Figure out the percent completed var css_completed_width = Math.floor( this.getPercentDone() * MaxBarWidth ); // Update the 'in progress' bar var class_name = this.isActive() ? 'in_progress' : 'incomplete_stopped'; var e = root._progress_complete_container; var str = 'torrent_progress_bar ' + class_name; if(css_completed_width == 0) { str += ' empty'; } e.className = str; e.style.width = css_completed_width + '%'; // Update the 'incomplete' bar e = root._progress_incomplete_container; if( e.className.indexOf( 'incomplete' ) == -1 ) e.className = 'torrent_progress_bar in_progress'; e.style.width = (MaxBarWidth - css_completed_width) + '%'; e.style.display = 'block'; // Create the 'peer details' label // Eg: 'Downloading from 36 of 40 peers - DL: 60.2 KB/s UL: 4.3 KB/s' if( !this.isDownloading( ) ) peer_details = this.stateStr( ); else { peer_details = 'Downloading from ' + this.peersSendingToUs() + ' of ' + this._peers_connected + ' peers - DL: ' + Math.formatBytes(this._download_speed) + '/s UL: ' + Math.formatBytes(this._upload_speed) + '/s'; } } else { // Update the 'in progress' bar var class_name = (this.isActive()) ? 'complete' : 'complete_stopped'; var e = root._progress_complete_container; e.className = 'torrent_progress_bar ' + class_name; // Create the 'progress details' label // Eg: '698.05 MB, uploaded 8.59 GB (Ratio: 12.3)' progress_details = Math.formatBytes( this._size ) + ', uploaded ' + Math.formatBytes( this._upload_total ) + ' (Ratio ' + Math.ratio( this._upload_total, this._download_total ) + ')'; // Hide the 'incomplete' bar root._progress_incomplete_container.style.display = 'none'; // Set progress to maximum root._progress_complete_container.style.width = MaxBarWidth + '%'; // Create the 'peer details' label // Eg: 'Seeding to 13 of 22 peers - UL: 36.2 KB/s' if( !this.isSeeding( ) ) peer_details = this.stateStr( ); else peer_details = 'Seeding to ' + this.peersGettingFromUs() + ' of ' + this._peers_connected + ' peers - UL: ' + Math.formatBytes(this._upload_speed) + '/s'; } // Update the progress details setInnerHTML( root._progress_details_container, progress_details ); // Update the peer details and pause/resume button e = root._pause_resume_button_image; if ( this.state() == Torrent._StatusPaused ) { e.alt = 'Resume'; e.className = "torrent_resume"; } else { e.alt = 'Pause'; e.className = "torrent_pause"; } if( this._error_message && this._error_message != '' && this._error_message != 'other' ) { peer_details = this._error_message; } setInnerHTML( root._peer_details_container, peer_details ); this.refreshFiles( ); }, refreshFiles: function() { jQuery.each(this._files, function () { this.refreshHTML(); } ); }, /* * Return true if this torrent is selected */ isSelected: function() { return this.element()[0].className.indexOf('selected') != -1; }, /** * @param filter one of Prefs._Filter* * @param search substring to look for, or null * @return true if it passes the test, false if it fails */ test: function( filter, search ) { var pass = false; switch( filter ) { case Prefs._FilterSeeding: pass = this.isSeeding(); break; case Prefs._FilterDownloading: pass = this.isDownloading(); break; case Prefs._FilterPaused: pass = !this.isActive(); break; default: pass = true; break; } if( !pass ) return false; if( !search || !search.length ) return pass; var pos = this._name_lc.indexOf( search.toLowerCase() ); pass = pos != -1; return pass; } }; /** Helper function for Torrent.sortTorrents(). */ Torrent.compareById = function( a, b ) { return a.id() - b.id(); }; /** Helper function for sortTorrents(). */ Torrent.compareByAge = function( a, b ) { return a.dateAdded() - b.dateAdded(); }; /** Helper function for sortTorrents(). */ Torrent.compareByName = function( a, b ) { return a._name_lc.compareTo( b._name_lc ); }; /** Helper function for sortTorrents(). */ Torrent.compareByTracker = function( a, b ) { return a._tracker.compareTo( b._tracker ); }; /** Helper function for sortTorrents(). */ Torrent.compareByState = function( a, b ) { return a.state() - b.state(); }; /** Helper function for sortTorrents(). */ Torrent.compareByActivity = function( a, b ) { return a.activity() - b.activity(); }; /** Helper function for sortTorrents(). */ Torrent.compareByProgress = function( a, b ) { if( a.getPercentDone() !== b.getPercentDone() ) return a.getPercentDone() - b.getPercentDone(); var a_ratio = Math.ratio( a._upload_total, a._download_total ); var b_ratio = Math.ratio( b._upload_total, b._download_total ); return a_ratio - b_ratio; } /** * @param torrents an array of Torrent objects * @param sortMethod one of Prefs._SortBy* * @param sortDirection Prefs._SortAscending or Prefs._SortDescending */ Torrent.sortTorrents = function( torrents, sortMethod, sortDirection ) { switch( sortMethod ) { case Prefs._SortByActivity: torrents.sort( this.compareByActivity ); break; case Prefs._SortByAge: torrents.sort( this.compareByAge ); break; case Prefs._SortByQueue: torrents.sort( this.compareById ); break; case Prefs._SortByProgress: torrents.sort( this.compareByProgress ); break; case Prefs._SortByState: torrents.sort( this.compareByState ); break; case Prefs._SortByTracker: torrents.sort( this.compareByTracker ); break; case Prefs._SortByName: torrents.sort( this.compareByName ); break; default: console.warn( "unknown sort method: " + sortMethod ); break; } if( sortDirection == Prefs._SortDescending ) torrents.reverse( ); return torrents; }; /** * @brief fast binary search to find a torrent * @param torrents an array of torrents sorted by Id * @param id the id to search for * @return the index, or -1 */ Torrent.indexOf = function( torrents, id ) { var low = 0; var high = torrents.length; while( low < high ) { var mid = Math.floor( ( low + high ) / 2 ); if( torrents[mid].id() < id ) low = mid + 1; else high = mid; } if( ( low < torrents.length ) && ( torrents[low].id() == id ) ) { return low; } else { return -1; // not found } }; /** * @param torrents an array of torrents sorted by Id * @param id the id to search for * @return the torrent, or null */ Torrent.lookup = function( torrents, id ) { var pos = Torrent.indexOf( torrents, id ); return pos >= 0 ? torrents[pos] : null; }; function TorrentFile(file_data) { this.initialize(file_data); } TorrentFile.prototype = { initialize: function(file_data) { this._dirty = true; this._torrent = file_data.torrent; var pos = file_data.name.indexOf('/'); if (pos >= 0) this.name = file_data.name.substring(pos + 1); else this.name = file_data.name; this.readAttributes(file_data); var li = document.createElement('li'); var wanted_div = document.createElement('div'); wanted_div.className = "file_wanted_control"; var pri_div = document.createElement('div'); pri_div.className = "file_priority_control"; var file_div = document.createElement('div'); file_div.className = "inspector_torrent_file_list_entry_name"; file_div.textContent = this.name; var prog_div = document.createElement('div'); prog_div.className = "inspector_torrent_file_list_entry_progress"; li.appendChild(wanted_div); li.appendChild(pri_div); li.appendChild(file_div); li.appendChild(prog_div); this._element = li; this._priority_control = $(pri_div); this._progress = $(prog_div); $(wanted_div).bind('click', { file: this }, this.fileWantedControlClicked); this._priority_control.bind('click', { file: this }, this.filePriorityControlClicked); }, readAttributes: function(file_data) { if( file_data.index != undefined && file_data.index != this._index ) { this._index = file_data.index; this._dirty = true; } if( file_data.bytesCompleted != undefined && file_data.bytesCompleted != this._done ) { this._done = file_data.bytesCompleted; this._dirty = true; } if( file_data.length != undefined && file_data.length != this._size ) { this._size = file_data.length; this._dirty = true; } if( file_data.priority != undefined && file_data.priority != this._prio ) { this._prio = file_data.priority; this._dirty = true; } if( file_data.wanted != undefined && file_data.wanted != this._wanted ) { this._wanted = file_data.wanted; this._dirty = true; } }, element: function() { return $(this._element); }, domElement: function() { return this._element; }, setPriority: function(priority) { if(this.element().hasClass('complete') || this._torrent._files.length == 1) return; var priority_level = { high: 1, normal: 0, low: -1 }[priority]; if (this._prio == priority_level) { return; } this._prio = priority_level; this._torrent._controller.changeFileCommand("priority-" + priority, this._torrent, this); this._dirty = true; }, setWanted: function(wanted) { this._dirty = true; this._wanted = wanted; this.element().toggleClass( 'skip', !wanted ); var command = wanted ? 'files-wanted' : 'files-unwanted'; this._torrent._controller.changeFileCommand(command, this._torrent, this); }, toggleWanted: function() { if(this.element().hasClass('complete') || this._torrent._files.length == 1) return; this.setWanted(!this._wanted); }, refreshHTML: function() { if( this._dirty ) { this._dirty = false; this.refreshProgressHTML(); this.refreshWantedHTML(); this.refreshPriorityHTML(); } }, refreshProgressHTML: function() { progress_details = Math.formatBytes(this._done) + ' of ' + Math.formatBytes(this._size) + ' (' + Math.ratio(100 * this._done, this._size) + '%)'; setInnerHTML(this._progress[0], progress_details); }, refreshWantedHTML: function() { var element = this.element(); this.element().toggleClass('skip', !this._wanted); this.element().toggleClass('complete', this._done>=this._size ); }, refreshPriorityHTML: function() { var priority = { '1': 'high', '0': 'normal', '-1': 'low' }[new String(this._prio)]; var off_priorities = [ 'high', 'normal', 'low' ].sort(function(a,b) { return (a == priority) ? 1 : -1; } ); this._priority_control.addClass(priority). removeClass(off_priorities[0]). removeClass(off_priorities[1]); }, fileWantedControlClicked: function(event) { event.data.file.toggleWanted(); }, filePriorityControlClicked: function(event) { var x = event.pageX; var target = this; while (target != null) { x = x - target.offsetLeft; target = target.offsetParent; } var file = event.data.file; if (x < 12) { file.setPriority('low'); } else if (x < 23) { file.setPriority('normal'); } else { file.setPriority('high'); } } };