Backport changes to v5.1.x branch

PR #22988.
This commit is contained in:
Vladimir Golovnev
2025-11-04 16:25:31 +03:00
committed by GitHub
24 changed files with 185 additions and 86 deletions

View File

@@ -191,6 +191,11 @@ jobs:
name: qBittorrent-CI_Windows-x64_libtorrent-${{ matrix.libt_version }} name: qBittorrent-CI_Windows-x64_libtorrent-${{ matrix.libt_version }}
path: upload path: upload
- name: Install NSIS
uses: repolevedavaj/install-nsis@265e893c16602d8ccfb0a9ca44173b084078917c # v1.0.3
with:
nsis-version: '3.11'
- name: Create installer - name: Create installer
run: | run: |
7z x -o"dist/windows/" "dist/windows/NSISPlugins.zip" 7z x -o"dist/windows/" "dist/windows/NSISPlugins.zip"

View File

@@ -86,6 +86,19 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
PROPERTIES PROPERTIES
MACOSX_PACKAGE_LOCATION Resources MACOSX_PACKAGE_LOCATION Resources
) )
# Generate lproj folders for the translations
foreach(TS_FILE IN LISTS QBT_TS_FILES)
string(FIND "${TS_FILE}" "_" POS)
math(EXPR START "${POS} + 1")
string(SUBSTRING "${TS_FILE}" ${START} -1 LPROJ_FOLDER)
string(REPLACE ".ts" ".lproj" LPROJ_FOLDER "${LPROJ_FOLDER}")
# @ is not valid as a language code for a lproj folder on MacOS
string(REPLACE "@" "-" LPROJ_FOLDER "${LPROJ_FOLDER}")
add_custom_command(TARGET qbt_app POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory
"$<TARGET_FILE_DIR:qbt_app>/../Resources/${LPROJ_FOLDER}"
)
endforeach()
# provide variables for substitution in dist/mac/Info.plist # provide variables for substitution in dist/mac/Info.plist
get_target_property(EXECUTABLE_NAME qbt_app OUTPUT_NAME) get_target_property(EXECUTABLE_NAME qbt_app OUTPUT_NAME)
# This variable name should be changed once qmake is no longer used. Refer to the discussion in PR #14813 # This variable name should be changed once qmake is no longer used. Refer to the discussion in PR #14813

View File

@@ -147,12 +147,14 @@ namespace
QString value(const QString &arg) const QString value(const QString &arg) const
{ {
QStringList parts = arg.split(u'='); const qsizetype index = arg.indexOf(u'=');
if (parts.size() == 2) if (index == -1)
return Utils::String::unquote(parts[1], u"'\""_s);
throw CommandLineParameterError(QCoreApplication::translate("CMD Options", "Parameter '%1' must follow syntax '%1=%2'", throw CommandLineParameterError(QCoreApplication::translate("CMD Options", "Parameter '%1' must follow syntax '%1=%2'",
"e.g. Parameter '--webui-port' must follow syntax '--webui-port=value'") "e.g. Parameter '--webui-port' must follow syntax '--webui-port=value'")
.arg(fullParameter(), u"<value>"_s)); .arg(fullParameter(), u"<value>"_s));
const QStringView val = QStringView(arg).sliced(index + 1);
return Utils::String::unquote(val, u"'\""_s).toString();
} }
QString value(const QProcessEnvironment &env, const QString &defaultValue = {}) const QString value(const QProcessEnvironment &env, const QString &defaultValue = {}) const
@@ -168,7 +170,7 @@ namespace
friend bool operator==(const StringOption &option, const QString &arg) friend bool operator==(const StringOption &option, const QString &arg)
{ {
return arg.startsWith(option.parameterAssignment()); return (arg == option.fullParameter()) || arg.startsWith(option.parameterAssignment());
} }
private: private:

View File

