(trunk Qt) sync the "trackers" tab with the GTK+ client and more.

This commit is contained in:
Charles Kerr 2010-07-27 19:43:32 +00:00
parent f9db0755c0
commit 914af1c9f8
17 changed files with 1417 additions and 813 deletions

View File

@ -104,31 +104,11 @@
"seedIdleMode" | number which seeding inactivity to use. See tr_inactvelimit
"seedRatioLimit" | double torrent-level seeding ratio
"seedRatioMode" | number which ratio to use. See tr_ratiolimit
"trackerAdd" | object (see below)
"trackerEdit" | object (see below)
"trackerRemove" | object (see below)
"trackerAdd" | array strings of URLs to add
"trackerRemove" | array strings of URLs to remove
"trackerReplace" | array pairs of old/new announce URLs
"uploadLimit" | number maximum upload speed (KBps)
"uploadLimited" | boolean true if "uploadLimit" is honored
|
----------------------+---------------------------------+
trackerAdd | an object containing: |
+-----------------------+---------+
| announce | string | announce URL of the tracker
| tier (optional) | number | tier to add the tracker to
----------------------+---------------------------------+
trackerEdit | an object containing: |
+-----------------------+---------+
| announce (or id) | string | announce URL of the tracker to modify
| id (or announce) | number | trackerId of the tracker to modify (see trackerStats)
+-----------------------+---------+
| announce-new | string | new announce URL for the tracker
| tier | number | tier to change the tracker to
----------------------+---------------------------------+
trackerRemove | an object containing: |
+-----------------------+---------+
| announce (or id) | string | announce URL of the tracker to remove
| id (or announce) | number | trackerId of the tracker to remove (see trackerStats)
+-----------------------+---------+
Just as an empty "ids" value is shorthand for "all ids", using an empty array
for "files-wanted", "files-unwanted", "priority-high", "priority-low", or

View File

