1
0
mirror of https://github.com/d47081/qBittorrent.git synced 2025-01-11 07:18:08 +00:00
qBittorrent/src/gui/addnewtorrentdialog.cpp
mxtsdev 5e6174c087
Add file name filter/blacklist
Blacklist filtered file names from being downloaded from torrent(s).
Files matching any of the filters in this list will have their priority automatically set to "Do not download".
See Options > Downloads >Do not download.

Closes #3369.
PR #17106.
2022-06-10 13:37:46 +08:00

1092 lines
40 KiB
C++

/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2012 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 "addnewtorrentdialog.h"
#include <algorithm>
#include <QAction>
#include <QDebug>
#include <QDir>
#include <QFileDialog>
#include <QMenu>
#include <QPushButton>
#include <QShortcut>
#include <QString>
#include <QUrl>
#include <QVector>
#include "base/bittorrent/downloadpriority.h"
#include "base/bittorrent/infohash.h"
#include "base/bittorrent/magneturi.h"
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h"
#include "base/bittorrent/torrentcontentlayout.h"
#include "base/global.h"
#include "base/net/downloadmanager.h"
#include "base/settingsstorage.h"
#include "base/torrentfileguard.h"
#include "base/utils/compare.h"
#include "base/utils/fs.h"
#include "base/utils/misc.h"
#include "autoexpandabledialog.h"
#include "lineedit.h"
#include "properties/proplistdelegate.h"
#include "raisedmessagebox.h"
#include "torrentcontentfiltermodel.h"
#include "torrentcontentmodel.h"
#include "ui_addnewtorrentdialog.h"
#include "uithememanager.h"
#include "utils.h"
namespace
{
#define SETTINGS_KEY(name) u"AddNewTorrentDialog/" name
const QString KEY_ENABLED = SETTINGS_KEY(u"Enabled"_qs);
const QString KEY_TOPLEVEL = SETTINGS_KEY(u"TopLevel"_qs);
const QString KEY_SAVEPATHHISTORY = SETTINGS_KEY(u"SavePathHistory"_qs);
const QString KEY_DOWNLOADPATHHISTORY = SETTINGS_KEY(u"DownloadPathHistory"_qs);
const QString KEY_SAVEPATHHISTORYLENGTH = SETTINGS_KEY(u"SavePathHistoryLength"_qs);
// just a shortcut
inline SettingsStorage *settings()
{
return SettingsStorage::instance();
}
class FileStorageAdaptor final : public BitTorrent::AbstractFileStorage
{
public:
FileStorageAdaptor(const BitTorrent::TorrentInfo &torrentInfo, PathList &filePaths)
: m_torrentInfo {torrentInfo}
, m_filePaths {filePaths}
{
Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount()));
}
int filesCount() const override
{
return m_torrentInfo.filesCount();
}
qlonglong fileSize(const int index) const override
{
Q_ASSERT((index >= 0) && (index < filesCount()));
return m_torrentInfo.fileSize(index);
}
Path filePath(const int index) const override
{
Q_ASSERT((index >= 0) && (index < filesCount()));
return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index));
}
void renameFile(const int index, const Path &newFilePath) override
{
Q_ASSERT((index >= 0) && (index < filesCount()));
const Path currentFilePath = filePath(index);
if (currentFilePath == newFilePath)
return;
if (m_filePaths.isEmpty())
m_filePaths = m_torrentInfo.filePaths();
m_filePaths[index] = newFilePath;
}
private:
const BitTorrent::TorrentInfo &m_torrentInfo;
PathList &m_filePaths;
};
// savePath is a folder, not an absolute file path
int indexOfPath(const FileSystemPathComboEdit *fsPathEdit, const Path &savePath)
{
for (int i = 0; i < fsPathEdit->count(); ++i)
{
if (fsPathEdit->item(i) == savePath)
return i;
}
return -1;
}
void setPath(FileSystemPathComboEdit *fsPathEdit, const Path &newPath)
{
int existingIndex = indexOfPath(fsPathEdit, newPath);
if (existingIndex < 0)
{
// New path, prepend to combo box
fsPathEdit->insertItem(0, newPath);
existingIndex = 0;
}
fsPathEdit->setCurrentIndex(existingIndex);
}
void updatePathHistory(const QString &settingsKey, const Path &path, const int maxLength)
{
// Add last used save path to the front of history
auto pathList = settings()->loadValue<QStringList>(settingsKey);
const int selectedSavePathIndex = pathList.indexOf(path.toString());
if (selectedSavePathIndex > -1)
pathList.move(selectedSavePathIndex, 0);
else
pathList.prepend(path.toString());
settings()->storeValue(settingsKey, QStringList(pathList.mid(0, maxLength)));
}
}
const int AddNewTorrentDialog::minPathHistoryLength;
const int AddNewTorrentDialog::maxPathHistoryLength;
AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::AddTorrentParams &inParams, QWidget *parent)
: QDialog(parent)
, m_ui(new Ui::AddNewTorrentDialog)
, m_filterLine(new LineEdit(this))
, m_torrentParams(inParams)
, m_storeDialogSize(SETTINGS_KEY(u"DialogSize"_qs))
, m_storeDefaultCategory(SETTINGS_KEY(u"DefaultCategory"_qs))
, m_storeRememberLastSavePath(SETTINGS_KEY(u"RememberLastSavePath"_qs))
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
, m_storeTreeHeaderState(u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_qs))
, m_storeSplitterState(u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_qs))
#else
, m_storeTreeHeaderState(SETTINGS_KEY(u"TreeHeaderState"_qs))
, m_storeSplitterState(SETTINGS_KEY(u"SplitterState"_qs))
#endif
{
// TODO: set dialog file properties using m_torrentParams.filePriorities
m_ui->setupUi(this);
m_ui->lblMetaLoading->setVisible(false);
m_ui->progMetaLoading->setVisible(false);
m_ui->buttonSave->setVisible(false);
connect(m_ui->buttonSave, &QPushButton::clicked, this, &AddNewTorrentDialog::saveTorrentFile);
m_ui->savePath->setMode(FileSystemPathEdit::Mode::DirectorySave);
m_ui->savePath->setDialogCaption(tr("Choose save path"));
m_ui->savePath->setMaxVisibleItems(20);
const auto *session = BitTorrent::Session::instance();
m_ui->downloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave);
m_ui->downloadPath->setDialogCaption(tr("Choose save path"));
m_ui->downloadPath->setMaxVisibleItems(20);
m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused()));
m_ui->comboTTM->blockSignals(true); // the TreeView size isn't correct if the slot does its job at this point
m_ui->comboTTM->setCurrentIndex(session->isAutoTMMDisabledByDefault() ? 0 : 1);
m_ui->comboTTM->blockSignals(false);
connect(m_ui->savePath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onSavePathChanged);
connect(m_ui->downloadPath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onDownloadPathChanged);
connect(m_ui->groupBoxDownloadPath, &QGroupBox::toggled, this, &AddNewTorrentDialog::onUseDownloadPathChanged);
m_ui->checkBoxRememberLastSavePath->setChecked(m_storeRememberLastSavePath);
m_ui->contentLayoutComboBox->setCurrentIndex(
static_cast<int>(m_torrentParams.contentLayout.value_or(session->torrentContentLayout())));
connect(m_ui->contentLayoutComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::contentLayoutChanged);
m_ui->sequentialCheckBox->setChecked(m_torrentParams.sequential);
m_ui->firstLastCheckBox->setChecked(m_torrentParams.firstLastPiecePriority);
m_ui->skipCheckingCheckBox->setChecked(m_torrentParams.skipChecking);
m_ui->doNotDeleteTorrentCheckBox->setVisible(TorrentFileGuard::autoDeleteMode() != TorrentFileGuard::Never);
// Load categories
QStringList categories = session->categories();
std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
const QString defaultCategory = m_storeDefaultCategory;
if (!m_torrentParams.category.isEmpty())
m_ui->categoryComboBox->addItem(m_torrentParams.category);
if (!defaultCategory.isEmpty())
m_ui->categoryComboBox->addItem(defaultCategory);
m_ui->categoryComboBox->addItem(u""_qs);
for (const QString &category : asConst(categories))
if (category != defaultCategory && category != m_torrentParams.category)
m_ui->categoryComboBox->addItem(category);
m_ui->contentTreeView->header()->setContextMenuPolicy(Qt::CustomContextMenu);
m_ui->contentTreeView->header()->setSortIndicator(0, Qt::AscendingOrder);
connect(m_ui->contentTreeView->header(), &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::displayColumnHeaderMenu);
// Torrent content filtering
m_filterLine->setPlaceholderText(tr("Filter files..."));
m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
connect(m_filterLine, &LineEdit::textChanged, this, &AddNewTorrentDialog::handleFilterTextChanged);
m_ui->contentFilterLayout->insertWidget(3, m_filterLine);
loadState();
// Signal / slots
connect(m_ui->doNotDeleteTorrentCheckBox, &QCheckBox::clicked, this, &AddNewTorrentDialog::doNotDeleteTorrentClicked);
QShortcut *editHotkey = new QShortcut(Qt::Key_F2, m_ui->contentTreeView, nullptr, nullptr, Qt::WidgetShortcut);
connect(editHotkey, &QShortcut::activated, this, &AddNewTorrentDialog::renameSelectedFile);
connect(m_ui->contentTreeView, &QAbstractItemView::doubleClicked, this, &AddNewTorrentDialog::renameSelectedFile);
// Default focus
if (m_ui->comboTTM->currentIndex() == 0) // 0 is Manual mode
m_ui->savePath->setFocus();
else
m_ui->categoryComboBox->setFocus();
}
void AddNewTorrentDialog::applyContentLayout()
{
Q_ASSERT(hasMetadata());
Q_ASSERT(!m_torrentParams.filePaths.isEmpty());
const auto originalContentLayout = (m_originalRootFolder.isEmpty()
? BitTorrent::TorrentContentLayout::NoSubfolder
: BitTorrent::TorrentContentLayout::Subfolder);
const int currentIndex = m_ui->contentLayoutComboBox->currentIndex();
const auto contentLayout = ((currentIndex == 0)
? originalContentLayout
: static_cast<BitTorrent::TorrentContentLayout>(currentIndex));
if (contentLayout != m_currentContentLayout)
{
PathList &filePaths = m_torrentParams.filePaths;
if (contentLayout == BitTorrent::TorrentContentLayout::NoSubfolder)
{
Path::stripRootFolder(filePaths);
}
else
{
const auto rootFolder = ((originalContentLayout == BitTorrent::TorrentContentLayout::Subfolder)
? m_originalRootFolder
: filePaths.at(0).removedExtension());
Path::addRootFolder(filePaths, rootFolder);
}
m_currentContentLayout = contentLayout;
}
}
AddNewTorrentDialog::~AddNewTorrentDialog()
{
saveState();
delete m_contentDelegate;
delete m_ui;
}
bool AddNewTorrentDialog::isEnabled()
{
return settings()->loadValue(KEY_ENABLED, true);
}
void AddNewTorrentDialog::setEnabled(const bool value)
{
settings()->storeValue(KEY_ENABLED, value);
}
bool AddNewTorrentDialog::isTopLevel()
{
return settings()->loadValue(KEY_TOPLEVEL, true);
}
void AddNewTorrentDialog::setTopLevel(const bool value)
{
settings()->storeValue(KEY_TOPLEVEL, value);
}
int AddNewTorrentDialog::savePathHistoryLength()
{
const int defaultHistoryLength = 8;
const int value = settings()->loadValue(KEY_SAVEPATHHISTORYLENGTH, defaultHistoryLength);
return std::clamp(value, minPathHistoryLength, maxPathHistoryLength);
}
void AddNewTorrentDialog::setSavePathHistoryLength(const int value)
{
const int clampedValue = qBound(minPathHistoryLength, value, maxPathHistoryLength);
const int oldValue = savePathHistoryLength();
if (clampedValue == oldValue)
return;
settings()->storeValue(KEY_SAVEPATHHISTORYLENGTH, clampedValue);
settings()->storeValue(KEY_SAVEPATHHISTORY
, QStringList(settings()->loadValue<QStringList>(KEY_SAVEPATHHISTORY).mid(0, clampedValue)));
}
void AddNewTorrentDialog::loadState()
{
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize);
m_ui->splitter->restoreState(m_storeSplitterState);;
}
void AddNewTorrentDialog::saveState()
{
m_storeDialogSize = size();
m_storeSplitterState = m_ui->splitter->saveState();
if (m_contentModel)
m_storeTreeHeaderState = m_ui->contentTreeView->header()->saveState();
}
void AddNewTorrentDialog::show(const QString &source, const BitTorrent::AddTorrentParams &inParams, QWidget *parent)
{
auto *dlg = new AddNewTorrentDialog(inParams, parent);
dlg->setAttribute(Qt::WA_DeleteOnClose);
if (Net::DownloadManager::hasSupportedScheme(source))
{
// Launch downloader
Net::DownloadManager::instance()->download(
Net::DownloadRequest(source).limit(MAX_TORRENT_SIZE)
, dlg, &AddNewTorrentDialog::handleDownloadFinished);
return;
}
const BitTorrent::MagnetUri magnetUri {source};
const bool isLoaded = magnetUri.isValid()
? dlg->loadMagnet(magnetUri)
: dlg->loadTorrentFile(source);
if (isLoaded)
dlg->QDialog::show();
else
delete dlg;
}
void AddNewTorrentDialog::show(const QString &source, QWidget *parent)
{
show(source, BitTorrent::AddTorrentParams(), parent);
}
bool AddNewTorrentDialog::loadTorrentFile(const QString &source)
{
const Path decodedPath {source.startsWith(u"file://", Qt::CaseInsensitive)
? QUrl::fromEncoded(source.toLocal8Bit()).toLocalFile()
: source};
const nonstd::expected<BitTorrent::TorrentInfo, QString> result = BitTorrent::TorrentInfo::loadFromFile(decodedPath);
if (!result)
{
RaisedMessageBox::critical(this, tr("Invalid torrent")
, tr("Failed to load the torrent: %1.\nError: %2", "Don't remove the '\n' characters. They insert a newline.")
.arg(decodedPath.toString(), result.error()));
return false;
}
m_torrentInfo = result.value();
m_torrentGuard = std::make_unique<TorrentFileGuard>(decodedPath);
return loadTorrentImpl();
}
bool AddNewTorrentDialog::loadTorrentImpl()
{
const auto torrentID = BitTorrent::TorrentID::fromInfoHash(m_torrentInfo.infoHash());
// Prevent showing the dialog if download is already present
if (BitTorrent::Session::instance()->isKnownTorrent(torrentID))
{
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(torrentID);
if (torrent)
{
if (torrent->isPrivate() || m_torrentInfo.isPrivate())
{
RaisedMessageBox::warning(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers haven't been merged because it is a private torrent.").arg(torrent->name()), QMessageBox::Ok);
}
else
{
torrent->addTrackers(m_torrentInfo.trackers());
torrent->addUrlSeeds(m_torrentInfo.urlSeeds());
RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers have been merged.").arg(torrent->name()), QMessageBox::Ok);
}
}
else
{
RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Torrent is already queued for processing."), QMessageBox::Ok);
}
return false;
}
m_ui->labelInfohash1Data->setText(m_torrentInfo.infoHash().v1().isValid() ? m_torrentInfo.infoHash().v1().toString() : tr("N/A"));
m_ui->labelInfohash2Data->setText(m_torrentInfo.infoHash().v2().isValid() ? m_torrentInfo.infoHash().v2().toString() : tr("N/A"));
setupTreeview();
TMMChanged(m_ui->comboTTM->currentIndex());
return true;
}
bool AddNewTorrentDialog::loadMagnet(const BitTorrent::MagnetUri &magnetUri)
{
if (!magnetUri.isValid())
{
RaisedMessageBox::critical(this, tr("Invalid magnet link"), tr("This magnet link was not recognized"));
return false;
}
m_torrentGuard = std::make_unique<TorrentFileGuard>();
const auto torrentID = BitTorrent::TorrentID::fromInfoHash(magnetUri.infoHash());
// Prevent showing the dialog if download is already present
if (BitTorrent::Session::instance()->isKnownTorrent(torrentID))
{
BitTorrent::Torrent *const torrent = BitTorrent::Session::instance()->findTorrent(torrentID);
if (torrent)
{
if (torrent->isPrivate())
{
RaisedMessageBox::warning(this, tr("Torrent is already present"), tr("Torrent '%1' is already in the transfer list. Trackers haven't been merged because it is a private torrent.").arg(torrent->name()), QMessageBox::Ok);
}
else
{
torrent->addTrackers(magnetUri.trackers());
torrent->addUrlSeeds(magnetUri.urlSeeds());
RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Magnet link '%1' is already in the transfer list. Trackers have been merged.").arg(torrent->name()), QMessageBox::Ok);
}
}
else
{
RaisedMessageBox::information(this, tr("Torrent is already present"), tr("Magnet link is already queued for processing."), QMessageBox::Ok);
}
return false;
}
connect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &AddNewTorrentDialog::updateMetadata);
// Set dialog title
const QString torrentName = magnetUri.name();
setWindowTitle(torrentName.isEmpty() ? tr("Magnet link") : torrentName);
setupTreeview();
TMMChanged(m_ui->comboTTM->currentIndex());
BitTorrent::Session::instance()->downloadMetadata(magnetUri);
setMetadataProgressIndicator(true, tr("Retrieving metadata..."));
m_ui->labelInfohash1Data->setText(magnetUri.infoHash().v1().isValid() ? magnetUri.infoHash().v1().toString() : tr("N/A"));
m_ui->labelInfohash2Data->setText(magnetUri.infoHash().v2().isValid() ? magnetUri.infoHash().v2().toString() : tr("N/A"));
m_magnetURI = magnetUri;
return true;
}
void AddNewTorrentDialog::showEvent(QShowEvent *event)
{
QDialog::showEvent(event);
if (!isTopLevel()) return;
activateWindow();
raise();
}
void AddNewTorrentDialog::updateDiskSpaceLabel()
{
// Determine torrent size
qlonglong torrentSize = 0;
if (hasMetadata())
{
if (m_contentModel)
{
const QVector<BitTorrent::DownloadPriority> priorities = m_contentModel->model()->getFilePriorities();
Q_ASSERT(priorities.size() == m_torrentInfo.filesCount());
for (int i = 0; i < priorities.size(); ++i)
{
if (priorities[i] > BitTorrent::DownloadPriority::Ignored)
torrentSize += m_torrentInfo.fileSize(i);
}
}
else
{
torrentSize = m_torrentInfo.totalSize();
}
}
const QString sizeString = tr("%1 (Free space on disk: %2)").arg(
((torrentSize > 0) ? Utils::Misc::friendlyUnit(torrentSize) : tr("Not available", "This size is unavailable."))
, Utils::Misc::friendlyUnit(Utils::Fs::freeDiskSpaceOnPath(m_ui->savePath->selectedPath())));
m_ui->labelSizeData->setText(sizeString);
}
void AddNewTorrentDialog::onSavePathChanged(const Path &newPath)
{
Q_UNUSED(newPath);
// Remember index
m_savePathIndex = m_ui->savePath->currentIndex();
updateDiskSpaceLabel();
}
void AddNewTorrentDialog::onDownloadPathChanged(const Path &newPath)
{
Q_UNUSED(newPath);
// Remember index
const int currentPathIndex = m_ui->downloadPath->currentIndex();
if (currentPathIndex >= 0)
m_downloadPathIndex = m_ui->downloadPath->currentIndex();
}
void AddNewTorrentDialog::onUseDownloadPathChanged(const bool checked)
{
m_useDownloadPath = checked;
m_ui->downloadPath->setCurrentIndex(checked ? m_downloadPathIndex : -1);
}
void AddNewTorrentDialog::categoryChanged(int index)
{
Q_UNUSED(index);
if (m_ui->comboTTM->currentIndex() == 1)
{
const auto *btSession = BitTorrent::Session::instance();
const QString categoryName = m_ui->categoryComboBox->currentText();
const Path savePath = btSession->categorySavePath(categoryName);
m_ui->savePath->setSelectedPath(savePath);
const Path downloadPath = btSession->categoryDownloadPath(categoryName);
m_ui->downloadPath->setSelectedPath(downloadPath);
m_ui->groupBoxDownloadPath->setChecked(!m_ui->downloadPath->selectedPath().isEmpty());
updateDiskSpaceLabel();
}
}
void AddNewTorrentDialog::contentLayoutChanged()
{
if (!hasMetadata())
return;
const auto filePriorities = m_contentModel->model()->getFilePriorities();
m_contentModel->model()->clear();
applyContentLayout();
m_contentModel->model()->setupModelData(FileStorageAdaptor(m_torrentInfo, m_torrentParams.filePaths));
m_contentModel->model()->updateFilesPriorities(filePriorities);
// Expand single-item folders recursively
QModelIndex currentIndex;
while (m_contentModel->rowCount(currentIndex) == 1)
{
currentIndex = m_contentModel->index(0, 0, currentIndex);
m_ui->contentTreeView->setExpanded(currentIndex, true);
}
}
void AddNewTorrentDialog::saveTorrentFile()
{
Q_ASSERT(hasMetadata());
const QString filter {tr("Torrent file (*%1)").arg(TORRENT_FILE_EXTENSION)};
Path path {QFileDialog::getSaveFileName(this, tr("Save as torrent file")
, QDir::home().absoluteFilePath(m_torrentInfo.name() + TORRENT_FILE_EXTENSION)
, filter)};
if (path.isEmpty()) return;
if (!path.hasExtension(TORRENT_FILE_EXTENSION))
path += TORRENT_FILE_EXTENSION;
const nonstd::expected<void, QString> result = m_torrentInfo.saveToFile(path);
if (!result)
{
QMessageBox::critical(this, tr("I/O Error")
, tr("Couldn't export torrent metadata file '%1'. Reason: %2.").arg(path.toString(), result.error()));
}
}
bool AddNewTorrentDialog::hasMetadata() const
{
return m_torrentInfo.isValid();
}
void AddNewTorrentDialog::populateSavePaths()
{
const auto *btSession = BitTorrent::Session::instance();
m_ui->savePath->blockSignals(true);
m_ui->savePath->clear();
const auto savePathHistory = settings()->loadValue<QStringList>(KEY_SAVEPATHHISTORY);
if (savePathHistory.size() > 0)
{
for (const QString &path : savePathHistory)
m_ui->savePath->addItem(Path(path));
}
else
{
m_ui->savePath->addItem(btSession->savePath());
}
if (m_savePathIndex >= 0)
{
m_ui->savePath->setCurrentIndex(std::min(m_savePathIndex, (m_ui->savePath->count() - 1)));
}
else
{
if (!m_torrentParams.savePath.isEmpty())
setPath(m_ui->savePath, m_torrentParams.savePath);
else if (!m_storeRememberLastSavePath)
setPath(m_ui->savePath, btSession->savePath());
else
m_ui->savePath->setCurrentIndex(0);
m_savePathIndex = m_ui->savePath->currentIndex();
}
m_ui->savePath->blockSignals(false);
m_ui->downloadPath->blockSignals(true);
m_ui->downloadPath->clear();
const auto downloadPathHistory = settings()->loadValue<QStringList>(KEY_DOWNLOADPATHHISTORY);
if (downloadPathHistory.size() > 0)
{
for (const QString &path : downloadPathHistory)
m_ui->downloadPath->addItem(Path(path));
}
else
{
m_ui->downloadPath->addItem(btSession->downloadPath());
}
if (m_downloadPathIndex >= 0)
{
m_ui->downloadPath->setCurrentIndex(m_useDownloadPath ? std::min(m_downloadPathIndex, (m_ui->downloadPath->count() - 1)) : -1);
m_ui->groupBoxDownloadPath->setChecked(m_useDownloadPath);
}
else
{
const bool useDownloadPath = m_torrentParams.useDownloadPath.value_or(btSession->isDownloadPathEnabled());
m_ui->groupBoxDownloadPath->setChecked(useDownloadPath);
if (!m_torrentParams.downloadPath.isEmpty())
setPath(m_ui->downloadPath, m_torrentParams.downloadPath);
else if (!m_storeRememberLastSavePath)
setPath(m_ui->downloadPath, btSession->downloadPath());
else
m_ui->downloadPath->setCurrentIndex(0);
m_downloadPathIndex = m_ui->downloadPath->currentIndex();
if (!useDownloadPath)
m_ui->downloadPath->setCurrentIndex(-1);
}
m_ui->downloadPath->blockSignals(false);
m_ui->groupBoxDownloadPath->blockSignals(false);
}
void AddNewTorrentDialog::displayContentTreeMenu()
{
const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0);
const auto applyPriorities = [this](const BitTorrent::DownloadPriority prio)
{
const QModelIndexList selectedRows = m_ui->contentTreeView->selectionModel()->selectedRows(0);
for (const QModelIndex &index : selectedRows)
{
m_contentModel->setData(index.sibling(index.row(), PRIORITY)
, static_cast<int>(prio));
}
};
const auto applyPrioritiesByOrder = [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->contentTreeView->selectionModel()->selectedRows(0);
const qsizetype priorityGroups = 3;
const auto priorityGroupSize = std::max<qsizetype>((selectedRows.length() / priorityGroups), 1);
for (qsizetype 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_contentModel->setData(index.sibling(index.row(), PRIORITY)
, static_cast<int>(priority));
}
};
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
if (selectedRows.size() == 1)
{
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_qs), tr("Rename..."), this, &AddNewTorrentDialog::renameSelectedFile);
menu->addSeparator();
QMenu *priorityMenu = menu->addMenu(tr("Priority"));
priorityMenu->addAction(tr("Do not download"), priorityMenu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::Ignored);
});
priorityMenu->addAction(tr("Normal"), priorityMenu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::Normal);
});
priorityMenu->addAction(tr("High"), priorityMenu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::High);
});
priorityMenu->addAction(tr("Maximum"), priorityMenu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::Maximum);
});
priorityMenu->addSeparator();
priorityMenu->addAction(tr("By shown file order"), priorityMenu, applyPrioritiesByOrder);
}
else
{
menu->addAction(tr("Do not download"), menu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::Ignored);
});
menu->addAction(tr("Normal priority"), menu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::Normal);
});
menu->addAction(tr("High priority"), menu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::High);
});
menu->addAction(tr("Maximum priority"), menu, [applyPriorities]()
{
applyPriorities(BitTorrent::DownloadPriority::Maximum);
});
menu->addSeparator();
menu->addAction(tr("Priority by shown file order"), menu, applyPrioritiesByOrder);
}
menu->popup(QCursor::pos());
}
void AddNewTorrentDialog::displayColumnHeaderMenu()
{
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->setToolTipsVisible(true);
QAction *resizeAction = menu->addAction(tr("Resize columns"), this, [this]()
{
for (int i = 0, count = m_ui->contentTreeView->header()->count(); i < count; ++i)
{
if (!m_ui->contentTreeView->isColumnHidden(i))
m_ui->contentTreeView->resizeColumnToContents(i);
}
});
resizeAction->setToolTip(tr("Resize all non-hidden columns to the size of their contents"));
menu->popup(QCursor::pos());
}
void AddNewTorrentDialog::accept()
{
// TODO: Check if destination actually exists
m_torrentParams.skipChecking = m_ui->skipCheckingCheckBox->isChecked();
// Category
m_torrentParams.category = m_ui->categoryComboBox->currentText();
if (m_ui->defaultCategoryCheckbox->isChecked())
m_storeDefaultCategory = m_torrentParams.category;
m_storeRememberLastSavePath = m_ui->checkBoxRememberLastSavePath->isChecked();
// Save file priorities
if (m_contentModel)
m_torrentParams.filePriorities = m_contentModel->model()->getFilePriorities();
m_torrentParams.addPaused = !m_ui->startTorrentCheckBox->isChecked();
m_torrentParams.contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_torrentParams.sequential = m_ui->sequentialCheckBox->isChecked();
m_torrentParams.firstLastPiecePriority = m_ui->firstLastCheckBox->isChecked();
const bool useAutoTMM = (m_ui->comboTTM->currentIndex() == 1); // 1 is Automatic mode. Handle all non 1 values as manual mode.
m_torrentParams.useAutoTMM = useAutoTMM;
if (!useAutoTMM)
{
const Path savePath = m_ui->savePath->selectedPath();
m_torrentParams.savePath = savePath;
updatePathHistory(KEY_SAVEPATHHISTORY, savePath, savePathHistoryLength());
m_torrentParams.useDownloadPath = m_ui->groupBoxDownloadPath->isChecked();
if (m_torrentParams.useDownloadPath)
{
const Path downloadPath = m_ui->downloadPath->selectedPath();
m_torrentParams.downloadPath = downloadPath;
updatePathHistory(KEY_DOWNLOADPATHHISTORY, downloadPath, savePathHistoryLength());
}
}
setEnabled(!m_ui->checkBoxNeverShow->isChecked());
// Add torrent
if (!hasMetadata())
BitTorrent::Session::instance()->addTorrent(m_magnetURI, m_torrentParams);
else
BitTorrent::Session::instance()->addTorrent(m_torrentInfo, m_torrentParams);
m_torrentGuard->markAsAddedToSession();
QDialog::accept();
}
void AddNewTorrentDialog::reject()
{
if (!hasMetadata())
{
setMetadataProgressIndicator(false);
BitTorrent::Session::instance()->cancelDownloadMetadata(m_magnetURI.infoHash().toTorrentID());
}
QDialog::reject();
}
void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata)
{
Q_ASSERT(metadata.isValid());
if (metadata.infoHash() != m_magnetURI.infoHash()) return;
disconnect(BitTorrent::Session::instance(), &BitTorrent::Session::metadataDownloaded, this, &AddNewTorrentDialog::updateMetadata);
// Good to go
m_torrentInfo = metadata;
setMetadataProgressIndicator(true, tr("Parsing metadata..."));
// Update UI
setupTreeview();
setMetadataProgressIndicator(false, tr("Metadata retrieval complete"));
m_ui->buttonSave->setVisible(true);
if (m_torrentInfo.infoHash().v2().isValid())
{
m_ui->buttonSave->setEnabled(false);
m_ui->buttonSave->setToolTip(tr("Cannot create v2 torrent until its data is fully downloaded."));
}
}
void AddNewTorrentDialog::setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText)
{
// Always show info label when waiting for metadata
m_ui->lblMetaLoading->setVisible(true);
m_ui->lblMetaLoading->setText(labelText);
m_ui->progMetaLoading->setVisible(visibleIndicator);
}
void AddNewTorrentDialog::setupTreeview()
{
if (!hasMetadata())
{
m_ui->labelCommentData->setText(tr("Not Available", "This comment is unavailable"));
m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable"));
// Prevent crash if something is typed in the filter. m_contentModel is not initialized at this point
m_filterLine->blockSignals(true);
}
else
{
// Set dialog title
setWindowTitle(m_torrentInfo.name());
// Set torrent information
m_ui->labelCommentData->setText(Utils::Misc::parseHtmlLinks(m_torrentInfo.comment().toHtmlEscaped()));
m_ui->labelDateData->setText(!m_torrentInfo.creationDate().isNull() ? QLocale().toString(m_torrentInfo.creationDate(), QLocale::ShortFormat) : tr("Not available"));
// Prepare content tree
m_contentModel = new TorrentContentFilterModel(this);
connect(m_contentModel->model(), &TorrentContentModel::filteredFilesChanged, this, &AddNewTorrentDialog::updateDiskSpaceLabel);
m_ui->contentTreeView->setModel(m_contentModel);
m_contentDelegate = new PropListDelegate(nullptr);
m_ui->contentTreeView->setItemDelegate(m_contentDelegate);
connect(m_ui->contentTreeView, &QAbstractItemView::clicked, m_ui->contentTreeView
, qOverload<const QModelIndex &>(&QAbstractItemView::edit));
connect(m_ui->contentTreeView, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::displayContentTreeMenu);
connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_contentModel, &TorrentContentFilterModel::selectAll);
connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_contentModel, &TorrentContentFilterModel::selectNone);
if (m_torrentParams.filePaths.isEmpty())
m_torrentParams.filePaths = m_torrentInfo.filePaths();
m_originalRootFolder = Path::findRootFolder(m_torrentInfo.filePaths());
m_currentContentLayout = (m_originalRootFolder.isEmpty()
? BitTorrent::TorrentContentLayout::NoSubfolder
: BitTorrent::TorrentContentLayout::Subfolder);
applyContentLayout();
// List files in torrent
m_contentModel->model()->setupModelData(FileStorageAdaptor(m_torrentInfo, m_torrentParams.filePaths));
if (const QByteArray state = m_storeTreeHeaderState; !state.isEmpty())
m_ui->contentTreeView->header()->restoreState(state);
m_filterLine->blockSignals(false);
// Hide useless columns after loading the header state
m_ui->contentTreeView->hideColumn(PROGRESS);
m_ui->contentTreeView->hideColumn(REMAINING);
m_ui->contentTreeView->hideColumn(AVAILABILITY);
// Expand single-item folders recursively
QModelIndex currentIndex;
while (m_contentModel->rowCount(currentIndex) == 1)
{
currentIndex = m_contentModel->index(0, 0, currentIndex);
m_ui->contentTreeView->setExpanded(currentIndex, true);
}
// Check file name blacklist for torrents that are manually added
QVector<BitTorrent::DownloadPriority> priorities = m_contentModel->model()->getFilePriorities();
Q_ASSERT(priorities.size() == m_torrentInfo.filesCount());
for (int i = 0; i < priorities.size(); ++i)
{
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
continue;
if (BitTorrent::Session::instance()->isFilenameExcluded(m_torrentInfo.filePath(i).filename()))
priorities[i] = BitTorrent::DownloadPriority::Ignored;
}
m_contentModel->model()->updateFilesPriorities(priorities);
}
updateDiskSpaceLabel();
}
void AddNewTorrentDialog::handleDownloadFinished(const Net::DownloadResult &downloadResult)
{
switch (downloadResult.status)
{
case Net::DownloadStatus::Success:
{
const nonstd::expected<BitTorrent::TorrentInfo, QString> result = BitTorrent::TorrentInfo::load(downloadResult.data);
if (!result)
{
RaisedMessageBox::critical(this, tr("Invalid torrent"), tr("Failed to load from URL: %1.\nError: %2")
.arg(downloadResult.url, result.error()));
return;
}
m_torrentInfo = result.value();
m_torrentGuard = std::make_unique<TorrentFileGuard>();
if (loadTorrentImpl())
open();
else
deleteLater();
}
break;
case Net::DownloadStatus::RedirectedToMagnet:
if (loadMagnet(BitTorrent::MagnetUri(downloadResult.magnet)))
open();
else
deleteLater();
break;
default:
RaisedMessageBox::critical(this, tr("Download Error"),
tr("Cannot download '%1': %2").arg(downloadResult.url, downloadResult.errorString));
deleteLater();
}
}
void AddNewTorrentDialog::TMMChanged(int index)
{
if (index != 1)
{ // 0 is Manual mode and 1 is Automatic mode. Handle all non 1 values as manual mode.
populateSavePaths();
m_ui->groupBoxSavePath->setEnabled(true);
}
else
{
const auto *session = BitTorrent::Session::instance();
m_ui->groupBoxSavePath->setEnabled(false);
m_ui->savePath->blockSignals(true);
m_ui->savePath->clear();
const Path savePath = session->categorySavePath(m_ui->categoryComboBox->currentText());
m_ui->savePath->addItem(savePath);
m_ui->downloadPath->blockSignals(true);
m_ui->downloadPath->clear();
const Path downloadPath = session->categoryDownloadPath(m_ui->categoryComboBox->currentText());
m_ui->downloadPath->addItem(downloadPath);
m_ui->groupBoxDownloadPath->blockSignals(true);
m_ui->groupBoxDownloadPath->setChecked(!downloadPath.isEmpty());
}
updateDiskSpaceLabel();
}
void AddNewTorrentDialog::doNotDeleteTorrentClicked(bool checked)
{
m_torrentGuard->setAutoRemove(!checked);
}
void AddNewTorrentDialog::renameSelectedFile()
{
if (hasMetadata())
{
FileStorageAdaptor fileStorageAdaptor {m_torrentInfo, m_torrentParams.filePaths};
m_ui->contentTreeView->renameSelectedFile(fileStorageAdaptor);
}
}
void AddNewTorrentDialog::handleFilterTextChanged(const QString &filter)
{
const QString pattern = Utils::String::wildcardToRegexPattern(filter);
m_contentModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
if (filter.isEmpty())
{
m_ui->contentTreeView->collapseAll();
m_ui->contentTreeView->expand(m_contentModel->index(0, 0));
}
else
{
m_ui->contentTreeView->expandAll();
}
}