1
0
Fork 0
mirror of https://github.com/transmission/transmission synced 2024-12-25 09:13:06 +00:00
transmission/web/javascript/torrent.js

797 lines
23 KiB
JavaScript

/*
* 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( );
this._file_model = [ ];
this._file_view = [ ];
// Create a new <li> 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' <div>
var e = document.createElement( 'div' );
e.className = 'torrent_name';
top_e.appendChild( e );
element._name_container = e;
// Create the 'progress details' <div>
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' <div>
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 <li>'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.initializeTorrentFilesInspectorGroup();
for( var i=0; data.files!=null && i<data.files.length; ++i ) {
var src = data.files[i];
var tgt = this._file_model[i];
if( !tgt )
tgt = this._file_model[i] = { };
tgt.index = i;
tgt.torrent = this;
tgt.length = src.length;
tgt.name = src.name;
}
// 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 <LI> 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.ensureFileListExists(); this.refreshFileView(); 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)
this.refreshFileModel( data );
},
refreshFileModel: function(data) {
for( var i=0; i<data.fileStats.length; ++i ) {
var src = data.fileStats[i];
var tgt = this._file_model[i];
if( !tgt )
tgt = this._file_model[i] = { };
tgt.wanted = src.wanted;
tgt.priority = src.priority;
tgt.bytesCompleted = src.bytesCompleted;
}
},
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.refreshFileView( );
},
refreshFileView: function() {
if( this._file_view.length )
for( var i=0; i<this._file_model.length; ++i )
this._file_view[i].update( this._file_model[i] );
},
ensureFileListExists: function() {
if( this._file_view.length == 0 ) {
for( var i=0; i<this._file_model.length; ++i ) {
var v = new TorrentFile( this._file_model[i] );
this._file_view[i] = v;
var e = v.domElement( );
e.className = (i % 2 ? 'even' : 'odd') + ' inspector_torrent_file_list_entry';
this._fileList.appendChild( e );
}
}
},
/*
* 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) {
//console.log( 'new TorrentFile ' + file_data.name );
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);
},
update: function(file_data) {
this.readAttributes(file_data);
this.refreshHTML();
},
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._file_model.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._file_model.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();
var class = element[0].className.replace(/ skip| complete/g, '').split(' ');
if(!this._wanted)
class.push('skip');
if(this._done>=this._size)
class.push('complete');
element[0].className = class.join(' ');
},
refreshPriorityHTML: function() {
var priority = { '1': 'high', '0': 'normal', '-1': 'low' }[new String(this._prio)];
var class = this._priority_control[0].className.replace(/ high| normal| low/g, '').split(' ');
class.push(priority)
this._priority_control[0].className = class.join(' ');
},
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'); }
}
};