@ -757,20 +757,17 @@ setFileDLs( tr_torrent * tor,
}
static tr_bool
findTrackerById( const tr_info * inf,
uint32_t id,
int * index )
findAnnounceUrl( const tr_tracker_info * t, int n, const char * url, int * pos )
{
int i;
tr_bool found = FALSE;
for( i = 0; i < inf->trackerCount; ++i )
for( i=0; i<n; ++i )
{
const tr_tracker_info * t = &inf->trackers[i];
if( t->id == id )
if( !strcmp( t[i].announce, url ) )
{
if( index ) *index = i;
found = TRUE;
if( pos ) *pos = i;
break;
}
}
@ -778,196 +775,157 @@ findTrackerById( const tr_info * inf,
return found;
}
static tr_bool
findTrackerByURL( const tr_info * inf,
const char * url,
int * index )
static int
copyTrackers( tr_tracker_info * tgt, const tr_tracker_info * src, int n )
{
int i;
tr_bool found = FALSE;
for( i = 0; i < inf->trackerCount; ++i )
int maxTier = -1;
for( i=0; i<n; ++i )
{
const tr_tracker_info * t = &inf->trackers[i];
if( !strcmp( t->announce, url ) )
{
if( index ) *index = i;
found = TRUE;
break;
}
tgt[i].tier = src[i].tier;
tgt[i].announce = tr_strdup( src[i].announce );
maxTier = MAX( maxTier, src[i].tier );
}
return found;
return maxTier;
}
static void
freeTrackers( tr_tracker_info * trackers, int n )
{
int i;
for( i=0; i<n; ++i )
tr_free( trackers[i].announce );
tr_free( trackers );
}
static const char*
addTracker( tr_torrent * tor,
tr_benc * tracker )
addTrackerUrls( tr_torrent * tor, tr_benc * urls )
{
int i;
int64_t tmp;
tr_bool duplicate = FALSE;
const char * errmsg = NULL;
const char * announce;
int n;
int tier;
tr_benc * val;
tr_tracker_info * trackers;
tr_bool changed = FALSE;
const tr_info * inf = tr_torrentInfo( tor );
const char * errmsg = NULL;
if( !tr_bencDictFindStr( tracker, "announce", &announce ) )
return "no announce url supplied";
/* make a working copy of the existing announce list */
n = inf->trackerCount;
trackers = tr_new0( tr_tracker_info, n + tr_bencListSize( urls ) );
tier = copyTrackers( trackers, inf->trackers, n );
duplicate = findTrackerByURL( inf, announce, NULL );
if( !duplicate )
/* and add the new ones */
i = 0;
while(( val = tr_bencListChild( urls, i++ ) ))
{
int tier, trackerCount;
tr_tracker_info * trackers = tr_new0( tr_tracker_info, inf->trackerCount + 1 );
const char * announce = NULL;
if( tr_bencDictFindInt( tracker, "tier", &tmp ) )
tier = (int)tmp;
else
tier = -1;
for( i = 0; i < inf->trackerCount; ++i )
if( tr_bencGetStr( val, &announce )
&& tr_urlIsValid( announce )
&& !findAnnounceUrl( trackers, n, announce, NULL ) )
{
const tr_tracker_info * t = &inf->trackers[i];
trackers[i].tier = t->tier;
trackers[i].announce = tr_strdup( t->announce );
trackers[n].tier = ++tier; /* add a new tier */
trackers[n].announce = tr_strdup( announce );
++n;
changed = TRUE;
}
trackers[i].tier = tier < 0 ? trackers[i-1].tier + 1 : tier;
trackers[i].announce = tr_strdup( announce );
trackerCount = inf->trackerCount + 1;
if( !tr_torrentSetAnnounceList( tor, trackers, trackerCount ) )
errmsg = "tracker URL was invalid";
for( i = 0; i < trackerCount; ++i )
tr_free( trackers[i].announce );
tr_free( trackers );
}
else
errmsg = "tracker already exists";
if( !changed )
errmsg = "invalid argument";
else if( !tr_torrentSetAnnounceList( tor, trackers, n ) )
errmsg = "error setting announce list";
freeTrackers( trackers, n );
return errmsg;
}
static const char*
editTracker( tr_torrent * tor,
tr_benc * tracker )
replaceTrackerUrls( tr_torrent * tor, tr_benc * urls )
{
int trackerIndex;
int64_t tmp;
tr_bool found = FALSE;
const char * errmsg = NULL;
const char * announce;
int i;
tr_benc * pair[2];
tr_tracker_info * trackers;
tr_bool changed = FALSE;
const tr_info * inf = tr_torrentInfo( tor );
const int n = inf->trackerCount;
const char * errmsg = NULL;
if( tr_bencDictFindInt( tracker, "id", &tmp ) )
found = findTrackerById( inf, (uint32_t)tmp, &trackerIndex );
else if( tr_bencDictFindStr( tracker, "announce", &announce ) )
found = findTrackerByURL( inf, announce, &trackerIndex );
else
errmsg = "no tracker supplied";
/* make a working copy of the existing announce list */
trackers = tr_new0( tr_tracker_info, n );
copyTrackers( trackers, inf->trackers, n );
if( found )
/* make the substitutions... */
i = 0;
while(((pair[0] = tr_bencListChild(urls,i))) &&
((pair[1] = tr_bencListChild(urls,i+1))))
{
int tier;
const char * new;
tr_bool rename = FALSE;
tr_bool move = FALSE;
const char * oldval;
const char * newval;
if( tr_bencDictFindStr( tracker, "announce-new", &new ) )
if( tr_bencGetStr( pair[0], &oldval )
&& tr_bencGetStr( pair[1], &newval )
&& strcmp( oldval, newval )
&& tr_urlIsValid( newval )
&& findAnnounceUrl( trackers, n, oldval, &i ) )
{
rename = !findTrackerByURL( inf, new, NULL );
if( !rename )
errmsg = "tracker already exists";
}
if( tr_bencDictFindInt( tracker, "tier", &tmp ) )
{
tier = (int)tmp;
move = TRUE;
tr_free( trackers[i].announce );
trackers[i].announce = tr_strdup( newval );
changed = TRUE;
}
if( ( rename || move ) && !errmsg )
{
int i, trackerCount;
tr_tracker_info * trackers = tr_new0( tr_tracker_info, inf->trackerCount );
for( i = 0; i < inf->trackerCount; ++i )
{
const tr_tracker_info * t = &inf->trackers[i];
if( i != trackerIndex )
{
trackers[i].tier = t->tier;
trackers[i].announce = tr_strdup( t->announce );
}
else
{
trackers[i].tier = move ? tier : t->tier;
trackers[i].announce = tr_strdup( rename ? new : t->announce );
}
}
trackerCount = i;
if( !tr_torrentSetAnnounceList( tor, trackers, trackerCount ) )
errmsg = "error setting announce list";
for( i = 0; i < trackerCount; ++i )
tr_free( trackers[i].announce );
tr_free( trackers );
}
else if( !errmsg )
errmsg = "no operation supplied";
i += 2;
}
else
errmsg = "tracker doesn't exists";
if( !changed )
errmsg = "invalid argument";
else if( !tr_torrentSetAnnounceList( tor, trackers, n ) )
errmsg = "error setting announce list";
freeTrackers( trackers, n );
return errmsg;
}
static const char*
removeTracker( tr_torrent * tor,
tr_benc * tracker )
removeTrackerUrls( tr_torrent * tor, tr_benc * urls )
{
int trackerIndex;
int64_t tmp;
tr_bool found = FALSE;
const char * errmsg = NULL;
const char * announce;
int i;
int n;
tr_benc * val;
tr_tracker_info * trackers;
tr_bool changed = FALSE;
const tr_info * inf = tr_torrentInfo( tor );
const char * errmsg = NULL;
if( tr_bencDictFindInt( tracker, "id", &tmp ) )
found = findTrackerById( inf, (uint32_t)tmp, &trackerIndex );
else if( tr_bencDictFindStr( tracker, "announce", &announce ) )
found = findTrackerByURL( inf, announce, &trackerIndex );
else
errmsg = "no tracker supplied";
/* make a working copy of the existing announce list */
n = inf->trackerCount;
trackers = tr_new0( tr_tracker_info, n );
copyTrackers( trackers, inf->trackers, n );
if( found )
/* remove the ones specified in the urls list */
i = 0;
while(( val = tr_bencListChild( urls, i++ )))
{
int i, j, trackerCount;
tr_tracker_info * trackers = tr_new0( tr_tracker_info, inf->trackerCount - 1 );
for( i = 0, j = 0; i < inf->trackerCount; ++i )
int pos;
const char * url;
if( tr_bencGetStr( val, &url ) && findAnnounceUrl( trackers, n, url, &pos ) )
{
if( i != trackerIndex )
{
const tr_tracker_info * t = &inf->trackers[i];
trackers[j].tier = t->tier;
trackers[j].announce = tr_strdup( t->announce );
++j;
}
tr_removeElementFromArray( trackers, pos, sizeof( tr_tracker_info ), n-- );
changed = TRUE;
}
trackerCount = j;
if( !tr_torrentSetAnnounceList( tor, trackers, trackerCount ) )
errmsg = "error setting announce list";
for( i = 0; i < trackerCount; ++i )
tr_free( trackers[i].announce );
tr_free( trackers );
}
else
errmsg = "tracker doesn't exists";
if( !changed )
errmsg = "invalid argument";
else if( !tr_torrentSetAnnounceList( tor, trackers, n ) )
errmsg = "error setting announce list";
freeTrackers( trackers, n );
return errmsg;
}
@ -988,7 +946,7 @@ torrentSet( tr_session * session,
int64_t tmp;
double d;
tr_benc * files;
tr_benc * tracker;
tr_benc * urls;
tr_bool boolVal;
tr_torrent * tor = torrents[i];
@ -1025,12 +983,12 @@ torrentSet( tr_session * session,
tr_torrentSetRatioLimit( tor, d );
if( tr_bencDictFindInt( args_in, "seedRatioMode", &tmp ) )
tr_torrentSetRatioMode( tor, tmp );
if( !errmsg && tr_bencDictFindDict( args_in, "trackerAdd", &tracker ) )
errmsg = addTracker( tor, tracker );
if( !errmsg && tr_bencDictFindDict( args_in, "trackerEdit", &tracker ) )
errmsg = editTracker( tor, tracker );
if( !errmsg && tr_bencDictFindDict( args_in, "trackerRemove", &tracker ) )
errmsg = removeTracker( tor, tracker );
if( !errmsg && tr_bencDictFindList( args_in, "trackerAdd", &urls ) )
errmsg = addTrackerUrls( tor, urls );
if( !errmsg && tr_bencDictFindList( args_in, "trackerRemove", &urls ) )
errmsg = removeTrackerUrls( tor, urls );
if( !errmsg && tr_bencDictFindList( args_in, "trackerReplace", &urls ) )
errmsg = replaceTrackerUrls( tor, urls );
notify( session, TR_RPC_TORRENT_CHANGED, tor );
}

View File

@ -12,7 +12,6 @@
#include <cassert>
#include <ctime>
#include <iostream>
#include <QCheckBox>
#include <QComboBox>
@ -26,12 +25,16 @@
#include <QHBoxLayout>
#include <QHeaderView>
#include <QInputDialog>
#include <QItemSelectionModel>
#include <QLabel>
#include <QList>
#include <QMap>
#include <QMessageBox>
#include <QPushButton>
#include <QRadioButton>
#include <QResizeEvent>
#include <QSpinBox>
#include <QStringList>
#include <QStyle>
#include <QTabWidget>
#include <QTextBrowser>
@ -53,6 +56,8 @@
#include "squeezelabel.h"
#include "torrent.h"
#include "torrent-model.h"
#include "tracker-delegate.h"
#include "tracker-model.h"
class Prefs;
class Session;
@ -163,7 +168,13 @@ Details :: Details( Session& session, Prefs& prefs, TorrentModel& model, QWidget
layout->addWidget( buttons );
QWidget::setAttribute( Qt::WA_DeleteOnClose, true );
QList<int> initKeys;
initKeys << Prefs :: SHOW_TRACKER_SCRAPES;
foreach( int key, initKeys )
refreshPref( key );
connect( &myTimer, SIGNAL(timeout()), this, SLOT(onTimer()));
connect( &myPrefs, SIGNAL(changed(int)), this, SLOT(refreshPref(int)) );
onTimer( );
myTimer.setSingleShot( false );
@ -190,7 +201,6 @@ Details :: setIds( const QSet<int>& ids )
}
myFileTreeView->clear( );
myIds = ids;
// listen to the new torrents
@ -206,6 +216,24 @@ Details :: setIds( const QSet<int>& ids )
onTimer( );
}
void
Details :: refreshPref( int key )
{
QString str;
switch( key )
{
case Prefs::SHOW_TRACKER_SCRAPES:
myTrackerDelegate->setShowMore( myPrefs.getBool( key ) );
myTrackerView->update( );
break;
default:
break;
}
}
/***
****
***/
@ -219,6 +247,12 @@ Details :: timeToStringRounded( int seconds )
void
Details :: onTimer( )
{
getNewData( );
}
void
Details :: getNewData( )
{
if( !myIds.empty( ) )
{
@ -680,190 +714,11 @@ Details :: refresh( )
myIdleSpin->blockSignals( false );
}
// tracker tab
//
QMap<QString,QTreeWidgetItem*> trackerTiers;
QMap<QString,QTreeWidgetItem*> trackerItems;
const time_t now( time( 0 ) );
const bool showScrape = myPrefs.getBool( Prefs::SHOW_TRACKER_SCRAPES );
foreach( const Torrent * t, torrents )
{
const QString idStr( QString::number( t->id( ) ) );
const TrackerStatsList trackerStats = t->trackerStats( );
///
/// Tracker tab
///
foreach( const TrackerStat& trackerStat, trackerStats )
{
QFont font;
QString str;
const QString tierKey( QString::number(trackerStat.tier) );
QTreeWidgetItem * tier = (QTreeWidgetItem*) myTrackerTiers.value( tierKey, 0 );
if( tier == 0 ) // check if has tier been created this pass
tier = (QTreeWidgetItem*) trackerTiers.value( tierKey, 0 );
if( tier == 0 ) // new tier
{
QFont tierFont;
tier = new QTreeWidgetItem( myTrackerTree );
myTrackerTree->addTopLevelItem( tier );
str = "Tier: " + QString::number( trackerStat.tier + 1 );
tier->setText( 0, str );
tierFont.setBold( true );
tier->setFont( 0, tierFont );
}
const QString key( idStr + tierKey + ":" + QString::number( trackerStat.id ) );
QTreeWidgetItem * item = (QTreeWidgetItem*) myTrackerItems.value( key, 0 );
if( item == 0 ) // new tracker
{
item = new QTreeWidgetItem( tier );
tier->addChild( item );
if( tier->childCount() == 1 )
tier->setExpanded( true );
}
str = trackerStat.host;
if( trackerStat.isBackup )
{
font.setItalic( true );
if( showScrape )
{
str += "\n";
str += "Tracker will be used as a backup";
}
}
else
{
font.setItalic( false );
if( trackerStat.hasAnnounced )
{
const QString tstr( timeToStringRounded( now - trackerStat.lastAnnounceTime ) );
str += "\n";
if( trackerStat.lastAnnounceSucceeded )
{
str += tr( "Got a list of %1 peers %2 ago" )
.arg( trackerStat.lastAnnouncePeerCount )
.arg( tstr );
}
else if( trackerStat.lastAnnounceTimedOut )
{
str += tr( "Peer list request timed out %1 ago; will retry" )
.arg( tstr );
}
else
{
str += tr( "Got an error %1 ago" )
.arg( tstr );
}
}
switch( trackerStat.announceState )
{
case TR_TRACKER_INACTIVE:
if( trackerStat.hasAnnounced )
{
str += "\n";
str += tr( "No updates scheduled" );
}
break;
case TR_TRACKER_WAITING:
{
const QString tstr( timeToStringRounded( trackerStat.nextAnnounceTime - now ) );
str += "\n";
str += tr( "Asking for more peers in %1" )
.arg( tstr );
}
break;
case TR_TRACKER_QUEUED:
str += "\n";
str += tr( "Queued to ask for more peers" );
break;
case TR_TRACKER_ACTIVE:
{
const QString tstr( timeToStringRounded( now - trackerStat.lastAnnounceStartTime ) );
str += "\n";
str += tr( "Asking for more peers now... %1" )
.arg( tstr );
}
break;
}
if( showScrape )
{
if( trackerStat.hasScraped )
{
const QString tstr( timeToStringRounded( now - trackerStat.lastScrapeTime ) );
str += "\n";
if( trackerStat.lastScrapeSucceeded )
{
str += tr( "Tracker had %1 seeders and %2 leechers %3 ago" )
.arg( trackerStat.seederCount )
.arg( trackerStat.leecherCount )
.arg( tstr );
}
else
{
str += tr( "Got a scrape error %1 ago" )
.arg( tstr );
}
}
switch( trackerStat.scrapeState )
{
case TR_TRACKER_INACTIVE:
break;
case TR_TRACKER_WAITING:
{
const QString tstr( timeToStringRounded( trackerStat.nextScrapeTime - now ) );
str += "\n";
str += tr( "Asking for peer counts in %1" )
.arg( tstr );
}
break;
case TR_TRACKER_QUEUED:
str += "\n";
str += tr( "Queued to ask for peer counts" );
break;
case TR_TRACKER_ACTIVE:
{
const QString tstr( timeToStringRounded( now - trackerStat.lastScrapeStartTime ) );
str += "\n";
str += tr( "Asking for peer counts now... %1" )
.arg( tstr );
}
break;
}
}
}
item->setText( 0, str );
item->setFont( 0, font );
item->setData( 0, TRACKERID, trackerStat.id );
item->setData( 0, TRACKERURL, trackerStat.announce );
item->setData( 0, TRACKERTIER, trackerStat.tier );
item->setData( 0, TORRENTID, t->id() );
tier->setData( 0, TRACKERID, -1 );
tier->setData( 0, TRACKERURL, QString() );
tier->setData( 0, TRACKERTIER, trackerStat.tier );
tier->setData( 0, TORRENTID, torrents.count() > 1 ? -1 : t->id() );
trackerTiers.insert( tierKey, tier );
trackerItems.insert( key, item );
}
}
QList<QTreeWidgetItem*> tierList = trackerTiers.values();
QList<QTreeWidgetItem*> itemList = trackerItems.values();
for( int i = 0; i < myTrackerTree->topLevelItemCount(); ++i )
{
QTreeWidgetItem * tier = myTrackerTree->topLevelItem( i );
for( int j = 0; j < tier->childCount(); ++j )
{
if( !itemList.contains( tier->child( j ) ) ) // tracker has disappeared
delete tier->takeChild( j-- );
}
if( !tierList.contains( tier ) ) // tier has disappeared
delete myTrackerTree->takeTopLevelItem( i-- );
}
myTrackerTiers = trackerTiers;
myTrackerItems = trackerItems;
myTrackerModel->refresh( myModel, myIds );
///
/// Peers tab
@ -1014,26 +869,31 @@ void
Details :: onHonorsSessionLimitsToggled( bool val )
{
mySession.torrentSet( myIds, "honorsSessionLimits", val );
getNewData( );
}
void
Details :: onDownloadLimitedToggled( bool val )
{
mySession.torrentSet( myIds, "downloadLimited", val );
getNewData( );
}
void
Details :: onDownloadLimitChanged( int val )
{
mySession.torrentSet( myIds, "downloadLimit", val );
getNewData( );
}
void
Details :: onUploadLimitedToggled( bool val )
{
mySession.torrentSet( myIds, "uploadLimited", val );
getNewData( );
}
void
Details :: onUploadLimitChanged( int val )
{
mySession.torrentSet( myIds, "uploadLimit", val );
getNewData( );
}
void
@ -1041,12 +901,14 @@ Details :: onIdleModeChanged( int index )
{
const int val = myIdleCombo->itemData( index ).toInt( );
mySession.torrentSet( myIds, "seedIdleMode", val );
getNewData( );
}
void
Details :: onIdleLimitChanged( int val )
{
mySession.torrentSet( myIds, "seedIdleLimit", val );
getNewData( );
}
void
@ -1060,12 +922,14 @@ void
Details :: onRatioLimitChanged( double val )
{
mySession.torrentSet( myIds, "seedRatioLimit", val );
getNewData( );
}
void
Details :: onMaxPeersChanged( int val )
{
mySession.torrentSet( myIds, "peer-limit", val );
getNewData( );
}
void
@ -1075,120 +939,114 @@ Details :: onBandwidthPriorityChanged( int index )
{
const int priority = myBandwidthPriorityCombo->itemData(index).toInt( );
mySession.torrentSet( myIds, "bandwidthPriority", priority );
getNewData( );
}
}
void
Details :: onTrackerSelectionChanged( )
{
const QList<QTreeWidgetItem*> items = myTrackerTree->selectedItems();
if( items.count() == 1 )
myEditTrackerButton->setEnabled( items.first()->data( 0, TRACKERID ).toInt() >= 0 );
const int selectionCount = myTrackerView->selectionModel()->selectedRows().size();
myEditTrackerButton->setEnabled( selectionCount == 1 );
myRemoveTrackerButton->setEnabled( selectionCount > 0 );
}
void
Details :: onAddTrackerClicked( )
{
bool ok = false;
const QString url = QInputDialog::getText( this,
tr( "Add URL " ),
tr( "Add tracker announce URL:" ),
QLineEdit::Normal, QString(), &ok );
if( !ok )
{
// user pressed "cancel" -- noop
}
else if( !QUrl(url).isValid( ) )
{
QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( url ) );
}
else
myEditTrackerButton->setEnabled( false );
myRemoveTrackerButton->setEnabled( !items.isEmpty() );
}
bool
Details :: findTrackerByURL( const QString& url, int torId )
{
bool duplicate = false;
foreach( QTreeWidgetItem * tracker, myTrackerItems.values() )
{
if( tracker->data( 0, TRACKERURL ).toString() == url &&
( torId == -1 || tracker->data( 0, TORRENTID ).toInt() == torId ) )
QSet<int> ids;
foreach( int id, myIds )
if( myTrackerModel->find( id, url ) == -1 )
ids.insert( id );
if( ids.empty( ) ) // all the torrents already have this tracker
{
duplicate = true;
break;
}
}
return duplicate;
}
void
Details :: onAddTrackerPushed( )
{
const QString urlString = QInputDialog::getText( this,
tr( "Add tracker announce URL " ),
NULL );
if( !urlString.isEmpty() )
{
if( !findTrackerByURL( urlString, -1 ) )
{
QByteArray url = urlString.toUtf8();
tr_benc top;
tr_bencInitDict( &top, 1 );
tr_bencDictAddStr( &top, "announce", url );
mySession.torrentSet( myIds, "trackerAdd", &top );
QMessageBox::warning( this, tr( "Error" ), tr( "Tracker already exists." ) );
}
else
QMessageBox::warning( this, "Error", "Tracker already exists." );
{
QStringList urls;
urls << url;
mySession.torrentSet( ids, "trackerAdd", urls );
getNewData( );
}
}
}
void
Details :: onEditTrackerPushed( )
Details :: onEditTrackerClicked( )
{
const QTreeWidgetItem * item = myTrackerTree->selectedItems().first();
const QString urlString = QInputDialog::getText( this,
tr( "Edit tracker announce URL " ),
NULL,
QLineEdit::Normal,
item->data( 0, TRACKERURL ).toString() );
if( !urlString.isEmpty() )
QItemSelectionModel * selectionModel = myTrackerView->selectionModel( );
QModelIndexList selectedRows = selectionModel->selectedRows( );
assert( selectedRows.size( ) == 1 );
QModelIndex i = selectionModel->currentIndex( );
const TrackerInfo trackerInfo = myTrackerModel->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>();
bool ok = false;
const QString newval = QInputDialog::getText( this,
tr( "Edit URL " ),
tr( "Edit tracker announce URL:" ),
QLineEdit::Normal,
trackerInfo.st.announce, &ok );
if( !ok )
{
const int torId = item->data( 0, TORRENTID ).toInt();
if( !findTrackerByURL( urlString, torId ) )
{
QByteArray url = urlString.toUtf8();
QSet<int> ids;
tr_benc top;
// user pressed "cancel" -- noop
}
else if( !QUrl(newval).isValid( ) )
{
QMessageBox::warning( this, tr( "Error" ), tr( "Invalid URL \"%1\"" ).arg( newval ) );
}
else
{
QSet<int> ids;
ids << trackerInfo.torrentId;
ids << torId;
tr_bencInitDict( &top, 2 );
tr_bencDictAddStr( &top, "announce", item->data( 0, TRACKERURL ).toByteArray() );
tr_bencDictAddStr( &top, "announce-new", url );
QStringList urls;
urls << trackerInfo.st.announce;
urls << newval;
mySession.torrentSet( ids, "trackerEdit", &top );
}
else
QMessageBox::warning( this, "Error", "Tracker already exists." );
mySession.torrentSet( ids, "trackerReplace", urls );
getNewData( );
}
}
void
Details :: removeTracker( const QTreeWidgetItem * item )
Details :: onRemoveTrackerClicked( )
{
QByteArray url = item->data( 0, TRACKERURL ).toByteArray();
const int torId = item->data( 0, TORRENTID ).toInt();
QSet<int> ids;
tr_benc top;
// make a map of torrentIds to announce URLs to remove
QItemSelectionModel * selectionModel = myTrackerView->selectionModel( );
QModelIndexList selectedRows = selectionModel->selectedRows( );
QMap<int,QStringList> torrentId_to_urls;
foreach( QModelIndex i, selectedRows )
{
const TrackerInfo inf = myTrackerModel->data( i, TrackerModel::TrackerRole ).value<TrackerInfo>();
torrentId_to_urls[ inf.torrentId ].append( inf.st.announce );
}
ids << torId;
tr_bencInitDict( &top, 1 );
tr_bencDictAddStr( &top, "announce", url );
mySession.torrentSet( ids, "trackerRemove", &top );
}
void
Details :: onRemoveTrackerPushed( )
{
const QList<QTreeWidgetItem*> items = myTrackerTree->selectedItems();
QSet<int> removedTiers;
foreach( const QTreeWidgetItem * item, items ) {
const bool isTier = item->data( 0, TRACKERID ).toInt() == -1;
const int curTier = item->data( 0, TRACKERTIER ).toInt();
if( isTier )
{
removedTiers << curTier;
for( int i = 0; i < item->childCount(); ++i )
removeTracker( item->child( i ) );
}
else if( !removedTiers.contains( curTier ) ) // skip trackers removed by clearing a tier
removeTracker( item );
// batch all of a tracker's torrents into one command
foreach( int id, torrentId_to_urls.keys( ) )
{
QSet<int> ids;
ids << id;
mySession.torrentSet( ids, "trackerRemove", torrentId_to_urls.value( id ) );
getNewData( );
}
}
@ -1306,25 +1164,24 @@ Details :: createTrackerTab( )
v2->setSpacing( HIG::PAD );
QStringList headers;
headers << tr("Trackers");
myTrackerTree = new QTreeWidget;
myTrackerTree->setHeaderLabels( headers );
myTrackerTree->setSelectionMode( QTreeWidget::ExtendedSelection );
myTrackerTree->setRootIsDecorated( false );
myTrackerTree->setIndentation( 2 );
myTrackerTree->setItemsExpandable( false );
myTrackerTree->setTextElideMode( Qt::ElideRight );
myTrackerTree->setAlternatingRowColors( true );
connect( myTrackerTree, SIGNAL(itemSelectionChanged()), this, SLOT(onTrackerSelectionChanged()));
h->addWidget( myTrackerTree, 1 );
myTrackerView = new QTreeView;
myTrackerView->setModel( myTrackerModel = new TrackerModel );
myTrackerView->setHeaderHidden( true );
myTrackerView->setSelectionMode( QTreeWidget::ExtendedSelection );
myTrackerView->setRootIsDecorated( false );
myTrackerView->setIndentation( 2 );
myTrackerView->setItemsExpandable( false );
myTrackerView->setAlternatingRowColors( true );
myTrackerView->setItemDelegate( myTrackerDelegate = new TrackerDelegate( ) );
connect( myTrackerView->selectionModel(), SIGNAL(selectionChanged(const QItemSelection&, const QItemSelection&)), this, SLOT(onTrackerSelectionChanged()));
h->addWidget( myTrackerView, 1 );
p = new QPushButton();
p->setIcon( getStockIcon( "list-add", QStyle::SP_DialogOpenButton ) );
p->setToolTip( "Add Tracker" );
myAddTrackerButton = p;
v2->addWidget( p, 1 );
connect( p, SIGNAL(clicked(bool)), this, SLOT(onAddTrackerPushed()));
connect( p, SIGNAL(clicked(bool)), this, SLOT(onAddTrackerClicked()));
p = new QPushButton();
p->setIcon( getStockIcon( "document-properties", QStyle::SP_DesktopIcon ) );
@ -1333,7 +1190,7 @@ Details :: createTrackerTab( )
p->setEnabled( false );
myEditTrackerButton = p;
v2->addWidget( p, 1 );
connect( p, SIGNAL(clicked(bool)), this, SLOT(onEditTrackerPushed()));
connect( p, SIGNAL(clicked(bool)), this, SLOT(onEditTrackerClicked()));
p = new QPushButton();
p->setIcon( getStockIcon( "list-remove", QStyle::SP_TrashIcon ) );
@ -1341,7 +1198,7 @@ Details :: createTrackerTab( )
p->setEnabled( false );
myRemoveTrackerButton = p;
v2->addWidget( p, 1 );
connect( p, SIGNAL(clicked(bool)), this, SLOT(onRemoveTrackerPushed()));
connect( p, SIGNAL(clicked(bool)), this, SLOT(onRemoveTrackerClicked()));
v2->addStretch( 1 );
@ -1428,6 +1285,7 @@ Details :: onFilePriorityChanged( const QSet<int>& indices, int priority )
default: key = "priority-normal"; break;
}
mySession.torrentSet( myIds, key, indices.toList( ) );
getNewData( );
}
void
@ -1435,4 +1293,5 @@ Details :: onFileWantedChanged( const QSet<int>& indices, bool wanted )
{
QString key( wanted ? "files-wanted" : "files-unwanted" );
mySession.torrentSet( myIds, key, indices.toList( ) );
getNewData( );
}