@@ -3151,7 +3151,7 @@ void SessionImpl::exportTorrentFile(const Torrent *torrent, const Path &folderPa
while (newTorrentPath.exists()) while (newTorrentPath.exists())
{ {
// Append number to torrent name to make it unique // Append number to torrent name to make it unique
torrentExportFilename = u"%1 %2.torrent"_s.arg(validName).arg(++counter); torrentExportFilename = u"%1 (%2).torrent"_s.arg(validName).arg(++counter);
newTorrentPath = folderPath / Path(torrentExportFilename); newTorrentPath = folderPath / Path(torrentExportFilename);
} }
@@ -5651,6 +5651,25 @@ void SessionImpl::fetchPendingAlerts(const lt::time_duration time)
m_nativeSession->pop_alerts(&m_alerts); m_nativeSession->pop_alerts(&m_alerts);
} }
void SessionImpl::endAlertSequence(const int alertType, const qsizetype alertCount)
{
qDebug() << "End alert sequence. Alert:" << lt::alert_name(alertType) << "Count:" << alertCount;
if (alertType == lt::add_torrent_alert::alert_type)
{
emit addTorrentAlertsReceived(alertCount);
if (!m_loadedTorrents.isEmpty())
{
if (isRestored())
m_torrentsQueueChanged = true;
emit torrentsLoaded(m_loadedTorrents);
m_loadedTorrents.clear();
}
}
}
TorrentContentLayout SessionImpl::torrentContentLayout() const TorrentContentLayout SessionImpl::torrentContentLayout() const
{ {
return m_torrentContentLayout; return m_torrentContentLayout;
@@ -5667,28 +5686,26 @@ void SessionImpl::readAlerts()
fetchPendingAlerts(); fetchPendingAlerts();
Q_ASSERT(m_loadedTorrents.isEmpty()); Q_ASSERT(m_loadedTorrents.isEmpty());
Q_ASSERT(m_receivedAddTorrentAlertsCount == 0);
if (!isRestored()) if (!isRestored())
m_loadedTorrents.reserve(MAX_PROCESSING_RESUMEDATA_COUNT); m_loadedTorrents.reserve(MAX_PROCESSING_RESUMEDATA_COUNT);
int previousAlertType = -1;
qsizetype alertSequenceSize = 0;
for (const lt::alert *a : m_alerts) for (const lt::alert *a : m_alerts)
{
const int alertType = a->type();
if ((alertType != previousAlertType) && (previousAlertType != -1))
{
endAlertSequence(previousAlertType, alertSequenceSize);
alertSequenceSize = 0;
}
handleAlert(a); handleAlert(a);
++alertSequenceSize;
if (m_receivedAddTorrentAlertsCount > 0) previousAlertType = alertType;
{
emit addTorrentAlertsReceived(m_receivedAddTorrentAlertsCount);
m_receivedAddTorrentAlertsCount = 0;
if (!m_loadedTorrents.isEmpty())
{
if (isRestored())
m_torrentsQueueChanged = true;
emit torrentsLoaded(m_loadedTorrents);
m_loadedTorrents.clear();
}
} }
endAlertSequence(previousAlertType, alertSequenceSize);
// Some torrents may become "finished" after different alerts handling. // Some torrents may become "finished" after different alerts handling.
processPendingFinishedTorrents(); processPendingFinishedTorrents();
@@ -5696,8 +5713,6 @@ void SessionImpl::readAlerts()
void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert) void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
{ {
++m_receivedAddTorrentAlertsCount;
if (alert->error) if (alert->error)
{ {
const QString msg = QString::fromStdString(alert->message()); const QString msg = QString::fromStdString(alert->message());

View File

@@ -607,6 +607,7 @@ namespace BitTorrent
void populateAdditionalTrackersFromURL(); void populateAdditionalTrackersFromURL();
void fetchPendingAlerts(lt::time_duration time = lt::time_duration::zero()); void fetchPendingAlerts(lt::time_duration time = lt::time_duration::zero());
void endAlertSequence(int alertType, qsizetype alertCount);
void moveTorrentStorage(const MoveStorageJob &job) const; void moveTorrentStorage(const MoveStorageJob &job) const;
void handleMoveTorrentStorageJobFinished(const Path &newPath); void handleMoveTorrentStorageJobFinished(const Path &newPath);

View File

@@ -60,7 +60,7 @@ namespace Http
inline const QString HEADER_X_CONTENT_TYPE_OPTIONS = u"x-content-type-options"_s; inline const QString HEADER_X_CONTENT_TYPE_OPTIONS = u"x-content-type-options"_s;
inline const QString HEADER_X_FORWARDED_FOR = u"x-forwarded-for"_s; inline const QString HEADER_X_FORWARDED_FOR = u"x-forwarded-for"_s;
inline const QString HEADER_X_FORWARDED_HOST = u"x-forwarded-host"_s; inline const QString HEADER_X_FORWARDED_HOST = u"x-forwarded-host"_s;
inline const QString HEADER_X_FORWARDED_PROTO = u"X-forwarded-proto"_s; inline const QString HEADER_X_FORWARDED_PROTO = u"x-forwarded-proto"_s;
inline const QString HEADER_X_FRAME_OPTIONS = u"x-frame-options"_s; inline const QString HEADER_X_FRAME_OPTIONS = u"x-frame-options"_s;
inline const QString HEADER_X_XSS_PROTECTION = u"x-xss-protection"_s; inline const QString HEADER_X_XSS_PROTECTION = u"x-xss-protection"_s;

View File

@@ -87,7 +87,7 @@ namespace
QPointer<SearchPluginManager> SearchPluginManager::m_instance = nullptr; QPointer<SearchPluginManager> SearchPluginManager::m_instance = nullptr;
SearchPluginManager::SearchPluginManager() SearchPluginManager::SearchPluginManager()
: m_updateUrl(u"https://searchplugins.qbittorrent.org/nova3/engines/"_s) : m_updateUrl(u"https://raw.githubusercontent.com/qbittorrent/search-plugins/refs/heads/master/nova3/engines/"_s)
{ {
Q_ASSERT(!m_instance); // only one instance is allowed Q_ASSERT(!m_instance); // only one instance is allowed
m_instance = this; m_instance = this;

View File

@@ -104,7 +104,7 @@ bool Utils::Fs::smartRemoveEmptyFolderTree(const Path &path)
if (!dir.isEmpty(QDir::Dirs | QDir::NoDotAndDotDot)) if (!dir.isEmpty(QDir::Dirs | QDir::NoDotAndDotDot))
continue; continue;
const QStringList tmpFileList = dir.entryList(QDir::Files); const QStringList tmpFileList = dir.entryList(QDir::Files | QDir::Hidden);
// deleteFilesList contains unwanted files, usually created by the OS // deleteFilesList contains unwanted files, usually created by the OS
// temp files on linux usually end with '~', e.g. `filename~` // temp files on linux usually end with '~', e.g. `filename~`

View File

@@ -45,7 +45,7 @@ namespace
RandomLayer() RandomLayer()
{ {
if (::getrandom(nullptr, 0, 0) < 0) if (unsigned char buf = 0; ::getrandom(&buf, sizeof(buf), 0) < 0)
{ {
if (errno == ENOSYS) if (errno == ENOSYS)
{ {

View File

@@ -508,7 +508,7 @@ void AddNewTorrentDialog::setCurrentContext(const std::shared_ptr<Context> conte
{ {
m_ui->lblMetaLoading->setVisible(false); m_ui->lblMetaLoading->setVisible(false);
m_ui->progMetaLoading->setVisible(false); m_ui->progMetaLoading->setVisible(false);
m_ui->buttonSave->setVisible(false); m_ui->buttonSave->setVisible(true);
setupTreeview(); setupTreeview();
} }
else else
@@ -520,6 +520,7 @@ void AddNewTorrentDialog::setCurrentContext(const std::shared_ptr<Context> conte
m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable")); m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable"));
updateDiskSpaceLabel(); updateDiskSpaceLabel();
setMetadataProgressIndicator(true, tr("Retrieving metadata...")); setMetadataProgressIndicator(true, tr("Retrieving metadata..."));
m_ui->buttonSave->setVisible(false);
} }
TMMChanged(m_ui->comboTMM->currentIndex()); TMMChanged(m_ui->comboTMM->currentIndex());

View File

@@ -38,19 +38,28 @@
HidableTabWidget::HidableTabWidget(QWidget *parent) HidableTabWidget::HidableTabWidget(QWidget *parent)
: QTabWidget(parent) : QTabWidget(parent)
{ {
// Skip single tab in keyboard navigation (no point navigating to it)
tabBar()->setFocusPolicy(Qt::NoFocus);
} }
void HidableTabWidget::tabInserted(const int index) void HidableTabWidget::tabInserted(const int index)
{ {
QTabWidget::tabInserted(index); QTabWidget::tabInserted(index);
tabBar()->setVisible(count() != 1); tabsCountChanged();
} }
void HidableTabWidget::tabRemoved(const int index) void HidableTabWidget::tabRemoved(const int index)
{ {
//QTabWidget::tabInserted(index);
QTabWidget::tabRemoved(index); QTabWidget::tabRemoved(index);
tabBar()->setVisible(count() != 1); tabsCountChanged();
}
void HidableTabWidget::tabsCountChanged()
{
const qsizetype tabsCount = count();
tabBar()->setVisible(tabsCount != 1);
// Skip single tab in keyboard navigation (no point navigating to it)
tabBar()->setFocusPolicy((tabsCount > 1) ? Qt::StrongFocus : Qt::NoFocus);
} }
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS

View File

@@ -42,6 +42,7 @@ public:
private: private:
void tabInserted(int index) override; void tabInserted(int index) override;
void tabRemoved(int index) override; void tabRemoved(int index) override;
void tabsCountChanged();
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
void paintEvent(QPaintEvent *event) override; void paintEvent(QPaintEvent *event) override;

View File

@@ -1660,7 +1660,7 @@ void MainWindow::handleUpdateCheckFinished(ProgramUpdater *updater, const bool i
updater->deleteLater(); updater->deleteLater();
}; };
const auto newVersion = updater->getNewVersion(); const ProgramUpdater::Version newVersion = updater->getNewVersion();
if (newVersion.isValid()) if (newVersion.isValid())
{ {
const QString msg {tr("A new version is available.") + u"<br/>" const QString msg {tr("A new version is available.") + u"<br/>"

View File

@@ -29,6 +29,8 @@
#include "programupdater.h" #include "programupdater.h"
#include <algorithm>
#include <libtorrent/version.hpp> #include <libtorrent/version.hpp>
#include <QtCore/qconfig.h> #include <QtCore/qconfig.h>
@@ -44,7 +46,6 @@
#include "base/logger.h" #include "base/logger.h"
#include "base/net/downloadmanager.h" #include "base/net/downloadmanager.h"
#include "base/preferences.h" #include "base/preferences.h"
#include "base/utils/version.h"
#include "base/version.h" #include "base/version.h"
namespace namespace
@@ -80,32 +81,50 @@ namespace
} }
} }
void ProgramUpdater::checkForUpdates() const void ProgramUpdater::checkForUpdates()
{ {
const auto USER_AGENT = QStringLiteral("qBittorrent/" QBT_VERSION_2 " ProgramUpdater (www.qbittorrent.org)");
const auto RSS_URL = u"https://www.fosshub.com/feed/5b8793a7f9ee5a5c3e97a3b2.xml"_s;
const auto FALLBACK_URL = u"https://www.qbittorrent.org/versions.json"_s;
// Don't change this User-Agent. In case our updater goes haywire, // Don't change this User-Agent. In case our updater goes haywire,
// the filehost can identify it and contact us. // the filehost can identify it and contact us.
Net::DownloadManager::instance()->download(Net::DownloadRequest(RSS_URL).userAgent(USER_AGENT) const auto USER_AGENT = QStringLiteral("qBittorrent/" QBT_VERSION_2 " ProgramUpdater (www.qbittorrent.org)");
, Preferences::instance()->useProxyForGeneralPurposes(), this, &ProgramUpdater::rssDownloadFinished); const auto FOSSHUB_URL = u"https://www.fosshub.com/feed/5b8793a7f9ee5a5c3e97a3b2.xml"_s;
Net::DownloadManager::instance()->download(Net::DownloadRequest(FALLBACK_URL).userAgent(USER_AGENT) const auto QBT_MAIN_URL = u"https://www.qbittorrent.org/versions.json"_s;
, Preferences::instance()->useProxyForGeneralPurposes(), this, &ProgramUpdater::fallbackDownloadFinished); const auto QBT_BACKUP_URL = u"https://qbittorrent.github.io/qBittorrent-website/versions.json"_s;
m_hasCompletedOneReq = false; Net::DownloadManager *netManager = Net::DownloadManager::instance();
const bool useProxy = Preferences::instance()->useProxyForGeneralPurposes();
m_pendingRequestCount = 3;
netManager->download(Net::DownloadRequest(FOSSHUB_URL).userAgent(USER_AGENT), useProxy, this, &ProgramUpdater::rssDownloadFinished);
// don't use the custom user agent for the following requests, disguise as a normal browser instead
netManager->download(Net::DownloadRequest(QBT_MAIN_URL), useProxy, this, [this](const Net::DownloadResult &result)
{
fallbackDownloadFinished(result, m_qbtMainVersion);
});
netManager->download(Net::DownloadRequest(QBT_BACKUP_URL), useProxy, this, [this](const Net::DownloadResult &result)
{
fallbackDownloadFinished(result, m_qbtBackupVersion);
});
} }
ProgramUpdater::Version ProgramUpdater::getNewVersion() const ProgramUpdater::Version ProgramUpdater::getNewVersion() const
{ {
return shouldUseFallback() ? m_fallbackRemoteVersion : m_remoteVersion; switch (getLatestRemoteSource())
{
case RemoteSource::Fosshub:
return m_fosshubVersion;
case RemoteSource::QbtMain:
return m_qbtMainVersion;
case RemoteSource::QbtBackup:
return m_qbtBackupVersion;
}
Q_UNREACHABLE();
} }
void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result) void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
{ {
if (result.status != Net::DownloadStatus::Success) if (result.status != Net::DownloadStatus::Success)
{ {
LogMsg(tr("Failed to download the update info. URL: %1. Error: %2").arg(result.url, result.errorString) , Log::WARNING); LogMsg(tr("Failed to download the program update info. URL: \"%1\". Error: \"%2\"").arg(result.url, result.errorString) , Log::WARNING);
handleFinishedRequest(); handleFinishedRequest();
return; return;
} }
@@ -150,10 +169,10 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
if (!version.isEmpty()) if (!version.isEmpty())
{ {
qDebug("Detected version is %s", qUtf8Printable(version)); qDebug("Detected version is %s", qUtf8Printable(version));
const ProgramUpdater::Version tmpVer {version}; const Version tmpVer {version};
if (isVersionMoreRecent(tmpVer)) if (isVersionMoreRecent(tmpVer))
{ {
m_remoteVersion = tmpVer; m_fosshubVersion = tmpVer;
m_updateURL = updateLink; m_updateURL = updateLink;
} }
} }
@@ -171,11 +190,13 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
handleFinishedRequest(); handleFinishedRequest();
} }
void ProgramUpdater::fallbackDownloadFinished(const Net::DownloadResult &result) void ProgramUpdater::fallbackDownloadFinished(const Net::DownloadResult &result, Version &version)
{ {
version = {};
if (result.status != Net::DownloadStatus::Success) if (result.status != Net::DownloadStatus::Success)
{ {
LogMsg(tr("Failed to download the update info. URL: %1. Error: %2").arg(result.url, result.errorString) , Log::WARNING); LogMsg(tr("Failed to download the program update info. URL: \"%1\". Error: \"%2\"").arg(result.url, result.errorString) , Log::WARNING);
handleFinishedRequest(); handleFinishedRequest();
return; return;
} }
@@ -190,9 +211,9 @@ void ProgramUpdater::fallbackDownloadFinished(const Net::DownloadResult &result)
if (const QJsonValue verJSON = json[platformKey][u"version"_s]; verJSON.isString()) if (const QJsonValue verJSON = json[platformKey][u"version"_s]; verJSON.isString())
{ {
const ProgramUpdater::Version tmpVer {verJSON.toString()}; const Version tmpVer {verJSON.toString()};
if (isVersionMoreRecent(tmpVer)) if (isVersionMoreRecent(tmpVer))
m_fallbackRemoteVersion = tmpVer; version = tmpVer;
} }
handleFinishedRequest(); handleFinishedRequest();
@@ -200,18 +221,33 @@ void ProgramUpdater::fallbackDownloadFinished(const Net::DownloadResult &result)
bool ProgramUpdater::updateProgram() const bool ProgramUpdater::updateProgram() const
{ {
return QDesktopServices::openUrl(shouldUseFallback() ? u"https://www.qbittorrent.org/download"_s : m_updateURL); switch (getLatestRemoteSource())
{
case RemoteSource::Fosshub:
return QDesktopServices::openUrl(m_updateURL);
case RemoteSource::QbtMain:
return QDesktopServices::openUrl(u"https://www.qbittorrent.org/download"_s);
case RemoteSource::QbtBackup:
return QDesktopServices::openUrl(u"https://qbittorrent.github.io/qBittorrent-website/download"_s);
}
Q_UNREACHABLE();
} }
void ProgramUpdater::handleFinishedRequest() void ProgramUpdater::handleFinishedRequest()
{ {
if (m_hasCompletedOneReq) --m_pendingRequestCount;
if (m_pendingRequestCount == 0)
emit updateCheckFinished(); emit updateCheckFinished();
else
m_hasCompletedOneReq = true;
} }
bool ProgramUpdater::shouldUseFallback() const ProgramUpdater::RemoteSource ProgramUpdater::getLatestRemoteSource() const
{ {
return m_fallbackRemoteVersion > m_remoteVersion; const Version max = std::max({m_fosshubVersion, m_qbtMainVersion, m_qbtBackupVersion});
if (max == m_fosshubVersion)
return RemoteSource::Fosshub;
if (max == m_qbtMainVersion)
return RemoteSource::QbtMain;
if (max == m_qbtBackupVersion)
return RemoteSource::QbtBackup;
Q_UNREACHABLE();
} }

View File

@@ -45,10 +45,11 @@ class ProgramUpdater final : public QObject
Q_DISABLE_COPY_MOVE(ProgramUpdater) Q_DISABLE_COPY_MOVE(ProgramUpdater)
public: public:
using QObject::QObject;
using Version = Utils::Version<4, 3>; using Version = Utils::Version<4, 3>;
void checkForUpdates() const; using QObject::QObject;
void checkForUpdates();
Version getNewVersion() const; Version getNewVersion() const;
bool updateProgram() const; bool updateProgram() const;
@@ -57,14 +58,22 @@ signals:
private slots: private slots:
void rssDownloadFinished(const Net::DownloadResult &result); void rssDownloadFinished(const Net::DownloadResult &result);
void fallbackDownloadFinished(const Net::DownloadResult &result); void fallbackDownloadFinished(const Net::DownloadResult &result, Version &version);
private: private:
void handleFinishedRequest(); enum class RemoteSource
bool shouldUseFallback() const; {
Fosshub,
QbtMain,
QbtBackup
};
mutable bool m_hasCompletedOneReq = false; void handleFinishedRequest();
Version m_remoteVersion; RemoteSource getLatestRemoteSource() const;
Version m_fallbackRemoteVersion;
int m_pendingRequestCount = 0;
Version m_fosshubVersion;
Version m_qbtMainVersion;
Version m_qbtBackupVersion;
QUrl m_updateURL; QUrl m_updateURL;
}; };

View File

@@ -819,21 +819,21 @@ void TransferListWidget::exportTorrent()
bool hasError = false; bool hasError = false;
for (const BitTorrent::Torrent *torrent : torrents) for (const BitTorrent::Torrent *torrent : torrents)
{ {
const QString validName = Utils::Fs::toValidFileName(torrent->name(), u"_"_s); const QString validName = Utils::Fs::toValidFileName(torrent->name());
const Path filePath = savePath / Path(validName + u".torrent"); QString torrentExportFilename = u"%1.torrent"_s.arg(validName);
if (filePath.exists()) Path newTorrentPath = savePath / Path(torrentExportFilename);
int counter = 0;
while (newTorrentPath.exists())
{ {
LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), tr("A file with the same name already exists")) , Log::WARNING); // Append number to torrent name to make it unique
hasError = true; torrentExportFilename = u"%1 (%2).torrent"_s.arg(validName).arg(++counter);
continue; newTorrentPath = savePath / Path(torrentExportFilename);
} }
const nonstd::expected<void, QString> result = torrent->exportToFile(filePath); if (const nonstd::expected<void, QString> result = torrent->exportToFile(newTorrentPath); !result)
if (!result)
{ {
LogMsg(errorMsg.arg(torrent->name(), filePath.toString(), result.error()) , Log::WARNING); LogMsg(errorMsg.arg(torrent->name(), newTorrentPath.toString(), result.error()) , Log::WARNING);
hasError = true; hasError = true;
continue;
} }
} }
@@ -1285,9 +1285,16 @@ void TransferListWidget::displayListMenu()
listMenu->popup(QCursor::pos()); listMenu->popup(QCursor::pos());
} }
void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex&) void TransferListWidget::currentChanged(const QModelIndex &current, const QModelIndex &previous)
{ {
qDebug("CURRENT CHANGED"); qDebug("CURRENT CHANGED");
// Call base class to ensure Qt's accessibility system is notified of focus changes.
// This is critical for screen readers to announce the currently selected torrent.
// Without this call, users relying on assistive technologies cannot effectively
// navigate the torrent list with keyboard arrow keys.
QTreeView::currentChanged(current, previous);
BitTorrent::Torrent *torrent = nullptr; BitTorrent::Torrent *torrent = nullptr;
if (current.isValid()) if (current.isValid())
{ {

View File

@@ -110,7 +110,7 @@ private slots:
void torrentDoubleClicked(); void torrentDoubleClicked();
void displayListMenu(); void displayListMenu();
void displayColumnHeaderMenu(); void displayColumnHeaderMenu();
void currentChanged(const QModelIndex &current, const QModelIndex&) override; void currentChanged(const QModelIndex &current, const QModelIndex &previous) override;
void setSelectedTorrentsSuperSeeding(bool enabled) const; void setSelectedTorrentsSuperSeeding(bool enabled) const;
void setSelectedTorrentsSequentialDownload(bool enabled) const; void setSelectedTorrentsSequentialDownload(bool enabled) const;
void setSelectedFirstLastPiecePrio(bool enabled) const; void setSelectedFirstLastPiecePrio(bool enabled) const;

View File

@@ -6,7 +6,7 @@
</head> </head>
<body> <body>
<p>I would like to thank the people who volunteered to translate qBittorrent.<br> <p>I would like to thank the people who volunteered to translate qBittorrent.<br>
Most of them translated via <a href="https://www.transifex.com/sledgehammer999/qbittorrent">Transifex</a> and some of them are mentioned below:<br> Most of them translated via <a href="https://explore.transifex.com/sledgehammer999/qbittorrent/">Transifex</a> and some of them are mentioned below:<br>
(the list might not be up to date) (the list might not be up to date)
</p> </p>
<ul> <ul>

View File

@@ -1210,7 +1210,7 @@ void AppController::getDirectoryContentAction()
QJsonArray ret; QJsonArray ret;
QDirIterator it {dirPath, (QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility))}; QDirIterator it {dirPath, (QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility))};
while (it.hasNext()) while (it.hasNext())
ret.append(it.next()); ret.append(Path(it.next()).toString());
setResult(ret); setResult(ret);
} }

View File

@@ -349,7 +349,7 @@ window.qBittorrent.DynamicTable ??= (() => {
} }
}, },
setupDynamicTableHeaderContextMenuClass: function() { setupDynamicTableHeaderContextMenuClass: () => {
DynamicTableHeaderContextMenuClass ??= class extends window.qBittorrent.ContextMenu.ContextMenu { DynamicTableHeaderContextMenuClass ??= class extends window.qBittorrent.ContextMenu.ContextMenu {
updateMenuItems() { updateMenuItems() {
for (let i = 0; i < this.dynamicTable.columns.length; ++i) { for (let i = 0; i < this.dynamicTable.columns.length; ++i) {
@@ -2316,7 +2316,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.updateGlobalCheckbox(); this.updateGlobalCheckbox();
}, },
_sortNodesByColumn: function(nodes, column) { _sortNodesByColumn: (nodes, column) => {
nodes.sort((row1, row2) => { nodes.sort((row1, row2) => {
// list folders before files when sorting by name // list folders before files when sorting by name
if (column.name === "original") { if (column.name === "original") {
@@ -2633,7 +2633,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.columns["availability"].updateTd = displayPercentage; this.columns["availability"].updateTd = displayPercentage;
}, },
_sortNodesByColumn: function(nodes, column) { _sortNodesByColumn: (nodes, column) => {
nodes.sort((row1, row2) => { nodes.sort((row1, row2) => {
// list folders before files when sorting by name // list folders before files when sorting by name
if (column.name === "name") { if (column.name === "name") {
@@ -3012,7 +3012,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.newColumn("checked", "", "", 30, true); this.newColumn("checked", "", "", 30, true);
this.newColumn("name", "", "", -1, true); this.newColumn("name", "", "", -1, true);
this.columns["checked"].updateTd = function(td, row) { this.columns["checked"].updateTd = (td, row) => {
if ($(`cbRssDlRule${row.rowId}`) === null) { if ($(`cbRssDlRule${row.rowId}`) === null) {
const checkbox = document.createElement("input"); const checkbox = document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";
@@ -3110,7 +3110,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.newColumn("checked", "", "", 30, true); this.newColumn("checked", "", "", 30, true);
this.newColumn("name", "", "", -1, true); this.newColumn("name", "", "", -1, true);
this.columns["checked"].updateTd = function(td, row) { this.columns["checked"].updateTd = (td, row) => {
if ($(`cbRssDlFeed${row.rowId}`) === null) { if ($(`cbRssDlFeed${row.rowId}`) === null) {
const checkbox = document.createElement("input"); const checkbox = document.createElement("input");
checkbox.type = "checkbox"; checkbox.type = "checkbox";

View File

@@ -103,7 +103,7 @@ window.qBittorrent.FileTree ??= (() => {
return nodes; return nodes;
}, },
_getArrayOfNodes: function(node, array) { _getArrayOfNodes: (node, array) => {
array.push(node); array.push(node);
node.children.each((child) => { node.children.each((child) => {
this._getArrayOfNodes(child, array); this._getArrayOfNodes(child, array);

View File

@@ -73,7 +73,7 @@ window.qBittorrent.pathAutofill ??= (() => {
.catch(error => {}); .catch(error => {});
}; };
function attachPathAutofill() { const attachPathAutofill = () => {
const directoryInputs = document.querySelectorAll(".pathDirectory:not(.pathAutoFillInitialized)"); const directoryInputs = document.querySelectorAll(".pathDirectory:not(.pathAutoFillInitialized)");
for (const input of directoryInputs) { for (const input of directoryInputs) {
input.addEventListener("input", function() { showPathSuggestions(this, "dirs"); }); input.addEventListener("input", function() { showPathSuggestions(this, "dirs"); });

View File

@@ -773,7 +773,7 @@ window.qBittorrent.Search ??= (() => {
$("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length; $("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
}; };
const loadSearchResultsData = function(searchId) { const loadSearchResultsData = (searchId) => {
const state = searchState.get(searchId); const state = searchState.get(searchId);
const url = new URL("api/v2/search/results", window.location); const url = new URL("api/v2/search/results", window.location);
url.search = new URLSearchParams({ url.search = new URLSearchParams({

View File

@@ -73,7 +73,7 @@
<div id="aboutTranslatorsContent" class="aboutTabContent invisible"> <div id="aboutTranslatorsContent" class="aboutTabContent invisible">
<p>I would like to thank the people who volunteered to translate qBittorrent.<br> <p>I would like to thank the people who volunteered to translate qBittorrent.<br>
Most of them translated via <a target="_blank" href="https://www.transifex.com/sledgehammer999/qbittorrent/">Transifex</a> and some of them are mentioned below:<br> Most of them translated via <a target="_blank" href="https://explore.transifex.com/sledgehammer999/qbittorrent/">Transifex</a> and some of them are mentioned below:<br>
(the list might not be up to date) (the list might not be up to date)
</p> </p>
<ul> <ul>