diff --git a/doc/rpc-spec.txt b/doc/rpc-spec.txt index 26d65dddc..c47b12afa 100644 --- a/doc/rpc-spec.txt +++ b/doc/rpc-spec.txt @@ -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 diff --git a/libtransmission/rpcimpl.c b/libtransmission/rpcimpl.c index d324575e9..eaa9418d2 100644 --- a/libtransmission/rpcimpl.c +++ b/libtransmission/rpcimpl.c @@ -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; itrackers[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; itrackers[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; itrackerCount; + 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 ); } diff --git a/qt/details.cc b/qt/details.cc index 87e593632..eaf0d5327 100644 --- a/qt/details.cc +++ b/qt/details.cc @@ -12,7 +12,6 @@ #include #include -#include #include #include @@ -26,12 +25,16 @@ #include #include #include +#include #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -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 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& ids ) } myFileTreeView->clear( ); - myIds = ids; // listen to the new torrents @@ -206,6 +216,24 @@ Details :: setIds( const QSet& 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 trackerTiers; - QMap 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 tierList = trackerTiers.values(); - QList 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 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 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(); + + 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 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 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 ids; - tr_benc top; + // make a map of torrentIds to announce URLs to remove + QItemSelectionModel * selectionModel = myTrackerView->selectionModel( ); + QModelIndexList selectedRows = selectionModel->selectedRows( ); + QMap torrentId_to_urls; + foreach( QModelIndex i, selectedRows ) + { + const TrackerInfo inf = myTrackerModel->data( i, TrackerModel::TrackerRole ).value(); + 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 items = myTrackerTree->selectedItems(); - QSet 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 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& indices, int priority ) default: key = "priority-normal"; break; } mySession.torrentSet( myIds, key, indices.toList( ) ); + getNewData( ); } void @@ -1435,4 +1293,5 @@ Details :: onFileWantedChanged( const QSet& indices, bool wanted ) { QString key( wanted ? "files-wanted" : "files-unwanted" ); mySession.torrentSet( myIds, key, indices.toList( ) ); + getNewData( ); } diff --git a/qt/details.h b/qt/details.h index 0d5071d80..716e39618 100644 --- a/qt/details.h +++ b/qt/details.h @@ -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 myTrackerTiers; + //QMap myTrackerItems; + QTreeWidget * myPeerTree; - QMap myTrackerTiers; - QMap myTrackerItems; QMap myPeers; + QWidgetList myWidgets; FileTreeView * myFileTreeView; private slots: + void refreshPref( int key ); void onBandwidthPriorityChanged( int ); void onFilePriorityChanged( const QSet& fileIndices, int ); void onFileWantedChanged( const QSet& 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( ); }; diff --git a/qt/favicon.cc b/qt/favicon.cc index 2e36bc886..4650d82bb 100644 --- a/qt/favicon.cc +++ b/qt/favicon.cc @@ -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."; diff --git a/qt/favicon.h b/qt/favicon.h index dcd3f12a3..2341cfc43 100644 --- a/qt/favicon.h +++ b/qt/favicon.h @@ -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 myPixmaps; - QString getHost( const QUrl& url ); - QString getCacheDir( ); void ensureCacheDirHasBeenScanned( ); diff --git a/qt/qtr.pro b/qt/qtr.pro index c8a3bac3a..44356a3cf 100644 --- a/qt/qtr.pro +++ b/qt/qtr.pro @@ -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 diff --git a/qt/session.cc b/qt/session.cc index 28d7c87b8..9aac79bb9 100644 --- a/qt/session.cc +++ b/qt/session.cc @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -405,6 +406,21 @@ Session :: torrentSet( const QSet& ids, const QString& key, bool value ) tr_bencFree( &top ); } +void +Session :: torrentSet( const QSet& 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& ids, const QString& key, const QList& value ) { @@ -420,20 +436,6 @@ Session :: torrentSet( const QSet& ids, const QString& key, const QList& 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& ids, const QString& location, bool doMove ) { diff --git a/qt/session.h b/qt/session.h index 88735290e..532f11052 100644 --- a/qt/session.h +++ b/qt/session.h @@ -21,6 +21,8 @@ #include #include +class QStringList; + #include extern "C" @@ -98,7 +100,7 @@ class Session: public QObject void torrentSet( const QSet& ids, const QString& key, int val ); void torrentSet( const QSet& ids, const QString& key, double val ); void torrentSet( const QSet& ids, const QString& key, const QList& val ); - void torrentSet( const QSet& ids, const QString& key, const tr_benc * value ); + void torrentSet( const QSet& ids, const QString& key, const QStringList& val ); void torrentSetLocation( const QSet& ids, const QString& path, bool doMove ); diff --git a/qt/torrent.cc b/qt/torrent.cc index 71d3760e8..ef0d5f200 100644 --- a/qt/torrent.cc +++ b/qt/torrent.cc @@ -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(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(QApplication::instance()); + return myApp->favicons.find( QUrl( announce ) ); +} + diff --git a/qt/torrent.h b/qt/torrent.h index 02a7a5b1e..98226ab73 100644 --- a/qt/torrent.h +++ b/qt/torrent.h @@ -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 TrackerStatsList; diff --git a/qt/tracker-delegate.cc b/qt/tracker-delegate.cc new file mode 100644 index 000000000..4556a3488 --- /dev/null +++ b/qt/tracker-delegate.cc @@ -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 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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( 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(); + 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(); + 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 = ""; + const QString err_markup_end = ""; + const QString timeout_markup_begin = ""; + const QString timeout_markup_end = ""; + const QString success_markup_begin = ""; + const QString success_markup_end = ""; + + // hostname + const QString host = Favicons::getHost( QUrl( inf.st.announce ) ); + str += inf.st.isBackup ? "" : ""; + str += host; + if( !key.isEmpty( ) ) str += " - " + key; + str += inf.st.isBackup ? "" : ""; + + // announce & scrape info + if( !inf.st.isBackup ) + { + if( inf.st.hasAnnounced ) + { + const QString tstr( timeToStringRounded( now - inf.st.lastAnnounceTime ) ); + str += "
\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 += "
\n"; + str += tr( "No updates scheduled" ); + } + break; + + case TR_TRACKER_WAITING: { + const QString tstr( timeToStringRounded( inf.st.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 - inf.st.lastAnnounceStartTime ) ); + str += "
\n"; + str += tr( "Asking for more peers now... %1" ).arg( tstr ); + break; + } + } + + if( myShowMore ) + { + if( inf.st.hasScraped ) + { + str += "
\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 += "
\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 += "
\n"; + str += tr( "Queued to ask for peer counts" ); + break; + } + + case TR_TRACKER_ACTIVE: { + str += "
\n"; + const QString tstr( timeToStringRounded( now - inf.st.lastScrapeStartTime ) ); + str += tr( "Asking for peer counts now... %1" ).arg( tstr ); + break; + } + } + } + } + + return str; +} + +#if 0 + + if( inf.isBackup ) + str += ""; + 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 diff --git a/qt/tracker-delegate.h b/qt/tracker-delegate.h new file mode 100644 index 000000000..840a60442 --- /dev/null +++ b/qt/tracker-delegate.h @@ -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 +#include + +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 diff --git a/qt/tracker-model.cc b/qt/tracker-model.cc new file mode 100644 index 000000000..7be9da9a6 --- /dev/null +++ b/qt/tracker-model.cc @@ -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 // std::sort() + +#include + +#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& ids ) +{ + // build a list of the TrackerInfos + QVector 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 +#include +#include + +#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 rows_t; + rows_t myRows; + + public: + void refresh( const TorrentModel&, const QSet& 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 diff --git a/qt/transmission_en.ts b/qt/transmission_en.ts index 20fc5ae34..53b18ffb1 100644 --- a/qt/transmission_en.ts +++ b/qt/transmission_en.ts @@ -37,88 +37,88 @@ Details - + Torrent Properties - + Information - + Peers - + Tracker - + Files - + Options - + None - + Mixed - - + + Unknown - + Finished - + Paused - + %1 (%2%) - + %1 (%2%); %3 Unverified - + %1 (+%2 corrupt) - + Active now - + %1 ago - + %1 (%Ln pieces @ %2) %1 (%Ln piece @ %2) @@ -126,7 +126,7 @@ - + %1 (%Ln pieces) %1 (%Ln piece) @@ -134,353 +134,385 @@ - + Private to this tracker -- DHT and PEX disabled - + Public torrent - + Created by %1 - + Created on %1 - + Created by %1 on %2 - + Got a list of %1 peers %2 ago - + Peer list request timed out %1 ago; will retry - + Got an error %1 ago - + No updates scheduled - + Asking for more peers in %1 - + Queued to ask for more peers - + Asking for more peers now... %1 - + Tracker had %1 seeders and %2 leechers %3 ago - + Got a scrape error %1 ago - + Asking for peer counts in %1 - + Queued to ask for peer counts - + Asking for peer counts now... %1 - - + + Encrypted connection - + Optimistic unchoke - + Downloading from this peer - + We would download from this peer if they would let us - + Uploading to peer - + We would upload to this peer if they asked - + Peer has unchoked us, but we're not interested - + We unchoked this peer, but they're not interested - + Peer was discovered through DHT - + Peer was discovered through Peer Exchange (PEX) - + Peer is an incoming connection - + Activity - + Torrent size: - + Have: - + Availability: - + Downloaded: - + Uploaded: - + Ratio: - + State: - + Running time: - + Remaining time: - + Last activity: - + Error: - + Details - + Location: - + Hash: - + Privacy: - + Origin: - + Comment: - + Add tracker announce URL - + + + Error + + + + + Tracker already exists. + + + + Edit tracker announce URL - - Speed - - - - - Honor global &limits - - - - - Limit &download speed (%1): + + Invalid URL "%1" + Speed + + + + + Honor global &limits + + + + + Limit &download speed (%1): + + + + Limit &upload speed (%1): - + High - + Normal - + Low - + Torrent &priority: - - Seed-Until Ratio + + Seeding Limits - - Use &global settings + + + Use Global Settings - - Seed &regardless of ratio + + Seed regardless of ratio - - &Seed torrent until its ratio reaches: + + Stop seeding at ratio: - + + &Ratio: + + + + + Seed regardless of activity + + + + + Stop seeding if idle for N minutes: + + + + + &Idle: + + + + Peer Connections - + &Maximum peers: - - Trackers - - - - + Show &more details - + Up - + Down - + % - + Status - + Address - + Client @@ -536,85 +568,85 @@ B/s - + kB/s - + MB/s - + GB/s - + TB/s - + B - + KB - + MB - + GB - + TB - + KiB - + MiB - + GiB - + TiB - + None - + %Ln day(s) - + %Ln day %Ln days @@ -622,7 +654,7 @@ %Ln hour(s) - + %Ln hour %Ln hours @@ -630,7 +662,7 @@ %Ln minute(s) - + %Ln minute %Ln minutes @@ -638,7 +670,7 @@ %Ln second(s) - + %Ln second %Ln seconds @@ -648,7 +680,7 @@ %1, %2 - + @@ -656,7 +688,7 @@ License - + @@ -894,7 +926,8 @@ - Alt+M + Alt+C + Alt+M @@ -1463,7 +1496,7 @@ To add another primary URL, add it after a blank line. - + Status unknown @@ -1502,7 +1535,6 @@ To add another primary URL, add it after a blank line. <b>Update succeeded!</b><p>Blocklist now has %Ln rules. - @@ -1542,7 +1574,7 @@ To add another primary URL, add it after a blank line. - + Privacy @@ -1653,51 +1685,57 @@ To add another primary URL, add it after a blank line. - Seeding + Seeding Limits + Seeding - &Seed torrent until its ratio reaches: + Stop seeding at &ratio: - + + Stop seeding if idle for &N minutes: + + + + Transmission Preferences - + Torrents - + Speed - + Network - + Web - + Not supported by remote sessions - + Enable &blocklist - + Enable &blocklist (%Ln rules) Enable &blocklist (%Ln rule) @@ -1761,7 +1799,7 @@ To add another primary URL, add it after a blank line. Session - + Add Torrent @@ -1868,47 +1906,47 @@ To add another primary URL, add it after a blank line. Torrent - + Waiting to verify local data - + Verifying local data - + Downloading - + Seeding - + Finished - + Paused - + Tracker gave a warning: %1 - + Tracker gave an error: %1 - + Error: %1 @@ -1947,7 +1985,7 @@ To add another primary URL, add it after a blank line. - + - @@ -1961,38 +1999,35 @@ To add another primary URL, add it after a blank line. Remaining time unknown - - - Down: %1, Up: %2 - - - Down: %1 + %1 %2, %3 %4 - Up: %1 + + %1 %2 + Up: %1 - + Idle - + Verifying local data (%1% tested) - + Ratio: %1, - + Downloading from %1 of %n connected peer(s) Downloading from %1 peer @@ -2000,15 +2035,14 @@ To add another primary URL, add it after a blank line. - + Downloading metadata from %n peer(s) (%1% done) - - + Seeding to %1 of %n connected peer(s) Seeding to %1 peer @@ -2237,6 +2271,69 @@ To add another primary URL, add it after a blank line. + + TrackerDelegate + + + Got a list of %1%2 peers%3 %4 ago + + + + + Peer list request timed out %1%2%3 ago; will retry + + + + + Got an error %1'%2'%3 %4 ago + + + + + No updates scheduled + + + + + Asking for more peers in %1 + + + + + Queued to ask for more peers + + + + + Asking for more peers now... <small>%1</small> + + + + + Tracker had %1%2 seeders%3 and %4%5 leechers%6 %7 ago + + + + + Got a scrape error %1'%2'%3 %4 ago + + + + + Asking for peer counts in %1 + + + + + Queued to ask for peer counts + + + + + Asking for peer counts now... <small>%1</small> + + + Utils diff --git a/qt/transmission_ru.ts b/qt/transmission_ru.ts index 0f89c560c..328cb2186 100644 --- a/qt/transmission_ru.ts +++ b/qt/transmission_ru.ts @@ -1853,73 +1853,73 @@ second %s is the version number Details - + Torrent Properties Свойства торрента - + Information Сведения - + Peers Узлы - + Tracker Трекер - + Files Файлы - + Options Параметры - + None Н/Д - + Mixed Смешанный - - + + Unknown Неизвестно - + Finished - + Paused Приостановлен - + %1 (%2%) - + %1 (%2%); %3 Unverified %1 (%2%); %3 непроверен - + %1 (+%2 corrupt) %1 (+%2 испорчен) @@ -1928,17 +1928,17 @@ second %s is the version number Остановлен - + Active now Активизирован - + %1 ago %1 назад - + %1 (%Ln pieces @ %2) %1 (%Ln часть @ %2) @@ -1947,7 +1947,7 @@ second %s is the version number - + %1 (%Ln pieces) %1 (%Ln часть) @@ -1956,27 +1956,27 @@ second %s is the version number - + Private to this tracker -- DHT and PEX disabled Приватно для этого трекера -- DHT и PEX выключены - + Public torrent Публичный торрент - + Created by %1 Создано при помощи %1 - + Created on %1 Создано %1 - + Created by %1 on %2 Создано %2 при помощи %1 @@ -1985,248 +1985,304 @@ second %s is the version number Сейчас - + Got a list of %1 peers %2 ago - + Peer list request timed out %1 ago; will retry - + Got an error %1 ago - + No updates scheduled - + Asking for more peers in %1 - + Queued to ask for more peers - + Asking for more peers now... %1 - + Tracker had %1 seeders and %2 leechers %3 ago - + Got a scrape error %1 ago - + Asking for peer counts in %1 - + Queued to ask for peer counts - + Asking for peer counts now... %1 - - + + Encrypted connection Зашифрованное соединение - + Optimistic unchoke Благоприятная передача - + Downloading from this peer Загрузка с этого узла - + We would download from this peer if they would let us Возможен приём данных от этого узла, если он позволит - + Uploading to peer Передача узлу - + We would upload to this peer if they asked Возможна раздача данных этому узлу, если он будет заинтересован - + Peer has unchoked us, but we're not interested Узел согласен передавать данные, но мы не заинтересованы - + We unchoked this peer, but they're not interested Передача узлу была разрешена, но он не заинтересован - + Peer was discovered through DHT - + Peer was discovered through Peer Exchange (PEX) Узел был обнаружен с помощью обмена узлами (PEX) - + Peer is an incoming connection Узел работает в режиме приёма - + Activity Активность - + Torrent size: Размер торрента: - + Have: В наличии: - + Availability: - + Downloaded: Загружено: - + Uploaded: Роздано: - + Ratio: Рейтинг: - + State: Состояние: - + Running time: Длительность: - + Remaining time: - + Last activity: Последняя активность: - + Error: Ошибка: - + Details Подробности - + Location: Местонахождение: - + Hash: Хеш: - + Privacy: Конфиденциальность: - + Origin: Происхождение: - + Comment: Комментарий: - + Add tracker announce URL - + + + Error + ошибок + + + + Tracker already exists. + + + + Edit tracker announce URL - - Speed - Скорость - - - - Honor global &limits - Использовать &глобальные ограничения - - - - Limit &download speed (%1): + + Invalid URL "%1" + Speed + Скорость + + + + Honor global &limits + Использовать &глобальные ограничения + + + + Limit &download speed (%1): + + + + Limit &upload speed (%1): - - Trackers - Трекеры + + Seeding Limits + Раздача - + + + Use Global Settings + + + + + Seed regardless of ratio + + + + + Stop seeding at ratio: + + + + + &Ratio: + + + + + Seed regardless of activity + + + + + Stop seeding if idle for N minutes: + + + + + &Idle: + + + + Trackers + Трекеры + + + Show &more details @@ -2239,52 +2295,48 @@ second %s is the version number Ограничить скорость &раздачи (КБ/с): - + High Высокий - + Normal Обычный - + Low Низкий - + Torrent &priority: &Приоритет торрента: - Seed-Until Ratio - Рейтинг для завершения раздачи + Рейтинг для завершения раздачи - Use &global settings - Использовать &глобальные настройки + Использовать &глобальные настройки - Seed &regardless of ratio - Раздавать &несмотря на рейтинг + Раздавать &несмотря на рейтинг - &Seed torrent until its ratio reaches: - &Раздавать до достижения рейтинга: + &Раздавать до достижения рейтинга: - + Peer Connections Соединения с узлами - + &Maximum peers: &Максимальное количество узлов: @@ -2325,32 +2377,32 @@ second %s is the version number Запрос дополнительных узлов можно будет сделать через: - + Up Раздача - + Down Приём - + % % - + Status Состояние - + Address Адрес - + Client Клиент @@ -2786,8 +2838,9 @@ second %s is the version number - Alt+M - + Alt+C + Alt+M + @@ -3436,7 +3489,7 @@ To add another primary URL, add it after a blank line. - + Status unknown Статус неизвестен @@ -3516,7 +3569,7 @@ To add another primary URL, add it after a blank line. - + Privacy Конфиденциальность @@ -3610,6 +3663,16 @@ To add another primary URL, add it after a blank line. Call scrip&t when torrent is completed + + + Stop seeding at &ratio: + + + + + Stop seeding if idle for &N minutes: + + Adding Torrents Добавление торрентов @@ -3639,51 +3702,51 @@ To add another primary URL, add it after a blank line. - Seeding + Seeding Limits + Seeding Раздача - &Seed torrent until its ratio reaches: - &Раздавать до достижения рейтинга: + &Раздавать до достижения рейтинга: - + Transmission Preferences Параметры Transmission - + Torrents Торренты - + Speed Скорость - + Network Сеть - + Web Веб-интерфейс - + Not supported by remote sessions Не поддерживается удаленными сеансами - + Enable &blocklist &Включить черный список - + Enable &blocklist (%Ln rules) Включить &чёрный список (%Ln правило) @@ -3745,7 +3808,7 @@ To add another primary URL, add it after a blank line. Session - + Add Torrent Добавить торрент @@ -3853,47 +3916,47 @@ To add another primary URL, add it after a blank line. Torrent - + Waiting to verify local data Ожидается проверка локальных данных - + Verifying local data Проверка локальных данных - + Downloading Приём - + Seeding Раздача - + Paused Приостановлен - + Finished - + Tracker gave a warning: %1 - + Tracker gave an error: %1 - + Error: %1 @@ -3932,7 +3995,7 @@ To add another primary URL, add it after a blank line. - + - - @@ -3947,22 +4010,27 @@ To add another primary URL, add it after a blank line. Оставшееся время неизвестно - - Down: %1, Up: %2 - Приём: %1, раздача: %2 + + %1 %2, %3 %4 + %1, %3 %4 {1 %2,?} + + + Down: %1, Up: %2 + Приём: %1, раздача: %2 - Down: %1 - Приём: %1 + Приём: %1 - Up: %1 - Раздача: %1 + + %1 %2 + Up: %1 + Раздача: %1 - + Idle Нет активности @@ -3975,17 +4043,17 @@ To add another primary URL, add it after a blank line. Ожидается проверка локальных данных - + Verifying local data (%1% tested) Проверка локальных данных (%1% проверено) - + Ratio: %1, Рейтинг: %1 - + Downloading from %1 of %n connected peer(s) Приём от %1 из %n подключённого узла @@ -3994,7 +4062,7 @@ To add another primary URL, add it after a blank line. - + Downloading metadata from %n peer(s) (%1% done) @@ -4003,7 +4071,7 @@ To add another primary URL, add it after a blank line. - + Seeding to %1 of %n connected peer(s) Раздача к %1 из %n подключённого узла @@ -4238,6 +4306,69 @@ To add another primary URL, add it after a blank line. Последний ответ от сервера %1 назад + + TrackerDelegate + + + Got a list of %1%2 peers%3 %4 ago + + + + + Peer list request timed out %1%2%3 ago; will retry + + + + + Got an error %1'%2'%3 %4 ago + + + + + No updates scheduled + + + + + Asking for more peers in %1 + + + + + Queued to ask for more peers + + + + + Asking for more peers now... <small>%1</small> + + + + + Tracker had %1%2 seeders%3 and %4%5 leechers%6 %7 ago + + + + + Got a scrape error %1'%2'%3 %4 ago + + + + + Asking for peer counts in %1 + + + + + Queued to ask for peer counts + + + + + Asking for peer counts now... <small>%1</small> + + + Utils