View File

@ -36,19 +36,15 @@ class QTreeWidgetItem;
class Session;
class Torrent;
class TorrentModel;
class TrackerDelegate;
class TrackerModel;
class Details: public QDialog
{
Q_OBJECT
private:
enum
{
TRACKERID = Qt::UserRole,
TRACKERURL,
TRACKERTIER,
TORRENTID
};
void getNewData( );
private slots:
void onTorrentChanged( );
@ -71,8 +67,6 @@ class Details: public QDialog
QString timeToStringRounded( int seconds );
QString trimToDesiredWidth( const QString& str );
void enableWhenChecked( QCheckBox *, QWidget * );
bool findTrackerByURL( const QString& url, int torId );
void removeTracker( const QTreeWidgetItem * item );
private:
Session& mySession;
@ -126,16 +120,21 @@ class Details: public QDialog
QLabel * myAnnounceResponseLabel;
QLabel * myAnnounceManualLabel;
QTreeWidget * myTrackerTree;
TrackerModel * myTrackerModel;
TrackerDelegate * myTrackerDelegate;
QTreeView * myTrackerView;
//QMap<QString,QTreeWidgetItem*> myTrackerTiers;
//QMap<QString,QTreeWidgetItem*> myTrackerItems;
QTreeWidget * myPeerTree;
QMap<QString,QTreeWidgetItem*> myTrackerTiers;
QMap<QString,QTreeWidgetItem*> myTrackerItems;
QMap<QString,QTreeWidgetItem*> myPeers;
QWidgetList myWidgets;
FileTreeView * myFileTreeView;
private slots:
void refreshPref( int key );
void onBandwidthPriorityChanged( int );
void onFilePriorityChanged( const QSet<int>& fileIndices, int );
void onFileWantedChanged( const QSet<int>& fileIndices, bool );
@ -150,9 +149,9 @@ class Details: public QDialog
void onIdleLimitChanged( int );
void onShowTrackerScrapesToggled( bool );
void onTrackerSelectionChanged( );
void onAddTrackerPushed( );
void onEditTrackerPushed( );
void onRemoveTrackerPushed( );
void onAddTrackerClicked( );
void onEditTrackerClicked( );
void onRemoveTrackerClicked( );
void onMaxPeersChanged( int );
void refresh( );
};

