Merge branch 'master' into multi-file-export

This commit is contained in:
Faris Mektem 2025-05-26 11:59:43 +01:00 committed by GitHub
commit c50e0b886b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
108 changed files with 2842 additions and 2802 deletions

View file

@ -52,13 +52,13 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
pip install zizmor pip install zizmor
IGNORE_RULEID='(.ruleId != "template-injection") IGNORE_RULEID='(.ruleId != "zizmor/template-injection")
and (.ruleId != "unpinned-uses")' and (.ruleId != "zizmor/unpinned-uses")'
IGNORE_ID='(.id != "template-injection") IGNORE_ID='(.id != "zizmor/template-injection")
and (.id != "unpinned-uses")' and (.id != "zizmor/unpinned-uses")'
zizmor \ zizmor \
--format sarif \ --format sarif \
--pedantic \ --persona auditor \
./ \ ./ \
| jq "(.runs[].results |= map(select($IGNORE_RULEID))) | jq "(.runs[].results |= map(select($IGNORE_RULEID)))
| (.runs[].tool.driver.rules |= map(select($IGNORE_ID)))" \ | (.runs[].tool.driver.rules |= map(select($IGNORE_ID)))" \

View file

@ -21,7 +21,7 @@ jobs:
matrix: matrix:
libt_version: ["2.0.11", "1.2.20"] libt_version: ["2.0.11", "1.2.20"]
qbt_gui: ["GUI=ON", "GUI=OFF"] qbt_gui: ["GUI=ON", "GUI=OFF"]
qt_version: ["6.5.2"] qt_version: ["6.6.3"]
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"
@ -162,6 +162,7 @@ jobs:
- name: Package AppImage - name: Package AppImage
run: | run: |
rm -f "${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/sqldrivers/libqsqlmimer.so"
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt ./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
rm qbittorrent/apprun-hooks/* rm qbittorrent/apprun-hooks/*
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh

View file

@ -16,7 +16,7 @@ jobs:
matrix: matrix:
libt_version: ["2.0.11"] libt_version: ["2.0.11"]
qbt_gui: ["GUI=ON"] qbt_gui: ["GUI=ON"]
qt_version: ["6.5.2"] qt_version: ["6.6.3"]
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"

View file

@ -69,7 +69,7 @@ repos:
- ts - ts
- repo: https://github.com/codespell-project/codespell.git - repo: https://github.com/codespell-project/codespell.git
rev: v2.4.0 rev: v2.4.1
hooks: hooks:
- id: codespell - id: codespell
name: Check spelling (codespell) name: Check spelling (codespell)
@ -88,7 +88,7 @@ repos:
- ts - ts
- repo: https://github.com/crate-ci/typos.git - repo: https://github.com/crate-ci/typos.git
rev: v1.29.4 rev: v1.32.0
hooks: hooks:
- id: typos - id: typos
name: Check spelling (typos) name: Check spelling (typos)

View file

@ -8,7 +8,7 @@ project(qBittorrent
# version requirements - older versions may work, but you are on your own # version requirements - older versions may work, but you are on your own
set(minBoostVersion 1.76) set(minBoostVersion 1.76)
set(minQt6Version 6.5.0) set(minQt6Version 6.6.0)
set(minOpenSSLVersion 3.0.2) set(minOpenSSLVersion 3.0.2)
set(minLibtorrent1Version 1.2.19) set(minLibtorrent1Version 1.2.19)
set(minLibtorrentVersion 2.0.10) set(minLibtorrentVersion 2.0.10)

View file

@ -11,7 +11,7 @@ qBittorrent - A BitTorrent client in C++ / Qt
- OpenSSL >= 3.0.2 - OpenSSL >= 3.0.2
- Qt 6.5.0 - 6.x - Qt 6.6.0 - 6.x
- zlib >= 1.2.11 - zlib >= 1.2.11

View file

@ -20,7 +20,7 @@ target_compile_features(qbt_common_cfg INTERFACE
) )
target_compile_definitions(qbt_common_cfg INTERFACE target_compile_definitions(qbt_common_cfg INTERFACE
QT_DISABLE_DEPRECATED_UP_TO=0x060500 QT_DISABLE_DEPRECATED_UP_TO=0x060600
QT_NO_CAST_FROM_ASCII QT_NO_CAST_FROM_ASCII
QT_NO_CAST_TO_ASCII QT_NO_CAST_TO_ASCII
QT_NO_CAST_FROM_BYTEARRAY QT_NO_CAST_FROM_BYTEARRAY

View file

@ -410,7 +410,7 @@ void Application::setMemoryWorkingSetLimit(const int size)
return; return;
m_storeMemoryWorkingSetLimit = size; m_storeMemoryWorkingSetLimit = size;
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit(); applyMemoryWorkingSetLimit();
#endif #endif
} }
@ -674,9 +674,7 @@ void Application::runExternalProgram(const QString &programTemplate, const BitTo
QProcess proc; QProcess proc;
proc.setProgram(command); proc.setProgram(command);
proc.setArguments(args); proc.setArguments(args);
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
if (proc.startDetached()) if (proc.startDetached())
{ {
@ -846,7 +844,7 @@ int Application::exec()
printf("%s\n", qUtf8Printable(loadingStr)); printf("%s\n", qUtf8Printable(loadingStr));
#endif #endif
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
applyMemoryWorkingSetLimit(); applyMemoryWorkingSetLimit();
#endif #endif
@ -1204,7 +1202,7 @@ void Application::shutdownCleanup([[maybe_unused]] QSessionManager &manager)
} }
#endif #endif
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
void Application::applyMemoryWorkingSetLimit() const void Application::applyMemoryWorkingSetLimit() const
{ {
const size_t MiB = 1024 * 1024; const size_t MiB = 1024 * 1024;

View file

@ -491,6 +491,12 @@ QString makeUsage(const QString &prgName)
{ {
const QString indentation {USAGE_INDENTATION, u' '}; const QString indentation {USAGE_INDENTATION, u' '};
#if defined(Q_OS_WIN)
const QString noSplashCommand = u"set QBT_NO_SPLASH=1 && " + prgName;
#else
const QString noSplashCommand = u"QBT_NO_SPLASH=1 " + prgName;
#endif
const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n' const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n'
+ indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [(<filename> | <url>)...]") + u'\n' + indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [(<filename> | <url>)...]") + u'\n'
@ -542,7 +548,7 @@ QString makeUsage(const QString &prgName)
"'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper " "'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper "
"case, '-' replaced with '_'). To pass flag values, set the variable to '1' or " "case, '-' replaced with '_'). To pass flag values, set the variable to '1' or "
"'TRUE'. For example, to disable the splash screen: "), 0) + u'\n' "'TRUE'. For example, to disable the splash screen: "), 0) + u'\n'
+ u"QBT_NO_SPLASH=1 " + prgName + u'\n' + noSplashCommand + u'\n'
+ wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n'; + wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n';
return text; return text;

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2020-2025 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -27,13 +27,14 @@
*/ */
#include "filesearcher.h" #include "filesearcher.h"
#include "base/bittorrent/common.h"
#include "base/bittorrent/infohash.h"
void FileSearcher::search(const BitTorrent::TorrentID &id, const PathList &originalFileNames #include <QPromise>
, const Path &savePath, const Path &downloadPath, const bool forceAppendExt)
#include "base/bittorrent/common.h"
namespace
{ {
const auto findInDir = [](const Path &dirPath, PathList &fileNames, const bool forceAppendExt) -> bool bool findInDir(const Path &dirPath, PathList &fileNames, const bool forceAppendExt)
{ {
bool found = false; bool found = false;
for (Path &fileName : fileNames) for (Path &fileName : fileNames)
@ -58,8 +59,12 @@ void FileSearcher::search(const BitTorrent::TorrentID &id, const PathList &origi
} }
return found; return found;
}; }
}
void FileSearcher::search(const PathList &originalFileNames, const Path &savePath
, const Path &downloadPath, const bool forceAppendExt, QPromise<FileSearchResult> &promise)
{
Path usedPath = savePath; Path usedPath = savePath;
PathList adjustedFileNames = originalFileNames; PathList adjustedFileNames = originalFileNames;
const bool found = findInDir(usedPath, adjustedFileNames, (forceAppendExt && downloadPath.isEmpty())); const bool found = findInDir(usedPath, adjustedFileNames, (forceAppendExt && downloadPath.isEmpty()));
@ -69,5 +74,5 @@ void FileSearcher::search(const BitTorrent::TorrentID &id, const PathList &origi
findInDir(usedPath, adjustedFileNames, forceAppendExt); findInDir(usedPath, adjustedFileNames, forceAppendExt);
} }
emit searchFinished(id, usedPath, adjustedFileNames); promise.addResult(FileSearchResult {.savePath = usedPath, .fileNames = adjustedFileNames});
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2020 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2020-2025 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -32,10 +32,13 @@
#include "base/path.h" #include "base/path.h"
namespace BitTorrent template <typename T> class QPromise;
struct FileSearchResult
{ {
class TorrentID; Path savePath;
} PathList fileNames;
};
class FileSearcher final : public QObject class FileSearcher final : public QObject
{ {
@ -43,12 +46,8 @@ class FileSearcher final : public QObject
Q_DISABLE_COPY_MOVE(FileSearcher) Q_DISABLE_COPY_MOVE(FileSearcher)
public: public:
FileSearcher() = default; using QObject::QObject;
public slots: void search(const PathList &originalFileNames, const Path &savePath
void search(const BitTorrent::TorrentID &id, const PathList &originalFileNames , const Path &downloadPath, bool forceAppendExt, QPromise<FileSearchResult> &promise);
, const Path &savePath, const Path &downloadPath, bool forceAppendExt);
signals:
void searchFinished(const BitTorrent::TorrentID &id, const Path &savePath, const PathList &fileNames);
}; };

View file

@ -493,7 +493,6 @@ namespace BitTorrent
void categoryOptionsChanged(const QString &categoryName); void categoryOptionsChanged(const QString &categoryName);
void fullDiskError(Torrent *torrent, const QString &msg); void fullDiskError(Torrent *torrent, const QString &msg);
void IPFilterParsed(bool error, int ruleCount); void IPFilterParsed(bool error, int ruleCount);
void loadTorrentFailed(const QString &error);
void metadataDownloaded(const TorrentInfo &info); void metadataDownloaded(const TorrentInfo &info);
void restored(); void restored();
void paused(); void paused();

View file

