mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-12-30 04:08:05 -06:00
Now it follows closely the definition of wildcard for glob patterns. The backslash (\) character is not an escape char in this context. In order to match one of the special characters, place it in square brackets (for example, [?]).
840 lines
33 KiB
C++
840 lines
33 KiB
C++
/*
|
|
* Bittorrent Client using Qt and libtorrent.
|
|
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
|
*
|
|
* This program is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation; either version 2
|
|
* of the License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
*
|
|
* In addition, as a special exception, the copyright holders give permission to
|
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
|
* and distribute the linked executables. You must obey the GNU General Public
|
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
|
* modify file(s), you may extend this exception to your version of the file(s),
|
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
|
* exception statement from your version.
|
|
*/
|
|
|
|
#include "propertieswidget.h"
|
|
|
|
#include <QClipboard>
|
|
#include <QDateTime>
|
|
#include <QDebug>
|
|
#include <QDir>
|
|
#include <QHeaderView>
|
|
#include <QListWidgetItem>
|
|
#include <QMenu>
|
|
#include <QSplitter>
|
|
#include <QShortcut>
|
|
#include <QStackedWidget>
|
|
#include <QThread>
|
|
#include <QUrl>
|
|
|
|
#include "base/bittorrent/downloadpriority.h"
|
|
#include "base/bittorrent/infohash.h"
|
|
#include "base/bittorrent/session.h"
|
|
#include "base/bittorrent/torrent.h"
|
|
#include "base/preferences.h"
|
|
#include "base/unicodestrings.h"
|
|
#include "base/utils/fs.h"
|
|
#include "base/utils/misc.h"
|
|
#include "base/utils/string.h"
|
|
#include "gui/autoexpandabledialog.h"
|
|
#include "gui/lineedit.h"
|
|
#include "gui/raisedmessagebox.h"
|
|
#include "gui/torrentcontentfiltermodel.h"
|
|
#include "gui/torrentcontentmodel.h"
|
|
#include "gui/uithememanager.h"
|
|
#include "gui/utils.h"
|
|
#include "downloadedpiecesbar.h"
|
|
#include "peerlistwidget.h"
|
|
#include "pieceavailabilitybar.h"
|
|
#include "proplistdelegate.h"
|
|
#include "proptabbar.h"
|
|
#include "speedwidget.h"
|
|
#include "trackerlistwidget.h"
|
|
#include "ui_propertieswidget.h"
|
|
|
|
#ifdef Q_OS_MACOS
|
|
#include "gui/macutilities.h"
|
|
#endif
|
|
|
|
PropertiesWidget::PropertiesWidget(QWidget *parent)
|
|
: QWidget(parent)
|
|
, m_ui(new Ui::PropertiesWidget())
|
|
, m_torrent(nullptr)
|
|
, m_handleWidth(-1)
|
|
{
|
|
m_ui->setupUi(this);
|
|
setAutoFillBackground(true);
|
|
|
|
m_state = VISIBLE;
|
|
|
|
// Set Properties list model
|
|
m_propListModel = new TorrentContentFilterModel(this);
|
|
m_ui->filesList->setModel(m_propListModel);
|
|
m_propListDelegate = new PropListDelegate(this);
|
|
m_ui->filesList->setItemDelegate(m_propListDelegate);
|
|
m_ui->filesList->setSortingEnabled(true);
|
|
|
|
// Torrent content filtering
|
|
m_contentFilterLine = new LineEdit(this);
|
|
m_contentFilterLine->setPlaceholderText(tr("Filter files..."));
|
|
m_contentFilterLine->setFixedWidth(Utils::Gui::scaledSize(this, 300));
|
|
connect(m_contentFilterLine, &LineEdit::textChanged, this, &PropertiesWidget::filterText);
|
|
m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
|
|
|
|
// SIGNAL/SLOTS
|
|
connect(m_ui->selectAllButton, &QPushButton::clicked, m_propListModel, &TorrentContentFilterModel::selectAll);
|
|
connect(m_ui->selectNoneButton, &QPushButton::clicked, m_propListModel, &TorrentContentFilterModel::selectNone);
|
|
connect(m_propListModel, &TorrentContentFilterModel::filteredFilesChanged, this, &PropertiesWidget::filteredFilesChanged);
|
|
connect(m_ui->listWebSeeds, &QWidget::customContextMenuRequested, this, &PropertiesWidget::displayWebSeedListMenu);
|
|
connect(m_propListDelegate, &PropListDelegate::filteredFilesChanged, this, &PropertiesWidget::filteredFilesChanged);
|
|
connect(m_ui->stackedProperties, &QStackedWidget::currentChanged, this, &PropertiesWidget::loadDynamicData);
|
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentSavePathChanged, this, &PropertiesWidget::updateSavePath);
|
|
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentMetadataReceived, this, &PropertiesWidget::updateTorrentInfos);
|
|
connect(m_ui->filesList, &QAbstractItemView::clicked
|
|
, m_ui->filesList, qOverload<const QModelIndex &>(&QAbstractItemView::edit));
|
|
connect(m_ui->filesList, &QWidget::customContextMenuRequested, this, &PropertiesWidget::displayFilesListMenu);
|
|
connect(m_ui->filesList, &QAbstractItemView::doubleClicked, this, &PropertiesWidget::openItem);
|
|
connect(m_ui->filesList->header(), &QHeaderView::sectionMoved, this, &PropertiesWidget::saveSettings);
|
|
connect(m_ui->filesList->header(), &QHeaderView::sectionResized, this, &PropertiesWidget::saveSettings);
|
|
connect(m_ui->filesList->header(), &QHeaderView::sortIndicatorChanged, this, &PropertiesWidget::saveSettings);
|
|
|
|
// set bar height relative to screen dpi
|
|
const int barHeight = Utils::Gui::scaledSize(this, 18);
|
|
|
|
// Downloaded pieces progress bar
|
|
m_ui->tempProgressBarArea->setVisible(false);
|
|
m_downloadedPieces = new DownloadedPiecesBar(this);
|
|
m_ui->groupBarLayout->addWidget(m_downloadedPieces, 0, 1);
|
|
m_downloadedPieces->setFixedHeight(barHeight);
|
|
m_downloadedPieces->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
|
|
// Pieces availability bar
|
|
m_ui->tempAvailabilityBarArea->setVisible(false);
|
|
m_piecesAvailability = new PieceAvailabilityBar(this);
|
|
m_ui->groupBarLayout->addWidget(m_piecesAvailability, 1, 1);
|
|
m_piecesAvailability->setFixedHeight(barHeight);
|
|
m_piecesAvailability->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
|
|
|
// Tracker list
|
|
m_trackerList = new TrackerListWidget(this);
|
|
m_ui->trackerUpButton->setIcon(UIThemeManager::instance()->getIcon("go-up"));
|
|
m_ui->trackerUpButton->setIconSize(Utils::Gui::smallIconSize());
|
|
m_ui->trackerDownButton->setIcon(UIThemeManager::instance()->getIcon("go-down"));
|
|
m_ui->trackerDownButton->setIconSize(Utils::Gui::smallIconSize());
|
|
connect(m_ui->trackerUpButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::moveSelectionUp);
|
|
connect(m_ui->trackerDownButton, &QPushButton::clicked, m_trackerList, &TrackerListWidget::moveSelectionDown);
|
|
m_ui->hBoxLayoutTrackers->insertWidget(0, m_trackerList);
|
|
// Peers list
|
|
m_peerList = new PeerListWidget(this);
|
|
m_ui->vBoxLayoutPeerPage->addWidget(m_peerList);
|
|
// Tab bar
|
|
m_tabBar = new PropTabBar(nullptr);
|
|
m_tabBar->setContentsMargins(0, 5, 0, 5);
|
|
m_ui->verticalLayout->addLayout(m_tabBar);
|
|
connect(m_tabBar, &PropTabBar::tabChanged, m_ui->stackedProperties, &QStackedWidget::setCurrentIndex);
|
|
connect(m_tabBar, &PropTabBar::tabChanged, this, &PropertiesWidget::saveSettings);
|
|
connect(m_tabBar, &PropTabBar::visibilityToggled, this, &PropertiesWidget::setVisibility);
|
|
connect(m_tabBar, &PropTabBar::visibilityToggled, this, &PropertiesWidget::saveSettings);
|
|
|
|
const auto *editWebSeedsHotkey = new QShortcut(Qt::Key_F2, m_ui->listWebSeeds, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(editWebSeedsHotkey, &QShortcut::activated, this, &PropertiesWidget::editWebSeed);
|
|
const auto *deleteWebSeedsHotkey = new QShortcut(QKeySequence::Delete, m_ui->listWebSeeds, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(deleteWebSeedsHotkey, &QShortcut::activated, this, &PropertiesWidget::deleteSelectedUrlSeeds);
|
|
connect(m_ui->listWebSeeds, &QListWidget::doubleClicked, this, &PropertiesWidget::editWebSeed);
|
|
|
|
const auto *renameFileHotkey = new QShortcut(Qt::Key_F2, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(renameFileHotkey, &QShortcut::activated, this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); });
|
|
const auto *openFileHotkeyReturn = new QShortcut(Qt::Key_Return, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(openFileHotkeyReturn, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile);
|
|
const auto *openFileHotkeyEnter = new QShortcut(Qt::Key_Enter, m_ui->filesList, nullptr, nullptr, Qt::WidgetShortcut);
|
|
connect(openFileHotkeyEnter, &QShortcut::activated, this, &PropertiesWidget::openSelectedFile);
|
|
|
|
configure();
|
|
connect(Preferences::instance(), &Preferences::changed, this, &PropertiesWidget::configure);
|
|
}
|
|
|
|
PropertiesWidget::~PropertiesWidget()
|
|
{
|
|
delete m_tabBar;
|
|
delete m_ui;
|
|
}
|
|
|
|
void PropertiesWidget::showPiecesAvailability(bool show)
|
|
{
|
|
m_ui->labelPiecesAvailability->setVisible(show);
|
|
m_piecesAvailability->setVisible(show);
|
|
m_ui->labelAverageAvailabilityVal->setVisible(show);
|
|
if (show || !m_downloadedPieces->isVisible())
|
|
m_ui->lineBelowBars->setVisible(show);
|
|
}
|
|
|
|
void PropertiesWidget::showPiecesDownloaded(bool show)
|
|
{
|
|
m_ui->labelDownloadedPieces->setVisible(show);
|
|
m_downloadedPieces->setVisible(show);
|
|
m_ui->labelProgressVal->setVisible(show);
|
|
if (show || !m_piecesAvailability->isVisible())
|
|
m_ui->lineBelowBars->setVisible(show);
|
|
}
|
|
|
|
void PropertiesWidget::setVisibility(const bool visible)
|
|
{
|
|
if (!visible && (m_state == VISIBLE))
|
|
{
|
|
const int tabBarHeight = m_tabBar->geometry().height(); // take height before hiding
|
|
auto *hSplitter = static_cast<QSplitter *>(parentWidget());
|
|
m_ui->stackedProperties->setVisible(false);
|
|
m_slideSizes = hSplitter->sizes();
|
|
hSplitter->handle(1)->setVisible(false);
|
|
hSplitter->handle(1)->setDisabled(true);
|
|
m_handleWidth = hSplitter->handleWidth();
|
|
hSplitter->setHandleWidth(0);
|
|
const QList<int> sizes {(hSplitter->geometry().height() - tabBarHeight), tabBarHeight};
|
|
hSplitter->setSizes(sizes);
|
|
setMaximumSize(maximumSize().width(), tabBarHeight);
|
|
m_state = REDUCED;
|
|
return;
|
|
}
|
|
|
|
if (visible && (m_state == REDUCED))
|
|
{
|
|
m_ui->stackedProperties->setVisible(true);
|
|
auto *hSplitter = static_cast<QSplitter *>(parentWidget());
|
|
if (m_handleWidth != -1)
|
|
hSplitter->setHandleWidth(m_handleWidth);
|
|
hSplitter->handle(1)->setDisabled(false);
|
|
hSplitter->handle(1)->setVisible(true);
|
|
hSplitter->setSizes(m_slideSizes);
|
|
m_state = VISIBLE;
|
|
setMaximumSize(maximumSize().width(), QWIDGETSIZE_MAX);
|
|
// Force refresh
|
|
loadDynamicData();
|
|
}
|
|
}
|
|
|
|
void PropertiesWidget::clear()
|
|
{
|
|
qDebug("Clearing torrent properties");
|
|
m_ui->labelSavePathVal->clear();
|
|
m_ui->labelCreatedOnVal->clear();
|
|
m_ui->labelTotalPiecesVal->clear();
|
|
m_ui->labelHashVal->clear();
|
|
m_ui->labelCommentVal->clear();
|
|
m_ui->labelProgressVal->clear();
|
|
m_ui->labelAverageAvailabilityVal->clear();
|
|
m_ui->labelWastedVal->clear();
|
|
m_ui->labelUpTotalVal->clear();
|
|
m_ui->labelDlTotalVal->clear();
|
|
m_ui->labelUpLimitVal->clear();
|
|
m_ui->labelDlLimitVal->clear();
|
|
m_ui->labelElapsedVal->clear();
|
|
m_ui->labelConnectionsVal->clear();
|
|
m_ui->labelReannounceInVal->clear();
|
|
m_ui->labelShareRatioVal->clear();
|
|
m_ui->listWebSeeds->clear();
|
|
m_ui->labelETAVal->clear();
|
|
m_ui->labelSeedsVal->clear();
|
|
m_ui->labelPeersVal->clear();
|
|
m_ui->labelDlSpeedVal->clear();
|
|
m_ui->labelUpSpeedVal->clear();
|
|
m_ui->labelTotalSizeVal->clear();
|
|
m_ui->labelCompletedOnVal->clear();
|
|
m_ui->labelLastSeenCompleteVal->clear();
|
|
m_ui->labelCreatedByVal->clear();
|
|
m_ui->labelAddedOnVal->clear();
|
|
m_trackerList->clear();
|
|
m_downloadedPieces->clear();
|
|
m_piecesAvailability->clear();
|
|
m_peerList->clear();
|
|
m_contentFilterLine->clear();
|
|
m_propListModel->model()->clear();
|
|
}
|
|
|
|
BitTorrent::Torrent *PropertiesWidget::getCurrentTorrent() const
|
|
{
|
|
return m_torrent;
|
|
}
|
|
|
|
TrackerListWidget *PropertiesWidget::getTrackerList() const
|
|
{
|
|
return m_trackerList;
|
|
}
|
|
|
|
PeerListWidget *PropertiesWidget::getPeerList() const
|
|
{
|
|
return m_peerList;
|
|
}
|
|
|
|
QTreeView *PropertiesWidget::getFilesList() const
|
|
{
|
|
return m_ui->filesList;
|
|
}
|
|
|
|
void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent)
|
|
{
|
|
if (torrent == m_torrent)
|
|
m_ui->labelSavePathVal->setText(Utils::Fs::toNativePath(m_torrent->savePath()));
|
|
}
|
|
|
|
void PropertiesWidget::loadTrackers(BitTorrent::Torrent *const torrent)
|
|
{
|
|
if (torrent == m_torrent)
|
|
m_trackerList->loadTrackers();
|
|
}
|
|
|
|
void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent)
|
|
{
|
|
if (torrent == m_torrent)
|
|
loadTorrentInfos(m_torrent);
|
|
}
|
|
|
|
void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
|
|
{
|
|
clear();
|
|
m_torrent = torrent;
|
|
m_downloadedPieces->setTorrent(m_torrent);
|
|
m_piecesAvailability->setTorrent(m_torrent);
|
|
if (!m_torrent) return;
|
|
|
|
// Save path
|
|
updateSavePath(m_torrent);
|
|
// Info hash (Truncated info hash (torrent ID) with libtorrent2)
|
|
// TODO: Update label for this property to express its meaning more clearly (or change it to display real info hash(es))
|
|
m_ui->labelHashVal->setText(m_torrent->id().toString());
|
|
m_propListModel->model()->clear();
|
|
if (m_torrent->hasMetadata())
|
|
{
|
|
// Creation date
|
|
m_ui->labelCreatedOnVal->setText(QLocale().toString(m_torrent->creationDate(), QLocale::ShortFormat));
|
|
|
|
m_ui->labelTotalSizeVal->setText(Utils::Misc::friendlyUnit(m_torrent->totalSize()));
|
|
|
|
// Comment
|
|
m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped()));
|
|
|
|
// URL seeds
|
|
loadUrlSeeds();
|
|
|
|
m_ui->labelCreatedByVal->setText(m_torrent->creator());
|
|
|
|
// List files in torrent
|
|
m_propListModel->model()->setupModelData(m_torrent->info());
|
|
|
|
// Expand single-item folders recursively
|
|
QModelIndex currentIndex;
|
|
while (m_propListModel->rowCount(currentIndex) == 1)
|
|
{
|
|
currentIndex = m_propListModel->index(0, 0, currentIndex);
|
|
m_ui->filesList->setExpanded(currentIndex, true);
|
|
}
|
|
|
|
// Load file priorities
|
|
m_propListModel->model()->updateFilesPriorities(m_torrent->filePriorities());
|
|
}
|
|
// Load dynamic data
|
|
loadDynamicData();
|
|
}
|
|
|
|
void PropertiesWidget::readSettings()
|
|
{
|
|
const Preferences *const pref = Preferences::instance();
|
|
// Restore splitter sizes
|
|
QStringList sizesStr = pref->getPropSplitterSizes().split(',');
|
|
if (sizesStr.size() == 2)
|
|
{
|
|
m_slideSizes << sizesStr.first().toInt();
|
|
m_slideSizes << sizesStr.last().toInt();
|
|
auto *hSplitter = static_cast<QSplitter *>(parentWidget());
|
|
hSplitter->setSizes(m_slideSizes);
|
|
}
|
|
const int currentTab = pref->getPropCurTab();
|
|
const bool visible = pref->getPropVisible();
|
|
m_ui->filesList->header()->restoreState(pref->getPropFileListState());
|
|
m_tabBar->setCurrentIndex(currentTab);
|
|
if (!visible)
|
|
setVisibility(false);
|
|
}
|
|
|
|
void PropertiesWidget::saveSettings()
|
|
{
|
|
Preferences *const pref = Preferences::instance();
|
|
pref->setPropVisible(m_state == VISIBLE);
|
|
// Splitter sizes
|
|
auto *hSplitter = static_cast<QSplitter *>(parentWidget());
|
|
QList<int> sizes;
|
|
if (m_state == VISIBLE)
|
|
sizes = hSplitter->sizes();
|
|
else
|
|
sizes = m_slideSizes;
|
|
qDebug("Sizes: %d", sizes.size());
|
|
if (sizes.size() == 2)
|
|
pref->setPropSplitterSizes(QString::number(sizes.first()) + ',' + QString::number(sizes.last()));
|
|
pref->setPropFileListState(m_ui->filesList->header()->saveState());
|
|
// Remember current tab
|
|
pref->setPropCurTab(m_tabBar->currentIndex());
|
|
}
|
|
|
|
void PropertiesWidget::reloadPreferences()
|
|
{
|
|
// Take program preferences into consideration
|
|
m_peerList->updatePeerHostNameResolutionState();
|
|
m_peerList->updatePeerCountryResolutionState();
|
|
}
|
|
|
|
void PropertiesWidget::loadDynamicData()
|
|
{
|
|
// Refresh only if the torrent handle is valid and visible
|
|
if (!m_torrent || (m_state != VISIBLE)) return;
|
|
|
|
// Transfer infos
|
|
switch (m_ui->stackedProperties->currentIndex())
|
|
{
|
|
case PropTabBar::MainTab:
|
|
{
|
|
m_ui->labelWastedVal->setText(Utils::Misc::friendlyUnit(m_torrent->wastedSize()));
|
|
|
|
m_ui->labelUpTotalVal->setText(tr("%1 (%2 this session)").arg(Utils::Misc::friendlyUnit(m_torrent->totalUpload())
|
|
, Utils::Misc::friendlyUnit(m_torrent->totalPayloadUpload())));
|
|
|
|
m_ui->labelDlTotalVal->setText(tr("%1 (%2 this session)").arg(Utils::Misc::friendlyUnit(m_torrent->totalDownload())
|
|
, Utils::Misc::friendlyUnit(m_torrent->totalPayloadDownload())));
|
|
|
|
m_ui->labelUpLimitVal->setText(m_torrent->uploadLimit() <= 0 ? QString::fromUtf8(C_INFINITY) : Utils::Misc::friendlyUnit(m_torrent->uploadLimit(), true));
|
|
|
|
m_ui->labelDlLimitVal->setText(m_torrent->downloadLimit() <= 0 ? QString::fromUtf8(C_INFINITY) : Utils::Misc::friendlyUnit(m_torrent->downloadLimit(), true));
|
|
|
|
QString elapsedString;
|
|
if (m_torrent->isSeed())
|
|
elapsedString = tr("%1 (seeded for %2)", "e.g. 4m39s (seeded for 3m10s)")
|
|
.arg(Utils::Misc::userFriendlyDuration(m_torrent->activeTime())
|
|
, Utils::Misc::userFriendlyDuration(m_torrent->seedingTime()));
|
|
else
|
|
elapsedString = Utils::Misc::userFriendlyDuration(m_torrent->activeTime());
|
|
m_ui->labelElapsedVal->setText(elapsedString);
|
|
|
|
m_ui->labelConnectionsVal->setText(tr("%1 (%2 max)", "%1 and %2 are numbers, e.g. 3 (10 max)")
|
|
.arg(m_torrent->connectionsCount())
|
|
.arg(m_torrent->connectionsLimit() < 0 ? QString::fromUtf8(C_INFINITY) : QString::number(m_torrent->connectionsLimit())));
|
|
|
|
m_ui->labelETAVal->setText(Utils::Misc::userFriendlyDuration(m_torrent->eta(), MAX_ETA));
|
|
|
|
// Update next announce time
|
|
m_ui->labelReannounceInVal->setText(Utils::Misc::userFriendlyDuration(m_torrent->nextAnnounce()));
|
|
|
|
// Update ratio info
|
|
const qreal ratio = m_torrent->realRatio();
|
|
m_ui->labelShareRatioVal->setText(ratio > BitTorrent::Torrent::MAX_RATIO ? QString::fromUtf8(C_INFINITY) : Utils::String::fromDouble(ratio, 2));
|
|
|
|
m_ui->labelSeedsVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)")
|
|
.arg(QString::number(m_torrent->seedsCount())
|
|
, QString::number(m_torrent->totalSeedsCount())));
|
|
|
|
m_ui->labelPeersVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)")
|
|
.arg(QString::number(m_torrent->leechsCount())
|
|
, QString::number(m_torrent->totalLeechersCount())));
|
|
|
|
const qlonglong dlDuration = m_torrent->activeTime() - m_torrent->finishedTime();
|
|
const QString dlAvg = Utils::Misc::friendlyUnit((m_torrent->totalDownload() / ((dlDuration == 0) ? -1 : dlDuration)), true);
|
|
m_ui->labelDlSpeedVal->setText(tr("%1 (%2 avg.)", "%1 and %2 are speed rates, e.g. 200KiB/s (100KiB/s avg.)")
|
|
.arg(Utils::Misc::friendlyUnit(m_torrent->downloadPayloadRate(), true), dlAvg));
|
|
|
|
const qlonglong ulDuration = m_torrent->activeTime();
|
|
const QString ulAvg = Utils::Misc::friendlyUnit((m_torrent->totalUpload() / ((ulDuration == 0) ? -1 : ulDuration)), true);
|
|
m_ui->labelUpSpeedVal->setText(tr("%1 (%2 avg.)", "%1 and %2 are speed rates, e.g. 200KiB/s (100KiB/s avg.)")
|
|
.arg(Utils::Misc::friendlyUnit(m_torrent->uploadPayloadRate(), true), ulAvg));
|
|
|
|
m_ui->labelLastSeenCompleteVal->setText(m_torrent->lastSeenComplete().isValid() ? QLocale().toString(m_torrent->lastSeenComplete(), QLocale::ShortFormat) : tr("Never"));
|
|
|
|
m_ui->labelCompletedOnVal->setText(m_torrent->completedTime().isValid() ? QLocale().toString(m_torrent->completedTime(), QLocale::ShortFormat) : QString {});
|
|
|
|
m_ui->labelAddedOnVal->setText(QLocale().toString(m_torrent->addedTime(), QLocale::ShortFormat));
|
|
|
|
if (m_torrent->hasMetadata())
|
|
{
|
|
m_ui->labelTotalPiecesVal->setText(tr("%1 x %2 (have %3)", "(torrent pieces) eg 152 x 4MB (have 25)").arg(m_torrent->piecesCount()).arg(Utils::Misc::friendlyUnit(m_torrent->pieceLength())).arg(m_torrent->piecesHave()));
|
|
|
|
if (!m_torrent->isSeed() && !m_torrent->isPaused() && !m_torrent->isQueued() && !m_torrent->isChecking())
|
|
{
|
|
// Pieces availability
|
|
showPiecesAvailability(true);
|
|
m_piecesAvailability->setAvailability(m_torrent->pieceAvailability());
|
|
m_ui->labelAverageAvailabilityVal->setText(Utils::String::fromDouble(m_torrent->distributedCopies(), 3));
|
|
}
|
|
else
|
|
{
|
|
showPiecesAvailability(false);
|
|
}
|
|
|
|
// Progress
|
|
qreal progress = m_torrent->progress() * 100.;
|
|
m_ui->labelProgressVal->setText(Utils::String::fromDouble(progress, 1) + '%');
|
|
m_downloadedPieces->setProgress(m_torrent->pieces(), m_torrent->downloadingPieces());
|
|
}
|
|
else
|
|
{
|
|
showPiecesAvailability(false);
|
|
}
|
|
}
|
|
break;
|
|
case PropTabBar::TrackersTab:
|
|
// Trackers
|
|
m_trackerList->loadTrackers();
|
|
break;
|
|
case PropTabBar::PeersTab:
|
|
// Load peers
|
|
m_peerList->loadPeers(m_torrent);
|
|
break;
|
|
case PropTabBar::FilesTab:
|
|
// Files progress
|
|
if (m_torrent->hasMetadata())
|
|
{
|
|
qDebug("Updating priorities in files tab");
|
|
m_ui->filesList->setUpdatesEnabled(false);
|
|
m_propListModel->model()->updateFilesProgress(m_torrent->filesProgress());
|
|
m_propListModel->model()->updateFilesAvailability(m_torrent->availableFileFractions());
|
|
// XXX: We don't update file priorities regularly for performance
|
|
// reasons. This means that priorities will not be updated if
|
|
// set from the Web UI.
|
|
// PropListModel->model()->updateFilesPriorities(h.file_priorities());
|
|
m_ui->filesList->setUpdatesEnabled(true);
|
|
}
|
|
break;
|
|
default:;
|
|
}
|
|
}
|
|
|
|
void PropertiesWidget::loadUrlSeeds()
|
|
{
|
|
m_ui->listWebSeeds->clear();
|
|
qDebug("Loading URL seeds");
|
|
const QVector<QUrl> hcSeeds = m_torrent->urlSeeds();
|
|
// Add url seeds
|
|
for (const QUrl &hcSeed : hcSeeds)
|
|
{
|
|
qDebug("Loading URL seed: %s", qUtf8Printable(hcSeed.toString()));
|
|
new QListWidgetItem(hcSeed.toString(), m_ui->listWebSeeds);
|
|
}
|
|
}
|
|
|
|
QString PropertiesWidget::getFullPath(const QModelIndex &index) const
|
|
{
|
|
if (m_propListModel->itemType(index) == TorrentContentModelItem::FileType)
|
|
{
|
|
const int fileIdx = m_propListModel->getFileIndex(index);
|
|
const QString filename {m_torrent->filePath(fileIdx)};
|
|
const QDir saveDir {m_torrent->savePath(true)};
|
|
const QString fullPath {Utils::Fs::expandPath(saveDir.absoluteFilePath(filename))};
|
|
return fullPath;
|
|
}
|
|
|
|
// folder type
|
|
const QModelIndex nameIndex {index.sibling(index.row(), TorrentContentModelItem::COL_NAME)};
|
|
QString folderPath {nameIndex.data().toString()};
|
|
for (QModelIndex modelIdx = m_propListModel->parent(nameIndex); modelIdx.isValid(); modelIdx = modelIdx.parent())
|
|
folderPath.prepend(modelIdx.data().toString() + '/');
|
|
|
|
const QDir saveDir {m_torrent->savePath(true)};
|
|
const QString fullPath {Utils::Fs::expandPath(saveDir.absoluteFilePath(folderPath))};
|
|
return fullPath;
|
|
}
|
|
|
|
void PropertiesWidget::openItem(const QModelIndex &index) const
|
|
{
|
|
if (!index.isValid())
|
|
return;
|
|
|
|
m_torrent->flushCache(); // Flush data
|
|
Utils::Gui::openPath(getFullPath(index));
|
|
}
|
|
|
|
void PropertiesWidget::openParentFolder(const QModelIndex &index) const
|
|
{
|
|
const QString path = getFullPath(index);
|
|
m_torrent->flushCache(); // Flush data
|
|
#ifdef Q_OS_MACOS
|
|
MacUtils::openFiles({path});
|
|
#else
|
|
Utils::Gui::openFolderSelect(path);
|
|
#endif
|
|
}
|
|
|
|
void PropertiesWidget::displayFilesListMenu(const QPoint &)
|
|
{
|
|
if (!m_torrent) return;
|
|
|
|
const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0);
|
|
if (selectedRows.empty()) return;
|
|
|
|
QMenu *menu = new QMenu(this);
|
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
if (selectedRows.size() == 1)
|
|
{
|
|
const QModelIndex index = selectedRows[0];
|
|
|
|
menu->addAction(UIThemeManager::instance()->getIcon("folder-documents"), tr("Open")
|
|
, this, [this, index]() { openItem(index); });
|
|
menu->addAction(UIThemeManager::instance()->getIcon("inode-directory"), tr("Open Containing Folder")
|
|
, this, [this, index]() { openParentFolder(index); });
|
|
menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Rename...")
|
|
, this, [this]() { m_ui->filesList->renameSelectedFile(*m_torrent); });
|
|
menu->addSeparator();
|
|
}
|
|
|
|
if (!m_torrent->isSeed())
|
|
{
|
|
const auto applyPriorities = [this](const BitTorrent::DownloadPriority prio)
|
|
{
|
|
const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0);
|
|
for (const QModelIndex &index : selectedRows)
|
|
{
|
|
m_propListModel->setData(index.sibling(index.row(), PRIORITY)
|
|
, static_cast<int>(prio));
|
|
}
|
|
|
|
// Save changes
|
|
filteredFilesChanged();
|
|
};
|
|
|
|
QMenu *subMenu = menu->addMenu(tr("Priority"));
|
|
|
|
subMenu->addAction(tr("Do not download"), subMenu, [applyPriorities]()
|
|
{
|
|
applyPriorities(BitTorrent::DownloadPriority::Ignored);
|
|
});
|
|
subMenu->addAction(tr("Normal"), subMenu, [applyPriorities]()
|
|
{
|
|
applyPriorities(BitTorrent::DownloadPriority::Normal);
|
|
});
|
|
subMenu->addAction(tr("High"), subMenu, [applyPriorities]()
|
|
{
|
|
applyPriorities(BitTorrent::DownloadPriority::High);
|
|
});
|
|
subMenu->addAction(tr("Maximum"), subMenu, [applyPriorities]()
|
|
{
|
|
applyPriorities(BitTorrent::DownloadPriority::Maximum);
|
|
});
|
|
subMenu->addSeparator();
|
|
subMenu->addAction(tr("By shown file order"), subMenu, [this]()
|
|
{
|
|
// Equally distribute the selected items into groups and for each group assign
|
|
// a download priority that will apply to each item. The number of groups depends on how
|
|
// many "download priority" are available to be assigned
|
|
|
|
const QModelIndexList selectedRows = m_ui->filesList->selectionModel()->selectedRows(0);
|
|
|
|
const int priorityGroups = 3;
|
|
const int priorityGroupSize = std::max((selectedRows.length() / priorityGroups), 1);
|
|
|
|
for (int i = 0; i < selectedRows.length(); ++i)
|
|
{
|
|
auto priority = BitTorrent::DownloadPriority::Ignored;
|
|
switch (i / priorityGroupSize)
|
|
{
|
|
case 0:
|
|
priority = BitTorrent::DownloadPriority::Maximum;
|
|
break;
|
|
case 1:
|
|
priority = BitTorrent::DownloadPriority::High;
|
|
break;
|
|
default:
|
|
case 2:
|
|
priority = BitTorrent::DownloadPriority::Normal;
|
|
break;
|
|
}
|
|
|
|
const QModelIndex &index = selectedRows[i];
|
|
m_propListModel->setData(index.sibling(index.row(), PRIORITY)
|
|
, static_cast<int>(priority));
|
|
}
|
|
});
|
|
}
|
|
|
|
// The selected torrent might have disappeared during exec()
|
|
// so we just close menu when an appropriate model is reset
|
|
connect(m_ui->filesList->model(), &QAbstractItemModel::modelAboutToBeReset
|
|
, menu, [menu]()
|
|
{
|
|
menu->setActiveAction(nullptr);
|
|
menu->close();
|
|
});
|
|
|
|
menu->popup(QCursor::pos());
|
|
}
|
|
|
|
void PropertiesWidget::displayWebSeedListMenu(const QPoint &)
|
|
{
|
|
if (!m_torrent) return;
|
|
|
|
const QModelIndexList rows = m_ui->listWebSeeds->selectionModel()->selectedRows();
|
|
|
|
QMenu *menu = new QMenu(this);
|
|
menu->setAttribute(Qt::WA_DeleteOnClose);
|
|
|
|
menu->addAction(UIThemeManager::instance()->getIcon("list-add"), tr("New Web seed"), this, &PropertiesWidget::askWebSeed);
|
|
|
|
if (!rows.isEmpty())
|
|
{
|
|
menu->addAction(UIThemeManager::instance()->getIcon("list-remove"), tr("Remove Web seed")
|
|
, this, &PropertiesWidget::deleteSelectedUrlSeeds);
|
|
menu->addSeparator();
|
|
menu->addAction(UIThemeManager::instance()->getIcon("edit-copy"), tr("Copy Web seed URL")
|
|
, this, &PropertiesWidget::copySelectedWebSeedsToClipboard);
|
|
menu->addAction(UIThemeManager::instance()->getIcon("edit-rename"), tr("Edit Web seed URL")
|
|
, this, &PropertiesWidget::editWebSeed);
|
|
}
|
|
|
|
menu->popup(QCursor::pos());
|
|
}
|
|
|
|
void PropertiesWidget::openSelectedFile()
|
|
{
|
|
const QModelIndexList selectedIndexes = m_ui->filesList->selectionModel()->selectedRows(0);
|
|
if (selectedIndexes.size() != 1)
|
|
return;
|
|
openItem(selectedIndexes.first());
|
|
}
|
|
|
|
void PropertiesWidget::configure()
|
|
{
|
|
// Speed widget
|
|
if (Preferences::instance()->isSpeedWidgetEnabled())
|
|
{
|
|
if (!m_speedWidget || !qobject_cast<SpeedWidget *>(m_speedWidget))
|
|
{
|
|
m_ui->speedLayout->removeWidget(m_speedWidget);
|
|
delete m_speedWidget;
|
|
m_speedWidget = new SpeedWidget {this};
|
|
m_ui->speedLayout->addWidget(m_speedWidget);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (!m_speedWidget || !qobject_cast<QLabel *>(m_speedWidget))
|
|
{
|
|
m_ui->speedLayout->removeWidget(m_speedWidget);
|
|
delete m_speedWidget;
|
|
auto *label = new QLabel(tr("<center><b>Speed graphs are disabled</b><p>You may change this setting in Advanced Options </center>"), this);
|
|
label->setAlignment(Qt::AlignHCenter | Qt::AlignVCenter);
|
|
m_speedWidget = label;
|
|
m_ui->speedLayout->addWidget(m_speedWidget);
|
|
}
|
|
}
|
|
}
|
|
|
|
void PropertiesWidget::askWebSeed()
|
|
{
|
|
bool ok = false;
|
|
// Ask user for a new url seed
|
|
const QString urlSeed = AutoExpandableDialog::getText(this, tr("New URL seed", "New HTTP source"),
|
|
tr("New URL seed:"), QLineEdit::Normal,
|
|
QLatin1String("http://www."), &ok);
|
|
if (!ok) return;
|
|
qDebug("Adding %s web seed", qUtf8Printable(urlSeed));
|
|
if (!m_ui->listWebSeeds->findItems(urlSeed, Qt::MatchFixedString).empty())
|
|
{
|
|
QMessageBox::warning(this, "qBittorrent",
|
|
tr("This URL seed is already in the list."),
|
|
QMessageBox::Ok);
|
|
return;
|
|
}
|
|
if (m_torrent)
|
|
m_torrent->addUrlSeeds({urlSeed});
|
|
// Refresh the seeds list
|
|
loadUrlSeeds();
|
|
}
|
|
|
|
void PropertiesWidget::deleteSelectedUrlSeeds()
|
|
{
|
|
const QList<QListWidgetItem *> selectedItems = m_ui->listWebSeeds->selectedItems();
|
|
if (selectedItems.isEmpty()) return;
|
|
|
|
QVector<QUrl> urlSeeds;
|
|
urlSeeds.reserve(selectedItems.size());
|
|
|
|
for (const QListWidgetItem *item : selectedItems)
|
|
urlSeeds << item->text();
|
|
|
|
m_torrent->removeUrlSeeds(urlSeeds);
|
|
// Refresh list
|
|
loadUrlSeeds();
|
|
}
|
|
|
|
void PropertiesWidget::copySelectedWebSeedsToClipboard() const
|
|
{
|
|
const QList<QListWidgetItem *> selectedItems = m_ui->listWebSeeds->selectedItems();
|
|
if (selectedItems.isEmpty()) return;
|
|
|
|
QStringList urlsToCopy;
|
|
for (const QListWidgetItem *item : selectedItems)
|
|
urlsToCopy << item->text();
|
|
|
|
QApplication::clipboard()->setText(urlsToCopy.join('\n'));
|
|
}
|
|
|
|
void PropertiesWidget::editWebSeed()
|
|
{
|
|
const QList<QListWidgetItem *> selectedItems = m_ui->listWebSeeds->selectedItems();
|
|
if (selectedItems.size() != 1) return;
|
|
|
|
const QListWidgetItem *selectedItem = selectedItems.last();
|
|
const QString oldSeed = selectedItem->text();
|
|
bool result;
|
|
const QString newSeed = AutoExpandableDialog::getText(this, tr("Web seed editing"),
|
|
tr("Web seed URL:"), QLineEdit::Normal,
|
|
oldSeed, &result);
|
|
if (!result) return;
|
|
|
|
if (!m_ui->listWebSeeds->findItems(newSeed, Qt::MatchFixedString).empty())
|
|
{
|
|
QMessageBox::warning(this, QLatin1String("qBittorrent"),
|
|
tr("This URL seed is already in the list."),
|
|
QMessageBox::Ok);
|
|
return;
|
|
}
|
|
|
|
m_torrent->removeUrlSeeds({oldSeed});
|
|
m_torrent->addUrlSeeds({newSeed});
|
|
loadUrlSeeds();
|
|
}
|
|
|
|
void PropertiesWidget::applyPriorities()
|
|
{
|
|
m_torrent->prioritizeFiles(m_propListModel->model()->getFilePriorities());
|
|
}
|
|
|
|
void PropertiesWidget::filteredFilesChanged()
|
|
{
|
|
if (m_torrent)
|
|
applyPriorities();
|
|
}
|
|
|
|
void PropertiesWidget::filterText(const QString &filter)
|
|
{
|
|
const QString pattern = Utils::String::wildcardToRegexPattern(filter);
|
|
m_propListModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
|
if (filter.isEmpty())
|
|
{
|
|
m_ui->filesList->collapseAll();
|
|
m_ui->filesList->expand(m_propListModel->index(0, 0));
|
|
}
|
|
else
|
|
{
|
|
m_ui->filesList->expandAll();
|
|
}
|
|
}
|