View File

@ -97,7 +97,9 @@ Favicons :: add( const QUrl& url )
if( !myPixmaps.contains( host ) )
{
// add a placholder s.t. we only ping the server once per session
myPixmaps.insert( host, QPixmap( ) );
QPixmap tmp( 16, 16 );
tmp.fill( Qt::transparent );
myPixmaps.insert( host, tmp );
// try to download the favicon
const QString path = "http://" + host + "/favicon.";

View File

@ -26,6 +26,10 @@ class Favicons: public QObject
{
Q_OBJECT;
public:
static QString getHost( const QUrl& url );
public:
Favicons();
@ -46,8 +50,6 @@ class Favicons: public QObject
QNetworkAccessManager * myNAM;
QMap<QString,QPixmap> myPixmaps;
QString getHost( const QUrl& url );
QString getCacheDir( );
void ensureCacheDirHasBeenScanned( );

View File

@ -36,7 +36,8 @@ SOURCES += about.cc app.cc dbus-adaptor.cc details.cc favicon.cc file-tree.cc \
relocate.cc session.cc session-dialog.cc squeezelabel.cc \
stats-dialog.cc torrent.cc torrent-delegate.cc \
torrent-delegate-min.cc torrent-filter.cc torrent-model.cc \
triconpushbutton.cc utils.cc watchdir.cc
tracker-delegate.cc tracker-model.cc triconpushbutton.cc \
utils.cc watchdir.cc
HEADERS += $$replace(SOURCES, .cc, .h)
HEADERS += speed.h types.h

View File

@ -22,6 +22,7 @@
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QSet>
#include <QStringList>
#include <QStyle>
#include <QTextStream>
@ -405,6 +406,21 @@ Session :: torrentSet( const QSet<int>& ids, const QString& key, bool value )
tr_bencFree( &top );
}
void
Session :: torrentSet( const QSet<int>& ids, const QString& key, const QStringList& value )
{
tr_benc top;
tr_bencInitDict( &top, 2 );
tr_bencDictAddStr( &top, "method", "torrent-set" );
tr_benc * args = tr_bencDictAddDict( &top, "arguments", 2 );
addOptionalIds( args, ids );
tr_benc * list( tr_bencDictAddList( args, key.toUtf8().constData(), value.size( ) ) );
foreach( const QString str, value )
tr_bencListAddStr( list, str.toUtf8().constData() );
exec( &top );
tr_bencFree( &top );
}
void
Session :: torrentSet( const QSet<int>& ids, const QString& key, const QList<int>& value )
{
@ -420,20 +436,6 @@ Session :: torrentSet( const QSet<int>& ids, const QString& key, const QList<int
tr_bencFree( &top );
}
void
Session :: torrentSet( const QSet<int>& ids, const QString& key, const tr_benc * value )
{
tr_benc top;
tr_bencInitDict( &top, 2 );
tr_bencDictAddStr( &top, "method", "torrent-set" );
tr_benc * args( tr_bencDictAddDict( &top, "arguments", 2 ) );
addOptionalIds( args, ids );
tr_benc * child( tr_bencDictAdd( args, key.toUtf8().constData() ) );
memcpy( child, value, sizeof(tr_benc) );
exec( &top );
tr_bencFree( &top );
}
void
Session :: torrentSetLocation( const QSet<int>& ids, const QString& location, bool doMove )
{

View File

@ -21,6 +21,8 @@
#include <QString>
#include <QUrl>
class QStringList;
#include <libtransmission/transmission.h>
extern "C"
@ -98,7 +100,7 @@ class Session: public QObject
void torrentSet( const QSet<int>& ids, const QString& key, int val );
void torrentSet( const QSet<int>& ids, const QString& key, double val );
void torrentSet( const QSet<int>& ids, const QString& key, const QList<int>& val );
void torrentSet( const QSet<int>& ids, const QString& key, const tr_benc * value );
void torrentSet( const QSet<int>& ids, const QString& key, const QStringList& val );
void torrentSetLocation( const QSet<int>& ids, const QString& path, bool doMove );

View File

@ -566,8 +566,10 @@ Torrent :: update( tr_benc * d )
int64_t i;
const char * str;
TrackerStat trackerStat;
if( tr_bencDictFindStr( child, "announce", &str ) )
if( tr_bencDictFindStr( child, "announce", &str ) ) {
trackerStat.announce = QString::fromUtf8( str );
dynamic_cast<MyApp*>(QApplication::instance())->favicons.add( QUrl( trackerStat.announce ) );
}
if( tr_bencDictFindInt( child, "announceState", &i ) )
trackerStat.announceState = i;
if( tr_bencDictFindInt( child, "downloadCount", &i ) )
@ -705,3 +707,11 @@ Torrent :: getError( ) const
return s;
}
QPixmap
TrackerStat :: getFavicon( ) const
{
MyApp * myApp = dynamic_cast<MyApp*>(QApplication::instance());
return myApp->favicons.find( QUrl( announce ) );
}

View File

@ -34,6 +34,7 @@ extern "C"
}
class Prefs;
class QPixmap;
class QStyle;
struct Peer
@ -86,6 +87,7 @@ struct TrackerStat
int scrapeState;
int seederCount;
int tier;
QPixmap getFavicon( ) const;
};
typedef QList<TrackerStat> TrackerStatsList;

304
qt/tracker-delegate.cc Normal file
View File