@ -30,6 +30,7 @@
#include "sessionimpl.h" #include "sessionimpl.h"
#include <algorithm> #include <algorithm>
#include <concepts>
#include <cstdint> #include <cstdint>
#include <ctime> #include <ctime>
#include <queue> #include <queue>
@ -61,6 +62,7 @@
#include <QDeadlineTimer> #include <QDeadlineTimer>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QFuture>
#include <QHostAddress> #include <QHostAddress>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
@ -69,6 +71,7 @@
#include <QMutexLocker> #include <QMutexLocker>
#include <QNetworkAddressEntry> #include <QNetworkAddressEntry>
#include <QNetworkInterface> #include <QNetworkInterface>
#include <QPromise>
#include <QRegularExpression> #include <QRegularExpression>
#include <QString> #include <QString>
#include <QThread> #include <QThread>
@ -313,6 +316,44 @@ namespace
break; break;
} }
} }
#ifdef QBT_USES_LIBTORRENT2
template <typename T>
concept HasInfoHashMember = requires (T t) { { t.info_hashes } -> std::convertible_to<InfoHash>; };
template <typename T>
concept HasInfoHashMemberFn = requires (T t) { { t.info_hashes() } -> std::convertible_to<InfoHash>; };
template <HasInfoHashMember T>
InfoHash getInfoHash(const T &t) { return t.info_hashes; }
template <HasInfoHashMemberFn T>
InfoHash getInfoHash(const T &t) { return t.info_hashes(); }
InfoHash getInfoHash(const lt::add_torrent_params &addTorrentParams)
{
const bool hasMetadata = (addTorrentParams.ti && addTorrentParams.ti->is_valid());
return hasMetadata ? getInfoHash(*addTorrentParams.ti) : InfoHash(addTorrentParams.info_hashes);
}
#else
template <typename T>
concept HasInfoHashMember = requires (T t) { { t.info_hash } -> std::convertible_to<InfoHash>; };
template <typename T>
concept HasInfoHashMemberFn = requires (T t) { { t.info_hash() } -> std::convertible_to<InfoHash>; };
template <HasInfoHashMember T>
InfoHash getInfoHash(const T &t) { return t.info_hash; }
template <HasInfoHashMemberFn T>
InfoHash getInfoHash(const T &t) { return t.info_hash(); }
InfoHash getInfoHash(const lt::add_torrent_params &addTorrentParams)
{
const bool hasMetadata = (addTorrentParams.ti && addTorrentParams.ti->is_valid());
return hasMetadata ? getInfoHash(*addTorrentParams.ti) : InfoHash(addTorrentParams.info_hash);
}
#endif
} }
struct BitTorrent::SessionImpl::ResumeSessionContext final : public QObject struct BitTorrent::SessionImpl::ResumeSessionContext final : public QObject
@ -470,11 +511,11 @@ SessionImpl::SessionImpl(QObject *parent)
, m_additionalTrackers(BITTORRENT_SESSION_KEY(u"AdditionalTrackers"_s)) , m_additionalTrackers(BITTORRENT_SESSION_KEY(u"AdditionalTrackers"_s))
, m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false) , m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false)
, m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s)) , m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s))
, m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r;}) , m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; })
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s), Torrent::NO_SEEDING_TIME_LIMIT , m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s)
, clampValue(Torrent::NO_SEEDING_TIME_LIMIT, Torrent::MAX_SEEDING_TIME)) , Torrent::NO_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_SEEDING_TIME_LIMIT))
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s), Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT , m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s)
, clampValue(Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, Torrent::MAX_INACTIVE_SEEDING_TIME)) , Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT))
, m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false) , m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false)
, m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false) , m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
, m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None) , m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
@ -622,7 +663,6 @@ SessionImpl::SessionImpl(QObject *parent)
m_fileSearcher = new FileSearcher; m_fileSearcher = new FileSearcher;
m_fileSearcher->moveToThread(m_ioThread.get()); m_fileSearcher->moveToThread(m_ioThread.get());
connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater); connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater);
connect(m_fileSearcher, &FileSearcher::searchFinished, this, &SessionImpl::fileSearchFinished);
m_torrentContentRemover = new TorrentContentRemover; m_torrentContentRemover = new TorrentContentRemover;
m_torrentContentRemover->moveToThread(m_ioThread.get()); m_torrentContentRemover->moveToThread(m_ioThread.get());
@ -1241,7 +1281,7 @@ qreal SessionImpl::globalMaxRatio() const
void SessionImpl::setGlobalMaxRatio(qreal ratio) void SessionImpl::setGlobalMaxRatio(qreal ratio)
{ {
if (ratio < 0) if (ratio < 0)
ratio = -1.; ratio = Torrent::NO_RATIO_LIMIT;
if (ratio != globalMaxRatio()) if (ratio != globalMaxRatio())
{ {
@ -1257,7 +1297,7 @@ int SessionImpl::globalMaxSeedingMinutes() const
void SessionImpl::setGlobalMaxSeedingMinutes(int minutes) void SessionImpl::setGlobalMaxSeedingMinutes(int minutes)
{ {
minutes = std::clamp(minutes, Torrent::NO_SEEDING_TIME_LIMIT, Torrent::MAX_SEEDING_TIME); minutes = std::max(minutes, Torrent::NO_SEEDING_TIME_LIMIT);
if (minutes != globalMaxSeedingMinutes()) if (minutes != globalMaxSeedingMinutes())
{ {
@ -1273,7 +1313,7 @@ int SessionImpl::globalMaxInactiveSeedingMinutes() const
void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes) void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes)
{ {
minutes = std::clamp(minutes, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, Torrent::MAX_INACTIVE_SEEDING_TIME); minutes = std::max(minutes, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT);
if (minutes != globalMaxInactiveSeedingMinutes()) if (minutes != globalMaxInactiveSeedingMinutes())
{ {
@ -1442,10 +1482,8 @@ void SessionImpl::processNextResumeData(ResumeSessionContext *context)
LoadTorrentParams resumeData = *loadResumeDataResult; LoadTorrentParams resumeData = *loadResumeDataResult;
bool needStore = false; bool needStore = false;
const InfoHash infoHash = getInfoHash(resumeData.ltAddTorrentParams);
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
const InfoHash infoHash {(resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hashes()
: resumeData.ltAddTorrentParams.info_hashes)};
const bool isHybrid = infoHash.isHybrid(); const bool isHybrid = infoHash.isHybrid();
const auto torrentIDv2 = TorrentID::fromInfoHash(infoHash); const auto torrentIDv2 = TorrentID::fromInfoHash(infoHash);
const auto torrentIDv1 = TorrentID::fromSHA1Hash(infoHash.v1()); const auto torrentIDv1 = TorrentID::fromSHA1Hash(infoHash.v1());
@ -1493,9 +1531,6 @@ void SessionImpl::processNextResumeData(ResumeSessionContext *context)
return; return;
} }
#else #else
const lt::sha1_hash infoHash = (resumeData.ltAddTorrentParams.ti
? resumeData.ltAddTorrentParams.ti->info_hash()
: resumeData.ltAddTorrentParams.info_hash);
if (torrentID != TorrentID::fromInfoHash(infoHash)) if (torrentID != TorrentID::fromInfoHash(infoHash))
{ {
LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"") LogMsg(tr("Failed to resume torrent: inconsistent torrent ID is detected. Torrent: \"%1\"")
@ -1585,16 +1620,23 @@ void SessionImpl::processNextResumeData(ResumeSessionContext *context)
#endif #endif
qDebug() << "Starting up torrent" << torrentID.toString() << "..."; qDebug() << "Starting up torrent" << torrentID.toString() << "...";
m_loadingTorrents.insert(torrentID, resumeData);
#ifdef QBT_USES_LIBTORRENT2
if (infoHash.isHybrid())
{
// this allows to know the being added hybrid torrent by its v1 info hash
// without having yet another mapping table
m_hybridTorrentsByAltID.insert(torrentIDv1, nullptr);
}
#endif
m_nativeSession->async_add_torrent(resumeData.ltAddTorrentParams); m_nativeSession->async_add_torrent(resumeData.ltAddTorrentParams);
m_addTorrentAlertHandlers.enqueue([this, resumeData = std::move(resumeData)](const lt::add_torrent_alert *alert)
{
if (alert->error)
{
const QString msg = QString::fromStdString(alert->message());
LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
}
else
{
Torrent *torrent = createTorrent(alert->handle, resumeData);
m_loadedTorrents.append(torrent);
LogMsg(tr("Restored torrent. Torrent: \"%1\"").arg(torrent->name()));
}
});
++context->processingResumeDataCount; ++context->processingResumeDataCount;
} }
@ -1642,11 +1684,7 @@ void SessionImpl::endStartup(ResumeSessionContext *context)
auto wakeupCheckTimer = new QTimer(this); auto wakeupCheckTimer = new QTimer(this);
connect(wakeupCheckTimer, &QTimer::timeout, this, [this] connect(wakeupCheckTimer, &QTimer::timeout, this, [this]
{ {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
const bool hasSystemSlept = m_wakeupCheckTimestamp.durationElapsed() > 100s; const bool hasSystemSlept = m_wakeupCheckTimestamp.durationElapsed() > 100s;
#else
const bool hasSystemSlept = m_wakeupCheckTimestamp.elapsed() > std::chrono::milliseconds(100s).count();
#endif
if (hasSystemSlept) if (hasSystemSlept)
{ {
LogMsg(tr("System wake-up event detected. Re-announcing to all the trackers...")); LogMsg(tr("System wake-up event detected. Re-announcing to all the trackers..."));
@ -2334,19 +2372,19 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
QString description; QString description;
if (const qreal ratio = torrent->realRatio(); if (const qreal ratio = torrent->realRatio();
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit)) (ratioLimit >= 0) && (ratio >= ratioLimit))
{ {
reached = true; reached = true;
description = tr("Torrent reached the share ratio limit."); description = tr("Torrent reached the share ratio limit.");
} }
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60; else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit)) (seedingTimeLimit >= 0) && (seedingTimeInMinutes >= seedingTimeLimit))
{ {
reached = true; reached = true;
description = tr("Torrent reached the seeding time limit."); description = tr("Torrent reached the seeding time limit.");
} }
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60; else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit)) (inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
{ {
reached = true; reached = true;
description = tr("Torrent reached the inactive seeding time limit."); description = tr("Torrent reached the inactive seeding time limit.");
@ -2380,31 +2418,6 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
} }
} }
void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames)
{
TorrentImpl *torrent = m_torrents.value(id);
if (torrent)
{
torrent->fileSearchFinished(savePath, fileNames);
return;
}
const auto loadingTorrentsIter = m_loadingTorrents.find(id);
if (loadingTorrentsIter != m_loadingTorrents.end())
{
LoadTorrentParams &params = loadingTorrentsIter.value();
lt::add_torrent_params &p = params.ltAddTorrentParams;
p.save_path = savePath.toString().toStdString();
const TorrentInfo torrentInfo {*p.ti};
const auto nativeIndexes = torrentInfo.nativeIndexes();
for (int i = 0; i < fileNames.size(); ++i)
p.renamed_files[nativeIndexes[i]] = fileNames[i].toString().toStdString();
m_nativeSession->async_add_torrent(p);
}
}
void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage) void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage)
{ {
if (errorMessage.isEmpty()) if (errorMessage.isEmpty())
@ -2426,7 +2439,7 @@ Torrent *SessionImpl::getTorrent(const TorrentID &id) const
Torrent *SessionImpl::findTorrent(const InfoHash &infoHash) const Torrent *SessionImpl::findTorrent(const InfoHash &infoHash) const
{ {
const auto id = TorrentID::fromInfoHash(infoHash); const auto id = TorrentID::fromInfoHash(infoHash);
if (Torrent *torrent = m_torrents.value(id); torrent) if (Torrent *torrent = m_torrents.value(id))
return torrent; return torrent;
if (!infoHash.isHybrid()) if (!infoHash.isHybrid())
@ -2543,7 +2556,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
return true; return true;
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
const InfoHash infoHash {nativeHandle.info_hashes()}; const InfoHash infoHash = getInfoHash(nativeHandle);
if (infoHash.isHybrid()) if (infoHash.isHybrid())
{ {
// if magnet link was hybrid initially then it is indexed also by v1 info hash // if magnet link was hybrid initially then it is indexed also by v1 info hash
@ -2772,14 +2785,6 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
// alternative ID can be useful to find existing torrent in case if hybrid torrent was added by v1 info hash // alternative ID can be useful to find existing torrent in case if hybrid torrent was added by v1 info hash
const auto altID = (infoHash.isHybrid() ? TorrentID::fromSHA1Hash(infoHash.v1()) : TorrentID()); const auto altID = (infoHash.isHybrid() ? TorrentID::fromSHA1Hash(infoHash.v1()) : TorrentID());
// We should not add the torrent if it is already
// processed or is pending to add to session
if (m_loadingTorrents.contains(id) || (infoHash.isHybrid() && m_loadingTorrents.contains(altID)))
{
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, tr("Duplicate torrent")});
return false;
}
if (Torrent *torrent = findTorrent(infoHash)) if (Torrent *torrent = findTorrent(infoHash))
{ {
// a duplicate torrent is being added // a duplicate torrent is being added
@ -2832,11 +2837,12 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
lt::add_torrent_params &p = loadTorrentParams.ltAddTorrentParams; lt::add_torrent_params &p = loadTorrentParams.ltAddTorrentParams;
p = source.ltAddTorrentParams(); p = source.ltAddTorrentParams();
bool isFindingIncompleteFiles = false;
const bool useAutoTMM = loadTorrentParams.useAutoTMM; const bool useAutoTMM = loadTorrentParams.useAutoTMM;
const Path actualSavePath = useAutoTMM ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath; const Path actualSavePath = useAutoTMM ? categorySavePath(loadTorrentParams.category) : loadTorrentParams.savePath;
bool needFindIncompleteFiles = false;
PathList filePaths;
if (hasMetadata) if (hasMetadata)
{ {
// Torrent that is being added with metadata is considered to be added as stopped // Torrent that is being added with metadata is considered to be added as stopped
@ -2851,7 +2857,7 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
Q_ASSERT(addTorrentParams.filePaths.isEmpty() || (addTorrentParams.filePaths.size() == torrentInfo.filesCount())); Q_ASSERT(addTorrentParams.filePaths.isEmpty() || (addTorrentParams.filePaths.size() == torrentInfo.filesCount()));
PathList filePaths = addTorrentParams.filePaths; filePaths = addTorrentParams.filePaths;
if (filePaths.isEmpty()) if (filePaths.isEmpty())
{ {
filePaths = torrentInfo.filePaths(); filePaths = torrentInfo.filePaths();
@ -2896,18 +2902,7 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
} }
if (!loadTorrentParams.hasFinishedStatus) if (!loadTorrentParams.hasFinishedStatus)
{ needFindIncompleteFiles = true;
const Path actualDownloadPath = useAutoTMM
? categoryDownloadPath(loadTorrentParams.category) : loadTorrentParams.downloadPath;
findIncompleteFiles(torrentInfo, actualSavePath, actualDownloadPath, filePaths);
isFindingIncompleteFiles = true;
}
if (!isFindingIncompleteFiles)
{
for (int index = 0; index < filePaths.size(); ++index)
p.renamed_files[nativeIndexes[index]] = filePaths.at(index).toString().toStdString();
}
const int internalFilesCount = torrentInfo.nativeInfo()->files().num_files(); // including .pad files const int internalFilesCount = torrentInfo.nativeInfo()->files().num_files(); // including .pad files
// Use qBittorrent default priority rather than libtorrent's (4) // Use qBittorrent default priority rather than libtorrent's (4)
@ -2927,8 +2922,6 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
loadTorrentParams.name = QString::fromStdString(p.name); loadTorrentParams.name = QString::fromStdString(p.name);
} }
p.save_path = actualSavePath.toString().toStdString();
if (isAddTrackersEnabled() && !(hasMetadata && p.ti->priv())) if (isAddTrackersEnabled() && !(hasMetadata && p.ti->priv()))
{ {
const auto maxTierIter = std::max_element(p.tracker_tiers.cbegin(), p.tracker_tiers.cend()); const auto maxTierIter = std::max_element(p.tracker_tiers.cbegin(), p.tracker_tiers.cend());
@ -2999,26 +2992,80 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
p.storage = customStorageConstructor; p.storage = customStorageConstructor;
#endif #endif
m_loadingTorrents.insert(id, loadTorrentParams); const auto resolveFileNames = [&, this]
if (infoHash.isHybrid()) {
m_hybridTorrentsByAltID.insert(altID, nullptr); if (!needFindIncompleteFiles)
if (!isFindingIncompleteFiles) return QtFuture::makeReadyValueFuture(FileSearchResult {.savePath = actualSavePath, .fileNames = filePaths});
const Path actualDownloadPath = loadTorrentParams.useAutoTMM
? categoryDownloadPath(loadTorrentParams.category) : loadTorrentParams.downloadPath;
return findIncompleteFiles(actualSavePath, actualDownloadPath, filePaths);
};
resolveFileNames().then(this, [this, id, loadTorrentParams = std::move(loadTorrentParams)](const FileSearchResult &result) mutable
{
lt::add_torrent_params &p = loadTorrentParams.ltAddTorrentParams;
p.save_path = result.savePath.toString().toStdString();
if (p.ti)
{
const TorrentInfo torrentInfo {*p.ti};
const auto nativeIndexes = torrentInfo.nativeIndexes();
for (int i = 0; i < result.fileNames.size(); ++i)
p.renamed_files[nativeIndexes[i]] = result.fileNames[i].toString().toStdString();
}
m_nativeSession->async_add_torrent(p); m_nativeSession->async_add_torrent(p);
m_addTorrentAlertHandlers.enqueue([this, loadTorrentParams = std::move(loadTorrentParams)](const lt::add_torrent_alert *alert)
{
if (alert->error)
{
const QString msg = QString::fromStdString(alert->message());
LogMsg(tr("Failed to add torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
const InfoHash infoHash = getInfoHash(alert->params);
const AddTorrentError::Kind errorKind = (alert->error == lt::errors::duplicate_torrent)
? AddTorrentError::DuplicateTorrent : AddTorrentError::Other;
emit addTorrentFailed(infoHash, {errorKind, msg});
}
else
{
if (loadTorrentParams.addToQueueTop)
alert->handle.queue_position_top();
TorrentImpl *torrent = createTorrent(alert->handle, loadTorrentParams);
m_loadedTorrents.append(torrent);
torrent->requestResumeData(lt::torrent_handle::save_info_dict);
LogMsg(tr("Added new torrent. Torrent: \"%1\"").arg(torrent->name()));
emit torrentAdded(torrent);
// The following is useless for newly added magnet
if (torrent->hasMetadata())
{
if (!torrentExportDirectory().isEmpty())
exportTorrentFile(torrent, torrentExportDirectory());
}
}
});
});
return true; return true;
} }
void SessionImpl::findIncompleteFiles(const TorrentInfo &torrentInfo, const Path &savePath QFuture<FileSearchResult> SessionImpl::findIncompleteFiles(const Path &savePath, const Path &downloadPath, const PathList &filePaths) const
, const Path &downloadPath, const PathList &filePaths) const
{ {
Q_ASSERT(filePaths.isEmpty() || (filePaths.size() == torrentInfo.filesCount())); QPromise<FileSearchResult> promise;
QFuture<FileSearchResult> future = promise.future();
const auto searchId = TorrentID::fromInfoHash(torrentInfo.infoHash()); promise.start();
const PathList originalFileNames = (filePaths.isEmpty() ? torrentInfo.filePaths() : filePaths); QMetaObject::invokeMethod(m_fileSearcher, [=, this, promise = std::move(promise)]() mutable
QMetaObject::invokeMethod(m_fileSearcher, [=, this]
{ {
m_fileSearcher->search(searchId, originalFileNames, savePath, downloadPath, isAppendExtensionEnabled()); m_fileSearcher->search(filePaths, savePath, downloadPath, isAppendExtensionEnabled(), promise);
promise.finish();
}); });
return future;
} }
void SessionImpl::enablePortMapping() void SessionImpl::enablePortMapping()
@ -3156,6 +3203,33 @@ bool SessionImpl::downloadMetadata(const TorrentDescriptor &torrentDescr)
// Adding torrent to libtorrent session // Adding torrent to libtorrent session
m_nativeSession->async_add_torrent(p); m_nativeSession->async_add_torrent(p);
m_downloadedMetadata.insert(id, {}); m_downloadedMetadata.insert(id, {});
m_addTorrentAlertHandlers.enqueue([this](const lt::add_torrent_alert *alert)
{
if (alert->error)
{
const QString msg = QString::fromStdString(alert->message());
LogMsg(tr("Failed to download torrent metadata. Reason: \"%1\"").arg(msg), Log::WARNING);
m_downloadedMetadata.remove(getInfoHash(alert->params).toTorrentID());
}
else
{
const InfoHash infoHash = getInfoHash(alert->handle);
const auto torrentID = TorrentID::fromInfoHash(infoHash);
if (const auto downloadedMetadataIter = m_downloadedMetadata.find(torrentID)
; downloadedMetadataIter != m_downloadedMetadata.end())
{
downloadedMetadataIter.value() = alert->handle;
if (infoHash.isHybrid())
{
// index hybrid magnet links by both v1 and v2 info hashes
const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
m_downloadedMetadata[altID] = alert->handle;
}
}
}
});
return true; return true;
} }
@ -5204,8 +5278,6 @@ bool SessionImpl::isKnownTorrent(const InfoHash &infoHash) const
// in case if hybrid torrent was added by v1 info hash // in case if hybrid torrent was added by v1 info hash
const auto altID = (isHybrid ? TorrentID::fromSHA1Hash(infoHash.v1()) : TorrentID()); const auto altID = (isHybrid ? TorrentID::fromSHA1Hash(infoHash.v1()) : TorrentID());
if (m_loadingTorrents.contains(id) || (isHybrid && m_loadingTorrents.contains(altID)))
return true;
if (m_downloadedMetadata.contains(id) || (isHybrid && m_downloadedMetadata.contains(altID))) if (m_downloadedMetadata.contains(id) || (isHybrid && m_downloadedMetadata.contains(altID)))
return true; return true;
return findTorrent(infoHash); return findTorrent(infoHash);
@ -5418,15 +5490,25 @@ bool SessionImpl::addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &new
return true; return true;
} }
lt::torrent_handle SessionImpl::reloadTorrent(const lt::torrent_handle &currentHandle, lt::add_torrent_params params)
{
m_nativeSession->remove_torrent(currentHandle, lt::session::delete_partfile);
auto *const extensionData = new ExtensionData;
params.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
params.storage = customStorageConstructor;
#endif
// libtorrent will post an add_torrent_alert anyway, so we have to add an empty handler to ignore it.
m_addTorrentAlertHandlers.enqueue({});
return m_nativeSession->add_torrent(std::move(params));
}
void SessionImpl::moveTorrentStorage(const MoveStorageJob &job) const void SessionImpl::moveTorrentStorage(const MoveStorageJob &job) const
{ {
#ifdef QBT_USES_LIBTORRENT2 const TorrentImpl *torrent = getTorrent(job.torrentHandle);
const auto id = TorrentID::fromInfoHash(job.torrentHandle.info_hashes()); const QString torrentName = (torrent ? torrent->name() : getInfoHash(job.torrentHandle).toTorrentID().toString());
#else
const auto id = TorrentID::fromInfoHash(job.torrentHandle.info_hash());
#endif
const TorrentImpl *torrent = m_torrents.value(id);
const QString torrentName = (torrent ? torrent->name() : id.toString());
LogMsg(tr("Start moving torrent. Torrent: \"%1\". Destination: \"%2\"").arg(torrentName, job.path.toString())); LogMsg(tr("Start moving torrent. Torrent: \"%1\". Destination: \"%2\"").arg(torrentName, job.path.toString()));
job.torrentHandle.move_storage(job.path.toString().toStdString(), toNative(job.mode)); job.torrentHandle.move_storage(job.path.toString().toStdString(), toNative(job.mode));
@ -5446,7 +5528,7 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath)
const bool torrentHasOutstandingJob = (iter != m_moveStorageQueue.cend()); const bool torrentHasOutstandingJob = (iter != m_moveStorageQueue.cend());
TorrentImpl *torrent = m_torrents.value(finishedJob.torrentHandle.info_hash()); TorrentImpl *torrent = getTorrent(finishedJob.torrentHandle);
if (torrent) if (torrent)
{ {
torrent->handleMoveStorageJobFinished(newPath, finishedJob.context, torrentHasOutstandingJob); torrent->handleMoveStorageJobFinished(newPath, finishedJob.context, torrentHasOutstandingJob);
@ -5454,8 +5536,9 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath)
else if (!torrentHasOutstandingJob) else if (!torrentHasOutstandingJob)
{ {
// Last job is completed for torrent that being removing, so actually remove it // Last job is completed for torrent that being removing, so actually remove it
const lt::torrent_handle nativeHandle {finishedJob.torrentHandle}; const lt::torrent_handle nativeHandle = finishedJob.torrentHandle;
const RemovingTorrentData &removingTorrentData = m_removingTorrents[nativeHandle.info_hash()]; const TorrentID torrentID = getInfoHash(nativeHandle).toTorrentID();
const RemovingTorrentData &removingTorrentData = m_removingTorrents[torrentID];
if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent) if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent)
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile); m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile);
} }
@ -5746,72 +5829,12 @@ void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
{ {
++m_receivedAddTorrentAlertsCount; ++m_receivedAddTorrentAlertsCount;
if (alert->error) Q_ASSERT(!m_addTorrentAlertHandlers.isEmpty());
{ if (m_addTorrentAlertHandlers.isEmpty()) [[unlikely]]
const QString msg = QString::fromStdString(alert->message());
LogMsg(tr("Failed to load torrent. Reason: \"%1\"").arg(msg), Log::WARNING);
emit loadTorrentFailed(msg);
const lt::add_torrent_params &params = alert->params;
const bool hasMetadata = (params.ti && params.ti->is_valid());
#ifdef QBT_USES_LIBTORRENT2
const InfoHash infoHash {(hasMetadata ? params.ti->info_hashes() : params.info_hashes)};
if (infoHash.isHybrid())
m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1()));
#else
const InfoHash infoHash {(hasMetadata ? params.ti->info_hash() : params.info_hash)};
#endif
if (const auto loadingTorrentsIter = m_loadingTorrents.constFind(TorrentID::fromInfoHash(infoHash))
; loadingTorrentsIter != m_loadingTorrents.cend())
{
const AddTorrentError::Kind errorKind = (alert->error == lt::errors::duplicate_torrent)
? AddTorrentError::DuplicateTorrent : AddTorrentError::Other;
emit addTorrentFailed(infoHash, {errorKind, msg});
m_loadingTorrents.erase(loadingTorrentsIter);
}
else if (const auto downloadedMetadataIter = m_downloadedMetadata.constFind(TorrentID::fromInfoHash(infoHash))
; downloadedMetadataIter != m_downloadedMetadata.cend())
{
m_downloadedMetadata.erase(downloadedMetadataIter);
if (infoHash.isHybrid())
{
// index hybrid magnet links by both v1 and v2 info hashes
const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
m_downloadedMetadata.remove(altID);
}
}
return; return;
}
#ifdef QBT_USES_LIBTORRENT2 if (const AddTorrentAlertHandler handleAlert = m_addTorrentAlertHandlers.dequeue())
const InfoHash infoHash {alert->handle.info_hashes()}; handleAlert(alert);
#else
const InfoHash infoHash {alert->handle.info_hash()};
#endif
const auto torrentID = TorrentID::fromInfoHash(infoHash);
if (const auto loadingTorrentsIter = m_loadingTorrents.constFind(torrentID)
; loadingTorrentsIter != m_loadingTorrents.cend())
{
const LoadTorrentParams params = loadingTorrentsIter.value();
m_loadingTorrents.erase(loadingTorrentsIter);
Torrent *torrent = createTorrent(alert->handle, params);
m_loadedTorrents.append(torrent);
}
else if (const auto downloadedMetadataIter = m_downloadedMetadata.find(torrentID)
; downloadedMetadataIter != m_downloadedMetadata.end())
{
downloadedMetadataIter.value() = alert->handle;
if (infoHash.isHybrid())
{
// index hybrid magnet links by both v1 and v2 info hashes
const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
m_downloadedMetadata[altID] = alert->handle;
}
}
} }
void SessionImpl::handleAlert(const lt::alert *alert) void SessionImpl::handleAlert(const lt::alert *alert)
@ -5933,12 +5956,11 @@ void SessionImpl::dispatchTorrentAlert(const lt::torrent_alert *alert)
--m_numResumeData; --m_numResumeData;
} }
const TorrentID torrentID {alert->handle.info_hash()}; TorrentImpl *torrent = getTorrent(alert->handle);
TorrentImpl *torrent = m_torrents.value(torrentID);
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
if (!torrent && (alert->type() == lt::metadata_received_alert::alert_type)) if (!torrent && (alert->type() == lt::metadata_received_alert::alert_type))
{ {
const InfoHash infoHash {alert->handle.info_hashes()}; const InfoHash infoHash = getInfoHash(alert->handle);
if (infoHash.isHybrid()) if (infoHash.isHybrid())
torrent = m_torrents.value(TorrentID::fromSHA1Hash(infoHash.v1())); torrent = m_torrents.value(TorrentID::fromSHA1Hash(infoHash.v1()));
} }
@ -5960,42 +5982,17 @@ void SessionImpl::dispatchTorrentAlert(const lt::torrent_alert *alert)
TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params) TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params)
{ {
auto *const torrent = new TorrentImpl(this, m_nativeSession, nativeHandle, params); auto *const torrent = new TorrentImpl(this, nativeHandle, params);
m_torrents.insert(torrent->id(), torrent); m_torrents.insert(torrent->id(), torrent);
if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid()) if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid())
m_hybridTorrentsByAltID.insert(TorrentID::fromSHA1Hash(infoHash.v1()), torrent); m_hybridTorrentsByAltID.insert(TorrentID::fromSHA1Hash(infoHash.v1()), torrent);
if (isRestored())
{
if (params.addToQueueTop)
nativeHandle.queue_position_top();
torrent->requestResumeData(lt::torrent_handle::save_info_dict);
// The following is useless for newly added magnet
if (torrent->hasMetadata())
{
if (!torrentExportDirectory().isEmpty())
exportTorrentFile(torrent, torrentExportDirectory());
}
}
if (((torrent->ratioLimit() >= 0) || (torrent->seedingTimeLimit() >= 0)) if (((torrent->ratioLimit() >= 0) || (torrent->seedingTimeLimit() >= 0))
&& !m_seedingLimitTimer->isActive()) && !m_seedingLimitTimer->isActive())
{ {
m_seedingLimitTimer->start(); m_seedingLimitTimer->start();
} }
if (!isRestored())
{
LogMsg(tr("Restored torrent. Torrent: \"%1\"").arg(torrent->name()));
}
else
{
LogMsg(tr("Added new torrent. Torrent: \"%1\"").arg(torrent->name()));
emit torrentAdded(torrent);
}
// Torrent could have error just after adding to libtorrent // Torrent could have error just after adding to libtorrent
if (torrent->hasError()) if (torrent->hasError())
LogMsg(tr("Torrent errored. Torrent: \"%1\". Error: \"%2\"").arg(torrent->name(), torrent->error()), Log::WARNING); LogMsg(tr("Torrent errored. Torrent: \"%1\". Error: \"%2\"").arg(torrent->name(), torrent->error()), Log::WARNING);
@ -6003,6 +6000,11 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle,
return torrent; return torrent;
} }
TorrentImpl *SessionImpl::getTorrent(const lt::torrent_handle &nativeHandle) const
{
return m_torrents.value(getInfoHash(nativeHandle).toTorrentID());
}
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/) void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/)
{ {
// We cannot consider `torrent_removed_alert` as a starting point for removing content, // We cannot consider `torrent_removed_alert` as a starting point for removing content,
@ -6012,34 +6014,19 @@ void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*a
void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert) void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert)
{ {
#ifdef QBT_USES_LIBTORRENT2 handleRemovedTorrent(getInfoHash(*alert).toTorrentID());
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
#else
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
#endif
handleRemovedTorrent(torrentID);
} }
void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert) void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert)
{ {
#ifdef QBT_USES_LIBTORRENT2 const TorrentID torrentID = getInfoHash(*alert).toTorrentID();
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
#else
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
#endif
const auto errorMessage = alert->error ? Utils::String::fromLocal8Bit(alert->error.message()) : QString(); const auto errorMessage = alert->error ? Utils::String::fromLocal8Bit(alert->error.message()) : QString();
handleRemovedTorrent(torrentID, errorMessage); handleRemovedTorrent(torrentID, errorMessage);
} }
void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert) void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert)
{ {
#ifdef QBT_USES_LIBTORRENT2 const TorrentID torrentID = getInfoHash(alert->handle).toTorrentID();
const InfoHash infoHash {alert->handle.info_hashes()};
#else
const InfoHash infoHash {alert->handle.info_hash()};
#endif
const auto torrentID = TorrentID::fromInfoHash(infoHash);
TorrentImpl *const torrent = m_torrents.value(torrentID); TorrentImpl *const torrent = m_torrents.value(torrentID);
if (!torrent) [[unlikely]] if (!torrent) [[unlikely]]
return; return;
@ -6053,7 +6040,8 @@ void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *
void SessionImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert *alert) void SessionImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert *alert)
{ {
const TorrentID torrentID {alert->handle.info_hash()}; const InfoHash infoHash = getInfoHash(alert->handle);
const TorrentID torrentID = infoHash.toTorrentID();
bool found = false; bool found = false;
if (const auto iter = m_downloadedMetadata.constFind(torrentID); iter != m_downloadedMetadata.cend()) if (const auto iter = m_downloadedMetadata.constFind(torrentID); iter != m_downloadedMetadata.cend())
@ -6062,7 +6050,6 @@ void SessionImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert
m_downloadedMetadata.erase(iter); m_downloadedMetadata.erase(iter);
} }
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
const InfoHash infoHash {alert->handle.info_hashes()};
if (infoHash.isHybrid()) if (infoHash.isHybrid())
{ {
const auto altID = TorrentID::fromSHA1Hash(infoHash.v1()); const auto altID = TorrentID::fromSHA1Hash(infoHash.v1());
@ -6084,7 +6071,7 @@ void SessionImpl::handleMetadataReceivedAlert(const lt::metadata_received_alert
void SessionImpl::handleFileErrorAlert(const lt::file_error_alert *alert) void SessionImpl::handleFileErrorAlert(const lt::file_error_alert *alert)
{ {
TorrentImpl *const torrent = m_torrents.value(alert->handle.info_hash()); TorrentImpl *const torrent = getTorrent(alert->handle);
if (!torrent) if (!torrent)
return; return;
@ -6155,7 +6142,7 @@ void SessionImpl::handlePeerBanAlert(const lt::peer_ban_alert *alert)
void SessionImpl::handleUrlSeedAlert(const lt::url_seed_alert *alert) void SessionImpl::handleUrlSeedAlert(const lt::url_seed_alert *alert)
{ {
const TorrentImpl *torrent = m_torrents.value(alert->handle.info_hash()); const TorrentImpl *torrent = getTorrent(alert->handle);
if (!torrent) if (!torrent)
return; return;
@ -6334,14 +6321,8 @@ void SessionImpl::handleStorageMovedAlert(const lt::storage_moved_alert *alert)
const Path newPath {QString::fromUtf8(alert->storage_path())}; const Path newPath {QString::fromUtf8(alert->storage_path())};
Q_ASSERT(newPath == currentJob.path); Q_ASSERT(newPath == currentJob.path);
#ifdef QBT_USES_LIBTORRENT2 TorrentImpl *torrent = getTorrent(currentJob.torrentHandle);
const auto id = TorrentID::fromInfoHash(currentJob.torrentHandle.info_hashes()); const QString torrentName = (torrent ? torrent->name() : getInfoHash(currentJob.torrentHandle).toTorrentID().toString());
#else
const auto id = TorrentID::fromInfoHash(currentJob.torrentHandle.info_hash());
#endif
TorrentImpl *torrent = m_torrents.value(id);
const QString torrentName = (torrent ? torrent->name() : id.toString());
LogMsg(tr("Moved torrent successfully. Torrent: \"%1\". Destination: \"%2\"").arg(torrentName, newPath.toString())); LogMsg(tr("Moved torrent successfully. Torrent: \"%1\". Destination: \"%2\"").arg(torrentName, newPath.toString()));
handleMoveTorrentStorageJobFinished(newPath); handleMoveTorrentStorageJobFinished(newPath);
@ -6354,14 +6335,8 @@ void SessionImpl::handleStorageMovedFailedAlert(const lt::storage_moved_failed_a
const MoveStorageJob &currentJob = m_moveStorageQueue.constFirst(); const MoveStorageJob &currentJob = m_moveStorageQueue.constFirst();
Q_ASSERT(currentJob.torrentHandle == alert->handle); Q_ASSERT(currentJob.torrentHandle == alert->handle);
#ifdef QBT_USES_LIBTORRENT2 TorrentImpl *torrent = getTorrent(currentJob.torrentHandle);
const auto id = TorrentID::fromInfoHash(currentJob.torrentHandle.info_hashes()); const QString torrentName = (torrent ? torrent->name() : getInfoHash(currentJob.torrentHandle).toTorrentID().toString());
#else
const auto id = TorrentID::fromInfoHash(currentJob.torrentHandle.info_hash());
#endif
TorrentImpl *torrent = m_torrents.value(id);
const QString torrentName = (torrent ? torrent->name() : id.toString());
const Path currentLocation = (torrent ? torrent->actualStorageLocation() const Path currentLocation = (torrent ? torrent->actualStorageLocation()
: Path(alert->handle.status(lt::torrent_handle::query_save_path).save_path)); : Path(alert->handle.status(lt::torrent_handle::query_save_path).save_path));
const QString errorMessage = QString::fromStdString(alert->message()); const QString errorMessage = QString::fromStdString(alert->message());
@ -6378,12 +6353,7 @@ void SessionImpl::handleStateUpdateAlert(const lt::state_update_alert *alert)
for (const lt::torrent_status &status : alert->status) for (const lt::torrent_status &status : alert->status)
{ {
#ifdef QBT_USES_LIBTORRENT2 TorrentImpl *const torrent = getTorrent(status.handle);
const auto id = TorrentID::fromInfoHash(status.info_hashes);
#else
const auto id = TorrentID::fromInfoHash(status.info_hash);
#endif
TorrentImpl *const torrent = m_torrents.value(id);
if (!torrent) if (!torrent)
continue; continue;
@ -6427,7 +6397,7 @@ void SessionImpl::handleI2PAlert(const lt::i2p_alert *alert) const
void SessionImpl::handleTrackerAlert(const lt::tracker_alert *alert) void SessionImpl::handleTrackerAlert(const lt::tracker_alert *alert)
{ {
TorrentImpl *torrent = m_torrents.value(alert->handle.info_hash()); TorrentImpl *torrent = getTorrent(alert->handle);
if (!torrent) if (!torrent)
return; return;
@ -6453,8 +6423,9 @@ void SessionImpl::handleTrackerAlert(const lt::tracker_alert *alert)
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert) void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert)
{ {
const auto torrentIDv1 = TorrentID::fromSHA1Hash(alert->metadata->info_hashes().v1); const InfoHash infoHash = getInfoHash(*alert->metadata);
const auto torrentIDv2 = TorrentID::fromSHA256Hash(alert->metadata->info_hashes().v2); const auto torrentIDv1 = TorrentID::fromSHA1Hash(infoHash.v1());
const auto torrentIDv2 = TorrentID::fromSHA256Hash(infoHash.v2());
TorrentImpl *torrent1 = m_torrents.value(torrentIDv1); TorrentImpl *torrent1 = m_torrents.value(torrentIDv1);
TorrentImpl *torrent2 = m_torrents.value(torrentIDv2); TorrentImpl *torrent2 = m_torrents.value(torrentIDv2);
if (torrent2) if (torrent2)

View file

@ -30,6 +30,7 @@
#pragma once #pragma once
#include <chrono> #include <chrono>
#include <functional>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -44,6 +45,7 @@
#include <QMap> #include <QMap>
#include <QMutex> #include <QMutex>
#include <QPointer> #include <QPointer>
#include <QQueue>
#include <QSet> #include <QSet>
#include <QThreadPool> #include <QThreadPool>
@ -61,12 +63,16 @@ class QString;
class QTimer; class QTimer;
class QUrl; class QUrl;
template <typename T> class QFuture;
class BandwidthScheduler; class BandwidthScheduler;
class FileSearcher; class FileSearcher;
class FilterParserThread; class FilterParserThread;
class FreeDiskSpaceChecker; class FreeDiskSpaceChecker;
class NativeSessionExtension; class NativeSessionExtension;
struct FileSearchResult;
namespace BitTorrent namespace BitTorrent
{ {
enum class MoveStorageMode; enum class MoveStorageMode;
@ -478,8 +484,9 @@ namespace BitTorrent
bool addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &newPath, MoveStorageMode mode, MoveStorageContext context); bool addMoveTorrentStorageJob(TorrentImpl *torrent, const Path &newPath, MoveStorageMode mode, MoveStorageContext context);
void findIncompleteFiles(const TorrentInfo &torrentInfo, const Path &savePath lt::torrent_handle reloadTorrent(const lt::torrent_handle &currentHandle, lt::add_torrent_params params);
, const Path &downloadPath, const PathList &filePaths = {}) const;
QFuture<FileSearchResult> findIncompleteFiles(const Path &savePath, const Path &downloadPath, const PathList &filePaths = {}) const;
void enablePortMapping(); void enablePortMapping();
void disablePortMapping(); void disablePortMapping();
@ -514,7 +521,6 @@ namespace BitTorrent
void generateResumeData(); void generateResumeData();
void handleIPFilterParsed(int ruleCount); void handleIPFilterParsed(int ruleCount);
void handleIPFilterError(); void handleIPFilterError();
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage); void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
private: private:
@ -604,6 +610,7 @@ namespace BitTorrent
#endif #endif
TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params); TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
TorrentImpl *getTorrent(const lt::torrent_handle &nativeHandle) const;
void saveResumeData(); void saveResumeData();
void saveTorrentsQueue(); void saveTorrentsQueue();
@ -810,11 +817,13 @@ namespace BitTorrent
FileSearcher *m_fileSearcher = nullptr; FileSearcher *m_fileSearcher = nullptr;
TorrentContentRemover *m_torrentContentRemover = nullptr; TorrentContentRemover *m_torrentContentRemover = nullptr;
using AddTorrentAlertHandler = std::function<void (const lt::add_torrent_alert *alert)>;
QQueue<AddTorrentAlertHandler> m_addTorrentAlertHandlers;
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata; QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
QHash<TorrentID, TorrentImpl *> m_torrents; QHash<TorrentID, TorrentImpl *> m_torrents;
QHash<TorrentID, TorrentImpl *> m_hybridTorrentsByAltID; QHash<TorrentID, TorrentImpl *> m_hybridTorrentsByAltID;
QHash<TorrentID, LoadTorrentParams> m_loadingTorrents;
QHash<TorrentID, RemovingTorrentData> m_removingTorrents; QHash<TorrentID, RemovingTorrentData> m_removingTorrents;
QHash<TorrentID, TorrentID> m_changedTorrentIDs; QHash<TorrentID, TorrentID> m_changedTorrentIDs;
QMap<QString, CategoryOptions> m_categories; QMap<QString, CategoryOptions> m_categories;

View file

@ -29,6 +29,8 @@
#include "torrent.h" #include "torrent.h"
#include <limits>
#include <QHash> #include <QHash>
#include "infohash.h" #include "infohash.h"
@ -51,9 +53,7 @@ namespace BitTorrent
const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2; const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2;
const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1; const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1;
const qreal Torrent::MAX_RATIO = 9999; const qreal Torrent::MAX_RATIO = std::numeric_limits<qreal>::infinity();
const int Torrent::MAX_SEEDING_TIME = 525600;
const int Torrent::MAX_INACTIVE_SEEDING_TIME = 525600;
TorrentID Torrent::id() const TorrentID Torrent::id() const
{ {

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -45,6 +45,8 @@ class QByteArray;
class QDateTime; class QDateTime;
class QUrl; class QUrl;
template <typename T> class QFuture;
namespace BitTorrent namespace BitTorrent
{ {
enum class DownloadPriority; enum class DownloadPriority;
@ -132,8 +134,6 @@ namespace BitTorrent
static const int NO_INACTIVE_SEEDING_TIME_LIMIT; static const int NO_INACTIVE_SEEDING_TIME_LIMIT;
static const qreal MAX_RATIO; static const qreal MAX_RATIO;
static const int MAX_SEEDING_TIME;
static const int MAX_INACTIVE_SEEDING_TIME;
using TorrentContentHandler::TorrentContentHandler; using TorrentContentHandler::TorrentContentHandler;
@ -275,10 +275,7 @@ namespace BitTorrent
virtual bool isDHTDisabled() const = 0; virtual bool isDHTDisabled() const = 0;
virtual bool isPEXDisabled() const = 0; virtual bool isPEXDisabled() const = 0;
virtual bool isLSDDisabled() const = 0; virtual bool isLSDDisabled() const = 0;
virtual QList<PeerInfo> peers() const = 0;
virtual QBitArray pieces() const = 0; virtual QBitArray pieces() const = 0;
virtual QBitArray downloadingPieces() const = 0;
virtual QList<int> pieceAvailability() const = 0;
virtual qreal distributedCopies() const = 0; virtual qreal distributedCopies() const = 0;
virtual qreal maxRatio() const = 0; virtual qreal maxRatio() const = 0;
virtual int maxSeedingTime() const = 0; virtual int maxSeedingTime() const = 0;
@ -325,10 +322,10 @@ namespace BitTorrent
virtual nonstd::expected<QByteArray, QString> exportToBuffer() const = 0; virtual nonstd::expected<QByteArray, QString> exportToBuffer() const = 0;
virtual nonstd::expected<void, QString> exportToFile(const Path &path) const = 0; virtual nonstd::expected<void, QString> exportToFile(const Path &path) const = 0;
virtual void fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const = 0; virtual QFuture<QList<PeerInfo>> fetchPeerInfo() const = 0;
virtual void fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const = 0; virtual QFuture<QList<QUrl>> fetchURLSeeds() const = 0;
virtual void fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const = 0; virtual QFuture<QList<int>> fetchPieceAvailability() const = 0;
virtual void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const = 0; virtual QFuture<QBitArray> fetchDownloadingPieces() const = 0;
TorrentID id() const; TorrentID id() const;
bool isRunning() const; bool isRunning() const;

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -34,6 +34,8 @@
#include "abstractfilestorage.h" #include "abstractfilestorage.h"
#include "downloadpriority.h" #include "downloadpriority.h"
template <typename T> class QFuture;
namespace BitTorrent namespace BitTorrent
{ {
class TorrentContentHandler : public QObject, public AbstractFileStorage class TorrentContentHandler : public QObject, public AbstractFileStorage
@ -52,8 +54,7 @@ namespace BitTorrent
* This is not the same as torrrent availability, it is just a fraction of pieces * This is not the same as torrrent availability, it is just a fraction of pieces
* that can be downloaded right now. It varies between 0 to 1. * that can be downloaded right now. It varies between 0 to 1.
*/ */
virtual QList<qreal> availableFileFractions() const = 0; virtual QFuture<QList<qreal>> fetchAvailableFileFractions() const = 0;
virtual void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const = 0;
virtual void prioritizeFiles(const QList<DownloadPriority> &priorities) = 0; virtual void prioritizeFiles(const QList<DownloadPriority> &priorities) = 0;
virtual void flushCache() const = 0; virtual void flushCache() const = 0;

View file

@ -124,18 +124,20 @@ void TorrentCreator::run()
// need to sort the file names by natural sort order // need to sort the file names by natural sort order
QStringList dirs = {m_params.sourcePath.data()}; QStringList dirs = {m_params.sourcePath.data()};
#ifdef Q_OS_WIN QDirIterator dirIter {m_params.sourcePath.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories};
// libtorrent couldn't handle .lnk files on Windows
// Also, Windows users do not expect torrent creator to traverse into .lnk files so skip over them
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks};
#else
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot};
#endif
QDirIterator dirIter {m_params.sourcePath.data(), dirFilters, QDirIterator::Subdirectories};
while (dirIter.hasNext()) while (dirIter.hasNext())
{ {
const QString filePath = dirIter.next(); const QFileInfo dirInfo = dirIter.nextFileInfo();
dirs.append(filePath);
#ifdef Q_OS_WIN
// .lnk to directory
// Windows users do not expect torrent creator to traverse into .lnk files so skip over them
if (dirInfo.isShortcut())
continue;
#endif
const QString dirPath = dirInfo.filePath();
dirs.append(dirPath);
} }
std::sort(dirs.begin(), dirs.end(), naturalLessThan); std::sort(dirs.begin(), dirs.end(), naturalLessThan);
@ -146,19 +148,29 @@ void TorrentCreator::run()
{ {
QStringList tmpNames; // natural sort files within each dir QStringList tmpNames; // natural sort files within each dir
#ifdef Q_OS_WIN QDirIterator fileIter {dir, QDir::Files};
const QDir::Filters fileFilters {QDir::Files | QDir::NoSymLinks};
#else
const QDir::Filters fileFilters {QDir::Files};
#endif
QDirIterator fileIter {dir, fileFilters};
while (fileIter.hasNext()) while (fileIter.hasNext())
{ {
const QFileInfo fileInfo = fileIter.nextFileInfo(); const QFileInfo fileInfo = fileIter.nextFileInfo();
const Path filePath {fileInfo.filePath()};
qint64 fileSize = fileInfo.size();
const Path relFilePath = parentPath.relativePathOf(Path(fileInfo.filePath())); #ifdef Q_OS_WIN
// .lnk to file
// libtorrent couldn't handle .lnk files on Windows
if (fileInfo.isShortcut())
continue;
// file symbolic link
// QFileInfo::size() failed to return the target file size
// and we need to redirect it manually
if (fileInfo.isSymbolicLink())
fileSize = QFileInfo(fileInfo.symLinkTarget()).size();
#endif
const Path relFilePath = parentPath.relativePathOf(filePath);
tmpNames.append(relFilePath.toString()); tmpNames.append(relFilePath.toString());
fileSizeMap[tmpNames.last()] = fileInfo.size(); fileSizeMap[tmpNames.last()] = fileSize;
} }
std::sort(tmpNames.begin(), tmpNames.end(), naturalLessThan); std::sort(tmpNames.begin(), tmpNames.end(), naturalLessThan);
@ -174,8 +186,8 @@ void TorrentCreator::run()
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
lt::create_torrent newTorrent {fs, m_params.pieceSize, toNativeTorrentFormatFlag(m_params.torrentFormat)}; lt::create_torrent newTorrent {fs, m_params.pieceSize, toNativeTorrentFormatFlag(m_params.torrentFormat)};
#else #else
lt::create_torrent newTorrent(fs, m_params.pieceSize, m_params.paddedFileSizeLimit lt::create_torrent newTorrent {fs, m_params.pieceSize, m_params.paddedFileSizeLimit
, (m_params.isAlignmentOptimized ? lt::create_torrent::optimize_alignment : lt::create_flags_t {})); , (m_params.isAlignmentOptimized ? lt::create_torrent::optimize_alignment : lt::create_flags_t {})};
#endif #endif
// Add url seeds // Add url seeds

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -51,7 +51,9 @@
#include <QByteArray> #include <QByteArray>
#include <QCache> #include <QCache>
#include <QDebug> #include <QDebug>
#include <QFuture>
#include <QPointer> #include <QPointer>
#include <QPromise>
#include <QSet> #include <QSet>
#include <QStringList> #include <QStringList>
#include <QUrl> #include <QUrl>
@ -67,6 +69,7 @@
#include "common.h" #include "common.h"
#include "downloadpriority.h" #include "downloadpriority.h"
#include "extensiondata.h" #include "extensiondata.h"
#include "filesearcher.h"
#include "loadtorrentparams.h" #include "loadtorrentparams.h"
#include "ltqbitarray.h" #include "ltqbitarray.h"
#include "lttypecast.h" #include "lttypecast.h"
@ -287,11 +290,9 @@ namespace
// TorrentImpl // TorrentImpl
TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession TorrentImpl::TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params)
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params)
: Torrent(session) : Torrent(session)
, m_session(session) , m_session(session)
, m_nativeSession(nativeSession)
, m_nativeHandle(nativeHandle) , m_nativeHandle(nativeHandle)
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
, m_infoHash(m_nativeHandle.info_hashes()) , m_infoHash(m_nativeHandle.info_hashes())
@ -1465,48 +1466,11 @@ bool TorrentImpl::isLSDDisabled() const
return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_lsd); return static_cast<bool>(m_nativeStatus.flags & lt::torrent_flags::disable_lsd);
} }
QList<PeerInfo> TorrentImpl::peers() const
{
std::vector<lt::peer_info> nativePeers;
m_nativeHandle.get_peer_info(nativePeers);
QList<PeerInfo> peers;
peers.reserve(static_cast<decltype(peers)::size_type>(nativePeers.size()));
for (const lt::peer_info &peer : nativePeers)
peers.append(PeerInfo(peer, pieces()));
return peers;
}
QBitArray TorrentImpl::pieces() const QBitArray TorrentImpl::pieces() const
{ {
return m_pieces; return m_pieces;
} }
QBitArray TorrentImpl::downloadingPieces() const
{
if (!hasMetadata())
return {};
std::vector<lt::partial_piece_info> queue;
m_nativeHandle.get_download_queue(queue);
QBitArray result {piecesCount()};
for (const lt::partial_piece_info &info : queue)
result.setBit(LT::toUnderlyingType(info.piece_index));
return result;
}
QList<int> TorrentImpl::pieceAvailability() const
{
std::vector<int> avail;
m_nativeHandle.piece_availability(avail);
return {avail.cbegin(), avail.cend()};
}
qreal TorrentImpl::distributedCopies() const qreal TorrentImpl::distributedCopies() const
{ {
return m_nativeStatus.distributed_copies; return m_nativeStatus.distributed_copies;
@ -1549,7 +1513,8 @@ qreal TorrentImpl::realRatio() const
const qreal ratio = upload / static_cast<qreal>(download); const qreal ratio = upload / static_cast<qreal>(download);
Q_ASSERT(ratio >= 0); Q_ASSERT(ratio >= 0);
return (ratio > MAX_RATIO) ? MAX_RATIO : ratio;
return ratio;
} }
int TorrentImpl::uploadPayloadRate() const int TorrentImpl::uploadPayloadRate() const
@ -1751,12 +1716,6 @@ void TorrentImpl::applyFirstLastPiecePriority(const bool enabled)
m_nativeHandle.prioritize_pieces(piecePriorities); m_nativeHandle.prioritize_pieces(piecePriorities);
} }
void TorrentImpl::fileSearchFinished(const Path &savePath, const PathList &fileNames)
{
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
endReceivedMetadataHandling(savePath, fileNames);
}
TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo) TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
{ {
const auto it = std::find_if(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end() const auto it = std::find_if(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
@ -1876,16 +1835,6 @@ void TorrentImpl::reload()
{ {
try try
{ {
m_completedFiles.fill(false);
m_filesProgress.fill(0);
m_pieces.fill(false);
m_nativeStatus.pieces.clear_all();
m_nativeStatus.num_pieces = 0;
const auto queuePos = m_nativeHandle.queue_position();
m_nativeSession->remove_torrent(m_nativeHandle, lt::session::delete_partfile);
lt::add_torrent_params p = m_ltAddTorrentParams; lt::add_torrent_params p = m_ltAddTorrentParams;
p.flags |= lt::torrent_flags::update_subscribe p.flags |= lt::torrent_flags::update_subscribe
| lt::torrent_flags::override_trackers | lt::torrent_flags::override_trackers
@ -1905,19 +1854,21 @@ void TorrentImpl::reload()
p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused); p.flags &= ~(lt::torrent_flags::auto_managed | lt::torrent_flags::paused);
} }
auto *const extensionData = new ExtensionData; const auto queuePos = m_nativeHandle.queue_position();
p.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
m_nativeHandle = m_nativeSession->add_torrent(p);
m_nativeStatus = extensionData->status; m_nativeHandle = m_session->reloadTorrent(m_nativeHandle, std::move(p));
m_nativeStatus = static_cast<ExtensionData *>(m_nativeHandle.userdata())->status;
if (queuePos >= lt::queue_position_t {}) if (queuePos >= lt::queue_position_t {})
m_nativeHandle.queue_position_set(queuePos); m_nativeHandle.queue_position_set(queuePos);
m_nativeStatus.queue_position = queuePos; m_nativeStatus.queue_position = queuePos;
m_completedFiles.fill(false);
m_filesProgress.fill(0);
m_pieces.fill(false);
m_nativeStatus.pieces.clear_all();
m_nativeStatus.num_pieces = 0;
updateState(); updateState();
} }
catch (const lt::system_error &err) catch (const lt::system_error &err)
@ -2150,7 +2101,7 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
// URL seed list have been changed by libtorrent for some reason, so we need to update cached one. // URL seed list have been changed by libtorrent for some reason, so we need to update cached one.
// Unfortunately, URL seed list containing in "resume data" is generated according to different rules // Unfortunately, URL seed list containing in "resume data" is generated according to different rules
// than the list we usually cache, so we have to request it from the appropriate source. // than the list we usually cache, so we have to request it from the appropriate source.
fetchURLSeeds([this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; }); fetchURLSeeds().then(this, [this](const QList<QUrl> &urlSeeds) { m_urlSeeds = urlSeeds; });
} }
if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && p->params.ti) if ((m_maintenanceJob == MaintenanceJob::HandleMetadata) && p->params.ti)
@ -2197,7 +2148,12 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
filePaths[i] = Path(it->second); filePaths[i] = Path(it->second);
} }
m_session->findIncompleteFiles(metadata, savePath(), downloadPath(), filePaths); m_session->findIncompleteFiles(savePath(), downloadPath(), filePaths).then(this
, [this](const FileSearchResult &result)
{
if (m_maintenanceJob == MaintenanceJob::HandleMetadata)
endReceivedMetadataHandling(result.savePath, result.fileNames);
});
} }
else else
{ {
@ -2712,8 +2668,6 @@ void TorrentImpl::setRatioLimit(qreal limit)
{ {
if (limit < USE_GLOBAL_RATIO) if (limit < USE_GLOBAL_RATIO)
limit = NO_RATIO_LIMIT; limit = NO_RATIO_LIMIT;
else if (limit > MAX_RATIO)
limit = MAX_RATIO;
if (m_ratioLimit != limit) if (m_ratioLimit != limit)
{ {
@ -2727,8 +2681,6 @@ void TorrentImpl::setSeedingTimeLimit(int limit)
{ {
if (limit < USE_GLOBAL_SEEDING_TIME) if (limit < USE_GLOBAL_SEEDING_TIME)
limit = NO_SEEDING_TIME_LIMIT; limit = NO_SEEDING_TIME_LIMIT;
else if (limit > MAX_SEEDING_TIME)
limit = MAX_SEEDING_TIME;
if (m_seedingTimeLimit != limit) if (m_seedingTimeLimit != limit)
{ {
@ -2742,8 +2694,6 @@ void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
{ {
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME) if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
limit = NO_INACTIVE_SEEDING_TIME_LIMIT; limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
else if (limit > MAX_INACTIVE_SEEDING_TIME)
limit = MAX_SEEDING_TIME;
if (m_inactiveSeedingTimeLimit != limit) if (m_inactiveSeedingTimeLimit != limit)
{ {
@ -2930,9 +2880,9 @@ nonstd::expected<void, QString> TorrentImpl::exportToFile(const Path &path) cons
return {}; return {};
} }
void TorrentImpl::fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const QFuture<QList<PeerInfo>> TorrentImpl::fetchPeerInfo() const
{ {
invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo> return invokeAsync([nativeHandle = m_nativeHandle, allPieces = pieces()]() -> QList<PeerInfo>
{ {
try try
{ {
@ -2947,13 +2897,12 @@ void TorrentImpl::fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHand
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const QFuture<QList<QUrl>> TorrentImpl::fetchURLSeeds() const
{ {
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl> return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<QUrl>
{ {
try try
{ {
@ -2967,13 +2916,12 @@ void TorrentImpl::fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler)
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const QFuture<QList<int>> TorrentImpl::fetchPieceAvailability() const
{ {
invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int> return invokeAsync([nativeHandle = m_nativeHandle]() -> QList<int>
{ {
try try
{ {
@ -2984,13 +2932,12 @@ void TorrentImpl::fetchPieceAvailability(std::function<void (QList<int>)> result
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const QFuture<QBitArray> TorrentImpl::fetchDownloadingPieces() const
{ {
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QBitArray
{ {
try try
{ {
@ -3009,13 +2956,12 @@ void TorrentImpl::fetchDownloadingPieces(std::function<void (QBitArray)> resultH
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const QFuture<QList<qreal>> TorrentImpl::fetchAvailableFileFractions() const
{ {
invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal> return invokeAsync([nativeHandle = m_nativeHandle, torrentInfo = m_torrentInfo]() -> QList<qreal>
{ {
if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0)) if (!torrentInfo.isValid() || (torrentInfo.filesCount() <= 0))
return {}; return {};
@ -3049,8 +2995,7 @@ void TorrentImpl::fetchAvailableFileFractions(std::function<void (QList<qreal>)>
catch (const std::exception &) {} catch (const std::exception &) {}
return {}; return {};
} });
, std::move(resultHandler));
} }
void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities) void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
@ -3090,47 +3035,17 @@ void TorrentImpl::prioritizeFiles(const QList<DownloadPriority> &priorities)
manageActualFilePaths(); manageActualFilePaths();
} }
QList<qreal> TorrentImpl::availableFileFractions() const template <typename Func>
QFuture<std::invoke_result_t<Func>> TorrentImpl::invokeAsync(Func &&func) const
{ {
Q_ASSERT(hasMetadata()); QPromise<std::invoke_result_t<Func>> promise;
const auto future = promise.future();
const int filesCount = this->filesCount(); promise.start();
if (filesCount <= 0) return {}; m_session->invokeAsync([func = std::forward<Func>(func), promise = std::move(promise)]() mutable
const QList<int> piecesAvailability = pieceAvailability();
// libtorrent returns empty array for seeding only torrents
if (piecesAvailability.empty()) return QList<qreal>(filesCount, -1);
QList<qreal> res;
res.reserve(filesCount);
for (int i = 0; i < filesCount; ++i)
{ {
const TorrentInfo::PieceRange filePieces = m_torrentInfo.filePieces(i); promise.addResult(func());
promise.finish();
int availablePieces = 0;
for (const int piece : filePieces)
availablePieces += (piecesAvailability[piece] > 0) ? 1 : 0;
const qreal availability = filePieces.isEmpty()
? 1 // the file has no pieces, so it is available by default
: static_cast<qreal>(availablePieces) / filePieces.size();
res.push_back(availability);
}
return res;
}
template <typename Func, typename Callback>
void TorrentImpl::invokeAsync(Func func, Callback resultHandler) const
{
m_session->invokeAsync([session = m_session
, func = std::move(func)
, resultHandler = std::move(resultHandler)
, thisTorrent = QPointer<const TorrentImpl>(this)]() mutable
{
session->invoke([result = func(), thisTorrent, resultHandler = std::move(resultHandler)]
{
if (thisTorrent)
resultHandler(result);
});
}); });
return future;
} }

View file

@ -94,8 +94,7 @@ namespace BitTorrent
Q_DISABLE_COPY_MOVE(TorrentImpl) Q_DISABLE_COPY_MOVE(TorrentImpl)
public: public:
TorrentImpl(SessionImpl *session, lt::session *nativeSession TorrentImpl(SessionImpl *session, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
, const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
~TorrentImpl() override; ~TorrentImpl() override;
bool isValid() const; bool isValid() const;
@ -203,10 +202,7 @@ namespace BitTorrent
bool isDHTDisabled() const override; bool isDHTDisabled() const override;
bool isPEXDisabled() const override; bool isPEXDisabled() const override;
bool isLSDDisabled() const override; bool isLSDDisabled() const override;
QList<PeerInfo> peers() const override;
QBitArray pieces() const override; QBitArray pieces() const override;
QBitArray downloadingPieces() const override;
QList<int> pieceAvailability() const override;
qreal distributedCopies() const override; qreal distributedCopies() const override;
qreal maxRatio() const override; qreal maxRatio() const override;
int maxSeedingTime() const override; int maxSeedingTime() const override;
@ -220,7 +216,6 @@ namespace BitTorrent
int connectionsCount() const override; int connectionsCount() const override;
int connectionsLimit() const override; int connectionsLimit() const override;
qlonglong nextAnnounce() const override; qlonglong nextAnnounce() const override;
QList<qreal> availableFileFractions() const override;
void setName(const QString &name) override; void setName(const QString &name) override;
void setSequentialDownload(bool enable) override; void setSequentialDownload(bool enable) override;
@ -258,11 +253,11 @@ namespace BitTorrent
nonstd::expected<QByteArray, QString> exportToBuffer() const override; nonstd::expected<QByteArray, QString> exportToBuffer() const override;
nonstd::expected<void, QString> exportToFile(const Path &path) const override; nonstd::expected<void, QString> exportToFile(const Path &path) const override;
void fetchPeerInfo(std::function<void (QList<PeerInfo>)> resultHandler) const override; QFuture<QList<PeerInfo>> fetchPeerInfo() const override;
void fetchURLSeeds(std::function<void (QList<QUrl>)> resultHandler) const override; QFuture<QList<QUrl>> fetchURLSeeds() const override;
void fetchPieceAvailability(std::function<void (QList<int>)> resultHandler) const override; QFuture<QList<int>> fetchPieceAvailability() const override;
void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const override; QFuture<QBitArray> fetchDownloadingPieces() const override;
void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const override; QFuture<QList<qreal>> fetchAvailableFileFractions() const override;
bool needSaveResumeData() const; bool needSaveResumeData() const;
@ -278,7 +273,6 @@ namespace BitTorrent
void requestResumeData(lt::resume_data_flags_t flags = {}); void requestResumeData(lt::resume_data_flags_t flags = {});
void deferredRequestResumeData(); void deferredRequestResumeData();
void handleMoveStorageJobFinished(const Path &path, MoveStorageContext context, bool hasOutstandingJob); void handleMoveStorageJobFinished(const Path &path, MoveStorageContext context, bool hasOutstandingJob);
void fileSearchFinished(const Path &savePath, const PathList &fileNames);
TrackerEntryStatus updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo); TrackerEntryStatus updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo);
void resetTrackerEntryStatuses(); void resetTrackerEntryStatuses();
@ -326,11 +320,10 @@ namespace BitTorrent
nonstd::expected<lt::entry, QString> exportTorrent() const; nonstd::expected<lt::entry, QString> exportTorrent() const;
template <typename Func, typename Callback> template <typename Func>
void invokeAsync(Func func, Callback resultHandler) const; QFuture<std::invoke_result_t<Func>> invokeAsync(Func &&func) const;
SessionImpl *const m_session = nullptr; SessionImpl *const m_session = nullptr;
lt::session *m_nativeSession = nullptr;
lt::torrent_handle m_nativeHandle; lt::torrent_handle m_nativeHandle;
mutable lt::torrent_status m_nativeStatus; mutable lt::torrent_status m_nativeStatus;
TorrentState m_state = TorrentState::Unknown; TorrentState m_state = TorrentState::Unknown;

View file

@ -42,7 +42,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
, m_downloadProcess {new QProcess(this)} , m_downloadProcess {new QProcess(this)}
{ {
m_downloadProcess->setProcessEnvironment(m_manager->proxyEnvironment()); m_downloadProcess->setProcessEnvironment(m_manager->proxyEnvironment());
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)) #ifdef Q_OS_UNIX
m_downloadProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); m_downloadProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif #endif
connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished) connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished)
@ -55,7 +55,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
url url
}; };
// Launch search // Launch search
m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly); m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executablePath.data(), params, QIODevice::ReadOnly);
} }
void SearchDownloadHandler::downloadProcessFinished(int exitcode) void SearchDownloadHandler::downloadProcessFinished(int exitcode)

View file

@ -71,8 +71,8 @@ SearchHandler::SearchHandler(const QString &pattern, const QString &category, co
{ {
// Load environment variables (proxy) // Load environment variables (proxy)
m_searchProcess->setProcessEnvironment(m_manager->proxyEnvironment()); m_searchProcess->setProcessEnvironment(m_manager->proxyEnvironment());
m_searchProcess->setProgram(Utils::ForeignApps::pythonInfo().executableName); m_searchProcess->setProgram(Utils::ForeignApps::pythonInfo().executablePath.data());
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)) #ifdef Q_OS_UNIX
m_searchProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); m_searchProcess->setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif #endif

View file

@ -546,7 +546,7 @@ void SearchPluginManager::update()
{ {
QProcess nova; QProcess nova;
nova.setProcessEnvironment(proxyEnvironment()); nova.setProcessEnvironment(proxyEnvironment());
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)) #ifdef Q_OS_UNIX
nova.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); nova.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif #endif
@ -556,7 +556,7 @@ void SearchPluginManager::update()
(engineLocation() / Path(u"/nova2.py"_s)).toString(), (engineLocation() / Path(u"/nova2.py"_s)).toString(),
u"--capabilities"_s u"--capabilities"_s
}; };
nova.start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly); nova.start(Utils::ForeignApps::pythonInfo().executablePath.data(), params, QIODevice::ReadOnly);
nova.waitForFinished(); nova.waitForFinished();
const auto capabilities = QString::fromUtf8(nova.readAllStandardOutput()); const auto capabilities = QString::fromUtf8(nova.readAllStandardOutput());

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Mike Tzou * Copyright (C) 2018-2025 Mike Tzou (Chocobo1)
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -30,6 +30,7 @@
#include "foreignapps.h" #include "foreignapps.h"
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
#include <algorithm>
#include <windows.h> #include <windows.h>
#endif #endif
@ -40,27 +41,30 @@
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
#include <QDir> #include <QDir>
#include <QScopeGuard>
#endif #endif
#include "base/global.h"
#include "base/logger.h" #include "base/logger.h"
#include "base/path.h"
#include "base/preferences.h" #include "base/preferences.h"
#include "base/utils/bytearray.h" #include "base/utils/bytearray.h"
#if defined(Q_OS_WIN)
#include "base/utils/compare.h"
#endif
using namespace Utils::ForeignApps; using namespace Utils::ForeignApps;
namespace namespace
{ {
bool testPythonInstallation(const QString &exeName, PythonInfo &info) bool testPythonInstallation(const Path &exePath, PythonInfo &info)
{ {
info = {}; info = {};
QProcess proc; QProcess proc;
#if defined(Q_OS_UNIX) && (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)) #ifdef Q_OS_UNIX
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif #endif
proc.start(exeName, {u"--version"_s}, QIODevice::ReadOnly); proc.start(exePath.data(), {u"--version"_s}, QIODevice::ReadOnly);
if (proc.waitForFinished() && (proc.exitCode() == QProcess::NormalExit)) if (proc.waitForFinished() && (proc.exitCode() == QProcess::NormalExit))
{ {
QByteArray procOutput = proc.readAllStandardOutput(); QByteArray procOutput = proc.readAllStandardOutput();
@ -83,9 +87,9 @@ namespace
if (!version.isValid()) if (!version.isValid())
return false; return false;
info = {exeName, version}; info = {.executablePath = exePath, .version = version};
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Found Python executable. Name: \"%1\". Version: \"%2\"") LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Found Python executable. Name: \"%1\". Version: \"%2\"")
.arg(info.executableName, info.version.toString()), Log::INFO); .arg(info.executablePath.toString(), info.version.toString()), Log::INFO);
return true; return true;
} }
@ -100,178 +104,160 @@ namespace
SYSTEM_64BIT SYSTEM_64BIT
}; };
QStringList getRegSubkeys(const HKEY handle) PathList getRegSubkeys(const HKEY handle)
{ {
QStringList keys; PathList keys;
DWORD cSubKeys = 0; DWORD cSubKeys = 0;
DWORD cMaxSubKeyLen = 0; DWORD cMaxSubKeyLen = 0;
LONG res = ::RegQueryInfoKeyW(handle, NULL, NULL, NULL, &cSubKeys, &cMaxSubKeyLen, NULL, NULL, NULL, NULL, NULL, NULL); const LSTATUS result = ::RegQueryInfoKeyW(handle, NULL, NULL, NULL, &cSubKeys, &cMaxSubKeyLen, NULL, NULL, NULL, NULL, NULL, NULL);
if (res == ERROR_SUCCESS) if (result == ERROR_SUCCESS)
{ {
++cMaxSubKeyLen; // For null character ++cMaxSubKeyLen; // For null character
LPWSTR lpName = new WCHAR[cMaxSubKeyLen]; LPWSTR lpName = new WCHAR[cMaxSubKeyLen] {0};
DWORD cName; [[maybe_unused]] const auto lpNameGuard = qScopeGuard([&lpName] { delete[] lpName; });
keys.reserve(cSubKeys);
for (DWORD i = 0; i < cSubKeys; ++i) for (DWORD i = 0; i < cSubKeys; ++i)
{ {
cName = cMaxSubKeyLen; DWORD cName = cMaxSubKeyLen;
res = ::RegEnumKeyExW(handle, i, lpName, &cName, NULL, NULL, NULL, NULL); const LSTATUS res = ::RegEnumKeyExW(handle, i, lpName, &cName, NULL, NULL, NULL, NULL);
if (res == ERROR_SUCCESS) if (res == ERROR_SUCCESS)
keys.push_back(QString::fromWCharArray(lpName)); keys.append(Path(QString::fromWCharArray(lpName)));
} }
delete[] lpName;
} }
return keys; return keys;
} }
QString getRegValue(const HKEY handle, const QString &name = {}) Path getRegValue(const HKEY handle, const QString &name = {})
{ {
const std::wstring nameWStr = name.toStdWString(); const std::wstring nameWStr = name.toStdWString();
DWORD type = 0; DWORD type = 0;
DWORD cbData = 0; DWORD cbData = 0;
// Discover the size of the value // Discover the size of the value
::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, NULL, &cbData); ::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, NULL, &cbData);
DWORD cBuffer = (cbData / sizeof(WCHAR)) + 1;
LPWSTR lpData = new WCHAR[cBuffer];
LONG res = ::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, reinterpret_cast<LPBYTE>(lpData), &cbData);
QString result; const DWORD cBuffer = (cbData / sizeof(WCHAR)) + 1;
LPWSTR lpData = new WCHAR[cBuffer] {0};
[[maybe_unused]] const auto lpDataGuard = qScopeGuard([&lpData] { delete[] lpData; });
const LSTATUS res = ::RegQueryValueExW(handle, nameWStr.c_str(), NULL, &type, reinterpret_cast<LPBYTE>(lpData), &cbData);
if (res == ERROR_SUCCESS) if (res == ERROR_SUCCESS)
{ return Path(QString::fromWCharArray(lpData));
lpData[cBuffer - 1] = 0;
result = QString::fromWCharArray(lpData);
}
delete[] lpData;
return result; return {};
} }
QString pythonSearchReg(const REG_SEARCH_TYPE type) PathList pythonSearchReg(const REG_SEARCH_TYPE type)
{ {
HKEY hkRoot; const HKEY hkRoot = (type == USER) ? HKEY_CURRENT_USER : HKEY_LOCAL_MACHINE;
if (type == USER) const REGSAM samDesired = KEY_READ
hkRoot = HKEY_CURRENT_USER; | ((type == SYSTEM_64BIT) ? KEY_WOW64_64KEY : KEY_WOW64_32KEY);
else PathList ret;
hkRoot = HKEY_LOCAL_MACHINE;
REGSAM samDesired = KEY_READ; HKEY hkPythonCore {0};
if (type == SYSTEM_32BIT) if (::RegOpenKeyExW(hkRoot, L"SOFTWARE\\Python\\PythonCore", 0, samDesired, &hkPythonCore) == ERROR_SUCCESS)
samDesired |= KEY_WOW64_32KEY;
else if (type == SYSTEM_64BIT)
samDesired |= KEY_WOW64_64KEY;
QString path;
LONG res = 0;
HKEY hkPythonCore;
res = ::RegOpenKeyExW(hkRoot, L"SOFTWARE\\Python\\PythonCore", 0, samDesired, &hkPythonCore);
if (res == ERROR_SUCCESS)
{ {
QStringList versions = getRegSubkeys(hkPythonCore); [[maybe_unused]] const auto hkPythonCoreGuard = qScopeGuard([&hkPythonCore] { ::RegCloseKey(hkPythonCore); });
versions.sort();
bool found = false; // start with the largest version
while (!found && !versions.empty()) PathList versions = getRegSubkeys(hkPythonCore);
// ordinary sort won't suffice, it needs to sort ["3.9", "3.10"] correctly
const Utils::Compare::NaturalCompare<Qt::CaseInsensitive> comparator;
std::sort(versions.begin(), versions.end(), [&comparator](const Path &left, const Path &right)
{ {
const std::wstring version = QString(versions.takeLast() + u"\\InstallPath").toStdWString(); return comparator(left.data(), right.data());
});
HKEY hkInstallPath; ret.reserve(versions.size() * 2);
res = ::RegOpenKeyExW(hkPythonCore, version.c_str(), 0, samDesired, &hkInstallPath);
if (res == ERROR_SUCCESS) while (!versions.empty())
{ {
qDebug("Detected possible Python v%ls location", version.c_str()); const std::wstring version = (versions.takeLast() / Path(u"InstallPath"_s)).toString().toStdWString();
path = getRegValue(hkInstallPath);
::RegCloseKey(hkInstallPath);
if (!path.isEmpty()) HKEY hkInstallPath {0};
if (::RegOpenKeyExW(hkPythonCore, version.c_str(), 0, samDesired, &hkInstallPath) == ERROR_SUCCESS)
{ {
const QDir baseDir {path}; [[maybe_unused]] const auto hkInstallPathGuard = qScopeGuard([&hkInstallPath] { ::RegCloseKey(hkInstallPath); });
if (baseDir.exists(u"python3.exe"_s)) const Path basePath = getRegValue(hkInstallPath);
{ if (basePath.isEmpty())
found = true; continue;
path = baseDir.filePath(u"python3.exe"_s);
} if (const Path path = (basePath / Path(u"python3.exe"_s)); path.exists())
else if (baseDir.exists(u"python.exe"_s)) ret.append(path);
{ if (const Path path = (basePath / Path(u"python.exe"_s)); path.exists())
found = true; ret.append(path);
path = baseDir.filePath(u"python.exe"_s);
} }
} }
} }
return ret;
} }
if (!found) PathList searchPythonPaths()
path = QString();
::RegCloseKey(hkPythonCore);
}
return path;
}
QString findPythonPath()
{ {
QString path = pythonSearchReg(USER); // From registry
if (!path.isEmpty()) PathList ret = pythonSearchReg(USER)
return path; + pythonSearchReg(SYSTEM_64BIT)
+ pythonSearchReg(SYSTEM_32BIT);
path = pythonSearchReg(SYSTEM_32BIT);
if (!path.isEmpty())
return path;
path = pythonSearchReg(SYSTEM_64BIT);
if (!path.isEmpty())
return path;
// Fallback: Detect python from default locations // Fallback: Detect python from default locations
const QFileInfoList dirs = QDir(u"C:/"_s).entryInfoList({u"Python*"_s}, QDir::Dirs, (QDir::Name | QDir::Reversed)); const QFileInfoList dirs = QDir(u"C:/"_s).entryInfoList({u"Python*"_s}, QDir::Dirs, (QDir::Name | QDir::Reversed));
for (const QFileInfo &info : dirs) for (const QFileInfo &info : dirs)
{ {
const QString py3Path {info.absolutePath() + u"/python3.exe"}; const Path absPath {info.absolutePath()};
if (QFile::exists(py3Path))
return py3Path;
const QString pyPath {info.absolutePath() + u"/python.exe"}; if (const Path path = (absPath / Path(u"python3.exe"_s)); path.exists())
if (QFile::exists(pyPath)) ret.append(path);
return pyPath; if (const Path path = (absPath / Path(u"python.exe"_s)); path.exists())
ret.append(path);
} }
return {}; return ret;
} }
#endif // Q_OS_WIN #endif // Q_OS_WIN
} }
bool Utils::ForeignApps::PythonInfo::isValid() const bool Utils::ForeignApps::PythonInfo::isValid() const
{ {
return (!executableName.isEmpty() && version.isValid()); return (executablePath.isValid() && version.isValid());
} }
bool Utils::ForeignApps::PythonInfo::isSupportedVersion() const bool Utils::ForeignApps::PythonInfo::isSupportedVersion() const
{ {
return (version >= Version {3, 9, 0}); return (version >= MINIMUM_SUPPORTED_VERSION);
} }
PythonInfo Utils::ForeignApps::pythonInfo() PythonInfo Utils::ForeignApps::pythonInfo()
{ {
static PythonInfo pyInfo; static PythonInfo pyInfo;
const QString preferredPythonPath = Preferences::instance()->getPythonExecutablePath().toString(); const Path preferredPythonPath = Preferences::instance()->getPythonExecutablePath();
if (pyInfo.isValid() && (preferredPythonPath == pyInfo.executableName)) if (pyInfo.isValid() && (preferredPythonPath == pyInfo.executablePath))
return pyInfo; return pyInfo;
const QString invalidVersionMessage = QCoreApplication::translate("Utils::ForeignApps"
, "Python failed to meet minimum version requirement. Path: \"%1\". Found version: \"%2\". Minimum supported version: \"%3\".");
if (!preferredPythonPath.isEmpty()) if (!preferredPythonPath.isEmpty())
{ {
if (testPythonInstallation(preferredPythonPath, pyInfo)) if (testPythonInstallation(preferredPythonPath, pyInfo))
{
if (pyInfo.isSupportedVersion())
return pyInfo; return pyInfo;
LogMsg(invalidVersionMessage.arg(pyInfo.executablePath.toString()
, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::WARNING);
}
else
{
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable. Path: \"%1\".") LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable. Path: \"%1\".")
.arg(preferredPythonPath), Log::WARNING); .arg(preferredPythonPath.toString()), Log::WARNING);
}
} }
else else
{ {
@ -279,24 +265,40 @@ PythonInfo Utils::ForeignApps::pythonInfo()
if (!pyInfo.isValid()) if (!pyInfo.isValid())
{ {
if (testPythonInstallation(u"python3"_s, pyInfo)) // search in `PATH` environment variable
const QString exeNames[] = {u"python3"_s, u"python"_s};
for (const QString &exeName : exeNames)
{
if (testPythonInstallation(Path(exeName), pyInfo))
{
if (pyInfo.isSupportedVersion())
return pyInfo; return pyInfo;
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python3` executable in PATH environment variable. PATH: \"%1\"")
.arg(qEnvironmentVariable("PATH")), Log::INFO);
if (testPythonInstallation(u"python"_s, pyInfo)) LogMsg(invalidVersionMessage.arg(pyInfo.executablePath.toString()
return pyInfo; , pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::INFO);
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python` executable in PATH environment variable. PATH: \"%1\"") }
.arg(qEnvironmentVariable("PATH")), Log::INFO); else
{
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `%1` executable in PATH environment variable. PATH: \"%2\"")
.arg(exeName, qEnvironmentVariable("PATH")), Log::INFO);
}
}
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
if (testPythonInstallation(findPythonPath(), pyInfo)) for (const Path &path : asConst(searchPythonPaths()))
{
if (testPythonInstallation(path, pyInfo))
{
if (pyInfo.isSupportedVersion())
return pyInfo; return pyInfo;
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find `python` executable in Windows Registry."), Log::INFO);
LogMsg(invalidVersionMessage.arg(pyInfo.executablePath.toString()
, pyInfo.version.toString(), PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()), Log::INFO);
}
}
#endif #endif
LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable"), Log::WARNING); LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Failed to find Python executable"), Log::WARNING);
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Mike Tzou * Copyright (C) 2018-2025 Mike Tzou (Chocobo1)
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -32,6 +32,7 @@
#include <QString> #include <QString>
#include "base/global.h" #include "base/global.h"
#include "base/path.h"
#include "base/utils/version.h" #include "base/utils/version.h"
namespace Utils::ForeignApps namespace Utils::ForeignApps
@ -45,8 +46,10 @@ namespace Utils::ForeignApps
bool isValid() const; bool isValid() const;
bool isSupportedVersion() const; bool isSupportedVersion() const;
QString executableName; Path executablePath;
Version version; Version version;
inline static const Version MINIMUM_SUPPORTED_VERSION {3, 9, 0};
}; };
PythonInfo pythonInfo(); PythonInfo pythonInfo();

View file

@ -42,7 +42,7 @@
uint32_t Utils::Random::rand(const uint32_t min, const uint32_t max) uint32_t Utils::Random::rand(const uint32_t min, const uint32_t max)
{ {
static RandomLayer layer; static const RandomLayer layer;
// new distribution is cheap: https://stackoverflow.com/a/19036349 // new distribution is cheap: https://stackoverflow.com/a/19036349
std::uniform_int_distribution<uint32_t> uniform(min, max); std::uniform_int_distribution<uint32_t> uniform(min, max);

View file

@ -27,6 +27,7 @@
*/ */
#include <cerrno> #include <cerrno>
#include <cstdio>
#include <cstring> #include <cstring>
#include <limits> #include <limits>
@ -44,6 +45,27 @@ namespace
RandomLayer() RandomLayer()
{ {
if (::getrandom(nullptr, 0, 0) < 0)
{
if (errno == ENOSYS)
{
// underlying kernel does not implement this system call
// fallback to `urandom`
m_randDev = fopen("/dev/urandom", "rb");
if (!m_randDev)
qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
}
else
{
qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
}
}
}
~RandomLayer()
{
if (m_randDev)
fclose(m_randDev);
} }
static constexpr result_type min() static constexpr result_type min()
@ -56,7 +78,15 @@ namespace
return std::numeric_limits<result_type>::max(); return std::numeric_limits<result_type>::max();
} }
result_type operator()() result_type operator()() const
{
if (!m_randDev)
return getRandomViaAPI();
return getRandomViaFile();
}
private:
result_type getRandomViaAPI() const
{ {
const int RETRY_MAX = 3; const int RETRY_MAX = 3;
@ -68,10 +98,21 @@ namespace
return buf; return buf;
if (result < 0) if (result < 0)
qFatal("getrandom() error. Reason: %s. Error code: %d.", std::strerror(errno), errno); qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
} }
qFatal("getrandom() failed. Reason: too many retries."); qFatal("getrandom() failed. Reason: too many retries.");
} }
result_type getRandomViaFile() const
{
result_type buf = 0;
if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
return buf;
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
}
FILE *m_randDev = nullptr;
}; };
} }

View file

@ -46,7 +46,7 @@ namespace
: m_randDev {fopen("/dev/urandom", "rb")} : m_randDev {fopen("/dev/urandom", "rb")}
{ {
if (!m_randDev) if (!m_randDev)
qFatal("Failed to open /dev/urandom. Reason: %s. Error code: %d.", std::strerror(errno), errno); qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
} }
~RandomLayer() ~RandomLayer()
@ -67,10 +67,10 @@ namespace
result_type operator()() const result_type operator()() const
{ {
result_type buf = 0; result_type buf = 0;
if (fread(&buf, sizeof(buf), 1, m_randDev) != 1) if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
qFatal("Read /dev/urandom error. Reason: %s. Error code: %d.", std::strerror(errno), errno);
return buf; return buf;
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
} }
private: private:

View file

@ -60,7 +60,7 @@ namespace
return std::numeric_limits<result_type>::max(); return std::numeric_limits<result_type>::max();
} }
result_type operator()() result_type operator()() const
{ {
result_type buf = 0; result_type buf = 0;
const bool result = m_processPrng(reinterpret_cast<PBYTE>(&buf), sizeof(buf)); const bool result = m_processPrng(reinterpret_cast<PBYTE>(&buf), sizeof(buf));

View file

@ -61,7 +61,7 @@ QString Utils::String::fromLocal8Bit(const std::string_view string)
QString Utils::String::wildcardToRegexPattern(const QString &pattern) QString Utils::String::wildcardToRegexPattern(const QString &pattern)
{ {
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 1))
return QRegularExpression::wildcardToRegularExpression(pattern return QRegularExpression::wildcardToRegularExpression(pattern
, (QRegularExpression::UnanchoredWildcardConversion | QRegularExpression::NonPathWildcardConversion)); , (QRegularExpression::UnanchoredWildcardConversion | QRegularExpression::NonPathWildcardConversion));
#else #else

View file

@ -32,12 +32,14 @@
#include <algorithm> #include <algorithm>
#include <functional> #include <functional>
#include <QtVersionChecks>
#include <QAction> #include <QAction>
#include <QByteArray> #include <QByteArray>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QFileDialog> #include <QFileDialog>
#include <QFuture>
#include <QList> #include <QList>
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
@ -242,14 +244,13 @@ public:
return QList<qreal>(filesCount(), 0); return QList<qreal>(filesCount(), 0);
} }
QList<qreal> availableFileFractions() const override QFuture<QList<qreal>> fetchAvailableFileFractions() const override
{ {
return QList<qreal>(filesCount(), 0); #if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 0))
} return QtFuture::makeReadyValueFuture(QList<qreal>(filesCount(), 0));
#else
void fetchAvailableFileFractions(std::function<void (QList<qreal>)> resultHandler) const override return QtFuture::makeReadyFuture(QList<qreal>(filesCount(), 0));
{ #endif
resultHandler(availableFileFractions());
} }
void prioritizeFiles(const QList<BitTorrent::DownloadPriority> &priorities) override void prioritizeFiles(const QList<BitTorrent::DownloadPriority> &priorities) override

View file

@ -64,7 +64,7 @@ namespace
QBITTORRENT_HEADER, QBITTORRENT_HEADER,
RESUME_DATA_STORAGE, RESUME_DATA_STORAGE,
TORRENT_CONTENT_REMOVE_OPTION, TORRENT_CONTENT_REMOVE_OPTION,
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
MEMORY_WORKING_SET_LIMIT, MEMORY_WORKING_SET_LIMIT,
#endif #endif
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
@ -209,7 +209,7 @@ void AdvancedSettings::saveAdvancedSettings() const
BitTorrent::Session *const session = BitTorrent::Session::instance(); BitTorrent::Session *const session = BitTorrent::Session::instance();
session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value<BitTorrent::ResumeDataStorageType>()); session->setResumeDataStorageType(m_comboBoxResumeDataStorage.currentData().value<BitTorrent::ResumeDataStorageType>());
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit // Physical memory (RAM) usage limit
app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value()); app()->setMemoryWorkingSetLimit(m_spinBoxMemoryWorkingSetLimit.value());
#endif #endif
@ -494,12 +494,11 @@ void AdvancedSettings::loadAdvancedSettings()
m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption()))); m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption())));
addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption); addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption);
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit // Physical memory (RAM) usage limit
m_spinBoxMemoryWorkingSetLimit.setMinimum(1); m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits<int>::max()); m_spinBoxMemoryWorkingSetLimit.setMaximum(std::numeric_limits<int>::max());
m_spinBoxMemoryWorkingSetLimit.setSuffix(tr(" MiB")); m_spinBoxMemoryWorkingSetLimit.setSuffix(tr(" MiB"));
m_spinBoxMemoryWorkingSetLimit.setToolTip(tr("This option is less effective on Linux"));
m_spinBoxMemoryWorkingSetLimit.setValue(app()->memoryWorkingSetLimit()); m_spinBoxMemoryWorkingSetLimit.setValue(app()->memoryWorkingSetLimit());
addRow(MEMORY_WORKING_SET_LIMIT, (tr("Physical memory (RAM) usage limit") + u' ' + makeLink(u"https://wikipedia.org/wiki/Working_set", u"(?)")) addRow(MEMORY_WORKING_SET_LIMIT, (tr("Physical memory (RAM) usage limit") + u' ' + makeLink(u"https://wikipedia.org/wiki/Working_set", u"(?)"))
, &m_spinBoxMemoryWorkingSetLimit); , &m_spinBoxMemoryWorkingSetLimit);