@ -0,0 +1,304 @@
/*
* This file Copyright (C) 2009-2010 Mnemosyne LLC
*
* This file is licensed by the GPL version 2. Works owned by the
* Transmission project are granted a special exemption to clause 2(b)
* so that the bulk of its code can remain under the MIT license.
* This exemption does not extend to derived works not owned by
* the Transmission project.
*
* $Id: torrent-delegate.cc 11051 2010-07-24 23:51:02Z charles $
*/
#include <iostream>
#include <QApplication>
#include <QBrush>
#include <QFont>
#include <QFontMetrics>
#include <QIcon>
#include <QModelIndex>
#include <QPainter>
#include <QPixmap>
#include <QPixmapCache>
#include <QStyleOptionProgressBarV2>
#include <QTextDocument>
#include <QUrl>
#include "favicon.h"
#include "formatter.h"
#include "torrent.h"
#include "tracker-delegate.h"
#include "tracker-model.h"
/***
****
***/
namespace
{
const int mySpacing = 6;
const QSize myMargin( 10, 6 );
}
QSize
TrackerDelegate :: margin( const QStyle& style ) const
{
Q_UNUSED( style );
return myMargin;
}
/***
****
***/
QSize
TrackerDelegate :: sizeHint( const QStyleOptionViewItem& option, const TrackerInfo& info ) const
{
Q_UNUSED( option );
QPixmap favicon = info.st.getFavicon( );
const QString text = TrackerDelegate :: getText( info );
QTextDocument textDoc;
textDoc.setHtml( text );
const QSize textSize = textDoc.size().toSize();
return QSize( myMargin.width() + favicon.width() + mySpacing + textSize.width() + myMargin.width(),
myMargin.height() + qMax<int>( favicon.height(), textSize.height() ) + myMargin.height() );
}
QSize
TrackerDelegate :: sizeHint( const QStyleOptionViewItem & option,
const QModelIndex & index ) const
{
const TrackerInfo trackerInfo = index.model()->data( index, TrackerModel::TrackerRole ).value<TrackerInfo>();
return sizeHint( option, trackerInfo );
}
void
TrackerDelegate :: paint( QPainter * painter,
const QStyleOptionViewItem & option,
const QModelIndex & index) const
{
const TrackerInfo trackerInfo = index.model()->data( index, TrackerModel::TrackerRole ).value<TrackerInfo>();
painter->save( );
painter->setClipRect( option.rect );
drawBackground( painter, option, index );
drawTracker( painter, option, trackerInfo );
drawFocus(painter, option, option.rect );
painter->restore( );
}
void
TrackerDelegate :: drawTracker( QPainter * painter,
const QStyleOptionViewItem & option,
const TrackerInfo & inf ) const
{
painter->save( );
QPixmap icon = inf.st.getFavicon( );
QRect iconArea( option.rect.x() + myMargin.width(),
option.rect.y() + myMargin.height(),
icon.width(),
icon.height() );
painter->drawPixmap( iconArea.x(), iconArea.y()+4, icon );
const int textWidth = option.rect.width() - myMargin.width()*2 - mySpacing - icon.width();
const int textX = myMargin.width() + icon.width() + mySpacing;
const QString text = getText( inf );
QTextDocument textDoc;
textDoc.setHtml( text );
const QRect textRect( textX, iconArea.y(), textWidth, option.rect.height() - myMargin.height()*2 );
painter->translate( textRect.topLeft( ) );
textDoc.drawContents( painter, textRect.translated( -textRect.topLeft( ) ) );
painter->restore( );
}
void
TrackerDelegate :: setShowMore( bool b )
{
myShowMore = b;
}
namespace
{
QString timeToStringRounded( int seconds )
{
if( seconds > 60 ) seconds -= ( seconds % 60 );
return Formatter::timeToString ( seconds );
}
}
QString
TrackerDelegate :: getText( const TrackerInfo& inf ) const
{
QString key;
QString str;
const time_t now( time( 0 ) );
const QString err_markup_begin = "<span style=\"color:red\">";
const QString err_markup_end = "</span>";
const QString timeout_markup_begin = "<span style=\"color:#224466\">";
const QString timeout_markup_end = "</span>";
const QString success_markup_begin = "<span style=\"color:#008B00\">";
const QString success_markup_end = "</span>";
// hostname
const QString host = Favicons::getHost( QUrl( inf.st.announce ) );
str += inf.st.isBackup ? "<i>" : "<b>";
str += host;
if( !key.isEmpty( ) ) str += " - " + key;
str += inf.st.isBackup ? "</i>" : "</b>";
// announce & scrape info
if( !inf.st.isBackup )
{
if( inf.st.hasAnnounced )
{
const QString tstr( timeToStringRounded( now - inf.st.lastAnnounceTime ) );
str += "<br/>\n";
if( inf.st.lastAnnounceSucceeded )
{
str += tr( "Got a list of %1%2 peers%3 %4 ago" )
.arg( success_markup_begin )
.arg( inf.st.lastAnnouncePeerCount )
.arg( success_markup_end )
.arg( tstr );
}
else if( inf.st.lastAnnounceTimedOut )
{
str += tr( "Peer list request timed out %1%2%3 ago; will retry" )
.arg( timeout_markup_begin )
.arg( tstr )
.arg( timeout_markup_end );
}
else
{
str += tr( "Got an error %1'%2'%3 %4 ago" )
.arg( err_markup_begin )
.arg( tstr )
.arg( err_markup_end )
.arg( tstr );
}
}
switch( inf.st.announceState )
{
case TR_TRACKER_INACTIVE:
if( inf.st.hasAnnounced ) {
str += "<br/>\n";
str += tr( "No updates scheduled" );
}
break;
case TR_TRACKER_WAITING: {
const QString tstr( timeToStringRounded( inf.st.nextAnnounceTime - now ) );
str += "<br/>\n";
str += tr( "Asking for more peers in %1" ).arg( tstr );
break;
}
case TR_TRACKER_QUEUED:
str += "<br/>\n";
str += tr( "Queued to ask for more peers" );
break;
case TR_TRACKER_ACTIVE: {
const QString tstr( timeToStringRounded( now - inf.st.lastAnnounceStartTime ) );
str += "<br/>\n";
str += tr( "Asking for more peers now... <small>%1</small>" ).arg( tstr );
break;
}
}
if( myShowMore )
{
if( inf.st.hasScraped )
{
str += "<br/>\n";
const QString tstr( timeToStringRounded( now - inf.st.lastScrapeTime ) );
if( inf.st.lastScrapeSucceeded )
{
str += tr( "Tracker had %1%2 seeders%3 and %4%5 leechers%6 %7 ago" )
.arg( success_markup_begin )
.arg( inf.st.seederCount )
.arg( success_markup_end )
.arg( success_markup_begin )
.arg( inf.st.leecherCount )
.arg( success_markup_end )
.arg( tstr );
}
else
{
str += tr( "Got a scrape error %1'%2'%3 %4 ago" )
.arg( err_markup_begin )
.arg( inf.st.lastScrapeResult )
.arg( err_markup_end )
.arg( tstr );
}
}
switch( inf.st.scrapeState )
{
case TR_TRACKER_INACTIVE:
break;
case TR_TRACKER_WAITING: {
str += "<br/>\n";
const QString tstr( timeToStringRounded( inf.st.nextScrapeTime - now ) );
str += tr( "Asking for peer counts in %1" ).arg( tstr );
break;
}
case TR_TRACKER_QUEUED: {
str += "<br/>\n";
str += tr( "Queued to ask for peer counts" );
break;
}
case TR_TRACKER_ACTIVE: {
str += "<br/>\n";
const QString tstr( timeToStringRounded( now - inf.st.lastScrapeStartTime ) );
str += tr( "Asking for peer counts now... <small>%1</small>" ).arg( tstr );
break;
}
}
}
}
return str;
}
#if 0
if( inf.isBackup )
str += "<i>";
QString announce;
int announceState;
int downloadCount;
bool hasAnnounced; bool hasScraped;
QString host;
int id;
bool isBackup;
int lastAnnouncePeerCount;
int lastAnnounceResult;
int lastAnnounceStartTime;
bool lastAnnounceSucceeded;
int lastAnnounceTime;
bool lastAnnounceTimedOut;
QString lastScrapeResult;
int lastScrapeStartTime;
bool lastScrapeSucceeded;
int lastScrapeTime;
bool lastScrapeTimedOut;
int leecherCount;
int nextAnnounceTime;
int nextScrapeTime;
int scrapeState;
int seederCount;
int tier;
}
#endif

50
qt/tracker-delegate.h Normal file
View File

@ -0,0 +1,50 @@
/*
* This file Copyright (C) 2009-2010 Mnemosyne LLC
*
* This file is licensed by the GPL version 2. Works owned by the
* Transmission project are granted a special exemption to clause 2(b)
* so that the bulk of its code can remain under the MIT license.
* This exemption does not extend to derived works not owned by
* the Transmission project.
*
* $Id: torrent-delegate.h 9868 2010-01-04 21:00:47Z charles $
*/
#ifndef QTR_TORRENT_DELEGATE_H
#define QTR_TORRENT_DELEGATE_H
#include <QItemDelegate>
#include <QSize>
class QPainter;
class QStyleOptionViewItem;
class QStyle;
class Session;
class TrackerInfo;
class TrackerDelegate: public QItemDelegate
{
Q_OBJECT
public:
TrackerDelegate( QObject * parent=0 ): QItemDelegate(parent), myShowMore(false) { }
virtual ~TrackerDelegate( ) { }
public:
QSize sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const;
void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const;
public:
void setShowMore( bool b );
protected:
QString getText( const TrackerInfo& ) const;
QSize margin( const QStyle& style ) const;
virtual QSize sizeHint( const QStyleOptionViewItem&, const TrackerInfo& ) const;
void drawTracker( QPainter*, const QStyleOptionViewItem&, const TrackerInfo& ) const;
private:
bool myShowMore;
};
#endif

154
qt/tracker-model.cc Normal file
View File

@ -0,0 +1,154 @@
/*
* This file Copyright (C) 2010 Mnemosyne LLC
*
* This file is licensed by the GPL version 2. Works owned by the
* Transmission project are granted a special exemption to clause 2(b)
* so that the bulk of its code can remain under the MIT license.
* This exemption does not extend to derived works not owned by
* the Transmission project.
*
* $Id$
*/
#include <algorithm> // std::sort()
#include <QUrl>
#include "app.h" // MyApp
#include "tracker-model.h"
int
TrackerModel :: rowCount( const QModelIndex& parent ) const
{
Q_UNUSED( parent );
return parent.isValid() ? 0 : myRows.size();
}
QVariant
TrackerModel :: data( const QModelIndex& index, int role ) const
{
QVariant var;
const int row = index.row( );
if( ( 0<=row ) && ( row<myRows.size( ) ) )
{
const TrackerInfo& trackerInfo = myRows.at( row );
switch( role )
{
case Qt::DisplayRole:
var = QString( trackerInfo.st.announce );
break;
case Qt::DecorationRole:
var = trackerInfo.st.getFavicon( );
break;
case TrackerRole:
var = qVariantFromValue( trackerInfo );
break;
default:
break;
}
}
return var;
}
/***
****
***/
struct CompareTrackers {
bool operator()( const TrackerInfo& a, const TrackerInfo& b ) const {
if( a.torrentId != b.torrentId ) return a.torrentId < b.torrentId;
if( a.st.tier != b.st.tier ) return a.st.tier < b.st.tier;
return a.st.announce < b.st.announce;
}
};
void
TrackerModel :: refresh( const TorrentModel& torrentModel, const QSet<int>& ids )
{
// build a list of the TrackerInfos
QVector<TrackerInfo> trackers;
foreach( int id, ids ) {
const Torrent * tor = torrentModel.getTorrentFromId( id );
if( tor != 0 ) {
const TrackerStatsList trackerList = tor->trackerStats( );
foreach( const TrackerStat& st, trackerList ) {
TrackerInfo trackerInfo;
trackerInfo.st = st;
trackerInfo.torrentId = id;
trackers.append( trackerInfo );
}
}
}
// sort 'em
CompareTrackers comp;
std::sort( trackers.begin(), trackers.end(), comp );
// merge 'em with the existing list
int old_index = 0;
int new_index = 0;
while( ( old_index < myRows.size() ) || ( new_index < trackers.size() ) )
{
if( old_index == myRows.size() )
{
// add this new row
beginInsertRows( QModelIndex( ), old_index, old_index );
myRows.insert( old_index, trackers.at( new_index ) );
endInsertRows( );
++old_index;
++new_index;
}
else if( new_index == trackers.size() )
{
// remove this old row
beginRemoveRows( QModelIndex( ), old_index, old_index );
myRows.remove( old_index );
endRemoveRows( );
}
else if( comp( myRows.at(old_index), trackers.at(new_index) ) )
{
// remove this old row
beginRemoveRows( QModelIndex( ), old_index, old_index );
myRows.remove( old_index );
endRemoveRows( );
}
else if( comp( trackers.at(new_index), myRows.at(old_index) ) )
{
// add this new row
beginInsertRows( QModelIndex( ), old_index, old_index );
myRows.insert( old_index, trackers.at( new_index ) );
endInsertRows( );
++old_index;
++new_index;
}
else // update existing row
{
myRows[old_index].st = trackers.at(new_index).st;
QModelIndex topLeft;
QModelIndex bottomRight;
dataChanged( index(old_index,0), index(old_index,0) );
++old_index;
++new_index;
}
}
}
int
TrackerModel :: find( int torrentId, const QString& url ) const
{
for( int i=0, n=myRows.size(); i<n; ++i ) {
const TrackerInfo& inf = myRows.at(i);
if( ( inf.torrentId == torrentId ) && ( url == inf.st.announce ) )
return i;
}
return -1;
}

51
qt/tracker-model.h Normal file
View File

@ -0,0 +1,51 @@
/*
* This file Copyright (C) 2010 Mnemosyne LLC
*
* This file is licensed by the GPL version 2. Works owned by the
* Transmission project are granted a special exemption to clause 2(b)
* so that the bulk of its code can remain under the MIT license.
* This exemption does not extend to derived works not owned by
* the Transmission project.
*
* $Id$
*/
#ifndef QTR_TRACKER_MODEL_H
#define QTR_TRACKER_MODEL_H
#include <QAbstractListModel>
#include <QSet>
#include <QVector>
#include "torrent.h"
#include "torrent-model.h"
struct TrackerInfo
{
TrackerStat st;
int torrentId;
};
Q_DECLARE_METATYPE(TrackerInfo)
class TrackerModel: public QAbstractListModel
{
Q_OBJECT
typedef QVector<TrackerInfo> rows_t;
rows_t myRows;
public:
void refresh( const TorrentModel&, const QSet<int>& ids );
int find( int torrentId, const QString& url ) const;
public:
virtual int rowCount( const QModelIndex& parent = QModelIndex() ) const;
virtual QVariant data( const QModelIndex& index, int role = Qt::DisplayRole ) const;
enum Role { TrackerRole = Qt::UserRole };
public:
TrackerModel( ) { }
virtual ~TrackerModel( ) { }
};
#endif

File diff suppressed because it is too large Load Diff

View File