View file

@ -93,7 +93,7 @@ private:
QSpinBox m_spinBoxHashingThreads; QSpinBox m_spinBoxHashingThreads;
#endif #endif
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_LINUX) && !defined(Q_OS_MACOS)
QSpinBox m_spinBoxMemoryWorkingSetLimit; QSpinBox m_spinBoxMemoryWorkingSetLimit;
#endif #endif

View file

@ -1616,14 +1616,14 @@ void MainWindow::on_actionSearchWidget_triggered()
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Old Python Runtime") const QMessageBox::StandardButton buttonPressed = QMessageBox::question(this, tr("Old Python Runtime")
, tr("Your Python version (%1) is outdated. Minimum requirement: %2.\nDo you want to install a newer version now?") , tr("Your Python version (%1) is outdated. Minimum requirement: %2.\nDo you want to install a newer version now?")
.arg(pyInfo.version.toString(), u"3.9.0") .arg(pyInfo.version.toString(), Utils::ForeignApps::PythonInfo::MINIMUM_SUPPORTED_VERSION.toString())
, (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes); , (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
if (buttonPressed == QMessageBox::Yes) if (buttonPressed == QMessageBox::Yes)
installPython(); installPython();
#else #else
QMessageBox::information(this, tr("Old Python Runtime") QMessageBox::information(this, tr("Old Python Runtime")
, tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: %2.") , tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: %2.")
.arg(pyInfo.version.toString(), u"3.9.0")); .arg(pyInfo.version.toString(), Utils::ForeignApps::PythonInfo::MINIMUM_SUPPORTED_VERSION.toString()));
#endif #endif
return; return;
} }

View file

@ -1031,7 +1031,7 @@
<item> <item>
<widget class="QGroupBox" name="duplicateTorrentGroup"> <widget class="QGroupBox" name="duplicateTorrentGroup">
<property name="title"> <property name="title">
<string>When duplicate torrent is being added</string> <string>When adding a duplicate torrent</string>
</property> </property>
<layout class="QVBoxLayout" name="duplicateTorrentBoxLayout"> <layout class="QVBoxLayout" name="duplicateTorrentBoxLayout">
<item> <item>
@ -1047,7 +1047,7 @@
<item> <item>
<widget class="QCheckBox" name="checkConfirmMergeTrackers"> <widget class="QCheckBox" name="checkConfirmMergeTrackers">
<property name="text"> <property name="text">
<string>Ask for merging trackers when torrent is being added manually</string> <string>Ask to merge trackers for manually added torrent</string>
</property> </property>
<property name="checked"> <property name="checked">
<bool>true</bool> <bool>true</bool>
@ -3028,9 +3028,6 @@ Disable encryption: Only connect to peers without protocol encryption</string>
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="maximum">
<double>9998.000000000000000</double>
</property>
<property name="singleStep"> <property name="singleStep">
<double>0.050000000000000</double> <double>0.050000000000000</double>
</property> </property>

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -33,6 +33,7 @@
#include <QApplication> #include <QApplication>
#include <QClipboard> #include <QClipboard>
#include <QFuture>
#include <QHeaderView> #include <QHeaderView>
#include <QHostAddress> #include <QHostAddress>
#include <QList> #include <QList>
@ -406,10 +407,13 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
return; return;
using TorrentPtr = QPointer<const BitTorrent::Torrent>; using TorrentPtr = QPointer<const BitTorrent::Torrent>;
torrent->fetchPeerInfo([this, torrent = TorrentPtr(torrent)](const QList<BitTorrent::PeerInfo> &peers) torrent->fetchPeerInfo().then(this, [this, torrent = TorrentPtr(torrent)](const QList<BitTorrent::PeerInfo> &peers)
{
if (const BitTorrent::Torrent *currentTorrent = m_properties->getCurrentTorrent();
!currentTorrent || (currentTorrent != torrent))
{ {
if (torrent != m_properties->getCurrentTorrent())
return; return;
}
// Remove I2P peers since they will be completely reloaded. // Remove I2P peers since they will be completely reloaded.
for (const QStandardItem *item : asConst(m_I2PPeerItems)) for (const QStandardItem *item : asConst(m_I2PPeerItems))

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -32,6 +32,7 @@
#include <QClipboard> #include <QClipboard>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QFuture>
#include <QListWidgetItem> #include <QListWidgetItem>
#include <QMenu> #include <QMenu>
#include <QMessageBox> #include <QMessageBox>
@ -440,10 +441,10 @@ void PropertiesWidget::loadDynamicData()
// Update ratio info // Update ratio info
const qreal ratio = m_torrent->realRatio(); const qreal ratio = m_torrent->realRatio();
m_ui->labelShareRatioVal->setText(ratio > BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(ratio, 2)); m_ui->labelShareRatioVal->setText(ratio >= BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(ratio, 2));
const qreal popularity = m_torrent->popularity(); const qreal popularity = m_torrent->popularity();
m_ui->labelPopularityVal->setText(popularity > BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(popularity, 2)); m_ui->labelPopularityVal->setText(popularity >= BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(popularity, 2));
m_ui->labelSeedsVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)") m_ui->labelSeedsVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)")
.arg(QString::number(m_torrent->seedsCount()) .arg(QString::number(m_torrent->seedsCount())
@ -471,17 +472,18 @@ void PropertiesWidget::loadDynamicData()
if (m_torrent->hasMetadata()) if (m_torrent->hasMetadata())
{ {
using TorrentPtr = QPointer<BitTorrent::Torrent>;
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())); 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->isFinished() && !m_torrent->isStopped() && !m_torrent->isQueued() && !m_torrent->isChecking()) if (!m_torrent->isFinished() && !m_torrent->isStopped() && !m_torrent->isQueued() && !m_torrent->isChecking())
{ {
// Pieces availability // Pieces availability
showPiecesAvailability(true); showPiecesAvailability(true);
m_torrent->fetchPieceAvailability([this, torrent = TorrentPtr(m_torrent)](const QList<int> &pieceAvailability)
using TorrentPtr = QPointer<BitTorrent::Torrent>;
m_torrent->fetchPieceAvailability().then(this
, [this, torrent = TorrentPtr(m_torrent)](const QList<int> &pieceAvailability)
{ {
if (torrent == m_torrent) if (m_torrent && (m_torrent == torrent))
m_piecesAvailability->setAvailability(pieceAvailability); m_piecesAvailability->setAvailability(pieceAvailability);
}); });
@ -496,9 +498,11 @@ void PropertiesWidget::loadDynamicData()
qreal progress = m_torrent->progress() * 100.; qreal progress = m_torrent->progress() * 100.;
m_ui->labelProgressVal->setText(Utils::String::fromDouble(progress, 1) + u'%'); m_ui->labelProgressVal->setText(Utils::String::fromDouble(progress, 1) + u'%');
m_torrent->fetchDownloadingPieces([this, torrent = TorrentPtr(m_torrent)](const QBitArray &downloadingPieces) using TorrentPtr = QPointer<BitTorrent::Torrent>;
m_torrent->fetchDownloadingPieces().then(this
, [this, torrent = TorrentPtr(m_torrent)](const QBitArray &downloadingPieces)
{ {
if (torrent == m_torrent) if (m_torrent && (m_torrent == torrent))
m_downloadedPieces->setProgress(m_torrent->pieces(), downloadingPieces); m_downloadedPieces->setProgress(m_torrent->pieces(), downloadingPieces);
}); });
} }
@ -525,9 +529,9 @@ void PropertiesWidget::loadUrlSeeds()
return; return;
using TorrentPtr = QPointer<BitTorrent::Torrent>; using TorrentPtr = QPointer<BitTorrent::Torrent>;
m_torrent->fetchURLSeeds([this, torrent = TorrentPtr(m_torrent)](const QList<QUrl> &urlSeeds) m_torrent->fetchURLSeeds().then(this, [this, torrent = TorrentPtr(m_torrent)](const QList<QUrl> &urlSeeds)
{ {
if (torrent != m_torrent) if (!m_torrent || (m_torrent != torrent))
return; return;
m_ui->listWebSeeds->clear(); m_ui->listWebSeeds->clear();

View file

@ -333,7 +333,7 @@ void RSSWidget::on_newFeedButton_clicked()
// Ask for feed URL // Ask for feed URL
const QString clipText = qApp->clipboard()->text(); const QString clipText = qApp->clipboard()->text();
const QString defaultURL = Net::DownloadManager::hasSupportedScheme(clipText) ? clipText : u"http://"_s; const QString defaultURL = Net::DownloadManager::hasSupportedScheme(clipText) ? clipText : u"https://"_s;
RSS::Feed *newFeed = nullptr; RSS::Feed *newFeed = nullptr;
RSSFeedDialog dialog {this}; RSSFeedDialog dialog {this};
@ -623,58 +623,60 @@ void RSSWidget::renderArticle(const RSS::Article *article) const
{ {
Q_ASSERT(article); Q_ASSERT(article);
const QString articleLink = article->link();
const QString highlightedBaseColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::Highlight).name(); const QString highlightedBaseColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::Highlight).name();
const QString highlightedBaseTextColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::HighlightedText).name(); const QString highlightedBaseTextColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::HighlightedText).name();
const QString alternateBaseColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::AlternateBase).name(); const QString alternateBaseColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::AlternateBase).name();
QString html = QString html = u"<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>"
u"<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>" + + u"<div style='background-color: \"%1\"; font-weight: bold; color: \"%2\";'>%3</div>"_s.arg(highlightedBaseColor, highlightedBaseTextColor, article->title());
u"<div style='background-color: \"%1\"; font-weight: bold; color: \"%2\";'>%3</div>"_s.arg(highlightedBaseColor, highlightedBaseTextColor, article->title()); if (const QDateTime articleDate = article->date(); articleDate.isValid())
if (article->date().isValid()) html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Date: "), QLocale::system().toString(articleDate.toLocalTime(), QLocale::ShortFormat));
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Date: "), QLocale::system().toString(article->date().toLocalTime()));
if (m_ui->feedListWidget->currentItem() == m_ui->feedListWidget->stickyUnreadItem()) if (m_ui->feedListWidget->currentItem() == m_ui->feedListWidget->stickyUnreadItem())
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Feed: "), article->feed()->title()); html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Feed: "), article->feed()->title());
if (!article->author().isEmpty()) if (const QString articleAuthor = article->author(); !articleAuthor.isEmpty())
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Author: "), article->author()); html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Author: "), articleAuthor);
if (!articleLink.isEmpty())
html += u"<div style='background-color: \"%1\";'><a href='%2' target='_blank'><b>%3</b></a></div>"_s.arg(alternateBaseColor, articleLink, tr("Open link"));
html += u"</div>" html += u"</div>"
u"<div style='margin-left: 5px; margin-right: 5px;'>"; u"<div style='margin-left: 5px; margin-right: 5px;'>";
if (Qt::mightBeRichText(article->description())) if (QString description = article->description(); Qt::mightBeRichText(description))
{ {
html += article->description(); html += description;
} }
else else
{ {
QString description = article->description();
QRegularExpression rx; QRegularExpression rx;
// If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice // If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice
rx.setPatternOptions(QRegularExpression::InvertedGreedinessOption rx.setPatternOptions(QRegularExpression::InvertedGreedinessOption
| QRegularExpression::CaseInsensitiveOption); | QRegularExpression::CaseInsensitiveOption);
rx.setPattern(u"\\[img\\](.+)\\[/img\\]"_s); rx.setPattern(u"\\[img\\](.+)\\[/img\\]"_s);
description = description.replace(rx, u"<img src=\"\\1\">"_s); description.replace(rx, u"<img src=\"\\1\">"_s);
rx.setPattern(u"\\[url=(\")?(.+)\\1\\]"_s); rx.setPattern(u"\\[url=(\")?(.+)\\1\\]"_s);
description = description.replace(rx, u"<a href=\"\\2\">"_s); description.replace(rx, u"<a href=\"\\2\">"_s)
description = description.replace(u"[/url]"_s, u"</a>"_s, Qt::CaseInsensitive); .replace(u"[/url]"_s, u"</a>"_s, Qt::CaseInsensitive);
rx.setPattern(u"\\[(/)?([bius])\\]"_s); rx.setPattern(u"\\[(/)?([bius])\\]"_s);
description = description.replace(rx, u"<\\1\\2>"_s); description.replace(rx, u"<\\1\\2>"_s);
rx.setPattern(u"\\[color=(\")?(.+)\\1\\]"_s); rx.setPattern(u"\\[color=(\")?(.+)\\1\\]"_s);
description = description.replace(rx, u"<span style=\"color:\\2\">"_s); description.replace(rx, u"<span style=\"color:\\2\">"_s)
description = description.replace(u"[/color]"_s, u"</span>"_s, Qt::CaseInsensitive); .replace(u"[/color]"_s, u"</span>"_s, Qt::CaseInsensitive);
rx.setPattern(u"\\[size=(\")?(.+)\\d\\1\\]"_s); rx.setPattern(u"\\[size=(\")?(.+)\\d\\1\\]"_s);
description = description.replace(rx, u"<span style=\"font-size:\\2px\">"_s); description.replace(rx, u"<span style=\"font-size:\\2px\">"_s)
description = description.replace(u"[/size]"_s, u"</span>"_s, Qt::CaseInsensitive); .replace(u"[/size]"_s, u"</span>"_s, Qt::CaseInsensitive);
html += u"<pre>" + description + u"</pre>"; html += u"<pre>" + description + u"</pre>";
} }
html += u"</div>";
// Supplement relative URLs to absolute ones // Supplement relative URLs to absolute ones
const QUrl url {article->link()}; const QUrl url {articleLink};
const QString baseUrl = url.toString(QUrl::RemovePath | QUrl::RemoveQuery); const QString baseUrl = url.toString(QUrl::RemovePath | QUrl::RemoveQuery);
convertRelativeUrlToAbsolute(html, baseUrl); convertRelativeUrlToAbsolute(html, baseUrl);
html += u"</div>";
m_ui->textBrowser->setHtml(html); m_ui->textBrowser->setHtml(html);
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006-2012 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006-2012 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -33,6 +33,7 @@
#include <QFileIconProvider> #include <QFileIconProvider>
#include <QFileInfo> #include <QFileInfo>
#include <QFuture>
#include <QIcon> #include <QIcon>
#include <QMimeData> #include <QMimeData>
#include <QPointer> #include <QPointer>
@ -219,18 +220,14 @@ void TorrentContentModel::updateFilesAvailability()
Q_ASSERT(m_contentHandler && m_contentHandler->hasMetadata()); Q_ASSERT(m_contentHandler && m_contentHandler->hasMetadata());
using HandlerPtr = QPointer<BitTorrent::TorrentContentHandler>; using HandlerPtr = QPointer<BitTorrent::TorrentContentHandler>;
m_contentHandler->fetchAvailableFileFractions([this, handler = HandlerPtr(m_contentHandler)](const QList<qreal> &availableFileFractions) m_contentHandler->fetchAvailableFileFractions().then(this
, [this, handler = HandlerPtr(m_contentHandler)](const QList<qreal> &availableFileFractions)
{ {
if (handler != m_contentHandler) if (!m_contentHandler || (m_contentHandler != handler))
return;
Q_ASSERT(m_filesIndex.size() == availableFileFractions.size());
// XXX: Why is this necessary?
if (m_filesIndex.size() != availableFileFractions.size()) [[unlikely]]
return; return;
for (int i = 0; i < m_filesIndex.size(); ++i) for (int i = 0; i < m_filesIndex.size(); ++i)
m_filesIndex[i]->setAvailability(availableFileFractions[i]); m_filesIndex[i]->setAvailability(availableFileFractions.value(i, 0));
// Update folders progress in the tree // Update folders progress in the tree
m_rootItem->recalculateProgress(); m_rootItem->recalculateProgress();
}); });

View file

@ -47,9 +47,6 @@
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="maximum">
<double>9998.000000000000000</double>
</property>
<property name="singleStep"> <property name="singleStep">
<double>0.050000000000000</double> <double>0.050000000000000</double>
</property> </property>

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -42,6 +42,7 @@
#include <QColor> #include <QColor>
#include <QDateTime> #include <QDateTime>
#include <QFuture>
#include <QList> #include <QList>
#include <QPointer> #include <QPointer>
#include <QScopeGuard> #include <QScopeGuard>
@ -309,9 +310,9 @@ void TrackerListModel::populate()
m_items->emplace_back(std::make_shared<Item>(u"** [LSD] **", privateTorrentMessage)); m_items->emplace_back(std::make_shared<Item>(u"** [LSD] **", privateTorrentMessage));
using TorrentPtr = QPointer<const BitTorrent::Torrent>; using TorrentPtr = QPointer<const BitTorrent::Torrent>;
m_torrent->fetchPeerInfo([this, torrent = TorrentPtr(m_torrent)](const QList<BitTorrent::PeerInfo> &peers) m_torrent->fetchPeerInfo().then(this, [this, torrent = TorrentPtr(m_torrent)](const QList<BitTorrent::PeerInfo> &peers)
{ {
if (torrent != m_torrent) if (!m_torrent || (m_torrent != torrent))
return; return;
// XXX: libtorrent should provide this info... // XXX: libtorrent should provide this info...

View file

@ -293,7 +293,7 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
if (hideValues && (value <= 0)) if (hideValues && (value <= 0))
return {}; return {};
return ((static_cast<int>(value) == -1) || (value > BitTorrent::Torrent::MAX_RATIO)) return ((static_cast<int>(value) == -1) || (value >= BitTorrent::Torrent::MAX_RATIO))
? C_INFINITY : Utils::String::fromDouble(value, 2); ? C_INFINITY : Utils::String::fromDouble(value, 2);
}; };

View file

@ -176,9 +176,7 @@ void Utils::Gui::openFolderSelect(const Path &path)
const int lineMaxLength = 64; const int lineMaxLength = 64;
QProcess proc; QProcess proc;
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors); proc.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif
proc.start(u"xdg-mime"_s, {u"query"_s, u"default"_s, u"inode/directory"_s}); proc.start(u"xdg-mime"_s, {u"query"_s, u"default"_s, u"inode/directory"_s});
proc.waitForFinished(); proc.waitForFinished();
const auto output = QString::fromLocal8Bit(proc.readLine(lineMaxLength).simplified()); const auto output = QString::fromLocal8Bit(proc.readLine(lineMaxLength).simplified());

View file

@ -96,7 +96,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent)
const auto adjustRatio = [](const qreal ratio) -> qreal const auto adjustRatio = [](const qreal ratio) -> qreal
{ {
return (ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio; return (ratio >= BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio;
}; };
const auto getLastActivityTime = [&torrent]() -> qlonglong const auto getLastActivityTime = [&torrent]() -> qlonglong

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2018-2025 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -28,6 +28,7 @@
#include "synccontroller.h" #include "synccontroller.h"
#include <QFuture>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QMetaObject> #include <QMetaObject>
@ -745,7 +746,7 @@ void SyncController::torrentPeersAction()
QVariantMap data; QVariantMap data;
QVariantHash peers; QVariantHash peers;
const QList<BitTorrent::PeerInfo> peersList = torrent->peers(); const QList<BitTorrent::PeerInfo> peersList = torrent->fetchPeerInfo().takeResult();
bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries(); bool resolvePeerCountries = Preferences::instance()->resolvePeerCountries();

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2018-2025 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -32,6 +32,7 @@
#include <functional> #include <functional>
#include <QBitArray> #include <QBitArray>
#include <QFuture>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
#include <QList> #include <QList>
@ -180,9 +181,11 @@ namespace
QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent) QJsonArray getStickyTrackers(const BitTorrent::Torrent *const torrent)
{ {
int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0; int seedsDHT = 0, seedsPeX = 0, seedsLSD = 0, leechesDHT = 0, leechesPeX = 0, leechesLSD = 0;
for (const BitTorrent::PeerInfo &peer : asConst(torrent->peers())) const QList<BitTorrent::PeerInfo> peersList = torrent->fetchPeerInfo().takeResult();
for (const BitTorrent::PeerInfo &peer : peersList)
{ {
if (peer.isConnecting()) continue; if (peer.isConnecting())
continue;
if (peer.isSeed()) if (peer.isSeed())
{ {
@ -520,8 +523,8 @@ void TorrentsController::propertiesAction()
{KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()}, {KEY_PROP_SEEDS_TOTAL, torrent->totalSeedsCount()},
{KEY_PROP_PEERS, torrent->leechsCount()}, {KEY_PROP_PEERS, torrent->leechsCount()},
{KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()}, {KEY_PROP_PEERS_TOTAL, torrent->totalLeechersCount()},
{KEY_PROP_RATIO, ((ratio > BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)}, {KEY_PROP_RATIO, ((ratio >= BitTorrent::Torrent::MAX_RATIO) ? -1 : ratio)},
{KEY_PROP_POPULARITY, ((popularity > BitTorrent::Torrent::MAX_RATIO) ? -1 : popularity)}, {KEY_PROP_POPULARITY, ((popularity >= BitTorrent::Torrent::MAX_RATIO) ? -1 : popularity)},
{KEY_PROP_REANNOUNCE, torrent->nextAnnounce()}, {KEY_PROP_REANNOUNCE, torrent->nextAnnounce()},
{KEY_PROP_TOTAL_SIZE, torrent->totalSize()}, {KEY_PROP_TOTAL_SIZE, torrent->totalSize()},
{KEY_PROP_PIECES_NUM, torrent->piecesCount()}, {KEY_PROP_PIECES_NUM, torrent->piecesCount()},
@ -727,7 +730,7 @@ void TorrentsController::filesAction()
{ {
const QList<BitTorrent::DownloadPriority> priorities = torrent->filePriorities(); const QList<BitTorrent::DownloadPriority> priorities = torrent->filePriorities();
const QList<qreal> fp = torrent->filesProgress(); const QList<qreal> fp = torrent->filesProgress();
const QList<qreal> fileAvailability = torrent->availableFileFractions(); const QList<qreal> fileAvailability = torrent->fetchAvailableFileFractions().takeResult();
const BitTorrent::TorrentInfo info = torrent->info(); const BitTorrent::TorrentInfo info = torrent->info();
for (const int index : asConst(fileIndexes)) for (const int index : asConst(fileIndexes))
{ {
@ -796,7 +799,7 @@ void TorrentsController::pieceStatesAction()
for (int i = 0; i < states.size(); ++i) for (int i = 0; i < states.size(); ++i)
pieceStates.append(static_cast<int>(states[i]) * 2); pieceStates.append(static_cast<int>(states[i]) * 2);
const QBitArray dlstates = torrent->downloadingPieces(); const QBitArray dlstates = torrent->fetchDownloadingPieces().takeResult();
for (int i = 0; i < states.size(); ++i) for (int i = 0; i < states.size(); ++i)
{ {
if (dlstates[i]) if (dlstates[i])

View file

@ -34,12 +34,15 @@ export default [
"no-undef": "off", "no-undef": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-var": "error", "no-var": "error",
"object-shorthand": ["error", "consistent"],
"operator-assignment": "error", "operator-assignment": "error",
"prefer-arrow-callback": "error", "prefer-arrow-callback": "error",
"prefer-const": "error", "prefer-const": "error",
"prefer-template": "error", "prefer-template": "error",
"radix": "error", "radix": "error",
"require-await": "error",
"PreferArrowFunctions/prefer-arrow-functions": "error", "PreferArrowFunctions/prefer-arrow-functions": "error",
"Stylistic/no-extra-semi": "error",
"Stylistic/no-mixed-operators": [ "Stylistic/no-mixed-operators": [
"error", "error",
{ {

View file

@ -12,7 +12,7 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Escape": case "Escape":
@ -26,13 +26,13 @@
if (hash === null) if (hash === null)
return; return;
$("peers").focus(); document.getElementById("peers").focus();
$("addPeersOk").addEventListener("click", (e) => { document.getElementById("addPeersOk").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const peers = $("peers").value.trim().split(/[\r\n]+/); const peers = document.getElementById("peers").value.trim().split(/[\r\n]+/);
if (peers.length === 0) if (peers.length === 0)
return; return;

View file

@ -12,7 +12,7 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Escape": case "Escape":
@ -22,8 +22,8 @@
} }
}); });
$("trackersUrls").focus(); document.getElementById("trackersUrls").focus();
$("addTrackersButton").addEventListener("click", (e) => { document.getElementById("addTrackersButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -31,7 +31,7 @@
method: "POST", method: "POST",
body: new URLSearchParams({ body: new URLSearchParams({
hash: new URLSearchParams(window.location.search).get("hash"), hash: new URLSearchParams(window.location.search).get("hash"),
urls: $("trackersUrls").value urls: document.getElementById("trackersUrls").value
}) })
}) })
.then((response) => { .then((response) => {

View file

@ -12,7 +12,7 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Escape": case "Escape":
@ -22,15 +22,15 @@
} }
}); });
$("urls").focus(); document.getElementById("urls").focus();
$("addWebSeedsButton").addEventListener("click", (e) => { document.getElementById("addWebSeedsButton").addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
fetch("api/v2/torrents/addWebSeeds", { fetch("api/v2/torrents/addWebSeeds", {
method: "POST", method: "POST",
body: new URLSearchParams({ body: new URLSearchParams({
hash: new URLSearchParams(window.location.search).get("hash"), hash: new URLSearchParams(window.location.search).get("hash"),
urls: $("urls").value.split("\n").map(w => encodeURIComponent(w.trim())).filter(w => (w.length > 0)).join("|") urls: document.getElementById("urls").value.split("\n").map(w => encodeURIComponent(w.trim())).filter(w => (w.length > 0)).join("|")
}) })
}) })
.then((response) => { .then((response) => {

View file

@ -12,14 +12,14 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
$("cancelBtn").focus(); document.getElementById("cancelBtn").focus();
$("cancelBtn").addEventListener("click", (e) => { document.getElementById("cancelBtn").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
window.parent.qBittorrent.Client.closeFrameWindow(window); window.parent.qBittorrent.Client.closeFrameWindow(window);
}); });
$("confirmBtn").addEventListener("click", (e) => { document.getElementById("confirmBtn").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View file

@ -12,14 +12,14 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
$("cancelBtn").focus(); document.getElementById("cancelBtn").focus();
$("cancelBtn").addEventListener("click", (e) => { document.getElementById("cancelBtn").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
window.parent.qBittorrent.Client.closeFrameWindow(window); window.parent.qBittorrent.Client.closeFrameWindow(window);
}); });
$("confirmBtn").addEventListener("click", (e) => { document.getElementById("confirmBtn").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View file

@ -12,14 +12,14 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
$("cancelBtn").focus(); document.getElementById("cancelBtn").focus();
$("cancelBtn").addEventListener("click", (e) => { document.getElementById("cancelBtn").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
window.parent.qBittorrent.Client.closeFrameWindow(window); window.parent.qBittorrent.Client.closeFrameWindow(window);
}); });
$("confirmBtn").addEventListener("click", (e) => { document.getElementById("confirmBtn").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();

View file

@ -12,18 +12,18 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const host = searchParams.get("host"); const host = searchParams.get("host");
$("confirmDeleteTrackerText").textContent = "QBT_TR(Are you sure you want to remove tracker %1 from all torrents?)QBT_TR[CONTEXT=TrackersFilterWidget]".replace("%1", host); document.getElementById("confirmDeleteTrackerText").textContent = "QBT_TR(Are you sure you want to remove tracker %1 from all torrents?)QBT_TR[CONTEXT=TrackersFilterWidget]".replace("%1", host);
$("cancelBtn").focus(); document.getElementById("cancelBtn").focus();
$("cancelBtn").addEventListener("click", (e) => { document.getElementById("cancelBtn").addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
window.parent.qBittorrent.Client.closeFrameWindow(window); window.parent.qBittorrent.Client.closeFrameWindow(window);
}); });
$("confirmBtn").addEventListener("click", (e) => { document.getElementById("confirmBtn").addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
fetch("api/v2/torrents/removeTrackers", { fetch("api/v2/torrents/removeTrackers", {

View file

@ -755,19 +755,6 @@ td.generalLabel {
} }
} }
.piecesbarWrapper {
position: relative;
width: 100%;
}
.piecesbarCanvas {
height: 100%;
image-rendering: pixelated;
inset: 0;
position: absolute;
width: 100%;
}
#watched_folders_tab { #watched_folders_tab {
border-collapse: collapse; border-collapse: collapse;
} }

View file

@ -167,22 +167,22 @@
if (encodedUrls !== null) { if (encodedUrls !== null) {
const urls = encodedUrls.split("|").map(decodeURIComponent); const urls = encodedUrls.split("|").map(decodeURIComponent);
if (urls.length > 0) if (urls.length > 0)
$("urls").value = urls.join("\n"); document.getElementById("urls").value = urls.join("\n");
} }
let submitted = false; let submitted = false;
$("downloadForm").addEventListener("submit", () => { document.getElementById("downloadForm").addEventListener("submit", (event) => {
$("startTorrentHidden").value = $("startTorrent").checked ? "false" : "true"; document.getElementById("startTorrentHidden").value = document.getElementById("startTorrent").checked ? "false" : "true";
$("dlLimitHidden").value = Number($("dlLimitText").value) * 1024; document.getElementById("dlLimitHidden").value = Number(document.getElementById("dlLimitText").value) * 1024;
$("upLimitHidden").value = Number($("upLimitText").value) * 1024; document.getElementById("upLimitHidden").value = Number(document.getElementById("upLimitText").value) * 1024;
$("download_spinner").style.display = "block"; document.getElementById("download_spinner").style.display = "block";
submitted = true; submitted = true;
}); });
$("download_frame").addEventListener("load", () => { document.getElementById("download_frame").addEventListener("load", (event) => {
if (submitted) if (submitted)
window.parent.qBittorrent.Client.closeFrameWindow(window); window.parent.qBittorrent.Client.closeFrameWindow(window);
}); });

View file

@ -37,7 +37,7 @@
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("applyButton").click(); document.getElementById("applyButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -48,7 +48,7 @@
const hashes = new URLSearchParams(window.location.search).get("hashes").split("|"); const hashes = new URLSearchParams(window.location.search).get("hashes").split("|");
const setDlLimit = () => { const setDlLimit = () => {
const limit = Number($("dllimitUpdatevalue").value) * 1024; const limit = Number(document.getElementById("dllimitUpdatevalue").value) * 1024;
if (hashes[0] === "global") { if (hashes[0] === "global") {
fetch("api/v2/transfer/setDownloadLimit", { fetch("api/v2/transfer/setDownloadLimit", {
method: "POST", method: "POST",
@ -56,7 +56,7 @@
limit: limit limit: limit
}) })
}) })
.then(async (response) => { .then((response) => {
if (!response.ok) if (!response.ok)
return; return;
@ -72,7 +72,7 @@
limit: limit limit: limit
}) })
}) })
.then(async (response) => { .then((response) => {
if (!response.ok) if (!response.ok)
return; return;
@ -81,7 +81,7 @@
} }
}; };
$("dllimitUpdatevalue").focus(); document.getElementById("dllimitUpdatevalue").focus();
MochaUI.addDlLimitSlider(hashes); MochaUI.addDlLimitSlider(hashes);
</script> </script>

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("submitButton").click(); document.getElementById("submitButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -30,16 +30,16 @@
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const currentUrl = searchParams.get("url"); const currentUrl = searchParams.get("url");
$("url").value = currentUrl; document.getElementById("url").value = currentUrl;
$("url").focus(); document.getElementById("url").focus();
$("url").setSelectionRange(0, currentUrl.length); document.getElementById("url").setSelectionRange(0, currentUrl.length);
$("submitButton").addEventListener("click", (e) => { document.getElementById("submitButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const newUrl = $("url").value.trim(); const newUrl = document.getElementById("url").value.trim();
if (newUrl === "") { if (newUrl === "") {
alert("QBT_TR(URL cannot be empty)QBT_TR[CONTEXT=RSSWidget]"); alert("QBT_TR(URL cannot be empty)QBT_TR[CONTEXT=RSSWidget]");
return; return;
@ -50,7 +50,7 @@
return; return;
} }
$("submitButton").disabled = true; document.getElementById("submitButton").disabled = true;
fetch("api/v2/rss/setFeedURL", { fetch("api/v2/rss/setFeedURL", {
method: "POST", method: "POST",
@ -64,7 +64,7 @@
alert((response.status === 409) alert((response.status === 409)
? await response.text() ? await response.text()
: "QBT_TR(Unable to update URL)QBT_TR[CONTEXT=RSSWidget]"); : "QBT_TR(Unable to update URL)QBT_TR[CONTEXT=RSSWidget]");
$("submitButton").disabled = false; document.getElementById("submitButton").disabled = false;
return; return;
} }

View file

@ -12,12 +12,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("editTrackerButton").click(); document.getElementById("editTrackerButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -31,10 +31,10 @@
if (currentUrl === null) if (currentUrl === null)
return; return;
$("trackerUrl").value = currentUrl; document.getElementById("trackerUrl").value = currentUrl;
$("trackerUrl").focus(); document.getElementById("trackerUrl").focus();
$("editTrackerButton").addEventListener("click", (e) => { document.getElementById("editTrackerButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -43,7 +43,7 @@
body: new URLSearchParams({ body: new URLSearchParams({
hash: searchParams.get("hash"), hash: searchParams.get("hash"),
origUrl: currentUrl, origUrl: currentUrl,
newUrl: $("trackerUrl").value newUrl: document.getElementById("trackerUrl").value
}) })
}) })
.then((response) => { .then((response) => {

View file

@ -12,12 +12,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("editWebSeedButton").click(); document.getElementById("editWebSeedButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -28,10 +28,10 @@
const searchParams = new URLSearchParams(window.location.search); const searchParams = new URLSearchParams(window.location.search);
const origUrl = searchParams.get("url"); const origUrl = searchParams.get("url");
$("url").value = decodeURIComponent(origUrl); document.getElementById("url").value = decodeURIComponent(origUrl);
$("url").focus(); document.getElementById("url").focus();
$("editWebSeedButton").addEventListener("click", (e) => { document.getElementById("editWebSeedButton").addEventListener("click", (e) => {
e.stopPropagation(); e.stopPropagation();
fetch("api/v2/torrents/editWebSeed", { fetch("api/v2/torrents/editWebSeed", {
@ -39,7 +39,7 @@
body: new URLSearchParams({ body: new URLSearchParams({
hash: searchParams.get("hash"), hash: searchParams.get("hash"),
origUrl: origUrl, origUrl: origUrl,
newUrl: encodeURIComponent($("url").value.trim()) newUrl: encodeURIComponent(document.getElementById("url").value.trim())
}) })
}) })
.then((response) => { .then((response) => {

View file

@ -27,6 +27,7 @@
<script defer src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script defer src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script defer src="scripts/lib/mocha.min.js"></script> <script defer src="scripts/lib/mocha.min.js"></script>
<script defer src="scripts/lib/jszip.min.js"></script> <script defer src="scripts/lib/jszip.min.js"></script>
<script defer src="scripts/monkeypatch.js?v=${CACHEID}"></script>
<script defer src="scripts/cache.js?v=${CACHEID}"></script> <script defer src="scripts/cache.js?v=${CACHEID}"></script>
<script defer src="scripts/localpreferences.js?v=${CACHEID}"></script> <script defer src="scripts/localpreferences.js?v=${CACHEID}"></script>
<script defer src="scripts/color-scheme.js?v=${CACHEID}"></script> <script defer src="scripts/color-scheme.js?v=${CACHEID}"></script>

View file

@ -14,12 +14,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("categoryNameButton").click(); document.getElementById("categoryNameButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -38,25 +38,25 @@
if (!uriCategoryName) if (!uriCategoryName)
return; return;
$("categoryName").disabled = true; document.getElementById("categoryName").disabled = true;
$("categoryName").value = window.qBittorrent.Misc.escapeHtml(uriCategoryName); document.getElementById("categoryName").value = window.qBittorrent.Misc.escapeHtml(uriCategoryName);
$("savePath").value = window.qBittorrent.Misc.escapeHtml(uriSavePath); document.getElementById("savePath").value = window.qBittorrent.Misc.escapeHtml(uriSavePath);
$("savePath").focus(); document.getElementById("savePath").focus();
} }
else if (uriAction === "createSubcategory") { else if (uriAction === "createSubcategory") {
$("categoryName").value = window.qBittorrent.Misc.escapeHtml(uriCategoryName); document.getElementById("categoryName").value = window.qBittorrent.Misc.escapeHtml(uriCategoryName);
$("categoryName").focus(); document.getElementById("categoryName").focus();
} }
else { else {
$("categoryName").focus(); document.getElementById("categoryName").focus();
} }
$("categoryNameButton").addEventListener("click", (e) => { document.getElementById("categoryNameButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const savePath = $("savePath").value.trim(); const savePath = document.getElementById("savePath").value.trim();
const categoryName = $("categoryName").value.trim(); const categoryName = document.getElementById("categoryName").value.trim();
const verifyCategoryName = (name) => { const verifyCategoryName = (name) => {
if ((name === null) || (name === "")) if ((name === null) || (name === ""))

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("submitButton").click(); document.getElementById("submitButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -27,19 +27,19 @@
} }
}); });
$("feedURL").focus(); document.getElementById("feedURL").focus();
$("submitButton").addEventListener("click", (e) => { document.getElementById("submitButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const feedURL = $("feedURL").value.trim(); const feedURL = document.getElementById("feedURL").value.trim();
if (feedURL === "") { if (feedURL === "") {
alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]");
return; return;
} }
$("submitButton").disabled = true; document.getElementById("submitButton").disabled = true;
const path = new URLSearchParams(window.location.search).get("path"); const path = new URLSearchParams(window.location.search).get("path");
fetch("api/v2/rss/addFeed", { fetch("api/v2/rss/addFeed", {
@ -53,7 +53,7 @@
if (!response.ok) { if (!response.ok) {
if (response.status === 409) if (response.status === 409)
alert(await response.text()); alert(await response.text());
$("submitButton").disabled = false; document.getElementById("submitButton").disabled = false;
return; return;
} }

View file

@ -14,12 +14,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("submitButton").click(); document.getElementById("submitButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -28,19 +28,19 @@
} }
}); });
$("folderName").focus(); document.getElementById("folderName").focus();
$("submitButton").addEventListener("click", (e) => { document.getElementById("submitButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const folderName = $("folderName").value.trim(); const folderName = document.getElementById("folderName").value.trim();
if (folderName === "") { if (folderName === "") {
alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]");
return; return;
} }
$("submitButton").disabled = true; document.getElementById("submitButton").disabled = true;
const path = new URLSearchParams(window.location.search).get("path"); const path = new URLSearchParams(window.location.search).get("path");
fetch("api/v2/rss/addFolder", { fetch("api/v2/rss/addFolder", {
@ -53,7 +53,7 @@
if (!response.ok) { if (!response.ok) {
if (response.status === 409) if (response.status === 409)
alert(await response.text()); alert(await response.text());
$("submitButton").disabled = false; document.getElementById("submitButton").disabled = false;
return; return;
} }

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("submitButton").click(); document.getElementById("submitButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -27,18 +27,18 @@
} }
}); });
$("name").focus(); document.getElementById("name").focus();
$("submitButton").addEventListener("click", (e) => { document.getElementById("submitButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const name = $("name").value.trim(); const name = document.getElementById("name").value.trim();
if (name === "") { if (name === "") {
alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]");
return; return;
} }
$("submitButton").disabled = true; document.getElementById("submitButton").disabled = true;
fetch("api/v2/rss/setRule", { fetch("api/v2/rss/setRule", {
method: "POST", method: "POST",

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("tagNameButton").click(); document.getElementById("tagNameButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -31,15 +31,15 @@
const uriAction = window.qBittorrent.Misc.safeTrim(searchParams.get("action")); const uriAction = window.qBittorrent.Misc.safeTrim(searchParams.get("action"));
if (uriAction === "create") if (uriAction === "create")
$("legendText").textContent = "QBT_TR(Tag:)QBT_TR[CONTEXT=TagFilterWidget]"; document.getElementById("legendText").textContent = "QBT_TR(Tag:)QBT_TR[CONTEXT=TagFilterWidget]";
$("tagName").focus(); document.getElementById("tagName").focus();
$("tagNameButton").addEventListener("click", (e) => { document.getElementById("tagNameButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const tagName = $("tagName").value.trim(); const tagName = document.getElementById("tagName").value.trim();
const verifyTagName = (name) => { const verifyTagName = (name) => {
if ((name === null) || (name === "")) if ((name === null) || (name === ""))
@ -64,7 +64,7 @@
tags: tagName tags: tagName
}) })
}) })
.then(async (response) => { .then((response) => {
if (!response.ok) if (!response.ok)
return; return;
@ -83,7 +83,7 @@
tags: tagName tags: tagName
}) })
}) })
.then(async (response) => { .then((response) => {
if (!response.ok) if (!response.ok)
return; return;

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("renameButton").click(); document.getElementById("renameButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -31,15 +31,15 @@
const name = searchParams.get("name"); const name = searchParams.get("name");
// set text field to current value // set text field to current value
if (name !== null) if (name !== null)
$("rename").value = name; document.getElementById("rename").value = name;
$("rename").focus(); document.getElementById("rename").focus();
$("renameButton").addEventListener("click", (e) => { document.getElementById("renameButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const name = $("rename").value.trim(); const name = document.getElementById("rename").value.trim();
if ((name === null) || (name === "")) if ((name === null) || (name === ""))
return; return;

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("renameButton").click(); document.getElementById("renameButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -29,16 +29,16 @@
const oldPath = new URLSearchParams(window.location.search).get("oldPath"); const oldPath = new URLSearchParams(window.location.search).get("oldPath");
$("rename").value = oldPath; document.getElementById("rename").value = oldPath;
$("rename").focus(); document.getElementById("rename").focus();
$("rename").setSelectionRange(0, oldPath.length); document.getElementById("rename").setSelectionRange(0, oldPath.length);
$("renameButton").addEventListener("click", (e) => { document.getElementById("renameButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const newPath = $("rename").value.trim(); const newPath = document.getElementById("rename").value.trim();
if (newPath === "") { if (newPath === "") {
alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]");
return; return;
@ -49,7 +49,7 @@
return; return;
} }
$("renameButton").disabled = true; document.getElementById("renameButton").disabled = true;
fetch("api/v2/rss/moveItem", { fetch("api/v2/rss/moveItem", {
method: "POST", method: "POST",
@ -62,7 +62,7 @@
if (!response.ok) { if (!response.ok) {
if (response.status === 409) if (response.status === 409)
alert(await response.text()); alert(await response.text());
$("renameButton").disabled = false; document.getElementById("renameButton").disabled = false;
return; return;
} }

View file

@ -14,12 +14,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("renameButton").click(); document.getElementById("renameButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -34,17 +34,17 @@
const isFolder = ((searchParams.get("isFolder")) === "true"); const isFolder = ((searchParams.get("isFolder")) === "true");
const oldName = window.qBittorrent.Filesystem.fileName(oldPath); const oldName = window.qBittorrent.Filesystem.fileName(oldPath);
$("rename").value = oldName; document.getElementById("rename").value = oldName;
$("rename").focus(); document.getElementById("rename").focus();
if (!isFolder) if (!isFolder)
$("rename").setSelectionRange(0, oldName.lastIndexOf(".")); document.getElementById("rename").setSelectionRange(0, oldName.lastIndexOf("."));
$("renameButton").addEventListener("click", (e) => { document.getElementById("renameButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const newName = $("rename").value.trim(); const newName = document.getElementById("rename").value.trim();
if (newName === "") { if (newName === "") {
alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]");
return; return;
@ -55,7 +55,7 @@
return; return;
} }
$("renameButton").disabled = true; document.getElementById("renameButton").disabled = true;
const parentPath = window.qBittorrent.Filesystem.folderName(oldPath); const parentPath = window.qBittorrent.Filesystem.folderName(oldPath);
const newPath = parentPath const newPath = parentPath
@ -72,7 +72,7 @@
.then((response) => { .then((response) => {
if (!response.ok) { if (!response.ok) {
alert("QBT_TR(Failed to update name)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Failed to update name)QBT_TR[CONTEXT=HttpServer]");
$("renameButton").disabled = false; document.getElementById("renameButton").disabled = false;
return; return;
} }

View file

@ -80,50 +80,50 @@
// Load Multi Rename Preferences // Load Multi Rename Preferences
const multiRenamePrefChecked = LocalPreferences.get("multirename_rememberPreferences", "true") === "true"; const multiRenamePrefChecked = LocalPreferences.get("multirename_rememberPreferences", "true") === "true";
$("multirename_rememberprefs_checkbox").checked = multiRenamePrefChecked; document.getElementById("multirename_rememberprefs_checkbox").checked = multiRenamePrefChecked;
if (multiRenamePrefChecked) { if (multiRenamePrefChecked) {
const multirename_search = LocalPreferences.get("multirename_search", ""); const multirename_search = LocalPreferences.get("multirename_search", "");
fileRenamer.setSearch(multirename_search); fileRenamer.setSearch(multirename_search);
$("multiRenameSearch").value = multirename_search; document.getElementById("multiRenameSearch").value = multirename_search;
const multirename_useRegex = LocalPreferences.get("multirename_useRegex", false); const multirename_useRegex = LocalPreferences.get("multirename_useRegex", false);
fileRenamer.useRegex = multirename_useRegex === "true"; fileRenamer.useRegex = multirename_useRegex === "true";
$("use_regex_search").checked = fileRenamer.useRegex; document.getElementById("use_regex_search").checked = fileRenamer.useRegex;
const multirename_matchAllOccurrences = LocalPreferences.get("multirename_matchAllOccurrences", false); const multirename_matchAllOccurrences = LocalPreferences.get("multirename_matchAllOccurrences", false);
fileRenamer.matchAllOccurrences = multirename_matchAllOccurrences === "true"; fileRenamer.matchAllOccurrences = multirename_matchAllOccurrences === "true";
$("match_all_occurrences").checked = fileRenamer.matchAllOccurrences; document.getElementById("match_all_occurrences").checked = fileRenamer.matchAllOccurrences;
const multirename_caseSensitive = LocalPreferences.get("multirename_caseSensitive", false); const multirename_caseSensitive = LocalPreferences.get("multirename_caseSensitive", false);
fileRenamer.caseSensitive = multirename_caseSensitive === "true"; fileRenamer.caseSensitive = multirename_caseSensitive === "true";
$("case_sensitive").checked = fileRenamer.caseSensitive; document.getElementById("case_sensitive").checked = fileRenamer.caseSensitive;
const multirename_replace = LocalPreferences.get("multirename_replace", ""); const multirename_replace = LocalPreferences.get("multirename_replace", "");
fileRenamer.setReplacement(multirename_replace); fileRenamer.setReplacement(multirename_replace);
$("multiRenameReplace").value = multirename_replace; document.getElementById("multiRenameReplace").value = multirename_replace;
const multirename_appliesTo = LocalPreferences.get("multirename_appliesTo", window.qBittorrent.MultiRename.AppliesTo.FilenameExtension); const multirename_appliesTo = LocalPreferences.get("multirename_appliesTo", window.qBittorrent.MultiRename.AppliesTo.FilenameExtension);
fileRenamer.appliesTo = window.qBittorrent.MultiRename.AppliesTo[multirename_appliesTo]; fileRenamer.appliesTo = window.qBittorrent.MultiRename.AppliesTo[multirename_appliesTo];
$("applies_to_option").value = fileRenamer.appliesTo; document.getElementById("applies_to_option").value = fileRenamer.appliesTo;
const multirename_includeFiles = LocalPreferences.get("multirename_includeFiles", true); const multirename_includeFiles = LocalPreferences.get("multirename_includeFiles", true);
fileRenamer.includeFiles = multirename_includeFiles === "true"; fileRenamer.includeFiles = multirename_includeFiles === "true";
$("include_files").checked = fileRenamer.includeFiles; document.getElementById("include_files").checked = fileRenamer.includeFiles;
const multirename_includeFolders = LocalPreferences.get("multirename_includeFolders", false); const multirename_includeFolders = LocalPreferences.get("multirename_includeFolders", false);
fileRenamer.includeFolders = multirename_includeFolders === "true"; fileRenamer.includeFolders = multirename_includeFolders === "true";
$("include_folders").checked = fileRenamer.includeFolders; document.getElementById("include_folders").checked = fileRenamer.includeFolders;
const multirename_fileEnumerationStart = LocalPreferences.get("multirename_fileEnumerationStart", 0); const multirename_fileEnumerationStart = LocalPreferences.get("multirename_fileEnumerationStart", 0);
fileRenamer.fileEnumerationStart = Number(multirename_fileEnumerationStart); fileRenamer.fileEnumerationStart = Number(multirename_fileEnumerationStart);
$("file_counter").value = fileRenamer.fileEnumerationStart; document.getElementById("file_counter").value = fileRenamer.fileEnumerationStart;
const multirename_replaceAll = LocalPreferences.get("multirename_replaceAll", false); const multirename_replaceAll = LocalPreferences.get("multirename_replaceAll", false);
fileRenamer.replaceAll = multirename_replaceAll === "true"; fileRenamer.replaceAll = multirename_replaceAll === "true";
const renameButtonValue = fileRenamer.replaceAll ? "Replace All" : "Replace"; const renameButtonValue = fileRenamer.replaceAll ? "Replace All" : "Replace";
$("renameOptions").value = renameButtonValue; document.getElementById("renameOptions").value = renameButtonValue;
$("renameButton").value = renameButtonValue; document.getElementById("renameButton").value = renameButtonValue;
} }
// Fires every time a row's selection changes // Fires every time a row's selection changes
@ -133,26 +133,26 @@
}; };
// Setup Search Events that control renaming // Setup Search Events that control renaming
$("multiRenameSearch").addEventListener("input", (e) => { document.getElementById("multiRenameSearch").addEventListener("input", (e) => {
const sanitized = e.target.value.replace(/\n/g, ""); const sanitized = e.target.value.replace(/\n/g, "");
$("multiRenameSearch").value = sanitized; document.getElementById("multiRenameSearch").value = sanitized;
// Search input has changed // Search input has changed
$("multiRenameSearch").style["border-color"] = ""; document.getElementById("multiRenameSearch").style["border-color"] = "";
LocalPreferences.set("multirename_search", sanitized); LocalPreferences.set("multirename_search", sanitized);
fileRenamer.setSearch(sanitized); fileRenamer.setSearch(sanitized);
}); });
$("use_regex_search").addEventListener("change", (e) => { document.getElementById("use_regex_search").addEventListener("change", (e) => {
fileRenamer.useRegex = e.target.checked; fileRenamer.useRegex = e.target.checked;
LocalPreferences.set("multirename_useRegex", e.target.checked); LocalPreferences.set("multirename_useRegex", e.target.checked);
fileRenamer.update(); fileRenamer.update();
}); });
$("match_all_occurrences").addEventListener("change", (e) => { document.getElementById("match_all_occurrences").addEventListener("change", (e) => {
fileRenamer.matchAllOccurrences = e.target.checked; fileRenamer.matchAllOccurrences = e.target.checked;
LocalPreferences.set("multirename_matchAllOccurrences", e.target.checked); LocalPreferences.set("multirename_matchAllOccurrences", e.target.checked);
fileRenamer.update(); fileRenamer.update();
}); });
$("case_sensitive").addEventListener("change", (e) => { document.getElementById("case_sensitive").addEventListener("change", (e) => {
fileRenamer.caseSensitive = e.target.checked; fileRenamer.caseSensitive = e.target.checked;
LocalPreferences.set("multirename_caseSensitive", e.target.checked); LocalPreferences.set("multirename_caseSensitive", e.target.checked);
fileRenamer.update(); fileRenamer.update();
@ -179,35 +179,35 @@
} }
}; };
fileRenamer.onInvalidRegex = (err) => { fileRenamer.onInvalidRegex = (err) => {
$("multiRenameSearch").style["border-color"] = "#CC0033"; document.getElementById("multiRenameSearch").style["border-color"] = "#CC0033";
}; };
// Setup Replace Events that control renaming // Setup Replace Events that control renaming
$("multiRenameReplace").addEventListener("input", (e) => { document.getElementById("multiRenameReplace").addEventListener("input", (e) => {
const sanitized = e.target.value.replace(/\n/g, ""); const sanitized = e.target.value.replace(/\n/g, "");
$("multiRenameReplace").value = sanitized; document.getElementById("multiRenameReplace").value = sanitized;
// Replace input has changed // Replace input has changed
$("multiRenameReplace").style["border-color"] = ""; document.getElementById("multiRenameReplace").style["border-color"] = "";
LocalPreferences.set("multirename_replace", sanitized); LocalPreferences.set("multirename_replace", sanitized);
fileRenamer.setReplacement(sanitized); fileRenamer.setReplacement(sanitized);
}); });
$("applies_to_option").addEventListener("change", (e) => { document.getElementById("applies_to_option").addEventListener("change", (e) => {
fileRenamer.appliesTo = e.target.value; fileRenamer.appliesTo = e.target.value;
LocalPreferences.set("multirename_appliesTo", e.target.value); LocalPreferences.set("multirename_appliesTo", e.target.value);
fileRenamer.update(); fileRenamer.update();
}); });
$("include_files").addEventListener("change", (e) => { document.getElementById("include_files").addEventListener("change", (e) => {
fileRenamer.includeFiles = e.target.checked; fileRenamer.includeFiles = e.target.checked;
LocalPreferences.set("multirename_includeFiles", e.target.checked); LocalPreferences.set("multirename_includeFiles", e.target.checked);
fileRenamer.update(); fileRenamer.update();
}); });
$("include_folders").addEventListener("change", (e) => { document.getElementById("include_folders").addEventListener("change", (e) => {
fileRenamer.includeFolders = e.target.checked; fileRenamer.includeFolders = e.target.checked;
LocalPreferences.set("multirename_includeFolders", e.target.checked); LocalPreferences.set("multirename_includeFolders", e.target.checked);
fileRenamer.update(); fileRenamer.update();
}); });
$("file_counter").addEventListener("input", (e) => { document.getElementById("file_counter").addEventListener("input", (e) => {
let value = e.target.valueAsNumber; let value = e.target.valueAsNumber;
if (!value) if (!value)
value = 0; value = 0;
@ -216,46 +216,46 @@
if (value > 99999999) if (value > 99999999)
value = 99999999; value = 99999999;
fileRenamer.fileEnumerationStart = value; fileRenamer.fileEnumerationStart = value;
$("file_counter").value = value; document.getElementById("file_counter").value = value;
LocalPreferences.set("multirename_fileEnumerationStart", value); LocalPreferences.set("multirename_fileEnumerationStart", value);
fileRenamer.update(); fileRenamer.update();
}); });
// Setup Rename Operation Events // Setup Rename Operation Events
$("renameButton").addEventListener("click", (e) => { document.getElementById("renameButton").addEventListener("click", (e) => {
// Disable Search Options // Disable Search Options
$("multiRenameSearch").disabled = true; document.getElementById("multiRenameSearch").disabled = true;
$("use_regex_search").disabled = true; document.getElementById("use_regex_search").disabled = true;
$("match_all_occurrences").disabled = true; document.getElementById("match_all_occurrences").disabled = true;
$("case_sensitive").disabled = true; document.getElementById("case_sensitive").disabled = true;
// Disable Replace Options // Disable Replace Options
$("multiRenameReplace").disabled = true; document.getElementById("multiRenameReplace").disabled = true;
$("applies_to_option").disabled = true; document.getElementById("applies_to_option").disabled = true;
$("include_files").disabled = true; document.getElementById("include_files").disabled = true;
$("include_folders").disabled = true; document.getElementById("include_folders").disabled = true;
$("file_counter").disabled = true; document.getElementById("file_counter").disabled = true;
// Disable Rename Buttons // Disable Rename Buttons
$("renameButton").disabled = true; document.getElementById("renameButton").disabled = true;
$("renameOptions").disabled = true; document.getElementById("renameOptions").disabled = true;
// Clear error text // Clear error text
$("rename_error").textContent = ""; document.getElementById("rename_error").textContent = "";
fileRenamer.rename(); fileRenamer.rename();
}); });
fileRenamer.onRenamed = (rows) => { fileRenamer.onRenamed = (rows) => {
// Disable Search Options // Disable Search Options
$("multiRenameSearch").disabled = false; document.getElementById("multiRenameSearch").disabled = false;
$("use_regex_search").disabled = false; document.getElementById("use_regex_search").disabled = false;
$("match_all_occurrences").disabled = false; document.getElementById("match_all_occurrences").disabled = false;
$("case_sensitive").disabled = false; document.getElementById("case_sensitive").disabled = false;
// Disable Replace Options // Disable Replace Options
$("multiRenameReplace").disabled = false; document.getElementById("multiRenameReplace").disabled = false;
$("applies_to_option").disabled = false; document.getElementById("applies_to_option").disabled = false;
$("include_files").disabled = false; document.getElementById("include_files").disabled = false;
$("include_folders").disabled = false; document.getElementById("include_folders").disabled = false;
$("file_counter").disabled = false; document.getElementById("file_counter").disabled = false;
// Disable Rename Buttons // Disable Rename Buttons
$("renameButton").disabled = false; document.getElementById("renameButton").disabled = false;
$("renameOptions").disabled = false; document.getElementById("renameOptions").disabled = false;
// Recreate table // Recreate table
let selectedRows = bulkRenameFilesTable.getSelectedRows().map(row => row.rowId.toString()); let selectedRows = bulkRenameFilesTable.getSelectedRows().map(row => row.rowId.toString());
@ -266,15 +266,15 @@
// Adjust file enumeration count by 1 when replacing single files to prevent naming conflicts // Adjust file enumeration count by 1 when replacing single files to prevent naming conflicts
if (!fileRenamer.replaceAll) { if (!fileRenamer.replaceAll) {
fileRenamer.fileEnumerationStart++; fileRenamer.fileEnumerationStart++;
$("file_counter").value = fileRenamer.fileEnumerationStart; document.getElementById("file_counter").value = fileRenamer.fileEnumerationStart;
} }
setupTable(selectedRows); setupTable(selectedRows);
}; };
fileRenamer.onRenameError = (response, row) => { fileRenamer.onRenameError = (response, row) => {
if (response.status === 409) if (response.status === 409)
$("rename_error").textContent = `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}\``; document.getElementById("rename_error").textContent = `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}\``;
}; };
$("renameOptions").addEventListener("change", (e) => { document.getElementById("renameOptions").addEventListener("change", (e) => {
const combobox = e.target; const combobox = e.target;
const replaceOperation = combobox.value; const replaceOperation = combobox.value;
if (replaceOperation === "Replace") if (replaceOperation === "Replace")
@ -284,9 +284,9 @@
else else
fileRenamer.replaceAll = false; fileRenamer.replaceAll = false;
LocalPreferences.set("multirename_replaceAll", fileRenamer.replaceAll); LocalPreferences.set("multirename_replaceAll", fileRenamer.replaceAll);
$("renameButton").value = replaceOperation; document.getElementById("renameButton").value = replaceOperation;
}); });
$("closeButton").addEventListener("click", (event) => { document.getElementById("closeButton").addEventListener("click", (event) => {
event.preventDefault(); event.preventDefault();
window.qBittorrent.Client.closeWindow(windowEl); window.qBittorrent.Client.closeWindow(windowEl);
}); });
@ -409,7 +409,7 @@
<label for="multirename_rememberprefs_checkbox">QBT_TR(Remember Multi-Rename settings)QBT_TR[CONTEXT=OptionsDialog]</label> <label for="multirename_rememberprefs_checkbox">QBT_TR(Remember Multi-Rename settings)QBT_TR[CONTEXT=OptionsDialog]</label>
</div> </div>
<hr> <hr>
<textarea id="multiRenameSearch" placeholder="QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" aria-label="QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea> <textarea id="multiRenameSearch" placeholder="QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" aria-label="QBT_TR(Search Files)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px; font-family: monospace;"></textarea>
<div class="formRow"> <div class="formRow">
<input type="checkbox" id="use_regex_search"> <input type="checkbox" id="use_regex_search">
<label for="use_regex_search">QBT_TR(Use regular expressions)QBT_TR[CONTEXT=PropertiesWidget]</label> <label for="use_regex_search">QBT_TR(Use regular expressions)QBT_TR[CONTEXT=PropertiesWidget]</label>
@ -423,7 +423,7 @@
<label for="case_sensitive">QBT_TR(Case sensitive)QBT_TR[CONTEXT=PropertiesWidget]</label> <label for="case_sensitive">QBT_TR(Case sensitive)QBT_TR[CONTEXT=PropertiesWidget]</label>
</div> </div>
<hr> <hr>
<textarea id="multiRenameReplace" placeholder="QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" aria-label="QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px;"></textarea> <textarea id="multiRenameReplace" placeholder="QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" aria-label="QBT_TR(Replacement Input)QBT_TR[CONTEXT=PropertiesWidget]" style="width: calc(100% - 8px); resize: vertical; min-height: 30px; font-family: monospace;"></textarea>
<select id="applies_to_option" name="applies_to_option" aria-label="QBT_TR(Apply to which filename part)QBT_TR[CONTEXT=PropertiesWidget]" style="width: 100%; margin-bottom: 5px;"> <select id="applies_to_option" name="applies_to_option" aria-label="QBT_TR(Apply to which filename part)QBT_TR[CONTEXT=PropertiesWidget]" style="width: 100%; margin-bottom: 5px;">
<option selected value="FilenameExtension">QBT_TR(Filename + Extension)QBT_TR[CONTEXT=PropertiesWidget]</option> <option selected value="FilenameExtension">QBT_TR(Filename + Extension)QBT_TR[CONTEXT=PropertiesWidget]</option>
<option value="Filename">QBT_TR(Filename)QBT_TR[CONTEXT=PropertiesWidget]</option> <option value="Filename">QBT_TR(Filename)QBT_TR[CONTEXT=PropertiesWidget]</option>

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("renameButton").click(); document.getElementById("renameButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -29,16 +29,16 @@
const oldName = new URLSearchParams(window.location.search).get("rule"); const oldName = new URLSearchParams(window.location.search).get("rule");
$("rename").value = oldName; document.getElementById("rename").value = oldName;
$("rename").focus(); document.getElementById("rename").focus();
$("rename").setSelectionRange(0, oldName.length); document.getElementById("rename").setSelectionRange(0, oldName.length);
$("renameButton").addEventListener("click", (e) => { document.getElementById("renameButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const newName = $("rename").value.trim(); const newName = document.getElementById("rename").value.trim();
if (newName === "") { if (newName === "") {
alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Name cannot be empty)QBT_TR[CONTEXT=HttpServer]");
return; return;
@ -49,7 +49,7 @@
return; return;
} }
$("renameButton").disabled = true; document.getElementById("renameButton").disabled = true;
fetch("api/v2/rss/renameRule", { fetch("api/v2/rss/renameRule", {
method: "POST", method: "POST",

View file

@ -53,7 +53,7 @@ window.qBittorrent.Cache ??= (() => {
class BuildInfoCache { class BuildInfoCache {
#m_store = {}; #m_store = {};
async init() { init() {
return fetch("api/v2/app/buildInfo", { return fetch("api/v2/app/buildInfo", {
method: "GET", method: "GET",
cache: "no-store" cache: "no-store"
@ -80,7 +80,7 @@ window.qBittorrent.Cache ??= (() => {
// onFailure: () => {}, // onFailure: () => {},
// onSuccess: () => {} // onSuccess: () => {}
// } // }
async init(obj = {}) { init(obj = {}) {
return fetch("api/v2/app/preferences", { return fetch("api/v2/app/preferences", {
method: "GET", method: "GET",
cache: "no-store" cache: "no-store"
@ -153,7 +153,7 @@ window.qBittorrent.Cache ??= (() => {
class QbtVersionCache { class QbtVersionCache {
#m_store = ""; #m_store = "";
async init() { init() {
return fetch("api/v2/app/version", { return fetch("api/v2/app/version", {
method: "GET", method: "GET",
cache: "no-store" cache: "no-store"

View file

@ -174,21 +174,23 @@ let selectedStatus = LocalPreferences.get("selected_filter", "all");
let setStatusFilter = () => {}; let setStatusFilter = () => {};
let toggleFilterDisplay = () => {}; let toggleFilterDisplay = () => {};
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.qBittorrent.LocalPreferences.upgrade();
let isSearchPanelLoaded = false; let isSearchPanelLoaded = false;
let isLogPanelLoaded = false; let isLogPanelLoaded = false;
let isRssPanelLoaded = false; let isRssPanelLoaded = false;
const saveColumnSizes = () => { const saveColumnSizes = () => {
const filters_width = $("Filters").getSize().x; const filters_width = document.getElementById("Filters").getSize().x;
LocalPreferences.set("filters_width", filters_width); LocalPreferences.set("filters_width", filters_width);
const properties_height_rel = $("propertiesPanel").getSize().y / Window.getSize().y; const properties_height_rel = document.getElementById("propertiesPanel").getSize().y / Window.getSize().y;
LocalPreferences.set("properties_height_rel", properties_height_rel); LocalPreferences.set("properties_height_rel", properties_height_rel);
}; };
window.addEventListener("resize", window.qBittorrent.Misc.createDebounceHandler(500, (e) => { window.addEventListener("resize", window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
// only save sizes if the columns are visible // only save sizes if the columns are visible
if (!$("mainColumn").classList.contains("invisible")) if (!document.getElementById("mainColumn").classList.contains("invisible"))
saveColumnSizes(); saveColumnSizes();
})); }));
@ -221,7 +223,7 @@ window.addEventListener("DOMContentLoaded", () => {
}); });
// start off hidden // start off hidden
$("searchTabColumn").classList.add("invisible"); document.getElementById("searchTabColumn").classList.add("invisible");
}; };
const buildRssTab = () => { const buildRssTab = () => {
@ -232,7 +234,7 @@ window.addEventListener("DOMContentLoaded", () => {
}); });
// start off hidden // start off hidden
$("rssTabColumn").classList.add("invisible"); document.getElementById("rssTabColumn").classList.add("invisible");
}; };
const buildLogTab = () => { const buildLogTab = () => {
@ -243,7 +245,7 @@ window.addEventListener("DOMContentLoaded", () => {
}); });
// start off hidden // start off hidden
$("logTabColumn").classList.add("invisible"); document.getElementById("logTabColumn").classList.add("invisible");
}; };
buildTransfersTab(); buildTransfersTab();
@ -342,28 +344,28 @@ window.addEventListener("DOMContentLoaded", () => {
// Show Top Toolbar is enabled by default // Show Top Toolbar is enabled by default
let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true"; let showTopToolbar = LocalPreferences.get("show_top_toolbar", "true") === "true";
if (!showTopToolbar) { if (!showTopToolbar) {
$("showTopToolbarLink").firstElementChild.style.opacity = "0"; document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "0";
$("mochaToolbar").classList.add("invisible"); document.getElementById("mochaToolbar").classList.add("invisible");
} }
// Show Status Bar is enabled by default // Show Status Bar is enabled by default
let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true"; let showStatusBar = LocalPreferences.get("show_status_bar", "true") === "true";
if (!showStatusBar) { if (!showStatusBar) {
$("showStatusBarLink").firstElementChild.style.opacity = "0"; document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "0";
$("desktopFooterWrapper").classList.add("invisible"); document.getElementById("desktopFooterWrapper").classList.add("invisible");
} }
// Show Filters Sidebar is enabled by default // Show Filters Sidebar is enabled by default
let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true"; let showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
if (!showFiltersSidebar) { if (!showFiltersSidebar) {
$("showFiltersSidebarLink").firstElementChild.style.opacity = "0"; document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "0";
$("filtersColumn").classList.add("invisible"); document.getElementById("filtersColumn").classList.add("invisible");
$("filtersColumn_handle").classList.add("invisible"); document.getElementById("filtersColumn_handle").classList.add("invisible");
} }
let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true"; let speedInTitle = LocalPreferences.get("speed_in_browser_title_bar") === "true";
if (!speedInTitle) if (!speedInTitle)
$("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0"; document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0";
// After showing/hiding the toolbar + status bar // After showing/hiding the toolbar + status bar
window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false"); window.qBittorrent.Client.showSearchEngine(LocalPreferences.get("show_search_engine") !== "false");
@ -497,7 +499,7 @@ window.addEventListener("DOMContentLoaded", () => {
if (!categoryList) if (!categoryList)
return; return;
[...categoryList.children].forEach((el) => { el.destroy(); }); [...categoryList.children].forEach((el) => { el.remove(); });
const categoryItemTemplate = document.getElementById("categoryFilterItem"); const categoryItemTemplate = document.getElementById("categoryFilterItem");
@ -614,11 +616,11 @@ window.addEventListener("DOMContentLoaded", () => {
}; };
const updateTagList = () => { const updateTagList = () => {
const tagFilterList = $("tagFilterList"); const tagFilterList = document.getElementById("tagFilterList");
if (tagFilterList === null) if (tagFilterList === null)
return; return;
[...tagFilterList.children].forEach((el) => { el.destroy(); }); [...tagFilterList.children].forEach((el) => { el.remove(); });
const tagItemTemplate = document.getElementById("tagFilterItem"); const tagItemTemplate = document.getElementById("tagFilterItem");
@ -667,11 +669,11 @@ window.addEventListener("DOMContentLoaded", () => {
}; };
const updateTrackerList = () => { const updateTrackerList = () => {
const trackerFilterList = $("trackerFilterList"); const trackerFilterList = document.getElementById("trackerFilterList");
if (trackerFilterList === null) if (trackerFilterList === null)
return; return;
[...trackerFilterList.children].forEach((el) => { el.destroy(); }); [...trackerFilterList.children].forEach((el) => { el.remove(); });
const trackerItemTemplate = document.getElementById("trackerFilterItem"); const trackerItemTemplate = document.getElementById("trackerFilterItem");
@ -763,7 +765,7 @@ window.addEventListener("DOMContentLoaded", () => {
}) })
.then(async (response) => { .then(async (response) => {
if (response.ok) { if (response.ok) {
$("error_div").textContent = ""; document.getElementById("error_div").textContent = "";
const responseJSON = await response.json(); const responseJSON = await response.json();
@ -933,7 +935,7 @@ window.addEventListener("DOMContentLoaded", () => {
syncData(window.qBittorrent.Client.getSyncMainDataInterval()); syncData(window.qBittorrent.Client.getSyncMainDataInterval());
}, },
(error) => { (error) => {
const errorDiv = $("error_div"); const errorDiv = document.getElementById("error_div");
if (errorDiv) if (errorDiv)
errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
syncRequestInProgress = false; syncRequestInProgress = false;
@ -964,12 +966,12 @@ window.addEventListener("DOMContentLoaded", () => {
if (serverState.dl_rate_limit > 0) if (serverState.dl_rate_limit > 0)
transfer_info += ` [${window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true)}]`; transfer_info += ` [${window.qBittorrent.Misc.friendlyUnit(serverState.dl_rate_limit, true)}]`;
transfer_info += ` (${window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false)})`; transfer_info += ` (${window.qBittorrent.Misc.friendlyUnit(serverState.dl_info_data, false)})`;
$("DlInfos").textContent = transfer_info; document.getElementById("DlInfos").textContent = transfer_info;
transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true); transfer_info = window.qBittorrent.Misc.friendlyUnit(serverState.up_info_speed, true);
if (serverState.up_rate_limit > 0) if (serverState.up_rate_limit > 0)
transfer_info += ` [${window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true)}]`; transfer_info += ` [${window.qBittorrent.Misc.friendlyUnit(serverState.up_rate_limit, true)}]`;
transfer_info += ` (${window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false)})`; transfer_info += ` (${window.qBittorrent.Misc.friendlyUnit(serverState.up_info_data, false)})`;
$("UpInfos").textContent = transfer_info; document.getElementById("UpInfos").textContent = transfer_info;
document.title = (speedInTitle document.title = (speedInTitle
? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] ` ? (`QBT_TR([D: %1, U: %2])QBT_TR[CONTEXT=MainWindow] `
@ -978,7 +980,7 @@ window.addEventListener("DOMContentLoaded", () => {
: "") : "")
+ window.qBittorrent.Client.mainTitle(); + window.qBittorrent.Client.mainTitle();
$("freeSpaceOnDisk").textContent = "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk)); document.getElementById("freeSpaceOnDisk").textContent = "QBT_TR(Free space: %1)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Misc.friendlyUnit(serverState.free_space_on_disk));
const externalIPsElement = document.getElementById("externalIPs"); const externalIPsElement = document.getElementById("externalIPs");
if (window.qBittorrent.Cache.preferences.get().status_bar_external_ip) { if (window.qBittorrent.Cache.preferences.get().status_bar_external_ip) {
@ -991,9 +993,9 @@ window.addEventListener("DOMContentLoaded", () => {
lastExternalAddressLabel = "QBT_TR(External IPs: %1, %2)QBT_TR[CONTEXT=HttpServer]"; lastExternalAddressLabel = "QBT_TR(External IPs: %1, %2)QBT_TR[CONTEXT=HttpServer]";
else if (hasIPv4Address || hasIPv6Address) else if (hasIPv4Address || hasIPv6Address)
lastExternalAddressLabel = "QBT_TR(External IP: %1%2)QBT_TR[CONTEXT=HttpServer]"; lastExternalAddressLabel = "QBT_TR(External IP: %1%2)QBT_TR[CONTEXT=HttpServer]";
// replace in reverse order ('%2' before '%1') in case address contains a % character. // https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses_(with_zone_index)
// for example, see https://en.wikipedia.org/wiki/IPv6_address#Scoped_literal_IPv6_addresses_(with_zone_index) lastExternalAddressLabel = lastExternalAddressLabel.replace("%1", lastExternalAddressV4).replace("%2", lastExternalAddressV6);
externalIPsElement.textContent = lastExternalAddressLabel.replace("%2", lastExternalAddressV6).replace("%1", lastExternalAddressV4); externalIPsElement.textContent = lastExternalAddressLabel;
externalIPsElement.classList.remove("invisible"); externalIPsElement.classList.remove("invisible");
externalIPsElement.previousElementSibling.classList.remove("invisible"); externalIPsElement.previousElementSibling.classList.remove("invisible");
} }
@ -1015,35 +1017,35 @@ window.addEventListener("DOMContentLoaded", () => {
// Statistics dialog // Statistics dialog
if (document.getElementById("statisticsContent")) { if (document.getElementById("statisticsContent")) {
$("AlltimeDL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false); document.getElementById("AlltimeDL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_dl, false);
$("AlltimeUL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false); document.getElementById("AlltimeUL").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.alltime_ul, false);
$("TotalWastedSession").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false); document.getElementById("TotalWastedSession").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_wasted_session, false);
$("GlobalRatio").textContent = serverState.global_ratio; document.getElementById("GlobalRatio").textContent = serverState.global_ratio;
$("TotalPeerConnections").textContent = serverState.total_peer_connections; document.getElementById("TotalPeerConnections").textContent = serverState.total_peer_connections;
$("ReadCacheHits").textContent = `${serverState.read_cache_hits}%`; document.getElementById("ReadCacheHits").textContent = `${serverState.read_cache_hits}%`;
$("TotalBuffersSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false); document.getElementById("TotalBuffersSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_buffers_size, false);
$("WriteCacheOverload").textContent = `${serverState.write_cache_overload}%`; document.getElementById("WriteCacheOverload").textContent = `${serverState.write_cache_overload}%`;
$("ReadCacheOverload").textContent = `${serverState.read_cache_overload}%`; document.getElementById("ReadCacheOverload").textContent = `${serverState.read_cache_overload}%`;
$("QueuedIOJobs").textContent = serverState.queued_io_jobs; document.getElementById("QueuedIOJobs").textContent = serverState.queued_io_jobs;
$("AverageTimeInQueue").textContent = `${serverState.average_time_queue} ms`; document.getElementById("AverageTimeInQueue").textContent = `${serverState.average_time_queue} ms`;
$("TotalQueuedSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false); document.getElementById("TotalQueuedSize").textContent = window.qBittorrent.Misc.friendlyUnit(serverState.total_queued_size, false);
} }
switch (serverState.connection_status) { switch (serverState.connection_status) {
case "connected": case "connected":
$("connectionStatus").src = "images/connected.svg"; document.getElementById("connectionStatus").src = "images/connected.svg";
$("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("connectionStatus").alt = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
$("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("connectionStatus").title = "QBT_TR(Connection status: Connected)QBT_TR[CONTEXT=MainWindow]";
break; break;
case "firewalled": case "firewalled":
$("connectionStatus").src = "images/firewalled.svg"; document.getElementById("connectionStatus").src = "images/firewalled.svg";
$("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("connectionStatus").alt = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
$("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("connectionStatus").title = "QBT_TR(Connection status: Firewalled)QBT_TR[CONTEXT=MainWindow]";
break; break;
default: default:
$("connectionStatus").src = "images/disconnected.svg"; document.getElementById("connectionStatus").src = "images/disconnected.svg";
$("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("connectionStatus").alt = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
$("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("connectionStatus").title = "QBT_TR(Connection status: Disconnected)QBT_TR[CONTEXT=MainWindow]";
break; break;
} }
@ -1052,20 +1054,20 @@ window.addEventListener("DOMContentLoaded", () => {
torrentsTable.columns["priority"].force_hide = !queueing_enabled; torrentsTable.columns["priority"].force_hide = !queueing_enabled;
torrentsTable.updateColumn("priority"); torrentsTable.updateColumn("priority");
if (queueing_enabled) { if (queueing_enabled) {
$("topQueuePosItem").classList.remove("invisible"); document.getElementById("topQueuePosItem").classList.remove("invisible");
$("increaseQueuePosItem").classList.remove("invisible"); document.getElementById("increaseQueuePosItem").classList.remove("invisible");
$("decreaseQueuePosItem").classList.remove("invisible"); document.getElementById("decreaseQueuePosItem").classList.remove("invisible");
$("bottomQueuePosItem").classList.remove("invisible"); document.getElementById("bottomQueuePosItem").classList.remove("invisible");
$("queueingButtons").classList.remove("invisible"); document.getElementById("queueingButtons").classList.remove("invisible");
$("queueingMenuItems").classList.remove("invisible"); document.getElementById("queueingMenuItems").classList.remove("invisible");
} }
else { else {
$("topQueuePosItem").classList.add("invisible"); document.getElementById("topQueuePosItem").classList.add("invisible");
$("increaseQueuePosItem").classList.add("invisible"); document.getElementById("increaseQueuePosItem").classList.add("invisible");
$("decreaseQueuePosItem").classList.add("invisible"); document.getElementById("decreaseQueuePosItem").classList.add("invisible");
$("bottomQueuePosItem").classList.add("invisible"); document.getElementById("bottomQueuePosItem").classList.add("invisible");
$("queueingButtons").classList.add("invisible"); document.getElementById("queueingButtons").classList.add("invisible");
$("queueingMenuItems").classList.add("invisible"); document.getElementById("queueingMenuItems").classList.add("invisible");
} }
} }
@ -1084,18 +1086,18 @@ window.addEventListener("DOMContentLoaded", () => {
const updateAltSpeedIcon = (enabled) => { const updateAltSpeedIcon = (enabled) => {
if (enabled) { if (enabled) {
$("alternativeSpeedLimits").src = "images/slow.svg"; document.getElementById("alternativeSpeedLimits").src = "images/slow.svg";
$("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
$("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: On)QBT_TR[CONTEXT=MainWindow]";
} }
else { else {
$("alternativeSpeedLimits").src = "images/slow_off.svg"; document.getElementById("alternativeSpeedLimits").src = "images/slow_off.svg";
$("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("alternativeSpeedLimits").alt = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
$("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]"; document.getElementById("alternativeSpeedLimits").title = "QBT_TR(Alternative speed limits: Off)QBT_TR[CONTEXT=MainWindow]";
} }
}; };
$("alternativeSpeedLimits").addEventListener("click", () => { document.getElementById("alternativeSpeedLimits").addEventListener("click", (event) => {
// Change icon immediately to give some feedback // Change icon immediately to give some feedback
updateAltSpeedIcon(!alternativeSpeedLimits); updateAltSpeedIcon(!alternativeSpeedLimits);
@ -1114,33 +1116,33 @@ window.addEventListener("DOMContentLoaded", () => {
}); });
}); });
$("DlInfos").addEventListener("click", () => { globalDownloadLimitFN(); }); document.getElementById("DlInfos").addEventListener("click", (event) => { globalDownloadLimitFN(); });
$("UpInfos").addEventListener("click", () => { globalUploadLimitFN(); }); document.getElementById("UpInfos").addEventListener("click", (event) => { globalUploadLimitFN(); });
$("showTopToolbarLink").addEventListener("click", (e) => { document.getElementById("showTopToolbarLink").addEventListener("click", (e) => {
showTopToolbar = !showTopToolbar; showTopToolbar = !showTopToolbar;
LocalPreferences.set("show_top_toolbar", showTopToolbar.toString()); LocalPreferences.set("show_top_toolbar", showTopToolbar.toString());
if (showTopToolbar) { if (showTopToolbar) {
$("showTopToolbarLink").firstElementChild.style.opacity = "1"; document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "1";
$("mochaToolbar").classList.remove("invisible"); document.getElementById("mochaToolbar").classList.remove("invisible");
} }
else { else {
$("showTopToolbarLink").firstElementChild.style.opacity = "0"; document.getElementById("showTopToolbarLink").firstElementChild.style.opacity = "0";
$("mochaToolbar").classList.add("invisible"); document.getElementById("mochaToolbar").classList.add("invisible");
} }
MochaUI.Desktop.setDesktopSize(); MochaUI.Desktop.setDesktopSize();
}); });
$("showStatusBarLink").addEventListener("click", (e) => { document.getElementById("showStatusBarLink").addEventListener("click", (e) => {
showStatusBar = !showStatusBar; showStatusBar = !showStatusBar;
LocalPreferences.set("show_status_bar", showStatusBar.toString()); LocalPreferences.set("show_status_bar", showStatusBar.toString());
if (showStatusBar) { if (showStatusBar) {
$("showStatusBarLink").firstElementChild.style.opacity = "1"; document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "1";
$("desktopFooterWrapper").classList.remove("invisible"); document.getElementById("desktopFooterWrapper").classList.remove("invisible");
} }
else { else {
$("showStatusBarLink").firstElementChild.style.opacity = "0"; document.getElementById("showStatusBarLink").firstElementChild.style.opacity = "0";
$("desktopFooterWrapper").classList.add("invisible"); document.getElementById("desktopFooterWrapper").classList.add("invisible");
} }
MochaUI.Desktop.setDesktopSize(); MochaUI.Desktop.setDesktopSize();
}); });
@ -1164,49 +1166,49 @@ window.addEventListener("DOMContentLoaded", () => {
navigator.registerProtocolHandler("magnet", templateUrl, navigator.registerProtocolHandler("magnet", templateUrl,
"qBittorrent WebUI magnet handler"); "qBittorrent WebUI magnet handler");
}; };
$("registerMagnetHandlerLink").addEventListener("click", (e) => { document.getElementById("registerMagnetHandlerLink").addEventListener("click", (e) => {
registerMagnetHandler(); registerMagnetHandler();
}); });
$("showFiltersSidebarLink").addEventListener("click", (e) => { document.getElementById("showFiltersSidebarLink").addEventListener("click", (e) => {
showFiltersSidebar = !showFiltersSidebar; showFiltersSidebar = !showFiltersSidebar;
LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString()); LocalPreferences.set("show_filters_sidebar", showFiltersSidebar.toString());
if (showFiltersSidebar) { if (showFiltersSidebar) {
$("showFiltersSidebarLink").firstElementChild.style.opacity = "1"; document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "1";
$("filtersColumn").classList.remove("invisible"); document.getElementById("filtersColumn").classList.remove("invisible");
$("filtersColumn_handle").classList.remove("invisible"); document.getElementById("filtersColumn_handle").classList.remove("invisible");
} }
else { else {
$("showFiltersSidebarLink").firstElementChild.style.opacity = "0"; document.getElementById("showFiltersSidebarLink").firstElementChild.style.opacity = "0";
$("filtersColumn").classList.add("invisible"); document.getElementById("filtersColumn").classList.add("invisible");
$("filtersColumn_handle").classList.add("invisible"); document.getElementById("filtersColumn_handle").classList.add("invisible");
} }
MochaUI.Desktop.setDesktopSize(); MochaUI.Desktop.setDesktopSize();
}); });
$("speedInBrowserTitleBarLink").addEventListener("click", (e) => { document.getElementById("speedInBrowserTitleBarLink").addEventListener("click", (e) => {
speedInTitle = !speedInTitle; speedInTitle = !speedInTitle;
LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString()); LocalPreferences.set("speed_in_browser_title_bar", speedInTitle.toString());
if (speedInTitle) if (speedInTitle)
$("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "1"; document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "1";
else else
$("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0"; document.getElementById("speedInBrowserTitleBarLink").firstElementChild.style.opacity = "0";
processServerState(); processServerState();
}); });
$("showSearchEngineLink").addEventListener("click", (e) => { document.getElementById("showSearchEngineLink").addEventListener("click", (e) => {
window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine()); window.qBittorrent.Client.showSearchEngine(!window.qBittorrent.Client.isShowSearchEngine());
LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString()); LocalPreferences.set("show_search_engine", window.qBittorrent.Client.isShowSearchEngine().toString());
updateTabDisplay(); updateTabDisplay();
}); });
$("showRssReaderLink").addEventListener("click", (e) => { document.getElementById("showRssReaderLink").addEventListener("click", (e) => {
window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader()); window.qBittorrent.Client.showRssReader(!window.qBittorrent.Client.isShowRssReader());
LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString()); LocalPreferences.set("show_rss_reader", window.qBittorrent.Client.isShowRssReader().toString());
updateTabDisplay(); updateTabDisplay();
}); });
$("showLogViewerLink").addEventListener("click", (e) => { document.getElementById("showLogViewerLink").addEventListener("click", (e) => {
window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer()); window.qBittorrent.Client.showLogViewer(!window.qBittorrent.Client.isShowLogViewer());
LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString()); LocalPreferences.set("show_log_viewer", window.qBittorrent.Client.isShowLogViewer().toString());
updateTabDisplay(); updateTabDisplay();
@ -1214,64 +1216,64 @@ window.addEventListener("DOMContentLoaded", () => {
const updateTabDisplay = () => { const updateTabDisplay = () => {
if (window.qBittorrent.Client.isShowRssReader()) { if (window.qBittorrent.Client.isShowRssReader()) {
$("showRssReaderLink").firstElementChild.style.opacity = "1"; document.getElementById("showRssReaderLink").firstElementChild.style.opacity = "1";
$("mainWindowTabs").classList.remove("invisible"); document.getElementById("mainWindowTabs").classList.remove("invisible");
$("rssTabLink").classList.remove("invisible"); document.getElementById("rssTabLink").classList.remove("invisible");
if (!MochaUI.Panels.instances.RssPanel) if (!MochaUI.Panels.instances.RssPanel)
addRssPanel(); addRssPanel();
} }
else { else {
$("showRssReaderLink").firstElementChild.style.opacity = "0"; document.getElementById("showRssReaderLink").firstElementChild.style.opacity = "0";
$("rssTabLink").classList.add("invisible"); document.getElementById("rssTabLink").classList.add("invisible");
if ($("rssTabLink").classList.contains("selected")) if (document.getElementById("rssTabLink").classList.contains("selected"))
$("transfersTabLink").click(); document.getElementById("transfersTabLink").click();
} }
if (window.qBittorrent.Client.isShowSearchEngine()) { if (window.qBittorrent.Client.isShowSearchEngine()) {
$("showSearchEngineLink").firstElementChild.style.opacity = "1"; document.getElementById("showSearchEngineLink").firstElementChild.style.opacity = "1";
$("mainWindowTabs").classList.remove("invisible"); document.getElementById("mainWindowTabs").classList.remove("invisible");
$("searchTabLink").classList.remove("invisible"); document.getElementById("searchTabLink").classList.remove("invisible");
if (!MochaUI.Panels.instances.SearchPanel) if (!MochaUI.Panels.instances.SearchPanel)
addSearchPanel(); addSearchPanel();
} }
else { else {
$("showSearchEngineLink").firstElementChild.style.opacity = "0"; document.getElementById("showSearchEngineLink").firstElementChild.style.opacity = "0";
$("searchTabLink").classList.add("invisible"); document.getElementById("searchTabLink").classList.add("invisible");
if ($("searchTabLink").classList.contains("selected")) if (document.getElementById("searchTabLink").classList.contains("selected"))
$("transfersTabLink").click(); document.getElementById("transfersTabLink").click();
} }
if (window.qBittorrent.Client.isShowLogViewer()) { if (window.qBittorrent.Client.isShowLogViewer()) {
$("showLogViewerLink").firstElementChild.style.opacity = "1"; document.getElementById("showLogViewerLink").firstElementChild.style.opacity = "1";
$("mainWindowTabs").classList.remove("invisible"); document.getElementById("mainWindowTabs").classList.remove("invisible");
$("logTabLink").classList.remove("invisible"); document.getElementById("logTabLink").classList.remove("invisible");
if (!MochaUI.Panels.instances.LogPanel) if (!MochaUI.Panels.instances.LogPanel)
addLogPanel(); addLogPanel();
} }
else { else {
$("showLogViewerLink").firstElementChild.style.opacity = "0"; document.getElementById("showLogViewerLink").firstElementChild.style.opacity = "0";
$("logTabLink").classList.add("invisible"); document.getElementById("logTabLink").classList.add("invisible");
if ($("logTabLink").classList.contains("selected")) if (document.getElementById("logTabLink").classList.contains("selected"))
$("transfersTabLink").click(); document.getElementById("transfersTabLink").click();
} }
// display no tabs // display no tabs
if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer()) if (!window.qBittorrent.Client.isShowRssReader() && !window.qBittorrent.Client.isShowSearchEngine() && !window.qBittorrent.Client.isShowLogViewer())
$("mainWindowTabs").classList.add("invisible"); document.getElementById("mainWindowTabs").classList.add("invisible");
}; };
$("StatisticsLink").addEventListener("click", () => { StatisticsLinkFN(); }); document.getElementById("StatisticsLink").addEventListener("click", (event) => { StatisticsLinkFN(); });
// main window tabs // main window tabs
const showTransfersTab = () => { const showTransfersTab = () => {
const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true"; const showFiltersSidebar = LocalPreferences.get("show_filters_sidebar", "true") === "true";
if (showFiltersSidebar) { if (showFiltersSidebar) {
$("filtersColumn").classList.remove("invisible"); document.getElementById("filtersColumn").classList.remove("invisible");
$("filtersColumn_handle").classList.remove("invisible"); document.getElementById("filtersColumn_handle").classList.remove("invisible");
} }
$("mainColumn").classList.remove("invisible"); document.getElementById("mainColumn").classList.remove("invisible");
$("torrentsFilterToolbar").classList.remove("invisible"); document.getElementById("torrentsFilterToolbar").classList.remove("invisible");
customSyncMainDataInterval = null; customSyncMainDataInterval = null;
syncData(100); syncData(100);
@ -1284,10 +1286,10 @@ window.addEventListener("DOMContentLoaded", () => {
}; };
const hideTransfersTab = () => { const hideTransfersTab = () => {
$("filtersColumn").classList.add("invisible"); document.getElementById("filtersColumn").classList.add("invisible");
$("filtersColumn_handle").classList.add("invisible"); document.getElementById("filtersColumn_handle").classList.add("invisible");
$("mainColumn").classList.add("invisible"); document.getElementById("mainColumn").classList.add("invisible");
$("torrentsFilterToolbar").classList.add("invisible"); document.getElementById("torrentsFilterToolbar").classList.add("invisible");
MochaUI.Desktop.resizePanels(); MochaUI.Desktop.resizePanels();
}; };
@ -1310,7 +1312,7 @@ window.addEventListener("DOMContentLoaded", () => {
searchTabInitialized = true; searchTabInitialized = true;
} }
$("searchTabColumn").classList.remove("invisible"); document.getElementById("searchTabColumn").classList.remove("invisible");
customSyncMainDataInterval = 30000; customSyncMainDataInterval = 30000;
hideTransfersTab(); hideTransfersTab();
hideRssTab(); hideRssTab();
@ -1321,7 +1323,7 @@ window.addEventListener("DOMContentLoaded", () => {
})(); })();
const hideSearchTab = () => { const hideSearchTab = () => {
$("searchTabColumn").classList.add("invisible"); document.getElementById("searchTabColumn").classList.add("invisible");
MochaUI.Desktop.resizePanels(); MochaUI.Desktop.resizePanels();
}; };
@ -1347,7 +1349,7 @@ window.addEventListener("DOMContentLoaded", () => {
window.qBittorrent.Rss.load(); window.qBittorrent.Rss.load();
} }
$("rssTabColumn").classList.remove("invisible"); document.getElementById("rssTabColumn").classList.remove("invisible");
customSyncMainDataInterval = 30000; customSyncMainDataInterval = 30000;
hideTransfersTab(); hideTransfersTab();
hideSearchTab(); hideSearchTab();
@ -1358,7 +1360,7 @@ window.addEventListener("DOMContentLoaded", () => {
})(); })();
const hideRssTab = () => { const hideRssTab = () => {
$("rssTabColumn").classList.add("invisible"); document.getElementById("rssTabColumn").classList.add("invisible");
window.qBittorrent.Rss && window.qBittorrent.Rss.unload(); window.qBittorrent.Rss && window.qBittorrent.Rss.unload();
MochaUI.Desktop.resizePanels(); MochaUI.Desktop.resizePanels();
}; };
@ -1385,7 +1387,7 @@ window.addEventListener("DOMContentLoaded", () => {
window.qBittorrent.Log.load(); window.qBittorrent.Log.load();
} }
$("logTabColumn").classList.remove("invisible"); document.getElementById("logTabColumn").classList.remove("invisible");
customSyncMainDataInterval = 30000; customSyncMainDataInterval = 30000;
hideTransfersTab(); hideTransfersTab();
hideSearchTab(); hideSearchTab();
@ -1396,7 +1398,7 @@ window.addEventListener("DOMContentLoaded", () => {
})(); })();
const hideLogTab = () => { const hideLogTab = () => {
$("logTabColumn").classList.add("invisible"); document.getElementById("logTabColumn").classList.add("invisible");
MochaUI.Desktop.resizePanels(); MochaUI.Desktop.resizePanels();
window.qBittorrent.Log && window.qBittorrent.Log.unload(); window.qBittorrent.Log && window.qBittorrent.Log.unload();
}; };
@ -1472,11 +1474,11 @@ window.addEventListener("DOMContentLoaded", () => {
tabsOnload: () => { tabsOnload: () => {
MochaUI.initializeTabs("panelTabs"); MochaUI.initializeTabs("panelTabs");
$("logMessageLink").addEventListener("click", (e) => { document.getElementById("logMessageLink").addEventListener("click", (e) => {
window.qBittorrent.Log.setCurrentTab("main"); window.qBittorrent.Log.setCurrentTab("main");
}); });
$("logPeerLink").addEventListener("click", (e) => { document.getElementById("logPeerLink").addEventListener("click", (e) => {
window.qBittorrent.Log.setCurrentTab("peer"); window.qBittorrent.Log.setCurrentTab("peer");
}); });
}, },
@ -1618,7 +1620,7 @@ window.addEventListener("DOMContentLoaded", () => {
// listen for changes to torrentsFilterInput // listen for changes to torrentsFilterInput
let torrentsFilterInputTimer = -1; let torrentsFilterInputTimer = -1;
$("torrentsFilterInput").addEventListener("input", () => { document.getElementById("torrentsFilterInput").addEventListener("input", (event) => {
clearTimeout(torrentsFilterInputTimer); clearTimeout(torrentsFilterInputTimer);
torrentsFilterInputTimer = setTimeout(() => { torrentsFilterInputTimer = setTimeout(() => {
torrentsFilterInputTimer = -1; torrentsFilterInputTimer = -1;
@ -1628,24 +1630,24 @@ window.addEventListener("DOMContentLoaded", () => {
document.getElementById("torrentsFilterToolbar").addEventListener("change", (e) => { torrentsTable.updateTable(); }); document.getElementById("torrentsFilterToolbar").addEventListener("change", (e) => { torrentsTable.updateTable(); });
$("transfersTabLink").addEventListener("click", () => { showTransfersTab(); }); document.getElementById("transfersTabLink").addEventListener("click", (event) => { showTransfersTab(); });
$("searchTabLink").addEventListener("click", () => { showSearchTab(); }); document.getElementById("searchTabLink").addEventListener("click", (event) => { showSearchTab(); });
$("rssTabLink").addEventListener("click", () => { showRssTab(); }); document.getElementById("rssTabLink").addEventListener("click", (event) => { showRssTab(); });
$("logTabLink").addEventListener("click", () => { showLogTab(); }); document.getElementById("logTabLink").addEventListener("click", (event) => { showLogTab(); });
updateTabDisplay(); updateTabDisplay();
const registerDragAndDrop = () => { const registerDragAndDrop = () => {
$("desktop").addEventListener("dragover", (ev) => { document.getElementById("desktop").addEventListener("dragover", (ev) => {
if (ev.preventDefault) if (ev.preventDefault)
ev.preventDefault(); ev.preventDefault();
}); });
$("desktop").addEventListener("dragenter", (ev) => { document.getElementById("desktop").addEventListener("dragenter", (ev) => {
if (ev.preventDefault) if (ev.preventDefault)
ev.preventDefault(); ev.preventDefault();
}); });
$("desktop").addEventListener("drop", (ev) => { document.getElementById("desktop").addEventListener("drop", (ev) => {
if (ev.preventDefault) if (ev.preventDefault)
ev.preventDefault(); ev.preventDefault();
@ -1679,7 +1681,7 @@ window.addEventListener("DOMContentLoaded", () => {
saveWindowSize(id); saveWindowSize(id);
}), }),
onContentLoaded: () => { onContentLoaded: () => {
const fileInput = $(`${id}_iframe`).contentDocument.getElementById("fileselect"); const fileInput = document.getElementById(`${id}_iframe`).contentDocument.getElementById("fileselect");
fileInput.files = droppedFiles; fileInput.files = droppedFiles;
} }
}); });
@ -1793,7 +1795,7 @@ window.addEventListener("DOMContentLoaded", () => {
}); });
}); });
window.addEventListener("load", async () => { window.addEventListener("load", async (event) => {
await window.qBittorrent.Client.initializeCaches(); await window.qBittorrent.Client.initializeCaches();
// switch to previously used tab // switch to previously used tab
@ -1801,22 +1803,22 @@ window.addEventListener("load", async () => {
switch (previouslyUsedTab) { switch (previouslyUsedTab) {
case "search": case "search":
if (window.qBittorrent.Client.isShowSearchEngine()) if (window.qBittorrent.Client.isShowSearchEngine())
$("searchTabLink").click(); document.getElementById("searchTabLink").click();
break; break;
case "rss": case "rss":
if (window.qBittorrent.Client.isShowRssReader()) if (window.qBittorrent.Client.isShowRssReader())
$("rssTabLink").click(); document.getElementById("rssTabLink").click();
break; break;
case "log": case "log":
if (window.qBittorrent.Client.isShowLogViewer()) if (window.qBittorrent.Client.isShowLogViewer())
$("logTabLink").click(); document.getElementById("logTabLink").click();
break; break;
case "transfers": case "transfers":
$("transfersTabLink").click(); document.getElementById("transfersTabLink").click();
break; break;
default: default:
console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`); console.error(`Unexpected 'selected_window_tab' value: ${previouslyUsedTab}`);
$("transfersTabLink").click(); document.getElementById("transfersTabLink").click();
break; break;
}; }
}); });

View file

@ -66,7 +66,7 @@ window.qBittorrent.ContextMenu ??= (() => {
}; };
// option diffs menu // option diffs menu
this.menu = $(this.options.menu); this.menu = document.getElementById(this.options.menu);
// fx // fx
this.fx = new Fx.Tween(this.menu, { this.fx = new Fx.Tween(this.menu, {
@ -190,7 +190,7 @@ window.qBittorrent.ContextMenu ??= (() => {
e.stopPropagation(); e.stopPropagation();
} }
// record this as the trigger // record this as the trigger
this.options.element = $(el); this.options.element = el;
this.adjustMenuPosition(e); this.adjustMenuPosition(e);
// show the menu // show the menu
this.show(); this.show();
@ -219,7 +219,7 @@ window.qBittorrent.ContextMenu ??= (() => {
}); });
// hide on body click // hide on body click
$(document.body).addEventListener("click", () => { document.body.addEventListener("click", (event) => {
this.hide(); this.hide();
this.options.element = null; this.options.element = null;
}); });
@ -292,7 +292,7 @@ window.qBittorrent.ContextMenu ??= (() => {
this.options.actions[action](element, this, action); this.options.actions[action](element, this, action);
return this; return this;
} }
}; }
class FilterListContextMenu extends ContextMenu { class FilterListContextMenu extends ContextMenu {
constructor(options) { constructor(options) {
@ -316,7 +316,7 @@ window.qBittorrent.ContextMenu ??= (() => {
.setEnabled("stopTorrents", torrentsVisible) .setEnabled("stopTorrents", torrentsVisible)
.setEnabled("deleteTorrents", torrentsVisible); .setEnabled("deleteTorrents", torrentsVisible);
} }
}; }
class TorrentsTableContextMenu extends ContextMenu { class TorrentsTableContextMenu extends ContextMenu {
updateMenuItems() { updateMenuItems() {
@ -457,7 +457,7 @@ window.qBittorrent.ContextMenu ??= (() => {
this.setEnabled("copyInfohash1", thereAreV1Hashes); this.setEnabled("copyInfohash1", thereAreV1Hashes);
this.setEnabled("copyInfohash2", thereAreV2Hashes); this.setEnabled("copyInfohash2", thereAreV2Hashes);
const contextTagList = $("contextTagList"); const contextTagList = document.getElementById("contextTagList");
for (const tag of tagMap.keys()) { for (const tag of tagMap.keys()) {
const checkbox = contextTagList.querySelector(`a[href="#Tag/${tag}"] input[type="checkbox"]`); const checkbox = contextTagList.querySelector(`a[href="#Tag/${tag}"] input[type="checkbox"]`);
const count = tagCount.get(tag); const count = tagCount.get(tag);
@ -477,13 +477,13 @@ window.qBittorrent.ContextMenu ??= (() => {
} }
updateCategoriesSubMenu(categories) { updateCategoriesSubMenu(categories) {
const contextCategoryList = $("contextCategoryList"); const contextCategoryList = document.getElementById("contextCategoryList");
[...contextCategoryList.children].forEach((el) => { el.destroy(); }); [...contextCategoryList.children].forEach((el) => { el.remove(); });
const createMenuItem = (text, imgURL, clickFn) => { const createMenuItem = (text, imgURL, clickFn) => {
const anchor = document.createElement("a"); const anchor = document.createElement("a");
anchor.textContent = text; anchor.textContent = text;
anchor.addEventListener("click", () => { clickFn(); }); anchor.addEventListener("click", clickFn);
const img = document.createElement("img"); const img = document.createElement("img");
img.src = imgURL; img.src = imgURL;
@ -495,8 +495,8 @@ window.qBittorrent.ContextMenu ??= (() => {
return item; return item;
}; };
contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentNewCategoryFN)); contextCategoryList.appendChild(createMenuItem("QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", (event) => { torrentNewCategoryFN(); }));
contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", () => { torrentSetCategoryFN(""); })); contextCategoryList.appendChild(createMenuItem("QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", (event) => { torrentSetCategoryFN(""); }));
const sortedCategories = [...categories.keys()]; const sortedCategories = [...categories.keys()];
sortedCategories.sort(window.qBittorrent.Misc.naturalSortCollator.compare); sortedCategories.sort(window.qBittorrent.Misc.naturalSortCollator.compare);
@ -527,13 +527,13 @@ window.qBittorrent.ContextMenu ??= (() => {
} }
updateTagsSubMenu(tags) { updateTagsSubMenu(tags) {
const contextTagList = $("contextTagList"); const contextTagList = document.getElementById("contextTagList");
contextTagList.replaceChildren(); contextTagList.replaceChildren();
const createMenuItem = (text, imgURL, clickFn) => { const createMenuItem = (text, imgURL, clickFn) => {
const anchor = document.createElement("a"); const anchor = document.createElement("a");
anchor.textContent = text; anchor.textContent = text;
anchor.addEventListener("click", () => { clickFn(); }); anchor.addEventListener("click", clickFn);
const img = document.createElement("img"); const img = document.createElement("img");
img.src = imgURL; img.src = imgURL;
@ -545,8 +545,8 @@ window.qBittorrent.ContextMenu ??= (() => {
return item; return item;
}; };
contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", torrentAddTagsFN)); contextTagList.appendChild(createMenuItem("QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]", "images/list-add.svg", (event) => { torrentAddTagsFN(); }));
contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", torrentRemoveAllTagsFN)); contextTagList.appendChild(createMenuItem("QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]", "images/edit-clear.svg", (event) => { torrentRemoveAllTagsFN(); }));
const sortedTags = [...tags.keys()]; const sortedTags = [...tags.keys()];
sortedTags.sort(window.qBittorrent.Misc.naturalSortCollator.compare); sortedTags.sort(window.qBittorrent.Misc.naturalSortCollator.compare);
@ -577,13 +577,13 @@ window.qBittorrent.ContextMenu ??= (() => {
contextTagList.appendChild(setTagItem); contextTagList.appendChild(setTagItem);
} }
} }
}; }
class StatusesFilterContextMenu extends FilterListContextMenu { class StatusesFilterContextMenu extends FilterListContextMenu {
updateMenuItems() { updateMenuItems() {
this.updateTorrentActions(); this.updateTorrentActions();
} }
}; }
class CategoriesFilterContextMenu extends FilterListContextMenu { class CategoriesFilterContextMenu extends FilterListContextMenu {
updateMenuItems() { updateMenuItems() {
@ -604,7 +604,7 @@ window.qBittorrent.ContextMenu ??= (() => {
this.updateTorrentActions(); this.updateTorrentActions();
} }
}; }
class TagsFilterContextMenu extends FilterListContextMenu { class TagsFilterContextMenu extends FilterListContextMenu {
updateMenuItems() { updateMenuItems() {
@ -616,7 +616,7 @@ window.qBittorrent.ContextMenu ??= (() => {
this.updateTorrentActions(); this.updateTorrentActions();
} }
}; }
class TrackersFilterContextMenu extends FilterListContextMenu { class TrackersFilterContextMenu extends FilterListContextMenu {
updateMenuItems() { updateMenuItems() {
@ -628,7 +628,7 @@ window.qBittorrent.ContextMenu ??= (() => {
this.updateTorrentActions(); this.updateTorrentActions();
} }
}; }
class SearchPluginsTableContextMenu extends ContextMenu { class SearchPluginsTableContextMenu extends ContextMenu {
updateMenuItems() { updateMenuItems() {
@ -642,7 +642,7 @@ window.qBittorrent.ContextMenu ??= (() => {
this.showItem("Uninstall"); this.showItem("Uninstall");
} }
}; }
class RssFeedContextMenu extends ContextMenu { class RssFeedContextMenu extends ContextMenu {
updateMenuItems() { updateMenuItems() {
@ -715,9 +715,9 @@ window.qBittorrent.ContextMenu ??= (() => {
break; break;
} }
} }
}; }
class RssArticleContextMenu extends ContextMenu {}; class RssArticleContextMenu extends ContextMenu {}
class RssDownloaderRuleContextMenu extends ContextMenu { class RssDownloaderRuleContextMenu extends ContextMenu {
adjustMenuPosition(e) { adjustMenuPosition(e) {
@ -727,8 +727,8 @@ window.qBittorrent.ContextMenu ??= (() => {
this.menu.style.left = "-999em"; this.menu.style.left = "-999em";
this.menu.style.top = "-999em"; this.menu.style.top = "-999em";
// position the menu // position the menu
let xPosMenu = e.pageX + this.options.offsets.x - $("rssdownloaderpage").offsetLeft; let xPosMenu = e.pageX + this.options.offsets.x - document.getElementById("rssdownloaderpage").offsetLeft;
let yPosMenu = e.pageY + this.options.offsets.y - $("rssdownloaderpage").offsetTop; let yPosMenu = e.pageY + this.options.offsets.y - document.getElementById("rssdownloaderpage").offsetTop;
if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth) if ((xPosMenu + this.menu.offsetWidth) > document.documentElement.clientWidth)
xPosMenu -= this.menu.offsetWidth; xPosMenu -= this.menu.offsetWidth;
if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight) if ((yPosMenu + this.menu.offsetHeight) > document.documentElement.clientHeight)
@ -765,7 +765,7 @@ window.qBittorrent.ContextMenu ??= (() => {
break; break;
} }
} }
}; }
return exports(); return exports();
})(); })();

View file

@ -55,7 +55,7 @@ window.qBittorrent.Download ??= (() => {
const option = document.createElement("option"); const option = document.createElement("option");
option.value = category.name; option.value = category.name;
option.textContent = category.name; option.textContent = category.name;
$("categorySelect").appendChild(option); document.getElementById("categorySelect").appendChild(option);
} }
}); });
}; };
@ -64,31 +64,31 @@ window.qBittorrent.Download ??= (() => {
const pref = window.parent.qBittorrent.Cache.preferences.get(); const pref = window.parent.qBittorrent.Cache.preferences.get();
defaultSavePath = pref.save_path; defaultSavePath = pref.save_path;
$("savepath").value = defaultSavePath; document.getElementById("savepath").value = defaultSavePath;
$("startTorrent").checked = !pref.add_stopped_enabled; document.getElementById("startTorrent").checked = !pref.add_stopped_enabled;
$("addToTopOfQueue").checked = pref.add_to_top_of_queue; document.getElementById("addToTopOfQueue").checked = pref.add_to_top_of_queue;
if (pref.auto_tmm_enabled) { if (pref.auto_tmm_enabled) {
$("autoTMM").selectedIndex = 1; document.getElementById("autoTMM").selectedIndex = 1;
$("savepath").disabled = true; document.getElementById("savepath").disabled = true;
} }
else { else {
$("autoTMM").selectedIndex = 0; document.getElementById("autoTMM").selectedIndex = 0;
} }
if (pref.torrent_stop_condition === "MetadataReceived") if (pref.torrent_stop_condition === "MetadataReceived")
$("stopCondition").selectedIndex = 1; document.getElementById("stopCondition").selectedIndex = 1;
else if (pref.torrent_stop_condition === "FilesChecked") else if (pref.torrent_stop_condition === "FilesChecked")
$("stopCondition").selectedIndex = 2; document.getElementById("stopCondition").selectedIndex = 2;
else else
$("stopCondition").selectedIndex = 0; document.getElementById("stopCondition").selectedIndex = 0;
if (pref.torrent_content_layout === "Subfolder") if (pref.torrent_content_layout === "Subfolder")
$("contentLayout").selectedIndex = 1; document.getElementById("contentLayout").selectedIndex = 1;
else if (pref.torrent_content_layout === "NoSubfolder") else if (pref.torrent_content_layout === "NoSubfolder")
$("contentLayout").selectedIndex = 2; document.getElementById("contentLayout").selectedIndex = 2;
else else
$("contentLayout").selectedIndex = 0; document.getElementById("contentLayout").selectedIndex = 0;
}; };
const changeCategorySelect = (item) => { const changeCategorySelect = (item) => {
@ -97,41 +97,41 @@ window.qBittorrent.Download ??= (() => {
item.nextElementSibling.value = ""; item.nextElementSibling.value = "";
item.nextElementSibling.select(); item.nextElementSibling.select();
if ($("autoTMM").selectedIndex === 1) if (document.getElementById("autoTMM").selectedIndex === 1)
$("savepath").value = defaultSavePath; document.getElementById("savepath").value = defaultSavePath;
} }
else { else {
item.nextElementSibling.hidden = true; item.nextElementSibling.hidden = true;
const text = item.options[item.selectedIndex].textContent; const text = item.options[item.selectedIndex].textContent;
item.nextElementSibling.value = text; item.nextElementSibling.value = text;
if ($("autoTMM").selectedIndex === 1) { if (document.getElementById("autoTMM").selectedIndex === 1) {
const categoryName = item.value; const categoryName = item.value;
const category = categories[categoryName]; const category = categories[categoryName];
let savePath = defaultSavePath; let savePath = defaultSavePath;
if (category !== undefined) if (category !== undefined)
savePath = (category["savePath"] !== "") ? category["savePath"] : `${defaultSavePath}/${categoryName}`; savePath = (category["savePath"] !== "") ? category["savePath"] : `${defaultSavePath}/${categoryName}`;
$("savepath").value = savePath; document.getElementById("savepath").value = savePath;
} }
} }
}; };
const changeTMM = (item) => { const changeTMM = (item) => {
if (item.selectedIndex === 1) { if (item.selectedIndex === 1) {
$("savepath").disabled = true; document.getElementById("savepath").disabled = true;
const categorySelect = $("categorySelect"); const categorySelect = document.getElementById("categorySelect");
const categoryName = categorySelect.options[categorySelect.selectedIndex].value; const categoryName = categorySelect.options[categorySelect.selectedIndex].value;
const category = categories[categoryName]; const category = categories[categoryName];
$("savepath").value = (category === undefined) ? "" : category["savePath"]; document.getElementById("savepath").value = (category === undefined) ? "" : category["savePath"];
} }
else { else {
$("savepath").disabled = false; document.getElementById("savepath").disabled = false;
$("savepath").value = defaultSavePath; document.getElementById("savepath").value = defaultSavePath;
} }
}; };
$(window).addEventListener("load", async () => { window.addEventListener("load", async (event) => {
// user might load this page directly (via browser magnet handler) // user might load this page directly (via browser magnet handler)
// so wait for crucial initialization to complete // so wait for crucial initialization to complete
await window.parent.qBittorrent.Client.initializeCaches(); await window.parent.qBittorrent.Client.initializeCaches();

File diff suppressed because it is too large Load diff

View file

@ -56,51 +56,51 @@ window.qBittorrent.FileTree ??= (() => {
}; };
Object.freeze(TriState); Object.freeze(TriState);
const FileTree = new Class({ class FileTree {
root: null, #root = null;
nodeMap: {}, #nodeMap = {}; // Object with Number as keys is faster than anything
setRoot: function(root) { setRoot(root) {
this.root = root; this.#root = root;
this.generateNodeMap(root); this.#generateNodeMap(root);
if (this.root.isFolder) if (this.#root.isFolder)
this.root.calculateSize(); this.#root.calculateSize();
}, }
getRoot: function() { getRoot() {
return this.root; return this.#root;
}, }
generateNodeMap: function(root) { #generateNodeMap(root) {
const stack = [root]; const stack = [root];
while (stack.length > 0) { while (stack.length > 0) {
const node = stack.pop(); const node = stack.pop();
// don't store root node in map // don't store root node in map
if (node.root !== null) if (node.root !== null)
this.nodeMap[node.rowId] = node; this.#nodeMap[node.rowId] = node;
stack.push(...node.children); stack.push(...node.children);
} }
}, }
getNode: function(rowId) { getNode(rowId) {
return (this.nodeMap[rowId] === undefined) // TODO: enforce caller sites to pass `rowId` as number and not string
? null const value = this.#nodeMap[Number(rowId)];
: this.nodeMap[rowId]; return (value !== undefined) ? value : null;
}, }
getRowId: (node) => { getRowId(node) {
return node.rowId; return node.rowId;
}, }
/** /**
* Returns the nodes in DFS in-order * Returns the nodes in DFS in-order
*/ */
toArray: function() { toArray() {
const ret = []; const ret = [];
const stack = this.root.children.toReversed(); const stack = this.#root.children.toReversed();
while (stack.length > 0) { while (stack.length > 0) {
const node = stack.pop(); const node = stack.pop();
ret.push(node); ret.push(node);
@ -108,45 +108,40 @@ window.qBittorrent.FileTree ??= (() => {
} }
return ret; return ret;
} }
}); }
const FileNode = new Class({ class FileNode {
name: "", name = "";
path: "", path = "";
rowId: null, rowId = null;
size: 0, size = 0;
checked: TriState.Unchecked, checked = TriState.Unchecked;
remaining: 0, remaining = 0;
progress: 0, progress = 0;
priority: FilePriority.Normal, priority = FilePriority.Normal;
availability: 0, availability = 0;
depth: 0, depth = 0;
root: null, root = null;
data: null, data = null;
isFolder: false, isFolder = false;
children: [], children = [];
}); }
const FolderNode = new Class({
Extends: FileNode,
class FolderNode extends FileNode {
/** /**
* Will automatically tick the checkbox for a folder if all subfolders and files are also ticked * Will automatically tick the checkbox for a folder if all subfolders and files are also ticked
*/ */
autoCheckFolders: true, autoCheckFolders = true;
isFolder = true;
initialize: function() { addChild(node) {
this.isFolder = true;
},
addChild: function(node) {
this.children.push(node); this.children.push(node);
}, }
/** /**
* Calculate size of node and its children * Calculate size of node and its children
*/ */
calculateSize: function() { calculateSize() {
const stack = [this]; const stack = [this];
const visited = []; const visited = [];
@ -202,7 +197,7 @@ window.qBittorrent.FileTree ??= (() => {
stack.pop(); stack.pop();
} }
} }
}); }
return exports(); return exports();
})(); })();

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Mike Tzou (Chocobo1)
* Copyright (C) 2019 Thomas Piccirello <thomas.piccirello@gmail.com> * Copyright (C) 2019 Thomas Piccirello <thomas.piccirello@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -32,7 +33,8 @@ window.qBittorrent ??= {};
window.qBittorrent.LocalPreferences ??= (() => { window.qBittorrent.LocalPreferences ??= (() => {
const exports = () => { const exports = () => {
return { return {
LocalPreferences: LocalPreferences LocalPreferences: LocalPreferences,
upgrade: upgrade
}; };
}; };
@ -53,6 +55,10 @@ window.qBittorrent.LocalPreferences ??= (() => {
} }
} }
size() {
return localStorage.length;
}
remove(key) { remove(key) {
try { try {
localStorage.removeItem(key); localStorage.removeItem(key);
@ -61,6 +67,40 @@ window.qBittorrent.LocalPreferences ??= (() => {
console.error(err); console.error(err);
} }
} }
}
const localPreferences = new LocalPreferences();
const upgrade = () => {
const MIGRATION_VERSION = 1;
const MIGRATION_VERSION_KEY = "MigrationVersion";
// clean start
if (localPreferences.size() === 0) {
localPreferences.set(MIGRATION_VERSION_KEY, MIGRATION_VERSION);
return;
}
// already in use
const version = Number(localPreferences.get(MIGRATION_VERSION_KEY)); // `0` on first initialization
if (version !== MIGRATION_VERSION) {
if (version < 1)
resetSideFilters();
localPreferences.set(MIGRATION_VERSION_KEY, MIGRATION_VERSION);
}
};
const resetSideFilters = () => {
// conditionally reset the filter to default to avoid none selected
const clear = (key) => {
const value = Number(localPreferences.get(key));
if ((value === 1) || (value === 2)) // affected values
localPreferences.remove(key);
};
clear("selected_category");
clear("selected_tag");
clear("selected_tracker");
}; };
return exports(); return exports();

View file

@ -151,7 +151,7 @@ let exportTorrentFN = () => {};
const initializeWindows = () => { const initializeWindows = () => {
saveWindowSize = (windowId) => { saveWindowSize = (windowId) => {
const size = $(windowId).getSize(); const size = document.getElementById(windowId).getSize();
LocalPreferences.set(`window_${windowId}_width`, size.x); LocalPreferences.set(`window_${windowId}_width`, size.x);
LocalPreferences.set(`window_${windowId}_height`, size.y); LocalPreferences.set(`window_${windowId}_height`, size.y);
}; };
@ -166,8 +166,8 @@ const initializeWindows = () => {
const addClickEvent = (el, fn) => { const addClickEvent = (el, fn) => {
["Link", "Button"].each((item) => { ["Link", "Button"].each((item) => {
if ($(el + item)) if (document.getElementById(el + item))
$(el + item).addEventListener("click", fn); document.getElementById(el + item).addEventListener("click", fn);
}); });
}; };

View file

@ -0,0 +1,72 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 bolshoytoster <toasterbig@gmail.com>
* Copyright (C) 2025 Mike Tzou (Chocobo1)
*
* 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.
*/
"use strict";
window.qBittorrent ??= {};
window.qBittorrent.MonkeyPatch ??= (() => {
const exports = () => {
return {
patch: patch
};
};
const patch = () => {
patchMootoolsDocumentId();
};
const patchMootoolsDocumentId = () => {
// Override MooTools' `document.id` (used for `$(id)`), which prevents it
// from allocating a `uniqueNumber` for elements that don't need it.
// MooTools and MochaUI use it internally.
if (document.id === undefined)
return;
document.id = (el) => {
if ((el === null) || (el === undefined))
return null;
switch (typeof el) {
case "object":
return el;
case "string":
return document.getElementById(el);
}
return null;
};
};
return exports();
})();
Object.freeze(window.qBittorrent.MonkeyPatch);
// execute now
window.qBittorrent.MonkeyPatch.patch();

View file

@ -76,16 +76,16 @@ window.qBittorrent.pathAutofill ??= (() => {
function attachPathAutofill() { function 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(event) { showPathSuggestions(this, "dirs"); });
input.classList.add("pathAutoFillInitialized"); input.classList.add("pathAutoFillInitialized");
} }
const fileInputs = document.querySelectorAll(".pathFile:not(.pathAutoFillInitialized)"); const fileInputs = document.querySelectorAll(".pathFile:not(.pathAutoFillInitialized)");
for (const input of fileInputs) { for (const input of fileInputs) {
input.addEventListener("input", function() { showPathSuggestions(this, "all"); }); input.addEventListener("input", function(event) { showPathSuggestions(this, "all"); });
input.classList.add("pathAutoFillInitialized"); input.classList.add("pathAutoFillInitialized");
} }
}; }
return exports(); return exports();
})(); })();

View file

@ -36,92 +36,81 @@ window.qBittorrent.PiecesBar ??= (() => {
}; };
}; };
const STATUS_DOWNLOADING = 1; class PiecesBar extends HTMLElement {
const STATUS_DOWNLOADED = 2; static #STATUS_DOWNLOADING = 1;
static #STATUS_DOWNLOADED = 2;
// absolute max width of 4096 // absolute max width of 4096
// this is to support all browsers for size of canvas elements // this is to support all browsers for size of canvas elements
// see https://github.com/jhildenbiddle/canvas-size#test-results // see https://github.com/jhildenbiddle/canvas-size#test-results
const MAX_CANVAS_WIDTH = 4096; static #MAX_CANVAS_WIDTH = 4096;
static #piecesBarUniqueId = 0;
let piecesBarUniqueId = 0; #canvasEl;
const PiecesBar = new Class({ #ctx;
initialize: (pieces, parameters) => { #pieces;
const vals = { #styles;
id: `piecesbar_${piecesBarUniqueId++}`, #id = ++PiecesBar.#piecesBarUniqueId;
width: 0, #resizeObserver;
height: 0,
constructor(pieces, styles = {}) {
super();
this.setPieces(pieces);
this.#styles = {
height: 12,
downloadingColor: "hsl(110deg 94% 27%)", // @TODO palette vars not supported for this value, apply average downloadingColor: "hsl(110deg 94% 27%)", // @TODO palette vars not supported for this value, apply average
haveColor: "hsl(210deg 55% 55%)", // @TODO palette vars not supported for this value, apply average haveColor: "hsl(210deg 55% 55%)", // @TODO palette vars not supported for this value, apply average
borderSize: 1, borderSize: 1,
borderColor: "var(--color-border-default)" borderColor: "var(--color-border-default)",
...styles
}; };
if (parameters && (typeOf(parameters) === "object")) this.#canvasEl = document.createElement("canvas");
Object.append(vals, parameters); this.#canvasEl.style.height = "100%";
vals.height = Math.max(vals.height, 12); this.#canvasEl.style.imageRendering = "pixelated";
this.#canvasEl.style.width = "100%";
this.#ctx = this.#canvasEl.getContext("2d");
const obj = document.createElement("div"); this.attachShadow({ mode: "open" });
obj.id = vals.id; this.shadowRoot.host.id = `piecesbar_${this.#id}`;
obj.className = "piecesbarWrapper"; this.shadowRoot.host.style.display = "block";
obj.style.border = `${vals.borderSize}px solid ${vals.borderColor}`; this.shadowRoot.host.style.height = `${this.#styles.height}px`;
obj.style.height = `${vals.height}px`; this.shadowRoot.host.style.border = `${this.#styles.borderSize}px solid ${this.#styles.borderColor}`;
obj.vals = vals; this.shadowRoot.append(this.#canvasEl);
obj.vals.pieces = [pieces, []].pick();
const canvas = document.createElement("canvas"); this.#resizeObserver = new ResizeObserver(window.qBittorrent.Misc.createDebounceHandler(100, () => {
canvas.id = `${vals.id}_canvas`; this.#refresh();
canvas.className = "piecesbarCanvas"; }));
canvas.width = `${vals.width - (2 * vals.borderSize)}`;
canvas.height = "1"; // will stretch vertically to take up the height of the parent
obj.vals.canvas = canvas;
obj.appendChild(obj.vals.canvas);
obj.setPieces = setPieces;
obj.refresh = refresh;
obj.clear = setPieces.bind(obj, []);
obj._drawStatus = drawStatus;
if (vals.width > 0)
obj.setPieces(vals.pieces);
else
setTimeout(() => { checkForParent(obj.id); });
return obj;
}
});
function setPieces(pieces) {
if (!Array.isArray(pieces))
pieces = [];
this.vals.pieces = pieces;
this.refresh(true);
} }
function refresh(force) { connectedCallback() {
if (!this.parentNode) this.#resizeObserver.observe(this);
this.#refresh();
}
clear() {
this.setPieces([]);
}
setPieces(pieces) {
this.#pieces = !Array.isArray(pieces) ? [] : pieces;
this.#refresh();
}
#refresh() {
if (!this.isConnected)
return; return;
const pieces = this.vals.pieces;
// if the number of pieces is small, use that for the width, // if the number of pieces is small, use that for the width,
// and have it stretch horizontally. // and have it stretch horizontally.
// this also limits the ratio below to >= 1 // this also limits the ratio below to >= 1
const width = Math.min(this.offsetWidth, pieces.length, MAX_CANVAS_WIDTH); const width = Math.min(this.offsetWidth, this.#pieces.length, PiecesBar.#MAX_CANVAS_WIDTH);
if ((this.vals.width === width) && !force)
return;
this.vals.width = width;
// change canvas size to fit exactly in the space // change canvas size to fit exactly in the space
this.vals.canvas.width = width - (2 * this.vals.borderSize); this.#canvasEl.width = width - (2 * this.#styles.borderSize);
const canvas = this.vals.canvas; this.#ctx.clearRect(0, 0, this.#canvasEl.width, this.#canvasEl.height);
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
const imageWidth = canvas.width; const imageWidth = this.#canvasEl.width;
if (imageWidth.length === 0) if (imageWidth.length === 0)
return; return;
@ -129,7 +118,7 @@ window.qBittorrent.PiecesBar ??= (() => {
let minStatus = Infinity; let minStatus = Infinity;
let maxStatus = 0; let maxStatus = 0;
for (const status of pieces) { for (const status of this.#pieces) {
if (status > maxStatus) if (status > maxStatus)
maxStatus = status; maxStatus = status;
if (status < minStatus) if (status < minStatus)
@ -141,9 +130,9 @@ window.qBittorrent.PiecesBar ??= (() => {
return; return;
// if all pieces are downloaded, fill entire image at once // if all pieces are downloaded, fill entire image at once
if (minStatus === STATUS_DOWNLOADED) { if (minStatus === PiecesBar.#STATUS_DOWNLOADED) {
ctx.fillStyle = this.vals.haveColor; this.#ctx.fillStyle = this.#styles.haveColor;
ctx.fillRect(0, 0, canvas.width, canvas.height); this.#ctx.fillRect(0, 0, this.#canvasEl.width, this.#canvasEl.height);
return; return;
} }
@ -168,7 +157,7 @@ window.qBittorrent.PiecesBar ??= (() => {
* +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+
*/ */
const ratio = pieces.length / imageWidth; const ratio = this.#pieces.length / imageWidth;
let lastValue = null; let lastValue = null;
let rectangleStart = 0; let rectangleStart = 0;
@ -181,8 +170,8 @@ window.qBittorrent.PiecesBar ??= (() => {
const piecesToInt = Math.ceil(piecesTo); const piecesToInt = Math.ceil(piecesTo);
const statusValues = { const statusValues = {
[STATUS_DOWNLOADING]: 0, [PiecesBar.#STATUS_DOWNLOADING]: 0,
[STATUS_DOWNLOADED]: 0 [PiecesBar.#STATUS_DOWNLOADED]: 0
}; };
// aggregate the status of each piece that contributes to this pixel // aggregate the status of each piece that contributes to this pixel
@ -192,35 +181,35 @@ window.qBittorrent.PiecesBar ??= (() => {
const pieceEnd = Math.min(piece + 1, piecesTo); const pieceEnd = Math.min(piece + 1, piecesTo);
const amount = pieceEnd - pieceStart; const amount = pieceEnd - pieceStart;
const status = pieces[piece]; const status = this.#pieces[piece];
if (status in statusValues) if (status in statusValues)
statusValues[status] += amount; statusValues[status] += amount;
} }
// normalize to interval [0, 1] // normalize to interval [0, 1]
statusValues[STATUS_DOWNLOADING] /= ratio; statusValues[PiecesBar.#STATUS_DOWNLOADING] /= ratio;
statusValues[STATUS_DOWNLOADED] /= ratio; statusValues[PiecesBar.#STATUS_DOWNLOADED] /= ratio;
// floats accumulate small errors, so smooth it out by rounding to hundredths place // floats accumulate small errors, so smooth it out by rounding to hundredths place
// this effectively limits each status to a value 1 in 100 // this effectively limits each status to a value 1 in 100
statusValues[STATUS_DOWNLOADING] = Math.round(statusValues[STATUS_DOWNLOADING] * 100) / 100; statusValues[PiecesBar.#STATUS_DOWNLOADING] = Math.round(statusValues[PiecesBar.#STATUS_DOWNLOADING] * 100) / 100;
statusValues[STATUS_DOWNLOADED] = Math.round(statusValues[STATUS_DOWNLOADED] * 100) / 100; statusValues[PiecesBar.#STATUS_DOWNLOADED] = Math.round(statusValues[PiecesBar.#STATUS_DOWNLOADED] * 100) / 100;
// float precision sometimes _still_ gives > 1 // float precision sometimes _still_ gives > 1
statusValues[STATUS_DOWNLOADING] = Math.min(statusValues[STATUS_DOWNLOADING], 1); statusValues[PiecesBar.#STATUS_DOWNLOADING] = Math.min(statusValues[PiecesBar.#STATUS_DOWNLOADING], 1);
statusValues[STATUS_DOWNLOADED] = Math.min(statusValues[STATUS_DOWNLOADED], 1); statusValues[PiecesBar.#STATUS_DOWNLOADED] = Math.min(statusValues[PiecesBar.#STATUS_DOWNLOADED], 1);
if (!lastValue) if (!lastValue)
lastValue = statusValues; lastValue = statusValues;
// group contiguous colors together and draw as a single rectangle // group contiguous colors together and draw as a single rectangle
if ((lastValue[STATUS_DOWNLOADING] === statusValues[STATUS_DOWNLOADING]) if ((lastValue[PiecesBar.#STATUS_DOWNLOADING] === statusValues[PiecesBar.#STATUS_DOWNLOADING])
&& (lastValue[STATUS_DOWNLOADED] === statusValues[STATUS_DOWNLOADED])) && (lastValue[PiecesBar.#STATUS_DOWNLOADED] === statusValues[PiecesBar.#STATUS_DOWNLOADED]))
continue; continue;
const rectangleWidth = x - rectangleStart; const rectangleWidth = x - rectangleStart;
this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue); this.#drawStatus(rectangleStart, rectangleWidth, lastValue);
lastValue = statusValues; lastValue = statusValues;
rectangleStart = x; rectangleStart = x;
@ -229,36 +218,29 @@ window.qBittorrent.PiecesBar ??= (() => {
// fill a rect at the end of the canvas // fill a rect at the end of the canvas
if (rectangleStart < imageWidth) { if (rectangleStart < imageWidth) {
const rectangleWidth = imageWidth - rectangleStart; const rectangleWidth = imageWidth - rectangleStart;
this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue); this.#drawStatus(rectangleStart, rectangleWidth, lastValue);
} }
} }
function drawStatus(ctx, start, width, statusValues) { #drawStatus(start, width, statusValues) {
// mix the colors by using transparency and a composite mode // mix the colors by using transparency and a composite mode
ctx.globalCompositeOperation = "lighten"; this.#ctx.globalCompositeOperation = "lighten";
if (statusValues[STATUS_DOWNLOADING]) { if (statusValues[PiecesBar.#STATUS_DOWNLOADING]) {
ctx.globalAlpha = statusValues[STATUS_DOWNLOADING]; this.#ctx.globalAlpha = statusValues[PiecesBar.#STATUS_DOWNLOADING];
ctx.fillStyle = this.vals.downloadingColor; this.#ctx.fillStyle = this.#styles.downloadingColor;
ctx.fillRect(start, 0, width, ctx.canvas.height); this.#ctx.fillRect(start, 0, width, this.#canvasEl.height);
} }
if (statusValues[STATUS_DOWNLOADED]) { if (statusValues[PiecesBar.#STATUS_DOWNLOADED]) {
ctx.globalAlpha = statusValues[STATUS_DOWNLOADED]; this.#ctx.globalAlpha = statusValues[PiecesBar.#STATUS_DOWNLOADED];
ctx.fillStyle = this.vals.haveColor; this.#ctx.fillStyle = this.#styles.haveColor;
ctx.fillRect(start, 0, width, ctx.canvas.height); this.#ctx.fillRect(start, 0, width, this.#canvasEl.height);
}
} }
} }
const checkForParent = (id) => { customElements.define("pieces-bar", PiecesBar);
const obj = $(id);
if (!obj)
return;
if (!obj.parentNode)
return setTimeout(() => { checkForParent(id); }, 100);
obj.refresh();
};
return exports(); return exports();
})(); })();

View file

@ -142,7 +142,7 @@ window.qBittorrent.ProgressBar ??= (() => {
} }
const ProgressBar_checkForParent = (id) => { const ProgressBar_checkForParent = (id) => {
const obj = $(id); const obj = document.getElementById(id);
if (!obj) if (!obj)
return; return;
if (!obj.parentNode) if (!obj.parentNode)

View file

@ -208,7 +208,7 @@ window.qBittorrent.PropFiles ??= (() => {
const rowIds = []; const rowIds = [];
const fileIds = []; const fileIds = [];
let priority = FilePriority.Ignored; let priority = FilePriority.Ignored;
const checkbox = $("tristate_cb"); const checkbox = document.getElementById("tristate_cb");
if (checkbox.state === "checked") { if (checkbox.state === "checked") {
setCheckboxUnchecked(checkbox); setCheckboxUnchecked(checkbox);
@ -245,7 +245,7 @@ window.qBittorrent.PropFiles ??= (() => {
}; };
const updateGlobalCheckbox = () => { const updateGlobalCheckbox = () => {
const checkbox = $("tristate_cb"); const checkbox = document.getElementById("tristate_cb");
if (torrentFilesTable.isAllCheckboxesChecked()) if (torrentFilesTable.isAllCheckboxesChecked())
setCheckboxChecked(checkbox); setCheckboxChecked(checkbox);
else if (torrentFilesTable.isAllCheckboxesUnchecked()) else if (torrentFilesTable.isAllCheckboxesUnchecked())
@ -297,7 +297,7 @@ window.qBittorrent.PropFiles ??= (() => {
ids.forEach((id) => { ids.forEach((id) => {
torrentFilesTable.setIgnored(id, ignore); torrentFilesTable.setIgnored(id, ignore);
const combobox = $(`comboPrio${id}`); const combobox = document.getElementById(`comboPrio${id}`);
if (combobox !== null) if (combobox !== null)
selectComboboxPriority(combobox, priority); selectComboboxPriority(combobox, priority);
}); });
@ -307,8 +307,8 @@ window.qBittorrent.PropFiles ??= (() => {
const loadTorrentFilesData = () => { const loadTorrentFilesData = () => {
if (document.hidden) if (document.hidden)
return; return;
if ($("propFiles").classList.contains("invisible") if (document.getElementById("propFiles").classList.contains("invisible")
|| $("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) {
// Tab changed, don't do anything // Tab changed, don't do anything
return; return;
} }
@ -586,10 +586,10 @@ window.qBittorrent.PropFiles ??= (() => {
// listen for changes to torrentFilesFilterInput // listen for changes to torrentFilesFilterInput
let torrentFilesFilterInputTimer = -1; let torrentFilesFilterInputTimer = -1;
$("torrentFilesFilterInput").addEventListener("input", () => { document.getElementById("torrentFilesFilterInput").addEventListener("input", (event) => {
clearTimeout(torrentFilesFilterInputTimer); clearTimeout(torrentFilesFilterInputTimer);
const value = $("torrentFilesFilterInput").value; const value = document.getElementById("torrentFilesFilterInput").value;
torrentFilesTable.setFilter(value); torrentFilesTable.setFilter(value);
torrentFilesFilterInputTimer = setTimeout(() => { torrentFilesFilterInputTimer = setTimeout(() => {

View file

@ -40,37 +40,37 @@ window.qBittorrent.PropGeneral ??= (() => {
const piecesBar = new window.qBittorrent.PiecesBar.PiecesBar([], { const piecesBar = new window.qBittorrent.PiecesBar.PiecesBar([], {
height: 18 height: 18
}); });
$("progress").appendChild(piecesBar); document.getElementById("progress").appendChild(piecesBar);
const clearData = () => { const clearData = () => {
document.getElementById("progressPercentage").textContent = ""; document.getElementById("progressPercentage").textContent = "";
$("time_elapsed").textContent = ""; document.getElementById("time_elapsed").textContent = "";
$("eta").textContent = ""; document.getElementById("eta").textContent = "";
$("nb_connections").textContent = ""; document.getElementById("nb_connections").textContent = "";
$("total_downloaded").textContent = ""; document.getElementById("total_downloaded").textContent = "";
$("total_uploaded").textContent = ""; document.getElementById("total_uploaded").textContent = "";
$("dl_speed").textContent = ""; document.getElementById("dl_speed").textContent = "";
$("up_speed").textContent = ""; document.getElementById("up_speed").textContent = "";
$("dl_limit").textContent = ""; document.getElementById("dl_limit").textContent = "";
$("up_limit").textContent = ""; document.getElementById("up_limit").textContent = "";
$("total_wasted").textContent = ""; document.getElementById("total_wasted").textContent = "";
$("seeds").textContent = ""; document.getElementById("seeds").textContent = "";
$("peers").textContent = ""; document.getElementById("peers").textContent = "";
$("share_ratio").textContent = ""; document.getElementById("share_ratio").textContent = "";
$("popularity").textContent = ""; document.getElementById("popularity").textContent = "";
$("reannounce").textContent = ""; document.getElementById("reannounce").textContent = "";
$("last_seen").textContent = ""; document.getElementById("last_seen").textContent = "";
$("total_size").textContent = ""; document.getElementById("total_size").textContent = "";
$("pieces").textContent = ""; document.getElementById("pieces").textContent = "";
$("created_by").textContent = ""; document.getElementById("created_by").textContent = "";
$("addition_date").textContent = ""; document.getElementById("addition_date").textContent = "";
$("completion_date").textContent = ""; document.getElementById("completion_date").textContent = "";
$("creation_date").textContent = ""; document.getElementById("creation_date").textContent = "";
$("torrent_hash_v1").textContent = ""; document.getElementById("torrent_hash_v1").textContent = "";
$("torrent_hash_v2").textContent = ""; document.getElementById("torrent_hash_v2").textContent = "";
$("save_path").textContent = ""; document.getElementById("save_path").textContent = "";
$("comment").textContent = ""; document.getElementById("comment").textContent = "";
$("private").textContent = ""; document.getElementById("private").textContent = "";
piecesBar.clear(); piecesBar.clear();
}; };
@ -78,8 +78,8 @@ window.qBittorrent.PropGeneral ??= (() => {
const loadTorrentData = () => { const loadTorrentData = () => {
if (document.hidden) if (document.hidden)
return; return;
if ($("propGeneral").classList.contains("invisible") if (document.getElementById("propGeneral").classList.contains("invisible")
|| $("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) {
// Tab changed, don't do anything // Tab changed, don't do anything
return; return;
} }
@ -100,13 +100,13 @@ window.qBittorrent.PropGeneral ??= (() => {
}) })
.then(async (response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
$("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; document.getElementById("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
clearTimeout(loadTorrentDataTimer); clearTimeout(loadTorrentDataTimer);
loadTorrentDataTimer = loadTorrentData.delay(10000); loadTorrentDataTimer = loadTorrentData.delay(10000);
return; return;
} }
$("error_div").textContent = ""; document.getElementById("error_div").textContent = "";
const data = await response.json(); const data = await response.json();
if (data) { if (data) {
@ -119,70 +119,70 @@ window.qBittorrent.PropGeneral ??= (() => {
.replace("%1", window.qBittorrent.Misc.friendlyDuration(data.time_elapsed)) .replace("%1", window.qBittorrent.Misc.friendlyDuration(data.time_elapsed))
.replace("%2", window.qBittorrent.Misc.friendlyDuration(data.seeding_time)) .replace("%2", window.qBittorrent.Misc.friendlyDuration(data.seeding_time))
: window.qBittorrent.Misc.friendlyDuration(data.time_elapsed); : window.qBittorrent.Misc.friendlyDuration(data.time_elapsed);
$("time_elapsed").textContent = timeElapsed; document.getElementById("time_elapsed").textContent = timeElapsed;
$("eta").textContent = window.qBittorrent.Misc.friendlyDuration(data.eta, window.qBittorrent.Misc.MAX_ETA); document.getElementById("eta").textContent = window.qBittorrent.Misc.friendlyDuration(data.eta, window.qBittorrent.Misc.MAX_ETA);
const nbConnections = "QBT_TR(%1 (%2 max))QBT_TR[CONTEXT=PropertiesWidget]" const nbConnections = "QBT_TR(%1 (%2 max))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", data.nb_connections) .replace("%1", data.nb_connections)
.replace("%2", ((data.nb_connections_limit < 0) ? "∞" : data.nb_connections_limit)); .replace("%2", ((data.nb_connections_limit < 0) ? "∞" : data.nb_connections_limit));
$("nb_connections").textContent = nbConnections; document.getElementById("nb_connections").textContent = nbConnections;
const totalDownloaded = "QBT_TR(%1 (%2 this session))QBT_TR[CONTEXT=PropertiesWidget]" const totalDownloaded = "QBT_TR(%1 (%2 this session))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.total_downloaded)) .replace("%1", window.qBittorrent.Misc.friendlyUnit(data.total_downloaded))
.replace("%2", window.qBittorrent.Misc.friendlyUnit(data.total_downloaded_session)); .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.total_downloaded_session));
$("total_downloaded").textContent = totalDownloaded; document.getElementById("total_downloaded").textContent = totalDownloaded;
const totalUploaded = "QBT_TR(%1 (%2 this session))QBT_TR[CONTEXT=PropertiesWidget]" const totalUploaded = "QBT_TR(%1 (%2 this session))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.total_uploaded)) .replace("%1", window.qBittorrent.Misc.friendlyUnit(data.total_uploaded))
.replace("%2", window.qBittorrent.Misc.friendlyUnit(data.total_uploaded_session)); .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.total_uploaded_session));
$("total_uploaded").textContent = totalUploaded; document.getElementById("total_uploaded").textContent = totalUploaded;
const dlSpeed = "QBT_TR(%1 (%2 avg.))QBT_TR[CONTEXT=PropertiesWidget]" const dlSpeed = "QBT_TR(%1 (%2 avg.))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.dl_speed, true)) .replace("%1", window.qBittorrent.Misc.friendlyUnit(data.dl_speed, true))
.replace("%2", window.qBittorrent.Misc.friendlyUnit(data.dl_speed_avg, true)); .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.dl_speed_avg, true));
$("dl_speed").textContent = dlSpeed; document.getElementById("dl_speed").textContent = dlSpeed;
const upSpeed = "QBT_TR(%1 (%2 avg.))QBT_TR[CONTEXT=PropertiesWidget]" const upSpeed = "QBT_TR(%1 (%2 avg.))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.up_speed, true)) .replace("%1", window.qBittorrent.Misc.friendlyUnit(data.up_speed, true))
.replace("%2", window.qBittorrent.Misc.friendlyUnit(data.up_speed_avg, true)); .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.up_speed_avg, true));
$("up_speed").textContent = upSpeed; document.getElementById("up_speed").textContent = upSpeed;
const dlLimit = (data.dl_limit === -1) const dlLimit = (data.dl_limit === -1)
? "∞" ? "∞"
: window.qBittorrent.Misc.friendlyUnit(data.dl_limit, true); : window.qBittorrent.Misc.friendlyUnit(data.dl_limit, true);
$("dl_limit").textContent = dlLimit; document.getElementById("dl_limit").textContent = dlLimit;
const upLimit = (data.up_limit === -1) const upLimit = (data.up_limit === -1)
? "∞" ? "∞"
: window.qBittorrent.Misc.friendlyUnit(data.up_limit, true); : window.qBittorrent.Misc.friendlyUnit(data.up_limit, true);
$("up_limit").textContent = upLimit; document.getElementById("up_limit").textContent = upLimit;
$("total_wasted").textContent = window.qBittorrent.Misc.friendlyUnit(data.total_wasted); document.getElementById("total_wasted").textContent = window.qBittorrent.Misc.friendlyUnit(data.total_wasted);
const seeds = "QBT_TR(%1 (%2 total))QBT_TR[CONTEXT=PropertiesWidget]" const seeds = "QBT_TR(%1 (%2 total))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", data.seeds) .replace("%1", data.seeds)
.replace("%2", data.seeds_total); .replace("%2", data.seeds_total);
$("seeds").textContent = seeds; document.getElementById("seeds").textContent = seeds;
const peers = "QBT_TR(%1 (%2 total))QBT_TR[CONTEXT=PropertiesWidget]" const peers = "QBT_TR(%1 (%2 total))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", data.peers) .replace("%1", data.peers)
.replace("%2", data.peers_total); .replace("%2", data.peers_total);
$("peers").textContent = peers; document.getElementById("peers").textContent = peers;
$("share_ratio").textContent = data.share_ratio.toFixed(2); document.getElementById("share_ratio").textContent = data.share_ratio.toFixed(2);
$("popularity").textContent = data.popularity.toFixed(2); document.getElementById("popularity").textContent = data.popularity.toFixed(2);
$("reannounce").textContent = window.qBittorrent.Misc.friendlyDuration(data.reannounce); document.getElementById("reannounce").textContent = window.qBittorrent.Misc.friendlyDuration(data.reannounce);
const lastSeen = (data.last_seen >= 0) const lastSeen = (data.last_seen >= 0)
? new Date(data.last_seen * 1000).toLocaleString() ? new Date(data.last_seen * 1000).toLocaleString()
: "QBT_TR(Never)QBT_TR[CONTEXT=PropertiesWidget]"; : "QBT_TR(Never)QBT_TR[CONTEXT=PropertiesWidget]";
$("last_seen").textContent = lastSeen; document.getElementById("last_seen").textContent = lastSeen;
const totalSize = (data.total_size >= 0) ? window.qBittorrent.Misc.friendlyUnit(data.total_size) : ""; const totalSize = (data.total_size >= 0) ? window.qBittorrent.Misc.friendlyUnit(data.total_size) : "";
$("total_size").textContent = totalSize; document.getElementById("total_size").textContent = totalSize;
const pieces = (data.pieces_num >= 0) const pieces = (data.pieces_num >= 0)
? "QBT_TR(%1 x %2 (have %3))QBT_TR[CONTEXT=PropertiesWidget]" ? "QBT_TR(%1 x %2 (have %3))QBT_TR[CONTEXT=PropertiesWidget]"
@ -190,40 +190,40 @@ window.qBittorrent.PropGeneral ??= (() => {
.replace("%2", window.qBittorrent.Misc.friendlyUnit(data.piece_size)) .replace("%2", window.qBittorrent.Misc.friendlyUnit(data.piece_size))
.replace("%3", data.pieces_have) .replace("%3", data.pieces_have)
: ""; : "";
$("pieces").textContent = pieces; document.getElementById("pieces").textContent = pieces;
$("created_by").textContent = data.created_by; document.getElementById("created_by").textContent = data.created_by;
const additionDate = (data.addition_date >= 0) const additionDate = (data.addition_date >= 0)
? new Date(data.addition_date * 1000).toLocaleString() ? new Date(data.addition_date * 1000).toLocaleString()
: "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]"; : "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
$("addition_date").textContent = additionDate; document.getElementById("addition_date").textContent = additionDate;
const completionDate = (data.completion_date >= 0) const completionDate = (data.completion_date >= 0)
? new Date(data.completion_date * 1000).toLocaleString() ? new Date(data.completion_date * 1000).toLocaleString()
: ""; : "";
$("completion_date").textContent = completionDate; document.getElementById("completion_date").textContent = completionDate;
const creationDate = (data.creation_date >= 0) const creationDate = (data.creation_date >= 0)
? new Date(data.creation_date * 1000).toLocaleString() ? new Date(data.creation_date * 1000).toLocaleString()
: ""; : "";
$("creation_date").textContent = creationDate; document.getElementById("creation_date").textContent = creationDate;
const torrentHashV1 = (data.infohash_v1 !== "") const torrentHashV1 = (data.infohash_v1 !== "")
? data.infohash_v1 ? data.infohash_v1
: "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
$("torrent_hash_v1").textContent = torrentHashV1; document.getElementById("torrent_hash_v1").textContent = torrentHashV1;
const torrentHashV2 = (data.infohash_v2 !== "") const torrentHashV2 = (data.infohash_v2 !== "")
? data.infohash_v2 ? data.infohash_v2
: "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]"; : "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]";
$("torrent_hash_v2").textContent = torrentHashV2; document.getElementById("torrent_hash_v2").textContent = torrentHashV2;
$("save_path").textContent = data.save_path; document.getElementById("save_path").textContent = data.save_path;
$("comment").innerHTML = window.qBittorrent.Misc.parseHtmlLinks(window.qBittorrent.Misc.escapeHtml(data.comment)); document.getElementById("comment").innerHTML = window.qBittorrent.Misc.parseHtmlLinks(window.qBittorrent.Misc.escapeHtml(data.comment));
$("private").textContent = (data.has_metadata document.getElementById("private").textContent = (data.has_metadata
? (data.private ? (data.private
? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]" ? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
: "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]") : "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]")
@ -246,13 +246,13 @@ window.qBittorrent.PropGeneral ??= (() => {
}) })
.then(async (response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
$("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; document.getElementById("error_div").textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
clearTimeout(loadTorrentDataTimer); clearTimeout(loadTorrentDataTimer);
loadTorrentDataTimer = loadTorrentData.delay(10000); loadTorrentDataTimer = loadTorrentData.delay(10000);
return; return;
} }
$("error_div").textContent = ""; document.getElementById("error_div").textContent = "";
const data = await response.json(); const data = await response.json();
if (data) if (data)

View file

@ -45,8 +45,8 @@ window.qBittorrent.PropPeers ??= (() => {
const loadTorrentPeersData = () => { const loadTorrentPeersData = () => {
if (document.hidden) if (document.hidden)
return; return;
if ($("propPeers").classList.contains("invisible") if (document.getElementById("propPeers").classList.contains("invisible")
|| $("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) {
syncTorrentPeersLastResponseId = 0; syncTorrentPeersLastResponseId = 0;
torrentPeersTable.clear(); torrentPeersTable.clear();
return; return;
@ -73,7 +73,7 @@ window.qBittorrent.PropPeers ??= (() => {
const responseJSON = await response.json(); const responseJSON = await response.json();
$("error_div").textContent = ""; document.getElementById("error_div").textContent = "";
if (responseJSON) { if (responseJSON) {
const full_update = (responseJSON["full_update"] === true); const full_update = (responseJSON["full_update"] === true);
if (full_update) if (full_update)

View file

@ -45,8 +45,8 @@ window.qBittorrent.PropTrackers ??= (() => {
const loadTrackersData = () => { const loadTrackersData = () => {
if (document.hidden) if (document.hidden)
return; return;
if ($("propTrackers").classList.contains("invisible") if (document.getElementById("propTrackers").classList.contains("invisible")
|| $("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) {
// Tab changed, don't do anything // Tab changed, don't do anything
return; return;
} }

View file

@ -45,8 +45,8 @@ window.qBittorrent.PropWebseeds ??= (() => {
const loadWebSeedsData = () => { const loadWebSeedsData = () => {
if (document.hidden) if (document.hidden)
return; return;
if ($("propWebSeeds").classList.contains("invisible") if (document.getElementById("propWebSeeds").classList.contains("invisible")
|| $("propertiesPanel_collapseToggle").classList.contains("panel-expand")) { || document.getElementById("propertiesPanel_collapseToggle").classList.contains("panel-expand")) {
// Tab changed, don't do anything // Tab changed, don't do anything
return; return;
} }

View file

@ -22,7 +22,7 @@ window.qBittorrent.MultiRename ??= (() => {
// Search Options // Search Options
_inner_search: "", _inner_search: "",
setSearch(val) { setSearch: function(val) {
this._inner_search = val; this._inner_search = val;
this._inner_update(); this._inner_update();
this.onChanged(this.matchedFiles); this.onChanged(this.matchedFiles);
@ -33,7 +33,7 @@ window.qBittorrent.MultiRename ??= (() => {
// Replacement Options // Replacement Options
_inner_replacement: "", _inner_replacement: "",
setReplacement(val) { setReplacement: function(val) {
this._inner_replacement = val; this._inner_replacement = val;
this._inner_update(); this._inner_update();
this.onChanged(this.matchedFiles); this.onChanged(this.matchedFiles);

View file

@ -107,7 +107,7 @@ window.qBittorrent.Search ??= (() => {
const init = () => { const init = () => {
// load "Search in" preference from local storage // load "Search in" preference from local storage
$("searchInTorrentName").value = (LocalPreferences.get("search_in_filter") === "names") ? "names" : "everywhere"; document.getElementById("searchInTorrentName").value = (LocalPreferences.get("search_in_filter") === "names") ? "names" : "everywhere";
const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: "#searchResultsTableDiv tbody tr", targets: "#searchResultsTableDiv tbody tr",
menu: "searchResultsTableMenu", menu: "searchResultsTableMenu",
@ -128,12 +128,12 @@ window.qBittorrent.Search ??= (() => {
// listen for changes to searchInNameFilter // listen for changes to searchInNameFilter
let searchInNameFilterTimer = -1; let searchInNameFilterTimer = -1;
$("searchInNameFilter").addEventListener("input", () => { document.getElementById("searchInNameFilter").addEventListener("input", (event) => {
clearTimeout(searchInNameFilterTimer); clearTimeout(searchInNameFilterTimer);
searchInNameFilterTimer = setTimeout(() => { searchInNameFilterTimer = setTimeout(() => {
searchInNameFilterTimer = -1; searchInNameFilterTimer = -1;
const value = $("searchInNameFilter").value; const value = document.getElementById("searchInNameFilter").value;
searchText.filterPattern = value; searchText.filterPattern = value;
searchFilterChanged(); searchFilterChanged();
}, window.qBittorrent.Misc.FILTER_INPUT_DELAY); }, window.qBittorrent.Misc.FILTER_INPUT_DELAY);
@ -200,15 +200,15 @@ window.qBittorrent.Search ??= (() => {
document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]"; document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Search)QBT_TR[CONTEXT=SearchEngineWidget]";
}); });
listItem.appendChild(tabElem); listItem.appendChild(tabElem);
$("searchTabs").appendChild(listItem); document.getElementById("searchTabs").appendChild(listItem);
searchResultsTabsContextMenu.addTarget(listItem); searchResultsTabsContextMenu.addTarget(listItem);
// unhide the results elements // unhide the results elements
if (numSearchTabs() >= 1) { if (numSearchTabs() >= 1) {
$("searchResultsNoSearches").classList.add("invisible"); document.getElementById("searchResultsNoSearches").classList.add("invisible");
$("searchResultsFilters").classList.remove("invisible"); document.getElementById("searchResultsFilters").classList.remove("invisible");
$("searchResultsTableContainer").classList.remove("invisible"); document.getElementById("searchResultsTableContainer").classList.remove("invisible");
$("searchTabsToolbar").classList.remove("invisible"); document.getElementById("searchTabsToolbar").classList.remove("invisible");
} }
// select new tab // select new tab
@ -248,7 +248,7 @@ window.qBittorrent.Search ??= (() => {
if (state && state.running) if (state && state.running)
stopSearch(searchId); stopSearch(searchId);
tab.destroy(); tab.remove();
fetch("api/v2/search/delete", { fetch("api/v2/search/delete", {
method: "POST", method: "POST",
@ -268,12 +268,12 @@ window.qBittorrent.Search ??= (() => {
resetSearchState(); resetSearchState();
resetFilters(); resetFilters();
$("numSearchResultsVisible").textContent = 0; document.getElementById("numSearchResultsVisible").textContent = 0;
$("numSearchResultsTotal").textContent = 0; document.getElementById("numSearchResultsTotal").textContent = 0;
$("searchResultsNoSearches").classList.remove("invisible"); document.getElementById("searchResultsNoSearches").classList.remove("invisible");
$("searchResultsFilters").classList.add("invisible"); document.getElementById("searchResultsFilters").classList.add("invisible");
$("searchResultsTableContainer").classList.add("invisible"); document.getElementById("searchResultsTableContainer").classList.add("invisible");
$("searchTabsToolbar").classList.add("invisible"); document.getElementById("searchTabsToolbar").classList.add("invisible");
} }
else if (isTabSelected && newTabToSelect) { else if (isTabSelected && newTabToSelect) {
setActiveTab(newTabToSelect); setActiveTab(newTabToSelect);
@ -335,23 +335,23 @@ window.qBittorrent.Search ??= (() => {
// restore filters // restore filters
searchText.pattern = state.searchPattern; searchText.pattern = state.searchPattern;
searchText.filterPattern = state.filterPattern; searchText.filterPattern = state.filterPattern;
$("searchInNameFilter").value = state.filterPattern; document.getElementById("searchInNameFilter").value = state.filterPattern;
searchSeedsFilter.min = state.seedsFilter.min; searchSeedsFilter.min = state.seedsFilter.min;
searchSeedsFilter.max = state.seedsFilter.max; searchSeedsFilter.max = state.seedsFilter.max;
$("searchMinSeedsFilter").value = state.seedsFilter.min; document.getElementById("searchMinSeedsFilter").value = state.seedsFilter.min;
$("searchMaxSeedsFilter").value = state.seedsFilter.max; document.getElementById("searchMaxSeedsFilter").value = state.seedsFilter.max;
searchSizeFilter.min = state.sizeFilter.min; searchSizeFilter.min = state.sizeFilter.min;
searchSizeFilter.minUnit = state.sizeFilter.minUnit; searchSizeFilter.minUnit = state.sizeFilter.minUnit;
searchSizeFilter.max = state.sizeFilter.max; searchSizeFilter.max = state.sizeFilter.max;
searchSizeFilter.maxUnit = state.sizeFilter.maxUnit; searchSizeFilter.maxUnit = state.sizeFilter.maxUnit;
$("searchMinSizeFilter").value = state.sizeFilter.min; document.getElementById("searchMinSizeFilter").value = state.sizeFilter.min;
$("searchMinSizePrefix").value = state.sizeFilter.minUnit; document.getElementById("searchMinSizePrefix").value = state.sizeFilter.minUnit;
$("searchMaxSizeFilter").value = state.sizeFilter.max; document.getElementById("searchMaxSizeFilter").value = state.sizeFilter.max;
$("searchMaxSizePrefix").value = state.sizeFilter.maxUnit; document.getElementById("searchMaxSizePrefix").value = state.sizeFilter.maxUnit;
const currentSearchPattern = $("searchPattern").value.trim(); const currentSearchPattern = document.getElementById("searchPattern").value.trim();
if (state.running && (state.searchPattern === currentSearchPattern)) { if (state.running && (state.searchPattern === currentSearchPattern)) {
// allow search to be stopped // allow search to be stopped
document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]"; document.getElementById("startSearchButton").lastChild.textContent = "QBT_TR(Stop)QBT_TR[CONTEXT=SearchEngineWidget]";
@ -360,7 +360,7 @@ window.qBittorrent.Search ??= (() => {
searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse); searchResultsTable.setSortedColumn(state.sort.column, state.sort.reverse);
$("searchInTorrentName").value = state.searchIn; document.getElementById("searchInTorrentName").value = state.searchIn;
} }
// must restore all filters before calling updateTable // must restore all filters before calling updateTable
@ -370,8 +370,8 @@ window.qBittorrent.Search ??= (() => {
if (rowsToSelect.length > 0) if (rowsToSelect.length > 0)
searchResultsTable.reselectRows(rowsToSelect); searchResultsTable.reselectRows(rowsToSelect);
$("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length; document.getElementById("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
$("numSearchResultsTotal").textContent = searchResultsTable.getRowSize(); document.getElementById("numSearchResultsTotal").textContent = searchResultsTable.getRowSize();
}; };
const getStatusIconElement = (text, image) => { const getStatusIconElement = (text, image) => {
@ -386,7 +386,7 @@ window.qBittorrent.Search ??= (() => {
}; };
const updateStatusIconElement = (searchId, text, image) => { const updateStatusIconElement = (searchId, text, image) => {
const searchTab = $(`${searchTabIdPrefix}${searchId}`); const searchTab = document.getElementById(`${searchTabIdPrefix}${searchId}`);
if (searchTab) { if (searchTab) {
const statusIcon = searchTab.querySelector(".statusIcon"); const statusIcon = searchTab.querySelector(".statusIcon");
statusIcon.alt = text; statusIcon.alt = text;
@ -439,7 +439,7 @@ window.qBittorrent.Search ??= (() => {
}; };
const getSelectedSearchId = () => { const getSelectedSearchId = () => {
const selectedTab = $("searchTabs").querySelector("li.selected"); const selectedTab = document.getElementById("searchTabs").querySelector("li.selected");
return selectedTab ? getSearchIdFromTab(selectedTab) : null; return selectedTab ? getSearchIdFromTab(selectedTab) : null;
}; };
@ -448,9 +448,9 @@ window.qBittorrent.Search ??= (() => {
const state = searchState.get(currentSearchId); const state = searchState.get(currentSearchId);
const isSearchRunning = state && state.running; const isSearchRunning = state && state.running;
if (!isSearchRunning || searchPatternChanged) { if (!isSearchRunning || searchPatternChanged) {
const pattern = $("searchPattern").value.trim(); const pattern = document.getElementById("searchPattern").value.trim();
const category = $("categorySelect").value; const category = document.getElementById("categorySelect").value;
const plugins = $("pluginsSelect").value; const plugins = document.getElementById("pluginsSelect").value;
if (!pattern || !category || !plugins) if (!pattern || !category || !plugins)
return; return;
@ -506,7 +506,7 @@ window.qBittorrent.Search ??= (() => {
const manageSearchPlugins = () => { const manageSearchPlugins = () => {
const id = "searchPlugins"; const id = "searchPlugins";
if (!$(id)) { if (!document.getElementById(id)) {
new MochaUI.Window({ new MochaUI.Window({
id: id, id: id,
title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]", title: "QBT_TR(Search plugins)QBT_TR[CONTEXT=PluginSelectDlg]",
@ -541,7 +541,7 @@ window.qBittorrent.Search ??= (() => {
const onSearchPatternChanged = () => { const onSearchPatternChanged = () => {
const currentSearchId = getSelectedSearchId(); const currentSearchId = getSelectedSearchId();
const state = searchState.get(currentSearchId); const state = searchState.get(currentSearchId);
const currentSearchPattern = $("searchPattern").value.trim(); const currentSearchPattern = document.getElementById("searchPattern").value.trim();
// start a new search if pattern has changed, otherwise allow the search to be stopped // start a new search if pattern has changed, otherwise allow the search to be stopped
if (state && (state.searchPattern === currentSearchPattern)) { if (state && (state.searchPattern === currentSearchPattern)) {
searchPatternChanged = false; searchPatternChanged = false;
@ -554,11 +554,11 @@ window.qBittorrent.Search ??= (() => {
}; };
const categorySelected = () => { const categorySelected = () => {
selectedCategory = $("categorySelect").value; selectedCategory = document.getElementById("categorySelect").value;
}; };
const pluginSelected = () => { const pluginSelected = () => {
selectedPlugin = $("pluginsSelect").value; selectedPlugin = document.getElementById("pluginsSelect").value;
if (selectedPlugin !== prevSelectedPlugin) { if (selectedPlugin !== prevSelectedPlugin) {
prevSelectedPlugin = selectedPlugin; prevSelectedPlugin = selectedPlugin;
@ -567,18 +567,18 @@ window.qBittorrent.Search ??= (() => {
}; };
const reselectCategory = () => { const reselectCategory = () => {
for (let i = 0; i < $("categorySelect").options.length; ++i) { for (let i = 0; i < document.getElementById("categorySelect").options.length; ++i) {
if ($("categorySelect").options[i].get("value") === selectedCategory) if (document.getElementById("categorySelect").options[i].get("value") === selectedCategory)
$("categorySelect").options[i].selected = true; document.getElementById("categorySelect").options[i].selected = true;
} }
categorySelected(); categorySelected();
}; };
const reselectPlugin = () => { const reselectPlugin = () => {
for (let i = 0; i < $("pluginsSelect").options.length; ++i) { for (let i = 0; i < document.getElementById("pluginsSelect").options.length; ++i) {
if ($("pluginsSelect").options[i].get("value") === selectedPlugin) if (document.getElementById("pluginsSelect").options[i].get("value") === selectedPlugin)
$("pluginsSelect").options[i].selected = true; document.getElementById("pluginsSelect").options[i].selected = true;
} }
pluginSelected(); pluginSelected();
@ -603,7 +603,7 @@ window.qBittorrent.Search ??= (() => {
option.value = category.id; option.value = category.id;
option.textContent = category.name; option.textContent = category.name;
categoryOptions.push(option); categoryOptions.push(option);
}; }
// first category is "All Categories" // first category is "All Categories"
if (categoryOptions.length > 1) { if (categoryOptions.length > 1) {
@ -614,10 +614,10 @@ window.qBittorrent.Search ??= (() => {
categoryOptions.splice(1, 0, option); categoryOptions.splice(1, 0, option);
} }
$("categorySelect").replaceChildren(...categoryOptions); document.getElementById("categorySelect").replaceChildren(...categoryOptions);
}; };
const selectedPlugin = $("pluginsSelect").value; const selectedPlugin = document.getElementById("pluginsSelect").value;
if ((selectedPlugin === "all") || (selectedPlugin === "enabled")) { if ((selectedPlugin === "all") || (selectedPlugin === "enabled")) {
const uniqueCategories = {}; const uniqueCategories = {};
@ -675,9 +675,9 @@ window.qBittorrent.Search ??= (() => {
const searchPluginsEmpty = (searchPlugins.length === 0); const searchPluginsEmpty = (searchPlugins.length === 0);
if (!searchPluginsEmpty) { if (!searchPluginsEmpty) {
$("searchResultsNoPlugins").classList.add("invisible"); document.getElementById("searchResultsNoPlugins").classList.add("invisible");
if (numSearchTabs() === 0) if (numSearchTabs() === 0)
$("searchResultsNoSearches").classList.remove("invisible"); document.getElementById("searchResultsNoSearches").classList.remove("invisible");
// sort plugins alphabetically // sort plugins alphabetically
const allPlugins = searchPlugins.sort((left, right) => { const allPlugins = searchPlugins.sort((left, right) => {
@ -695,11 +695,11 @@ window.qBittorrent.Search ??= (() => {
pluginOptions.splice(2, 0, createOption("──────────", undefined, true)); pluginOptions.splice(2, 0, createOption("──────────", undefined, true));
} }
$("pluginsSelect").replaceChildren(...pluginOptions); document.getElementById("pluginsSelect").replaceChildren(...pluginOptions);
$("searchPattern").disabled = searchPluginsEmpty; document.getElementById("searchPattern").disabled = searchPluginsEmpty;
$("categorySelect").disabled = searchPluginsEmpty; document.getElementById("categorySelect").disabled = searchPluginsEmpty;
$("pluginsSelect").disabled = searchPluginsEmpty; document.getElementById("pluginsSelect").disabled = searchPluginsEmpty;
document.getElementById("startSearchButton").disabled = searchPluginsEmpty; document.getElementById("startSearchButton").disabled = searchPluginsEmpty;
if (window.qBittorrent.SearchPlugins !== undefined) if (window.qBittorrent.SearchPlugins !== undefined)
@ -721,25 +721,25 @@ window.qBittorrent.Search ??= (() => {
const resetFilters = () => { const resetFilters = () => {
searchText.filterPattern = ""; searchText.filterPattern = "";
$("searchInNameFilter").value = ""; document.getElementById("searchInNameFilter").value = "";
searchSeedsFilter.min = 0; searchSeedsFilter.min = 0;
searchSeedsFilter.max = 0; searchSeedsFilter.max = 0;
$("searchMinSeedsFilter").value = searchSeedsFilter.min; document.getElementById("searchMinSeedsFilter").value = searchSeedsFilter.min;
$("searchMaxSeedsFilter").value = searchSeedsFilter.max; document.getElementById("searchMaxSeedsFilter").value = searchSeedsFilter.max;
searchSizeFilter.min = 0.00; searchSizeFilter.min = 0.00;
searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6 searchSizeFilter.minUnit = 2; // B = 0, KiB = 1, MiB = 2, GiB = 3, TiB = 4, PiB = 5, EiB = 6
searchSizeFilter.max = 0.00; searchSizeFilter.max = 0.00;
searchSizeFilter.maxUnit = 3; searchSizeFilter.maxUnit = 3;
$("searchMinSizeFilter").value = searchSizeFilter.min; document.getElementById("searchMinSizeFilter").value = searchSizeFilter.min;
$("searchMinSizePrefix").value = searchSizeFilter.minUnit; document.getElementById("searchMinSizePrefix").value = searchSizeFilter.minUnit;
$("searchMaxSizeFilter").value = searchSizeFilter.max; document.getElementById("searchMaxSizeFilter").value = searchSizeFilter.max;
$("searchMaxSizePrefix").value = searchSizeFilter.maxUnit; document.getElementById("searchMaxSizePrefix").value = searchSizeFilter.maxUnit;
}; };
const getSearchInTorrentName = () => { const getSearchInTorrentName = () => {
return ($("searchInTorrentName").value === "names") ? "names" : "everywhere"; return (document.getElementById("searchInTorrentName").value === "names") ? "names" : "everywhere";
}; };
const searchInTorrentName = () => { const searchInTorrentName = () => {
@ -748,29 +748,29 @@ window.qBittorrent.Search ??= (() => {
}; };
const searchSeedsFilterChanged = () => { const searchSeedsFilterChanged = () => {
searchSeedsFilter.min = $("searchMinSeedsFilter").value; searchSeedsFilter.min = document.getElementById("searchMinSeedsFilter").value;
searchSeedsFilter.max = $("searchMaxSeedsFilter").value; searchSeedsFilter.max = document.getElementById("searchMaxSeedsFilter").value;
searchFilterChanged(); searchFilterChanged();
}; };
const searchSizeFilterChanged = () => { const searchSizeFilterChanged = () => {
searchSizeFilter.min = $("searchMinSizeFilter").value; searchSizeFilter.min = document.getElementById("searchMinSizeFilter").value;
searchSizeFilter.minUnit = $("searchMinSizePrefix").value; searchSizeFilter.minUnit = document.getElementById("searchMinSizePrefix").value;
searchSizeFilter.max = $("searchMaxSizeFilter").value; searchSizeFilter.max = document.getElementById("searchMaxSizeFilter").value;
searchSizeFilter.maxUnit = $("searchMaxSizePrefix").value; searchSizeFilter.maxUnit = document.getElementById("searchMaxSizePrefix").value;
searchFilterChanged(); searchFilterChanged();
}; };
const searchSizeFilterPrefixChanged = () => { const searchSizeFilterPrefixChanged = () => {
if ((Number($("searchMinSizeFilter").value) !== 0) || (Number($("searchMaxSizeFilter").value) !== 0)) if ((Number(document.getElementById("searchMinSizeFilter").value) !== 0) || (Number(document.getElementById("searchMaxSizeFilter").value) !== 0))
searchSizeFilterChanged(); searchSizeFilterChanged();
}; };
const searchFilterChanged = () => { const searchFilterChanged = () => {
searchResultsTable.updateTable(); searchResultsTable.updateTable();
$("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length; document.getElementById("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
}; };
const loadSearchResultsData = function(searchId) { const loadSearchResultsData = function(searchId) {
@ -799,7 +799,7 @@ window.qBittorrent.Search ??= (() => {
return; return;
} }
$("error_div").textContent = ""; document.getElementById("error_div").textContent = "";
const state = searchState.get(searchId); const state = searchState.get(searchId);
// check if user stopped the search prior to receiving the response // check if user stopped the search prior to receiving the response
@ -842,8 +842,8 @@ window.qBittorrent.Search ??= (() => {
for (const row of newRows) for (const row of newRows)
searchResultsTable.updateRowData(row); searchResultsTable.updateRowData(row);
$("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length; document.getElementById("numSearchResultsVisible").textContent = searchResultsTable.getFilteredAndSortedRows().length;
$("numSearchResultsTotal").textContent = searchResultsTable.getRowSize(); document.getElementById("numSearchResultsTotal").textContent = searchResultsTable.getRowSize();
searchResultsTable.updateTable(); searchResultsTable.updateTable();
} }

View file

@ -30,7 +30,7 @@
MochaUI.extend({ MochaUI.extend({
addUpLimitSlider: (hashes) => { addUpLimitSlider: (hashes) => {
if ($("uplimitSliderarea")) { if (document.getElementById("uplimitSliderarea")) {
// Get global upload limit // Get global upload limit
fetch("api/v2/transfer/uploadLimit", { fetch("api/v2/transfer/uploadLimit", {
method: "GET", method: "GET",
@ -61,29 +61,29 @@ MochaUI.extend({
if (up_limit < 0) if (up_limit < 0)
up_limit = 0; up_limit = 0;
maximum = 10000; maximum = 10000;
new Slider($("uplimitSliderarea"), $("uplimitSliderknob"), { new Slider(document.getElementById("uplimitSliderarea"), document.getElementById("uplimitSliderknob"), {
steps: maximum, steps: maximum,
offset: 0, offset: 0,
initialStep: Math.round(up_limit), initialStep: Math.round(up_limit),
onChange: (pos) => { onChange: (pos) => {
if (pos > 0) { if (pos > 0) {
$("uplimitUpdatevalue").value = pos; document.getElementById("uplimitUpdatevalue").value = pos;
$("upLimitUnit").style.visibility = "visible"; document.getElementById("upLimitUnit").style.visibility = "visible";
} }
else { else {
$("uplimitUpdatevalue").value = "∞"; document.getElementById("uplimitUpdatevalue").value = "∞";
$("upLimitUnit").style.visibility = "hidden"; document.getElementById("upLimitUnit").style.visibility = "hidden";
} }
} }
}); });
// Set default value // Set default value
if (up_limit === 0) { if (up_limit === 0) {
$("uplimitUpdatevalue").value = "∞"; document.getElementById("uplimitUpdatevalue").value = "∞";
$("upLimitUnit").style.visibility = "hidden"; document.getElementById("upLimitUnit").style.visibility = "hidden";
} }
else { else {
$("uplimitUpdatevalue").value = Math.round(up_limit); document.getElementById("uplimitUpdatevalue").value = Math.round(up_limit);
$("upLimitUnit").style.visibility = "visible"; document.getElementById("upLimitUnit").style.visibility = "visible";
} }
} }
else { else {
@ -108,29 +108,29 @@ MochaUI.extend({
} }
if (up_limit < 0) if (up_limit < 0)
up_limit = 0; up_limit = 0;
new Slider($("uplimitSliderarea"), $("uplimitSliderknob"), { new Slider(document.getElementById("uplimitSliderarea"), document.getElementById("uplimitSliderknob"), {
steps: maximum, steps: maximum,
offset: 0, offset: 0,
initialStep: Math.round(up_limit / 1024), initialStep: Math.round(up_limit / 1024),
onChange: (pos) => { onChange: (pos) => {
if (pos > 0) { if (pos > 0) {
$("uplimitUpdatevalue").value = pos; document.getElementById("uplimitUpdatevalue").value = pos;
$("upLimitUnit").style.visibility = "visible"; document.getElementById("upLimitUnit").style.visibility = "visible";
} }
else { else {
$("uplimitUpdatevalue").value = "∞"; document.getElementById("uplimitUpdatevalue").value = "∞";
$("upLimitUnit").style.visibility = "hidden"; document.getElementById("upLimitUnit").style.visibility = "hidden";
} }
} }
}); });
// Set default value // Set default value
if (up_limit === 0) { if (up_limit === 0) {
$("uplimitUpdatevalue").value = "∞"; document.getElementById("uplimitUpdatevalue").value = "∞";
$("upLimitUnit").style.visibility = "hidden"; document.getElementById("upLimitUnit").style.visibility = "hidden";
} }
else { else {
$("uplimitUpdatevalue").value = Math.round(up_limit / 1024); document.getElementById("uplimitUpdatevalue").value = Math.round(up_limit / 1024);
$("upLimitUnit").style.visibility = "visible"; document.getElementById("upLimitUnit").style.visibility = "visible";
} }
}); });
} }
@ -139,7 +139,7 @@ MochaUI.extend({
}, },
addDlLimitSlider: (hashes) => { addDlLimitSlider: (hashes) => {
if ($("dllimitSliderarea")) { if (document.getElementById("dllimitSliderarea")) {
// Get global upload limit // Get global upload limit
fetch("api/v2/transfer/downloadLimit", { fetch("api/v2/transfer/downloadLimit", {
method: "GET", method: "GET",
@ -170,29 +170,29 @@ MochaUI.extend({
if (dl_limit < 0) if (dl_limit < 0)
dl_limit = 0; dl_limit = 0;
maximum = 10000; maximum = 10000;
new Slider($("dllimitSliderarea"), $("dllimitSliderknob"), { new Slider(document.getElementById("dllimitSliderarea"), document.getElementById("dllimitSliderknob"), {
steps: maximum, steps: maximum,
offset: 0, offset: 0,
initialStep: Math.round(dl_limit), initialStep: Math.round(dl_limit),
onChange: (pos) => { onChange: (pos) => {
if (pos > 0) { if (pos > 0) {
$("dllimitUpdatevalue").value = pos; document.getElementById("dllimitUpdatevalue").value = pos;
$("dlLimitUnit").style.visibility = "visible"; document.getElementById("dlLimitUnit").style.visibility = "visible";
} }
else { else {
$("dllimitUpdatevalue").value = "∞"; document.getElementById("dllimitUpdatevalue").value = "∞";
$("dlLimitUnit").style.visibility = "hidden"; document.getElementById("dlLimitUnit").style.visibility = "hidden";
} }
} }
}); });
// Set default value // Set default value
if (dl_limit === 0) { if (dl_limit === 0) {
$("dllimitUpdatevalue").value = "∞"; document.getElementById("dllimitUpdatevalue").value = "∞";
$("dlLimitUnit").style.visibility = "hidden"; document.getElementById("dlLimitUnit").style.visibility = "hidden";
} }
else { else {
$("dllimitUpdatevalue").value = Math.round(dl_limit); document.getElementById("dllimitUpdatevalue").value = Math.round(dl_limit);
$("dlLimitUnit").style.visibility = "visible"; document.getElementById("dlLimitUnit").style.visibility = "visible";
} }
} }
else { else {
@ -217,29 +217,29 @@ MochaUI.extend({
} }
if (dl_limit < 0) if (dl_limit < 0)
dl_limit = 0; dl_limit = 0;
new Slider($("dllimitSliderarea"), $("dllimitSliderknob"), { new Slider(document.getElementById("dllimitSliderarea"), document.getElementById("dllimitSliderknob"), {
steps: maximum, steps: maximum,
offset: 0, offset: 0,
initialStep: Math.round(dl_limit / 1024), initialStep: Math.round(dl_limit / 1024),
onChange: (pos) => { onChange: (pos) => {
if (pos > 0) { if (pos > 0) {
$("dllimitUpdatevalue").value = pos; document.getElementById("dllimitUpdatevalue").value = pos;
$("dlLimitUnit").style.visibility = "visible"; document.getElementById("dlLimitUnit").style.visibility = "visible";
} }
else { else {
$("dllimitUpdatevalue").value = "∞"; document.getElementById("dllimitUpdatevalue").value = "∞";
$("dlLimitUnit").style.visibility = "hidden"; document.getElementById("dlLimitUnit").style.visibility = "hidden";
} }
} }
}); });
// Set default value // Set default value
if (dl_limit === 0) { if (dl_limit === 0) {
$("dllimitUpdatevalue").value = "∞"; document.getElementById("dllimitUpdatevalue").value = "∞";
$("dlLimitUnit").style.visibility = "hidden"; document.getElementById("dlLimitUnit").style.visibility = "hidden";
} }
else { else {
$("dllimitUpdatevalue").value = Math.round(dl_limit / 1024); document.getElementById("dllimitUpdatevalue").value = Math.round(dl_limit / 1024);
$("dlLimitUnit").style.visibility = "visible"; document.getElementById("dlLimitUnit").style.visibility = "visible";
} }
}); });
} }

View file

@ -13,12 +13,12 @@
<script> <script>
"use strict"; "use strict";
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("setLocationButton").click(); document.getElementById("setLocationButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -32,17 +32,17 @@
// set text field to current value // set text field to current value
if (path !== null) if (path !== null)
$("setLocation").value = decodeURIComponent(path); document.getElementById("setLocation").value = decodeURIComponent(path);
$("setLocation").focus(); document.getElementById("setLocation").focus();
$("setLocationButton").addEventListener("click", (e) => { document.getElementById("setLocationButton").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// check field // check field
const location = $("setLocation").value.trim(); const location = document.getElementById("setLocation").value.trim();
if ((location === null) || (location === "")) { if ((location === null) || (location === "")) {
$("error_div").textContent = "QBT_TR(Save path is empty)QBT_TR[CONTEXT=TorrentsController]"; document.getElementById("error_div").textContent = "QBT_TR(Save path is empty)QBT_TR[CONTEXT=TorrentsController]";
return; return;
} }
@ -55,7 +55,7 @@
}) })
.then(async (response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
$("error_div").textContent = await response.text(); document.getElementById("error_div").textContent = await response.text();
return; return;
} }

View file

@ -15,12 +15,12 @@
const UseGlobalLimit = -2; const UseGlobalLimit = -2;
const NoLimit = -1; const NoLimit = -1;
window.addEventListener("DOMContentLoaded", () => { window.addEventListener("DOMContentLoaded", (event) => {
window.addEventListener("keydown", (event) => { window.addEventListener("keydown", (event) => {
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("save").click(); document.getElementById("save").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -56,23 +56,23 @@
else { else {
setSelectedRadioValue("shareLimit", "custom"); setSelectedRadioValue("shareLimit", "custom");
if (values.ratioLimit >= 0) { if (values.ratioLimit >= 0) {
$("setRatio").checked = true; document.getElementById("setRatio").checked = true;
$("ratio").value = values.ratioLimit.toFixed(2); document.getElementById("ratio").value = values.ratioLimit.toFixed(2);
} }
if (values.seedingTimeLimit >= 0) { if (values.seedingTimeLimit >= 0) {
$("setTotalMinutes").checked = true; document.getElementById("setTotalMinutes").checked = true;
$("totalMinutes").value = values.seedingTimeLimit; document.getElementById("totalMinutes").value = values.seedingTimeLimit;
} }
if (values.inactiveSeedingTimeLimit >= 0) { if (values.inactiveSeedingTimeLimit >= 0) {
$("setInactiveMinutes").checked = true; document.getElementById("setInactiveMinutes").checked = true;
$("inactiveMinutes").value = values.inactiveSeedingTimeLimit; document.getElementById("inactiveMinutes").value = values.inactiveSeedingTimeLimit;
} }
} }
shareLimitChanged(); shareLimitChanged();
$("default").focus(); document.getElementById("default").focus();
$("save").addEventListener("click", (e) => { document.getElementById("save").addEventListener("click", (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@ -91,9 +91,9 @@
ratioLimitValue = seedingTimeLimitValue = inactiveSeedingTimeLimitValue = NoLimit; ratioLimitValue = seedingTimeLimitValue = inactiveSeedingTimeLimitValue = NoLimit;
} }
else if (shareLimit === "custom") { else if (shareLimit === "custom") {
ratioLimitValue = $("setRatio").checked ? $("ratio").value : -1; ratioLimitValue = document.getElementById("setRatio").checked ? document.getElementById("ratio").value : -1;
seedingTimeLimitValue = $("setTotalMinutes").checked ? $("totalMinutes").value : -1; seedingTimeLimitValue = document.getElementById("setTotalMinutes").checked ? document.getElementById("totalMinutes").value : -1;
inactiveSeedingTimeLimitValue = $("setInactiveMinutes").checked ? $("inactiveMinutes").value : -1; inactiveSeedingTimeLimitValue = document.getElementById("setInactiveMinutes").checked ? document.getElementById("inactiveMinutes").value : -1;
} }
else { else {
return; return;
@ -108,7 +108,7 @@
inactiveSeedingTimeLimit: inactiveSeedingTimeLimitValue inactiveSeedingTimeLimit: inactiveSeedingTimeLimitValue
}) })
}) })
.then(async (response) => { .then((response) => {
if (!response.ok) if (!response.ok)
return; return;
@ -139,26 +139,26 @@
const shareLimitChanged = () => { const shareLimitChanged = () => {
const customShareLimit = getSelectedRadioValue("shareLimit") === "custom"; const customShareLimit = getSelectedRadioValue("shareLimit") === "custom";
$("setRatio").disabled = !customShareLimit; document.getElementById("setRatio").disabled = !customShareLimit;
$("setTotalMinutes").disabled = !customShareLimit; document.getElementById("setTotalMinutes").disabled = !customShareLimit;
$("setInactiveMinutes").disabled = !customShareLimit; document.getElementById("setInactiveMinutes").disabled = !customShareLimit;
enableInputBoxes(); enableInputBoxes();
$("save").disabled = !isFormValid(); document.getElementById("save").disabled = !isFormValid();
}; };
const enableInputBoxes = () => { const enableInputBoxes = () => {
$("ratio").disabled = $("setRatio").disabled || !$("setRatio").checked; document.getElementById("ratio").disabled = document.getElementById("setRatio").disabled || !document.getElementById("setRatio").checked;
$("totalMinutes").disabled = $("setTotalMinutes").disabled || !$("setTotalMinutes").checked; document.getElementById("totalMinutes").disabled = document.getElementById("setTotalMinutes").disabled || !document.getElementById("setTotalMinutes").checked;
$("inactiveMinutes").disabled = $("setInactiveMinutes").disabled || !$("setInactiveMinutes").checked; document.getElementById("inactiveMinutes").disabled = document.getElementById("setInactiveMinutes").disabled || !document.getElementById("setInactiveMinutes").checked;
$("save").disabled = !isFormValid(); document.getElementById("save").disabled = !isFormValid();
}; };
const isFormValid = () => { const isFormValid = () => {
return !((getSelectedRadioValue("shareLimit") === "custom") && !$("setRatio").checked return !((getSelectedRadioValue("shareLimit") === "custom") && !document.getElementById("setRatio").checked
&& !$("setTotalMinutes").checked && !$("setInactiveMinutes").checked); && !document.getElementById("setTotalMinutes").checked && !document.getElementById("setInactiveMinutes").checked);
}; };
</script> </script>
</head> </head>
@ -172,17 +172,17 @@
<div style="margin-left: 40px; margin-bottom: 5px;"> <div style="margin-left: 40px; margin-bottom: 5px;">
<input type="checkbox" id="setRatio" class="shareLimitInput" onclick="enableInputBoxes()"> <input type="checkbox" id="setRatio" class="shareLimitInput" onclick="enableInputBoxes()">
<label id="ratioLabel" for="setRatio">QBT_TR(ratio)QBT_TR[CONTEXT=UpDownRatioDialog]</label> <label id="ratioLabel" for="setRatio">QBT_TR(ratio)QBT_TR[CONTEXT=UpDownRatioDialog]</label>
<input type="number" id="ratio" value="0.00" step=".01" min="0" max="9999" class="shareLimitInput" aria-labelledby="ratioLabel"> <input type="number" id="ratio" value="0.00" step=".01" min="0" class="shareLimitInput" aria-labelledby="ratioLabel">
</div> </div>
<div style="margin-left: 40px; margin-bottom: 5px;"> <div style="margin-left: 40px; margin-bottom: 5px;">
<input type="checkbox" id="setTotalMinutes" class="shareLimitInput" onclick="enableInputBoxes()"> <input type="checkbox" id="setTotalMinutes" class="shareLimitInput" onclick="enableInputBoxes()">
<label id="totalMinutesLabel" for="setTotalMinutes">QBT_TR(total minutes)QBT_TR[CONTEXT=UpDownRatioDialog]</label> <label id="totalMinutesLabel" for="setTotalMinutes">QBT_TR(total minutes)QBT_TR[CONTEXT=UpDownRatioDialog]</label>
<input type="number" id="totalMinutes" value="0" step="1" min="0" max="525600" class="shareLimitInput" aria-labelledby="totalMinutesLabel"> <input type="number" id="totalMinutes" value="0" step="1" min="0" class="shareLimitInput" aria-labelledby="totalMinutesLabel">
</div> </div>
<div style="margin-left: 40px; margin-bottom: 5px;"> <div style="margin-left: 40px; margin-bottom: 5px;">
<input type="checkbox" id="setInactiveMinutes" class="shareLimitInput" onclick="enableInputBoxes()"> <input type="checkbox" id="setInactiveMinutes" class="shareLimitInput" onclick="enableInputBoxes()">
<label id="inactiveMinutesLabel" for="setInactiveMinutes">QBT_TR(inactive minutes)QBT_TR[CONTEXT=UpDownRatioDialog]</label> <label id="inactiveMinutesLabel" for="setInactiveMinutes">QBT_TR(inactive minutes)QBT_TR[CONTEXT=UpDownRatioDialog]</label>
<input type="number" id="inactiveMinutes" value="0" step="1" min="0" max="525600" class="shareLimitInput" aria-labelledby="inactiveMinutesLabel"> <input type="number" id="inactiveMinutes" value="0" step="1" min="0" class="shareLimitInput" aria-labelledby="inactiveMinutesLabel">
</div> </div>
<div style="text-align: center; padding-top: 10px;"> <div style="text-align: center; padding-top: 10px;">
<input type="button" value="QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]" id="save"> <input type="button" value="QBT_TR(Save)QBT_TR[CONTEXT=HttpServer]" id="save">

View file

@ -160,23 +160,23 @@
let submitted = false; let submitted = false;
$("uploadForm").addEventListener("submit", () => { document.getElementById("uploadForm").addEventListener("submit", (event) => {
$("startTorrentHidden").value = $("startTorrent").checked ? "false" : "true"; document.getElementById("startTorrentHidden").value = document.getElementById("startTorrent").checked ? "false" : "true";
$("dlLimitHidden").value = Number($("dlLimitText").value) * 1024; document.getElementById("dlLimitHidden").value = Number(document.getElementById("dlLimitText").value) * 1024;
$("upLimitHidden").value = Number($("upLimitText").value) * 1024; document.getElementById("upLimitHidden").value = Number(document.getElementById("upLimitText").value) * 1024;
$("upload_spinner").style.display = "block"; document.getElementById("upload_spinner").style.display = "block";
submitted = true; submitted = true;
}); });
$("upload_frame").addEventListener("load", () => { document.getElementById("upload_frame").addEventListener("load", (event) => {
if (submitted) if (submitted)
window.parent.qBittorrent.Client.closeFrameWindow(window); window.parent.qBittorrent.Client.closeFrameWindow(window);
}); });
if ((Browser.platform === "ios") || ((Browser.platform === "mac") && (navigator.maxTouchPoints > 1))) if ((Browser.platform === "ios") || ((Browser.platform === "mac") && (navigator.maxTouchPoints > 1)))
$("fileselect").accept = ".torrent"; document.getElementById("fileselect").accept = ".torrent";
window.qBittorrent.pathAutofill.attachPathAutofill(); window.qBittorrent.pathAutofill.attachPathAutofill();
</script> </script>

View file

@ -37,7 +37,7 @@
switch (event.key) { switch (event.key) {
case "Enter": case "Enter":
event.preventDefault(); event.preventDefault();
$("applyButton").click(); document.getElementById("applyButton").click();
break; break;
case "Escape": case "Escape":
event.preventDefault(); event.preventDefault();
@ -48,7 +48,7 @@
const hashes = new URLSearchParams(window.location.search).get("hashes").split("|"); const hashes = new URLSearchParams(window.location.search).get("hashes").split("|");
const setUpLimit = () => { const setUpLimit = () => {
const limit = Number($("uplimitUpdatevalue").value) * 1024; const limit = Number(document.getElementById("uplimitUpdatevalue").value) * 1024;
if (hashes[0] === "global") { if (hashes[0] === "global") {
fetch("api/v2/transfer/setUploadLimit", { fetch("api/v2/transfer/setUploadLimit", {
method: "POST", method: "POST",
@ -81,7 +81,7 @@
} }
}; };
$("uplimitUpdatevalue").focus(); document.getElementById("uplimitUpdatevalue").focus();
MochaUI.addUpLimitSlider(hashes); MochaUI.addUpLimitSlider(hashes);
</script> </script>

View file

@ -855,12 +855,12 @@
const qbtVersion = window.parent.qBittorrent.Cache.qbtVersion.get(); const qbtVersion = window.parent.qBittorrent.Cache.qbtVersion.get();
const buildInfo = window.parent.qBittorrent.Cache.buildInfo.get(); const buildInfo = window.parent.qBittorrent.Cache.buildInfo.get();
$("qbittorrentVersion").textContent = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`; document.getElementById("qbittorrentVersion").textContent = `qBittorrent ${qbtVersion} QBT_TR(WebUI)QBT_TR[CONTEXT=OptionsDialog]`;
$("qtVersion").textContent = buildInfo.qt; document.getElementById("qtVersion").textContent = buildInfo.qt;
$("libtorrentVersion").textContent = buildInfo.libtorrent; document.getElementById("libtorrentVersion").textContent = buildInfo.libtorrent;
$("boostVersion").textContent = buildInfo.boost; document.getElementById("boostVersion").textContent = buildInfo.boost;
$("opensslVersion").textContent = buildInfo.openssl; document.getElementById("opensslVersion").textContent = buildInfo.openssl;
$("zlibVersion").textContent = buildInfo.zlib; document.getElementById("zlibVersion").textContent = buildInfo.zlib;
$("qbittorrentVersion").textContent += ` (${buildInfo.bitness}-bit)`; document.getElementById("qbittorrentVersion").textContent += ` (${buildInfo.bitness}-bit)`;
})(); })();
</script> </script>

View file

@ -16,34 +16,34 @@
(() => { (() => {
MochaUI.initializeTabs("aboutTabs"); MochaUI.initializeTabs("aboutTabs");
$("aboutAboutLink").addEventListener("click", () => { document.getElementById("aboutAboutLink").addEventListener("click", (event) => {
Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible"))); Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible")));
$("aboutAboutContent").classList.remove("invisible"); document.getElementById("aboutAboutContent").classList.remove("invisible");
}); });
$("aboutAuthorLink").addEventListener("click", () => { document.getElementById("aboutAuthorLink").addEventListener("click", (event) => {
Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible"))); Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible")));
$("aboutAuthorContent").classList.remove("invisible"); document.getElementById("aboutAuthorContent").classList.remove("invisible");
}); });
$("aboutSpecialThanksLink").addEventListener("click", () => { document.getElementById("aboutSpecialThanksLink").addEventListener("click", (event) => {
Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible"))); Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible")));
$("aboutSpecialThanksContent").classList.remove("invisible"); document.getElementById("aboutSpecialThanksContent").classList.remove("invisible");
}); });
$("aboutTranslatorsLink").addEventListener("click", () => { document.getElementById("aboutTranslatorsLink").addEventListener("click", (event) => {
Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible"))); Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible")));
$("aboutTranslatorsContent").classList.remove("invisible"); document.getElementById("aboutTranslatorsContent").classList.remove("invisible");
}); });
$("aboutLicenseLink").addEventListener("click", () => { document.getElementById("aboutLicenseLink").addEventListener("click", (event) => {
Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible"))); Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible")));
$("aboutLicenseContent").classList.remove("invisible"); document.getElementById("aboutLicenseContent").classList.remove("invisible");
}); });
$("aboutSoftwareUsedLink").addEventListener("click", () => { document.getElementById("aboutSoftwareUsedLink").addEventListener("click", (event) => {
Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible"))); Array.prototype.forEach.call(document.querySelectorAll(".aboutTabContent"), (tab => tab.classList.add("invisible")));
$("aboutSoftwareUsedContent").classList.remove("invisible"); document.getElementById("aboutSoftwareUsedContent").classList.remove("invisible");
}); });
})(); })();
</script> </script>

View file

@ -104,7 +104,7 @@
}; };
const deleteCookie = (element) => { const deleteCookie = (element) => {
element.closest("tr").destroy(); element.closest("tr").remove();
}; };
const save = () => { const save = () => {

View file

@ -61,11 +61,11 @@
} }
}); });
$("newPluginPath").select(); document.getElementById("newPluginPath").select();
}; };
const newPluginOk = () => { const newPluginOk = () => {
const path = $("newPluginPath").value.trim(); const path = document.getElementById("newPluginPath").value.trim();
if (path) { if (path) {
fetch("api/v2/search/installPlugin", { fetch("api/v2/search/installPlugin", {
method: "POST", method: "POST",

View file

@ -188,7 +188,7 @@
let selectedLogLevels = JSON.parse(LocalPreferences.get("qbt_selected_log_levels")) || ["1", "2", "4", "8"]; let selectedLogLevels = JSON.parse(LocalPreferences.get("qbt_selected_log_levels")) || ["1", "2", "4", "8"];
const init = () => { const init = () => {
for (const option of $("logLevelSelect").options) for (const option of document.getElementById("logLevelSelect").options)
option.toggleAttribute("selected", selectedLogLevels.includes(option.value)); option.toggleAttribute("selected", selectedLogLevels.includes(option.value));
selectBox = new vanillaSelectBox("#logLevelSelect", { selectBox = new vanillaSelectBox("#logLevelSelect", {
@ -282,7 +282,7 @@
}; };
const filterTextChanged = () => { const filterTextChanged = () => {
const value = $("filterTextInput").value.trim(); const value = document.getElementById("filterTextInput").value.trim();
if (inputtedFilterText !== value) { if (inputtedFilterText !== value) {
inputtedFilterText = value; inputtedFilterText = value;
logFilterChanged(); logFilterChanged();
@ -306,14 +306,14 @@
currentSelectedTab = tab; currentSelectedTab = tab;
if (currentSelectedTab === "main") { if (currentSelectedTab === "main") {
selectBox.enable(); selectBox.enable();
$("logMessageView").classList.remove("invisible"); document.getElementById("logMessageView").classList.remove("invisible");
$("logPeerView").classList.add("invisible"); document.getElementById("logPeerView").classList.add("invisible");
resetTableTimer("peer"); resetTableTimer("peer");
} }
else { else {
selectBox.disable(); selectBox.disable();
$("logMessageView").classList.add("invisible"); document.getElementById("logMessageView").classList.add("invisible");
$("logPeerView").classList.remove("invisible"); document.getElementById("logPeerView").classList.remove("invisible");
resetTableTimer("main"); resetTableTimer("main");
} }
@ -331,8 +331,8 @@
if (curTab === undefined) if (curTab === undefined)
curTab = currentSelectedTab; curTab = currentSelectedTab;
$("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength; document.getElementById("numFilteredLogs").textContent = tableInfo[curTab].instance.filteredLength;
$("numTotalLogs").textContent = tableInfo[curTab].instance.getRowSize(); document.getElementById("numTotalLogs").textContent = tableInfo[curTab].instance.getRowSize();
}; };
const syncLogData = (curTab) => { const syncLogData = (curTab) => {
@ -368,7 +368,7 @@
}) })
.then(async (response) => { .then(async (response) => {
if (!response.ok) { if (!response.ok) {
const errorDiv = $("error_div"); const errorDiv = document.getElementById("error_div");
if (errorDiv) if (errorDiv)
errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]"; errorDiv.textContent = "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]";
tableInfo[curTab].progress = false; tableInfo[curTab].progress = false;
@ -376,9 +376,9 @@
return; return;
} }
$("error_div").textContent = ""; document.getElementById("error_div").textContent = "";
if ($("logTabColumn").classList.contains("invisible")) if (document.getElementById("logTabColumn").classList.contains("invisible"))
return; return;
const responseJSON = await response.json(); const responseJSON = await response.json();

Some files were not shown because too many files have changed in this diff Show more