@ -1853,73 +1853,73 @@ second %s is the version number
<context>
<name>Details</name>
<message>
<location filename="details.cc" line="145"/>
<location filename="details.cc" line="151"/>
<source>Torrent Properties</source>
<translation>Свойства торрента</translation>
</message>
<message>
<location filename="details.cc" line="149"/>
<location filename="details.cc" line="155"/>
<source>Information</source>
<translation>Сведения</translation>
</message>
<message>
<location filename="details.cc" line="151"/>
<location filename="details.cc" line="157"/>
<source>Peers</source>
<translation>Узлы</translation>
</message>
<message>
<location filename="details.cc" line="153"/>
<location filename="details.cc" line="159"/>
<source>Tracker</source>
<translation>Трекер</translation>
</message>
<message>
<location filename="details.cc" line="155"/>
<location filename="details.cc" line="161"/>
<source>Files</source>
<translation>Файлы</translation>
</message>
<message>
<location filename="details.cc" line="157"/>
<location filename="details.cc" line="163"/>
<source>Options</source>
<translation>Параметры</translation>
</message>
<message>
<location filename="details.cc" line="256"/>
<location filename="details.cc" line="287"/>
<source>None</source>
<translation>Н/Д</translation>
</message>
<message>
<location filename="details.cc" line="257"/>
<location filename="details.cc" line="288"/>
<source>Mixed</source>
<translation>Смешанный</translation>
</message>
<message>
<location filename="details.cc" line="258"/>
<location filename="details.cc" line="441"/>
<location filename="details.cc" line="289"/>
<location filename="details.cc" line="472"/>
<source>Unknown</source>
<translation>Неизвестно</translation>
</message>
<message>
<location filename="details.cc" line="290"/>
<location filename="details.cc" line="321"/>
<source>Finished</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="292"/>
<location filename="details.cc" line="323"/>
<source>Paused</source>
<translation type="unfinished">Приостановлен</translation>
</message>
<message>
<location filename="details.cc" line="328"/>
<location filename="details.cc" line="359"/>
<source>%1 (%2%)</source>
<translation></translation>
</message>
<message>
<location filename="details.cc" line="332"/>
<location filename="details.cc" line="363"/>
<source>%1 (%2%); %3 Unverified</source>
<translation>%1 (%2%); %3 непроверен</translation>
</message>
<message>
<location filename="details.cc" line="363"/>
<location filename="details.cc" line="394"/>
<source>%1 (+%2 corrupt)</source>
<translation>%1 (+%2 испорчен)</translation>
</message>
@ -1928,17 +1928,17 @@ second %s is the version number
<translation type="obsolete">Остановлен</translation>
</message>
<message>
<location filename="details.cc" line="461"/>
<location filename="details.cc" line="492"/>
<source>Active now</source>
<translation>Активизирован</translation>
</message>
<message>
<location filename="details.cc" line="463"/>
<location filename="details.cc" line="494"/>
<source>%1 ago</source>
<translation>%1 назад</translation>
</message>
<message numerus="yes">
<location filename="details.cc" line="504"/>
<location filename="details.cc" line="535"/>
<source>%1 (%Ln pieces @ %2)</source>
<translation>
<numerusform>%1 (%Ln часть @ %2)</numerusform>
@ -1947,7 +1947,7 @@ second %s is the version number
</translation>
</message>
<message numerus="yes">
<location filename="details.cc" line="508"/>
<location filename="details.cc" line="539"/>
<source>%1 (%Ln pieces)</source>
<translation>
<numerusform>%1 (%Ln часть)</numerusform>
@ -1956,27 +1956,27 @@ second %s is the version number
</translation>
</message>
<message>
<location filename="details.cc" line="532"/>
<location filename="details.cc" line="563"/>
<source>Private to this tracker -- DHT and PEX disabled</source>
<translation>Приватно для этого трекера -- DHT и PEX выключены</translation>
</message>
<message>
<location filename="details.cc" line="533"/>
<location filename="details.cc" line="564"/>
<source>Public torrent</source>
<translation>Публичный торрент</translation>
</message>
<message>
<location filename="details.cc" line="572"/>
<location filename="details.cc" line="603"/>
<source>Created by %1</source>
<translation>Создано при помощи %1</translation>
</message>
<message>
<location filename="details.cc" line="574"/>
<location filename="details.cc" line="605"/>
<source>Created on %1</source>
<translation>Создано %1</translation>
</message>
<message>
<location filename="details.cc" line="576"/>
<location filename="details.cc" line="607"/>
<source>Created by %1 on %2</source>
<translation>Создано %2 при помощи %1</translation>
</message>
@ -1985,248 +1985,304 @@ second %s is the version number
<translation type="obsolete">Сейчас</translation>
</message>
<message>
<location filename="details.cc" line="735"/>
<location filename="details.cc" line="778"/>
<source>Got a list of %1 peers %2 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="741"/>
<location filename="details.cc" line="784"/>
<source>Peer list request timed out %1 ago; will retry</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="746"/>
<location filename="details.cc" line="789"/>
<source>Got an error %1 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="756"/>
<location filename="details.cc" line="799"/>
<source>No updates scheduled</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="763"/>
<location filename="details.cc" line="806"/>
<source>Asking for more peers in %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="769"/>
<location filename="details.cc" line="812"/>
<source>Queued to ask for more peers</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="775"/>
<location filename="details.cc" line="818"/>
<source>Asking for more peers now... %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="788"/>
<location filename="details.cc" line="831"/>
<source>Tracker had %1 seeders and %2 leechers %3 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="795"/>
<location filename="details.cc" line="838"/>
<source>Got a scrape error %1 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="807"/>
<location filename="details.cc" line="850"/>
<source>Asking for peer counts in %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="813"/>
<location filename="details.cc" line="856"/>
<source>Queued to ask for peer counts</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="819"/>
<location filename="details.cc" line="862"/>
<source>Asking for peer counts now... %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="883"/>
<location filename="details.cc" line="904"/>
<location filename="details.cc" line="926"/>
<location filename="details.cc" line="947"/>
<source>Encrypted connection</source>
<translation>Зашифрованное соединение</translation>
</message>
<message>
<location filename="details.cc" line="897"/>
<location filename="details.cc" line="940"/>
<source>Optimistic unchoke</source>
<translation>Благоприятная передача</translation>
</message>
<message>
<location filename="details.cc" line="898"/>
<location filename="details.cc" line="941"/>
<source>Downloading from this peer</source>
<translation>Загрузка с этого узла</translation>
</message>
<message>
<location filename="details.cc" line="899"/>
<location filename="details.cc" line="942"/>
<source>We would download from this peer if they would let us</source>
<translation>Возможен приём данных от этого узла, если он позволит</translation>
</message>
<message>
<location filename="details.cc" line="900"/>
<location filename="details.cc" line="943"/>
<source>Uploading to peer</source>
<translation>Передача узлу</translation>
</message>
<message>
<location filename="details.cc" line="901"/>
<location filename="details.cc" line="944"/>
<source>We would upload to this peer if they asked</source>
<translation>Возможна раздача данных этому узлу, если он будет заинтересован</translation>
</message>
<message>
<location filename="details.cc" line="902"/>
<location filename="details.cc" line="945"/>
<source>Peer has unchoked us, but we&apos;re not interested</source>
<translation>Узел согласен передавать данные, но мы не заинтересованы</translation>
</message>
<message>
<location filename="details.cc" line="903"/>
<location filename="details.cc" line="946"/>
<source>We unchoked this peer, but they&apos;re not interested</source>
<translation>Передача узлу была разрешена, но он не заинтересован</translation>
</message>
<message>
<location filename="details.cc" line="905"/>
<location filename="details.cc" line="948"/>
<source>Peer was discovered through DHT</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="906"/>
<location filename="details.cc" line="949"/>
<source>Peer was discovered through Peer Exchange (PEX)</source>
<translation>Узел был обнаружен с помощью обмена узлами (PEX)</translation>
</message>
<message>
<location filename="details.cc" line="907"/>
<location filename="details.cc" line="950"/>
<source>Peer is an incoming connection</source>
<translation>Узел работает в режиме приёма</translation>
</message>
<message>
<location filename="details.cc" line="963"/>
<location filename="details.cc" line="1006"/>
<source>Activity</source>
<translation>Активность</translation>
</message>
<message>
<location filename="details.cc" line="964"/>
<location filename="details.cc" line="1007"/>
<source>Torrent size:</source>
<translation>Размер торрента:</translation>
</message>
<message>
<location filename="details.cc" line="965"/>
<location filename="details.cc" line="1008"/>
<source>Have:</source>
<translation>В наличии:</translation>
</message>
<message>
<location filename="details.cc" line="966"/>
<location filename="details.cc" line="1009"/>
<source>Availability:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="967"/>
<location filename="details.cc" line="1010"/>
<source>Downloaded:</source>
<translation>Загружено:</translation>
</message>
<message>
<location filename="details.cc" line="968"/>
<location filename="details.cc" line="1011"/>
<source>Uploaded:</source>
<translation>Роздано:</translation>
</message>
<message>
<location filename="details.cc" line="969"/>
<location filename="details.cc" line="1012"/>
<source>Ratio:</source>
<translation>Рейтинг:</translation>
</message>
<message>
<location filename="details.cc" line="970"/>
<location filename="details.cc" line="1013"/>
<source>State:</source>
<translation>Состояние:</translation>
</message>
<message>
<location filename="details.cc" line="971"/>
<location filename="details.cc" line="1014"/>
<source>Running time:</source>
<translation>Длительность:</translation>
</message>
<message>
<location filename="details.cc" line="972"/>
<location filename="details.cc" line="1015"/>
<source>Remaining time:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="973"/>
<location filename="details.cc" line="1016"/>
<source>Last activity:</source>
<translation>Последняя активность:</translation>
</message>
<message>
<location filename="details.cc" line="974"/>
<location filename="details.cc" line="1017"/>
<source>Error:</source>
<translation>Ошибка:</translation>
</message>
<message>
<location filename="details.cc" line="978"/>
<location filename="details.cc" line="1021"/>
<source>Details</source>
<translation>Подробности</translation>
</message>
<message>
<location filename="details.cc" line="979"/>
<location filename="details.cc" line="1022"/>
<source>Location:</source>
<translation>Местонахождение:</translation>
</message>
<message>
<location filename="details.cc" line="980"/>
<location filename="details.cc" line="1023"/>
<source>Hash:</source>
<translation>Хеш:</translation>
</message>
<message>
<location filename="details.cc" line="981"/>
<location filename="details.cc" line="1024"/>
<source>Privacy:</source>
<translation>Конфиденциальность:</translation>
</message>
<message>
<location filename="details.cc" line="982"/>
<location filename="details.cc" line="1025"/>
<source>Origin:</source>
<translation>Происхождение:</translation>
</message>
<message>
<location filename="details.cc" line="984"/>
<location filename="details.cc" line="1027"/>
<source>Comment:</source>
<translation>Комментарий:</translation>
</message>
<message>
<location filename="details.cc" line="1100"/>
<location filename="details.cc" line="1125"/>
<source>Add tracker announce URL </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1124"/>
<location filename="details.cc" line="1137"/>
<location filename="details.cc" line="1165"/>
<source>Error</source>
<translation type="unfinished">ошибок</translation>
</message>
<message>
<location filename="details.cc" line="1137"/>
<source>Tracker already exists.</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1158"/>
<source>Edit tracker announce URL </source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1196"/>
<source>Speed</source>
<translation>Скорость</translation>
</message>
<message>
<location filename="details.cc" line="1198"/>
<source>Honor global &amp;limits</source>
<translation>Использовать &amp;глобальные ограничения</translation>
</message>
<message>
<location filename="details.cc" line="1203"/>
<source>Limit &amp;download speed (%1):</source>
<location filename="details.cc" line="1165"/>
<source>Invalid URL &quot;%1&quot;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1213"/>
<source>Speed</source>
<translation>Скорость</translation>
</message>
<message>
<location filename="details.cc" line="1215"/>
<source>Honor global &amp;limits</source>
<translation>Использовать &amp;глобальные ограничения</translation>
</message>
<message>
<location filename="details.cc" line="1220"/>
<source>Limit &amp;download speed (%1):</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1230"/>
<source>Limit &amp;upload speed (%1):</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1297"/>
<source>Trackers</source>
<translation type="unfinished">Трекеры</translation>
<location filename="details.cc" line="1249"/>
<source>Seeding Limits</source>
<translation type="unfinished">Раздача</translation>
</message>
<message>
<location filename="details.cc" line="1340"/>
<location filename="details.cc" line="1254"/>
<location filename="details.cc" line="1268"/>
<source>Use Global Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1255"/>
<source>Seed regardless of ratio</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1256"/>
<source>Stop seeding at ratio:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1263"/>
<source>&amp;Ratio:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1269"/>
<source>Seed regardless of activity</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1270"/>
<source>Stop seeding if idle for N minutes:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="details.cc" line="1277"/>
<source>&amp;Idle:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Trackers</source>
<translation type="obsolete">Трекеры</translation>
</message>
<message>
<location filename="details.cc" line="1359"/>
<source>Show &amp;more details</source>
<translation type="unfinished"></translation>
</message>
@ -2239,52 +2295,48 @@ second %s is the version number
<translation type="obsolete">Ограничить скорость &amp;раздачи (КБ/с):</translation>
</message>
<message>
<location filename="details.cc" line="1224"/>
<location filename="details.cc" line="1241"/>
<source>High</source>
<translation>Высокий</translation>
</message>
<message>
<location filename="details.cc" line="1225"/>
<location filename="details.cc" line="1242"/>
<source>Normal</source>
<translation>Обычный</translation>
</message>
<message>
<location filename="details.cc" line="1226"/>
<location filename="details.cc" line="1243"/>
<source>Low</source>
<translation>Низкий</translation>
</message>
<message>
<location filename="details.cc" line="1228"/>
<location filename="details.cc" line="1245"/>
<source>Torrent &amp;priority:</source>
<translation>&amp;Приоритет торрента:</translation>
</message>
<message>
<location filename="details.cc" line="1232"/>
<source>Seed-Until Ratio</source>
<translation>Рейтинг для завершения раздачи</translation>
<translation type="obsolete">Рейтинг для завершения раздачи</translation>
</message>
<message>
<location filename="details.cc" line="1234"/>
<source>Use &amp;global settings</source>
<translation>Использовать &amp;глобальные настройки</translation>
<translation type="obsolete">Использовать &amp;глобальные настройки</translation>
</message>
<message>
<location filename="details.cc" line="1240"/>
<source>Seed &amp;regardless of ratio</source>
<translation>Раздавать &amp;несмотря на рейтинг</translation>
<translation type="obsolete">Раздавать &amp;несмотря на рейтинг</translation>
</message>
<message>
<location filename="details.cc" line="1248"/>
<source>&amp;Seed torrent until its ratio reaches:</source>
<translation>&amp;Раздавать до достижения рейтинга:</translation>
<translation type="obsolete">&amp;Раздавать до достижения рейтинга:</translation>
</message>
<message>
<location filename="details.cc" line="1261"/>
<location filename="details.cc" line="1281"/>
<source>Peer Connections</source>
<translation>Соединения с узлами</translation>
</message>
<message>
<location filename="details.cc" line="1267"/>
<location filename="details.cc" line="1287"/>
<source>&amp;Maximum peers:</source>
<translation>&amp;Максимальное количество узлов:</translation>
</message>
@ -2325,32 +2377,32 @@ second %s is the version number
<translation type="obsolete">Запрос дополнительных узлов можно будет сделать через:</translation>
</message>
<message>
<location filename="details.cc" line="1362"/>
<location filename="details.cc" line="1381"/>
<source>Up</source>
<translation>Раздача</translation>
</message>
<message>
<location filename="details.cc" line="1362"/>
<location filename="details.cc" line="1381"/>
<source>Down</source>
<translation>Приём</translation>
</message>
<message>
<location filename="details.cc" line="1362"/>
<location filename="details.cc" line="1381"/>
<source>%</source>
<translation>%</translation>
</message>
<message>
<location filename="details.cc" line="1362"/>
<location filename="details.cc" line="1381"/>
<source>Status</source>
<translation>Состояние</translation>
</message>
<message>
<location filename="details.cc" line="1362"/>
<location filename="details.cc" line="1381"/>
<source>Address</source>
<translation>Адрес</translation>
</message>
<message>
<location filename="details.cc" line="1362"/>
<location filename="details.cc" line="1381"/>
<source>Client</source>
<translation>Клиент</translation>
</message>
@ -2786,8 +2838,9 @@ second %s is the version number
</message>
<message>
<location filename="mainwin.ui" line="336"/>
<source>Alt+M</source>
<translation></translation>
<source>Alt+C</source>
<oldsource>Alt+M</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="mainwin.ui" line="344"/>
@ -3436,7 +3489,7 @@ To add another primary URL, add it after a blank line.</source>
</message>
<message>
<location filename="prefs-dialog.cc" line="373"/>
<location filename="prefs-dialog.cc" line="730"/>
<location filename="prefs-dialog.cc" line="735"/>
<source>Status unknown</source>
<translation>Статус неизвестен</translation>
</message>
@ -3516,7 +3569,7 @@ To add another primary URL, add it after a blank line.</source>
</message>
<message>
<location filename="prefs-dialog.cc" line="475"/>
<location filename="prefs-dialog.cc" line="625"/>
<location filename="prefs-dialog.cc" line="630"/>
<source>Privacy</source>
<translation>Конфиденциальность</translation>
</message>
@ -3610,6 +3663,16 @@ To add another primary URL, add it after a blank line.</source>
<source>Call scrip&amp;t when torrent is completed</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="600"/>
<source>Stop seeding at &amp;ratio:</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="605"/>
<source>Stop seeding if idle for &amp;N minutes:</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Adding Torrents</source>
<translation type="obsolete">Добавление торрентов</translation>
@ -3639,51 +3702,51 @@ To add another primary URL, add it after a blank line.</source>
</message>
<message>
<location filename="prefs-dialog.cc" line="598"/>
<source>Seeding</source>
<source>Seeding Limits</source>
<oldsource>Seeding</oldsource>
<translation type="unfinished">Раздача</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="600"/>
<source>&amp;Seed torrent until its ratio reaches:</source>
<translation>&amp;Раздавать до достижения рейтинга:</translation>
<translation type="obsolete">&amp;Раздавать до достижения рейтинга:</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="620"/>
<location filename="prefs-dialog.cc" line="625"/>
<source>Transmission Preferences</source>
<translation>Параметры Transmission</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="623"/>
<location filename="prefs-dialog.cc" line="628"/>
<source>Torrents</source>
<translation>Торренты</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="624"/>
<location filename="prefs-dialog.cc" line="629"/>
<source>Speed</source>
<translation>Скорость</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="626"/>
<location filename="prefs-dialog.cc" line="631"/>
<source>Network</source>
<translation>Сеть</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="627"/>
<location filename="prefs-dialog.cc" line="632"/>
<source>Web</source>
<translation>Веб-интерфейс</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="656"/>
<location filename="prefs-dialog.cc" line="661"/>
<source>Not supported by remote sessions</source>
<translation>Не поддерживается удаленными сеансами</translation>
</message>
<message>
<location filename="prefs-dialog.cc" line="682"/>
<location filename="prefs-dialog.cc" line="687"/>
<source>Enable &amp;blocklist</source>
<translation>&amp;Включить черный список</translation>
</message>
<message numerus="yes">
<location filename="prefs-dialog.cc" line="684"/>
<location filename="prefs-dialog.cc" line="689"/>
<source>Enable &amp;blocklist (%Ln rules)</source>
<translation>
<numerusform>Включить &amp;чёрный список (%Ln правило)</numerusform>
@ -3745,7 +3808,7 @@ To add another primary URL, add it after a blank line.</source>
<context>
<name>Session</name>
<message>
<location filename="session.cc" line="765"/>
<location filename="session.cc" line="769"/>
<source>Add Torrent</source>
<translation>Добавить торрент</translation>
</message>
@ -3853,47 +3916,47 @@ To add another primary URL, add it after a blank line.</source>
<context>
<name>Torrent</name>
<message>
<location filename="torrent.cc" line="677"/>
<location filename="torrent.cc" line="683"/>
<source>Waiting to verify local data</source>
<translation>Ожидается проверка локальных данных</translation>
</message>
<message>
<location filename="torrent.cc" line="678"/>
<location filename="torrent.cc" line="684"/>
<source>Verifying local data</source>
<translation>Проверка локальных данных</translation>
</message>
<message>
<location filename="torrent.cc" line="679"/>
<location filename="torrent.cc" line="685"/>
<source>Downloading</source>
<translation>Приём</translation>
</message>
<message>
<location filename="torrent.cc" line="680"/>
<location filename="torrent.cc" line="686"/>
<source>Seeding</source>
<translation>Раздача</translation>
</message>
<message>
<location filename="torrent.cc" line="681"/>
<location filename="torrent.cc" line="687"/>
<source>Paused</source>
<translation>Приостановлен</translation>
</message>
<message>
<location filename="torrent.cc" line="681"/>
<location filename="torrent.cc" line="687"/>
<source>Finished</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="torrent.cc" line="694"/>
<location filename="torrent.cc" line="700"/>
<source>Tracker gave a warning: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="torrent.cc" line="695"/>
<location filename="torrent.cc" line="701"/>
<source>Tracker gave an error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="torrent.cc" line="696"/>
<location filename="torrent.cc" line="702"/>
<source>Error: %1</source>
<translation type="unfinished"></translation>
</message>
@ -3932,7 +3995,7 @@ To add another primary URL, add it after a blank line.</source>
</message>
<message>
<location filename="torrent-delegate.cc" line="150"/>
<location filename="torrent-delegate.cc" line="250"/>
<location filename="torrent-delegate.cc" line="252"/>
<source> - </source>
<translation> - </translation>
</message>
@ -3947,22 +4010,27 @@ To add another primary URL, add it after a blank line.</source>
<translation>Оставшееся время неизвестно</translation>
</message>
<message>
<location filename="torrent-delegate.cc" line="174"/>
<source>Down: %1, Up: %2</source>
<translation>Приём: %1, раздача: %2</translation>
<location filename="torrent-delegate.cc" line="176"/>
<source>%1 %2, %3 %4</source>
<translation type="unfinished">%1, %3 %4 {1 %2,?}</translation>
</message>
<message>
<source>Down: %1, Up: %2</source>
<translation type="obsolete">Приём: %1, раздача: %2</translation>
</message>
<message>
<location filename="torrent-delegate.cc" line="176"/>
<source>Down: %1</source>
<translation>Приём: %1</translation>
<translation type="obsolete">Приём: %1</translation>
</message>
<message>
<location filename="torrent-delegate.cc" line="178"/>
<source>Up: %1</source>
<translation>Раздача: %1</translation>
<location filename="torrent-delegate.cc" line="180"/>
<source>%1 %2</source>
<oldsource>Up: %1</oldsource>
<translation type="unfinished">Раздача: %1</translation>
</message>
<message>
<location filename="torrent-delegate.cc" line="180"/>
<location filename="torrent-delegate.cc" line="182"/>
<source>Idle</source>
<translation>Нет активности</translation>
</message>
@ -3975,17 +4043,17 @@ To add another primary URL, add it after a blank line.</source>
<translation type="obsolete">Ожидается проверка локальных данных</translation>
</message>
<message>
<location filename="torrent-delegate.cc" line="193"/>
<location filename="torrent-delegate.cc" line="195"/>
<source>Verifying local data (%1% tested)</source>
<translation>Проверка локальных данных (%1% проверено)</translation>
</message>
<message>
<location filename="torrent-delegate.cc" line="199"/>
<location filename="torrent-delegate.cc" line="201"/>
<source>Ratio: %1, </source>
<translation>Рейтинг: %1</translation>
</message>
<message numerus="yes">
<location filename="torrent-delegate.cc" line="230"/>
<location filename="torrent-delegate.cc" line="232"/>
<source>Downloading from %1 of %n connected peer(s)</source>
<translation>
<numerusform>Приём от %1 из %n подключённого узла</numerusform>
@ -3994,7 +4062,7 @@ To add another primary URL, add it after a blank line.</source>
</translation>
</message>
<message numerus="yes">
<location filename="torrent-delegate.cc" line="233"/>
<location filename="torrent-delegate.cc" line="235"/>
<source>Downloading metadata from %n peer(s) (%1% done)</source>
<translation type="unfinished">
<numerusform></numerusform>
@ -4003,7 +4071,7 @@ To add another primary URL, add it after a blank line.</source>
</translation>
</message>
<message numerus="yes">
<location filename="torrent-delegate.cc" line="238"/>
<location filename="torrent-delegate.cc" line="240"/>
<source>Seeding to %1 of %n connected peer(s)</source>
<translation>
<numerusform>Раздача к %1 из %n подключённого узла</numerusform>
@ -4238,6 +4306,69 @@ To add another primary URL, add it after a blank line.</source>
<translation>Последний ответ от сервера %1 назад</translation>
</message>
</context>
<context>
<name>TrackerDelegate</name>
<message>
<location filename="tracker-delegate.cc" line="161"/>
<source>Got a list of %1%2 peers%3 %4 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="169"/>
<source>Peer list request timed out %1%2%3 ago; will retry</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="176"/>
<source>Got an error %1&apos;%2&apos;%3 %4 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="189"/>
<source>No updates scheduled</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="196"/>
<source>Asking for more peers in %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="202"/>
<source>Queued to ask for more peers</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="208"/>
<source>Asking for more peers now... &lt;small&gt;%1&lt;/small&gt;</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="221"/>
<source>Tracker had %1%2 seeders%3 and %4%5 leechers%6 %7 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="232"/>
<source>Got a scrape error %1&apos;%2&apos;%3 %4 ago</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="248"/>
<source>Asking for peer counts in %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="254"/>
<source>Queued to ask for peer counts</source>
<translation type="unfinished"></translation>
</message>
<message>
<location filename="tracker-delegate.cc" line="261"/>
<source>Asking for peer counts now... &lt;small&gt;%1&lt;/small&gt;</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>Utils</name>
<message>