Backport changes to v5.0.x branch

PR #20996.
This commit is contained in:
Vladimir Golovnev 2024-08-02 21:22:49 +03:00 committed by GitHub
commit 195eae5f3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
125 changed files with 6093 additions and 5209 deletions

View file

@ -23,7 +23,6 @@ jobs:
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"
openssl_root: "$(brew --prefix openssl@3)"
libtorrent_path: "${{ github.workspace }}/../libtorrent" libtorrent_path: "${{ github.workspace }}/../libtorrent"
steps: steps:
@ -70,7 +69,7 @@ jobs:
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
archives: qtbase qtdeclarative qtsvg qttools archives: qtbase qtdeclarative qtsvg qttools
@ -94,8 +93,7 @@ jobs:
-DCMAKE_CXX_STANDARD=17 \ -DCMAKE_CXX_STANDARD=17 \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}" \
-Ddeprecated-functions=OFF \ -Ddeprecated-functions=OFF
-DOPENSSL_ROOT_DIR="${{ env.openssl_root }}"
cmake --build build cmake --build build
sudo cmake --install build sudo cmake --install build
@ -109,7 +107,6 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \ -DBOOST_ROOT="${{ env.boost_path }}" \
-DOPENSSL_ROOT_DIR="${{ env.openssl_root }}" \
-DTESTING=ON \ -DTESTING=ON \
-DVERBOSE_CONFIGURE=ON \ -DVERBOSE_CONFIGURE=ON \
-D${{ matrix.qbt_gui }} -D${{ matrix.qbt_gui }}

View file

@ -53,7 +53,7 @@ jobs:
python-version: '3.7' python-version: '3.7'
- name: Install tools (search engine) - name: Install tools (search engine)
run: pip install bandit pycodestyle pyflakes run: pip install bandit mypy pycodestyle pyflakes pyright
- name: Gather files (search engine) - name: Gather files (search engine)
run: | run: |
@ -61,6 +61,16 @@ jobs:
echo $PY_FILES echo $PY_FILES
echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV" echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV"
- name: Check typings (search engine)
run: |
MYPYPATH="src/searchengine/nova3" \
mypy \
--follow-imports skip \
--strict \
$PY_FILES
pyright \
$PY_FILES
- name: Lint code (search engine) - name: Lint code (search engine)
run: | run: |
pyflakes $PY_FILES pyflakes $PY_FILES

View file

@ -64,7 +64,7 @@ jobs:
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
archives: icu qtbase qtdeclarative qtsvg qttools archives: icu qtbase qtdeclarative qtsvg qttools
@ -138,12 +138,12 @@ jobs:
curl \ curl \
-L \ -L \
-Z \ -Z \
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \ -O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage \
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \ -O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage \
-O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage -O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
chmod +x \ chmod +x \
linuxdeploy-x86_64.AppImage \ linuxdeploy-static-x86_64.AppImage \
linuxdeploy-plugin-qt-x86_64.AppImage \ linuxdeploy-plugin-qt-static-x86_64.AppImage \
linuxdeploy-plugin-appimage-x86_64.AppImage linuxdeploy-plugin-appimage-x86_64.AppImage
- name: Prepare files for AppImage - name: Prepare files for AppImage
@ -156,12 +156,12 @@ jobs:
- name: Package AppImage - name: Package AppImage
run: | run: |
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt ./linuxdeploy-static-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
NO_APPSTREAM=1 \ NO_APPSTREAM=1 \
OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \ OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --output appimage ./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --output appimage
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View file

@ -93,7 +93,7 @@ jobs:
move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}" move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}"
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: "6.7.0" version: "6.7.0"
archives: qtbase qtsvg qttools archives: qtbase qtsvg qttools
@ -153,26 +153,26 @@ jobs:
copy build/qbittorrent.pdb upload/qBittorrent copy build/qbittorrent.pdb upload/qBittorrent
copy dist/windows/qt.conf upload/qBittorrent copy dist/windows/qt.conf upload/qBittorrent
# runtimes # runtimes
copy "${{ env.Qt6_DIR }}/bin/Qt6Core.dll" upload/qBittorrent copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Network.dll" upload/qBittorrent copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
mkdir upload/qBittorrent/plugins/iconengines mkdir upload/qBittorrent/plugins/iconengines
copy "${{ env.Qt6_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines copy "${{ env.Qt_ROOT_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
mkdir upload/qBittorrent/plugins/imageformats mkdir upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
mkdir upload/qBittorrent/plugins/platforms mkdir upload/qBittorrent/plugins/platforms
copy "${{ env.Qt6_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms copy "${{ env.Qt_ROOT_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
mkdir upload/qBittorrent/plugins/sqldrivers mkdir upload/qBittorrent/plugins/sqldrivers
copy "${{ env.Qt6_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers copy "${{ env.Qt_ROOT_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers
mkdir upload/qBittorrent/plugins/styles mkdir upload/qBittorrent/plugins/styles
copy "${{ env.Qt6_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles copy "${{ env.Qt_ROOT_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles
mkdir upload/qBittorrent/plugins/tls mkdir upload/qBittorrent/plugins/tls
copy "${{ env.Qt6_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls copy "${{ env.Qt_ROOT_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls
# cmake additionals # cmake additionals
mkdir upload/cmake mkdir upload/cmake
copy build/compile_commands.json upload/cmake copy build/compile_commands.json upload/cmake

View file

@ -52,7 +52,7 @@ jobs:
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}" mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v3 uses: jurplel/install-qt-action@v4
with: with:
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
archives: icu qtbase qtdeclarative qtsvg qttools archives: icu qtbase qtdeclarative qtsvg qttools

View file

@ -38,6 +38,8 @@ add_library(qbt_base STATIC
bittorrent/torrent.h bittorrent/torrent.h
bittorrent/torrentcontenthandler.h bittorrent/torrentcontenthandler.h
bittorrent/torrentcontentlayout.h bittorrent/torrentcontentlayout.h
bittorrent/torrentcontentremoveoption.h
bittorrent/torrentcontentremover.h
bittorrent/torrentcreationmanager.h bittorrent/torrentcreationmanager.h
bittorrent/torrentcreationtask.h bittorrent/torrentcreationtask.h
bittorrent/torrentcreator.h bittorrent/torrentcreator.h
@ -145,6 +147,7 @@ add_library(qbt_base STATIC
bittorrent/sslparameters.cpp bittorrent/sslparameters.cpp
bittorrent/torrent.cpp bittorrent/torrent.cpp
bittorrent/torrentcontenthandler.cpp bittorrent/torrentcontenthandler.cpp
bittorrent/torrentcontentremover.cpp
bittorrent/torrentcreationmanager.cpp bittorrent/torrentcreationmanager.cpp
bittorrent/torrentcreationtask.cpp bittorrent/torrentcreationtask.cpp
bittorrent/torrentcreator.cpp bittorrent/torrentcreator.cpp

View file

@ -40,7 +40,6 @@
#include <QRegularExpression> #include <QRegularExpression>
#include <QThread> #include <QThread>
#include "base/algorithm.h"
#include "base/exceptions.h" #include "base/exceptions.h"
#include "base/global.h" #include "base/global.h"
#include "base/logger.h" #include "base/logger.h"

View file

@ -240,11 +240,11 @@ void CustomDiskIOThread::handleCompleteFiles(lt::storage_index_t storage, const
lt::storage_interface *customStorageConstructor(const lt::storage_params &params, lt::file_pool &pool) lt::storage_interface *customStorageConstructor(const lt::storage_params &params, lt::file_pool &pool)
{ {
return new CustomStorage {params, pool}; return new CustomStorage(params, pool);
} }
CustomStorage::CustomStorage(const lt::storage_params &params, lt::file_pool &filePool) CustomStorage::CustomStorage(const lt::storage_params &params, lt::file_pool &filePool)
: lt::default_storage {params, filePool} : lt::default_storage(params, filePool)
, m_savePath {params.path} , m_savePath {params.path}
{ {
} }

View file

@ -37,17 +37,12 @@
#include "addtorrentparams.h" #include "addtorrentparams.h"
#include "categoryoptions.h" #include "categoryoptions.h"
#include "sharelimitaction.h" #include "sharelimitaction.h"
#include "torrentcontentremoveoption.h"
#include "trackerentry.h" #include "trackerentry.h"
#include "trackerentrystatus.h" #include "trackerentrystatus.h"
class QString; class QString;
enum DeleteOption
{
DeleteTorrent,
DeleteTorrentAndFiles
};
namespace BitTorrent namespace BitTorrent
{ {
class InfoHash; class InfoHash;
@ -58,6 +53,12 @@ namespace BitTorrent
struct CacheStatus; struct CacheStatus;
struct SessionStatus; struct SessionStatus;
enum class TorrentRemoveOption
{
KeepContent,
RemoveContent
};
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised // Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files. // since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779 // https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
@ -425,7 +426,7 @@ namespace BitTorrent
virtual void setExcludedFileNamesEnabled(bool enabled) = 0; virtual void setExcludedFileNamesEnabled(bool enabled) = 0;
virtual QStringList excludedFileNames() const = 0; virtual QStringList excludedFileNames() const = 0;
virtual void setExcludedFileNames(const QStringList &newList) = 0; virtual void setExcludedFileNames(const QStringList &newList) = 0;
virtual bool isFilenameExcluded(const QString &fileName) const = 0; virtual void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) = 0;
virtual QStringList bannedIPs() const = 0; virtual QStringList bannedIPs() const = 0;
virtual void setBannedIPs(const QStringList &newList) = 0; virtual void setBannedIPs(const QStringList &newList) = 0;
virtual ResumeDataStorageType resumeDataStorageType() const = 0; virtual ResumeDataStorageType resumeDataStorageType() const = 0;
@ -434,6 +435,8 @@ namespace BitTorrent
virtual void setMergeTrackersEnabled(bool enabled) = 0; virtual void setMergeTrackersEnabled(bool enabled) = 0;
virtual bool isStartPaused() const = 0; virtual bool isStartPaused() const = 0;
virtual void setStartPaused(bool value) = 0; virtual void setStartPaused(bool value) = 0;
virtual TorrentContentRemoveOption torrentContentRemoveOption() const = 0;
virtual void setTorrentContentRemoveOption(TorrentContentRemoveOption option) = 0;
virtual bool isRestored() const = 0; virtual bool isRestored() const = 0;
@ -453,7 +456,7 @@ namespace BitTorrent
virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0; virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0;
virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams &params = {}) = 0; virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams &params = {}) = 0;
virtual bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteOption::DeleteTorrent) = 0; virtual bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) = 0;
virtual bool downloadMetadata(const TorrentDescriptor &torrentDescr) = 0; virtual bool downloadMetadata(const TorrentDescriptor &torrentDescr) = 0;
virtual bool cancelDownloadMetadata(const TorrentID &id) = 0; virtual bool cancelDownloadMetadata(const TorrentID &id) = 0;

View file

@ -101,6 +101,7 @@
#include "nativesessionextension.h" #include "nativesessionextension.h"
#include "portforwarderimpl.h" #include "portforwarderimpl.h"
#include "resumedatastorage.h" #include "resumedatastorage.h"
#include "torrentcontentremover.h"
#include "torrentdescriptor.h" #include "torrentdescriptor.h"
#include "torrentimpl.h" #include "torrentimpl.h"
#include "tracker.h" #include "tracker.h"
@ -525,6 +526,7 @@ SessionImpl::SessionImpl(QObject *parent)
, m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3} , m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3}
, m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3} , m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3}
, m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3} , m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3}
, m_torrentContentRemoveOption {BITTORRENT_SESSION_KEY(u"TorrentContentRemoveOption"_s), TorrentContentRemoveOption::MoveToTrash}
, m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)} , m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)}
, m_seedingLimitTimer {new QTimer(this)} , m_seedingLimitTimer {new QTimer(this)}
, m_resumeDataTimer {new QTimer(this)} , m_resumeDataTimer {new QTimer(this)}
@ -550,7 +552,14 @@ SessionImpl::SessionImpl(QObject *parent)
, this, [this]() { m_recentErroredTorrents.clear(); }); , this, [this]() { m_recentErroredTorrents.clear(); });
m_seedingLimitTimer->setInterval(10s); m_seedingLimitTimer->setInterval(10s);
connect(m_seedingLimitTimer, &QTimer::timeout, this, &SessionImpl::processShareLimits); connect(m_seedingLimitTimer, &QTimer::timeout, this, [this]
{
// We shouldn't iterate over `m_torrents` in the loop below
// since `deleteTorrent()` modifies it indirectly
const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
for (TorrentImpl *torrent : torrents)
processTorrentShareLimits(torrent);
});
initializeNativeSession(); initializeNativeSession();
configureComponents(); configureComponents();
@ -586,6 +595,11 @@ SessionImpl::SessionImpl(QObject *parent)
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); connect(m_fileSearcher, &FileSearcher::searchFinished, this, &SessionImpl::fileSearchFinished);
m_torrentContentRemover = new TorrentContentRemover;
m_torrentContentRemover->moveToThread(m_ioThread.get());
connect(m_ioThread.get(), &QThread::finished, m_torrentContentRemover, &QObject::deleteLater);
connect(m_torrentContentRemover, &TorrentContentRemover::jobFinished, this, &SessionImpl::torrentContentRemovingFinished);
m_ioThread->start(); m_ioThread->start();
initMetrics(); initMetrics();
@ -604,7 +618,7 @@ SessionImpl::~SessionImpl()
{ {
m_nativeSession->pause(); m_nativeSession->pause();
const qint64 timeout = (m_shutdownTimeout >= 0) ? (m_shutdownTimeout * 1000) : -1; const auto timeout = (m_shutdownTimeout >= 0) ? (static_cast<qint64>(m_shutdownTimeout) * 1000) : -1;
const QDeadlineTimer shutdownDeadlineTimer {timeout}; const QDeadlineTimer shutdownDeadlineTimer {timeout};
if (m_torrentsQueueChanged) if (m_torrentsQueueChanged)
@ -2236,21 +2250,16 @@ void SessionImpl::populateAdditionalTrackers()
m_additionalTrackerEntries = parseTrackerEntries(additionalTrackers()); m_additionalTrackerEntries = parseTrackerEntries(additionalTrackers());
} }
void SessionImpl::processShareLimits() void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
{ {
if (!torrent->isFinished() || torrent->isForced())
return;
const auto effectiveLimit = []<typename T>(const T limit, const T useGlobalLimit, const T globalLimit) -> T const auto effectiveLimit = []<typename T>(const T limit, const T useGlobalLimit, const T globalLimit) -> T
{ {
return (limit == useGlobalLimit) ? globalLimit : limit; return (limit == useGlobalLimit) ? globalLimit : limit;
}; };
// We shouldn't iterate over `m_torrents` in the loop below
// since `deleteTorrent()` modifies it indirectly
const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
for (const auto &[torrentID, torrent] : torrents.asKeyValueRange())
{
if (!torrent->isFinished() || torrent->isForced())
continue;
const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio()); const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio());
const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes()); const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes());
const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes()); const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes());
@ -2285,12 +2294,12 @@ void SessionImpl::processShareLimits()
if (shareLimitAction == ShareLimitAction::Remove) if (shareLimitAction == ShareLimitAction::Remove)
{ {
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName)); LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
deleteTorrent(torrentID); removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent);
} }
else if (shareLimitAction == ShareLimitAction::RemoveWithContent) else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
{ {
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName)); LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
deleteTorrent(torrentID, DeleteTorrentAndFiles); removeTorrent(torrent->id(), TorrentRemoveOption::RemoveContent);
} }
else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped()) else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
{ {
@ -2303,7 +2312,6 @@ void SessionImpl::processShareLimits()
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName)); LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
} }
} }
}
} }
void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames) void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames)
@ -2331,6 +2339,19 @@ void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath,
} }
} }
void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage)
{
if (errorMessage.isEmpty())
{
LogMsg(tr("Torrent content removed. Torrent: \"%1\"").arg(torrentName));
}
else
{
LogMsg(tr("Failed to remove torrent content. Torrent: \"%1\". Error: \"%2\"")
.arg(torrentName, errorMessage), Log::WARNING);
}
}
Torrent *SessionImpl::getTorrent(const TorrentID &id) const Torrent *SessionImpl::getTorrent(const TorrentID &id) const
{ {
return m_torrents.value(id); return m_torrents.value(id);
@ -2377,22 +2398,25 @@ void SessionImpl::banIP(const QString &ip)
// Delete a torrent from the session, given its hash // Delete a torrent from the session, given its hash
// and from the disk, if the corresponding deleteOption is chosen // and from the disk, if the corresponding deleteOption is chosen
bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption) bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption deleteOption)
{ {
TorrentImpl *const torrent = m_torrents.take(id); TorrentImpl *const torrent = m_torrents.take(id);
if (!torrent) if (!torrent)
return false; return false;
qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrent->id().toString())); const TorrentID torrentID = torrent->id();
const QString torrentName = torrent->name();
qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrentID.toString()));
emit torrentAboutToBeRemoved(torrent); emit torrentAboutToBeRemoved(torrent);
if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid()) if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid())
m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1())); m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1()));
// Remove it from session // Remove it from session
if (deleteOption == DeleteTorrent) if (deleteOption == TorrentRemoveOption::KeepContent)
{ {
m_removingTorrents[torrent->id()] = {torrent->name(), {}, deleteOption}; m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), {}, deleteOption};
const lt::torrent_handle nativeHandle {torrent->nativeHandle()}; const lt::torrent_handle nativeHandle {torrent->nativeHandle()};
const auto iter = std::find_if(m_moveStorageQueue.begin(), m_moveStorageQueue.end() const auto iter = std::find_if(m_moveStorageQueue.begin(), m_moveStorageQueue.end()
@ -2414,7 +2438,7 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp
} }
else else
{ {
m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption}; m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), torrent->actualFilePaths(), deleteOption};
if (m_moveStorageQueue.size() > 1) if (m_moveStorageQueue.size() > 1)
{ {
@ -2429,12 +2453,13 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp
m_moveStorageQueue.erase(iter); m_moveStorageQueue.erase(iter);
} }
m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_files); m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_partfile);
} }
// Remove it from torrent resume directory // Remove it from torrent resume directory
m_resumeDataStorage->remove(torrent->id()); m_resumeDataStorage->remove(torrentID);
LogMsg(tr("Torrent removed. Torrent: \"%1\"").arg(torrentName));
delete torrent; delete torrent;
return true; return true;
} }
@ -2462,7 +2487,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
} }
#endif #endif
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files); m_nativeSession->remove_torrent(nativeHandle);
return true; return true;
} }
@ -2769,26 +2794,22 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
Q_ASSERT(p.file_priorities.empty()); Q_ASSERT(p.file_priorities.empty());
Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size())); Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size()));
QList<DownloadPriority> filePriorities = addTorrentParams.filePriorities;
if (filePriorities.isEmpty() && isExcludedFileNamesEnabled())
{
// Check file name blacklist when priorities are not explicitly set
applyFilenameFilter(filePaths, filePriorities);
}
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)
p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal)); p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal));
if (addTorrentParams.filePriorities.isEmpty()) if (!filePriorities.isEmpty())
{ {
if (isExcludedFileNamesEnabled()) for (int i = 0; i < filePriorities.size(); ++i)
{ p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(filePriorities[i]);
// Check file name blacklist when priorities are not explicitly set
for (int i = 0; i < filePaths.size(); ++i)
{
if (isFilenameExcluded(filePaths.at(i).filename()))
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = lt::dont_download;
}
}
}
else
{
for (int i = 0; i < addTorrentParams.filePriorities.size(); ++i)
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(addTorrentParams.filePriorities[i]);
} }
Q_ASSERT(p.ti); Q_ASSERT(p.ti);
@ -3874,21 +3895,41 @@ void SessionImpl::populateExcludedFileNamesRegExpList()
for (const QString &str : excludedNames) for (const QString &str : excludedNames)
{ {
const QString pattern = QRegularExpression::anchoredPattern(QRegularExpression::wildcardToRegularExpression(str)); const QString pattern = QRegularExpression::wildcardToRegularExpression(str);
const QRegularExpression re {pattern, QRegularExpression::CaseInsensitiveOption}; const QRegularExpression re {pattern, QRegularExpression::CaseInsensitiveOption};
m_excludedFileNamesRegExpList.append(re); m_excludedFileNamesRegExpList.append(re);
} }
} }
bool SessionImpl::isFilenameExcluded(const QString &fileName) const void SessionImpl::applyFilenameFilter(const PathList &files, QList<DownloadPriority> &priorities)
{ {
if (!isExcludedFileNamesEnabled()) if (!isExcludedFileNamesEnabled())
return false; return;
return std::any_of(m_excludedFileNamesRegExpList.begin(), m_excludedFileNamesRegExpList.end(), [&fileName](const QRegularExpression &re) const auto isFilenameExcluded = [patterns = m_excludedFileNamesRegExpList](const Path &fileName)
{ {
return re.match(fileName).hasMatch(); return std::any_of(patterns.begin(), patterns.end(), [&fileName](const QRegularExpression &re)
{
Path path = fileName;
while (!re.match(path.filename()).hasMatch())
{
path = path.parentPath();
if (path.isEmpty())
return false;
}
return true;
}); });
};
priorities.resize(files.count(), DownloadPriority::Normal);
for (int i = 0; i < priorities.size(); ++i)
{
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
continue;
if (isFilenameExcluded(files.at(i)))
priorities[i] = BitTorrent::DownloadPriority::Ignored;
}
} }
void SessionImpl::setBannedIPs(const QStringList &newList) void SessionImpl::setBannedIPs(const QStringList &newList)
@ -3957,6 +3998,16 @@ void SessionImpl::setStartPaused(const bool value)
m_startPaused = value; m_startPaused = value;
} }
TorrentContentRemoveOption SessionImpl::torrentContentRemoveOption() const
{
return m_torrentContentRemoveOption;
}
void SessionImpl::setTorrentContentRemoveOption(const TorrentContentRemoveOption option)
{
m_torrentContentRemoveOption = option;
}
QStringList SessionImpl::bannedIPs() const QStringList SessionImpl::bannedIPs() const
{ {
return m_bannedIPs; return m_bannedIPs;
@ -5002,18 +5053,7 @@ void SessionImpl::handleTorrentChecked(TorrentImpl *const torrent)
void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent) void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent)
{ {
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name())); m_pendingFinishedTorrents.append(torrent);
emit torrentFinished(torrent);
if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
exportTorrentFile(torrent, exportPath);
const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent)
{
return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored());
});
if (!hasUnfinishedTorrents)
emit allTorrentsFinished();
} }
void SessionImpl::handleTorrentResumeDataReady(TorrentImpl *const torrent, const LoadTorrentParams &data) void SessionImpl::handleTorrentResumeDataReady(TorrentImpl *const torrent, const LoadTorrentParams &data)
@ -5141,7 +5181,7 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath)
// 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 RemovingTorrentData &removingTorrentData = m_removingTorrents[nativeHandle.info_hash()];
if (removingTorrentData.deleteOption == DeleteTorrent) if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent)
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile); m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile);
} }
} }
@ -5660,74 +5700,32 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle,
return torrent; return torrent;
} }
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert *alert) void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/)
{ {
#ifdef QBT_USES_LIBTORRENT2 // We cannot consider `torrent_removed_alert` as a starting point for removing content,
const auto id = TorrentID::fromInfoHash(alert->info_hashes); // because it has an inconsistent posting time between different versions of libtorrent,
#else // so files may still be in use in some cases.
const auto id = TorrentID::fromInfoHash(alert->info_hash);
#endif
const auto removingTorrentDataIter = m_removingTorrents.find(id);
if (removingTorrentDataIter != m_removingTorrents.end())
{
if (removingTorrentDataIter->deleteOption == DeleteTorrent)
{
LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
m_removingTorrents.erase(removingTorrentDataIter);
}
}
} }
void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert) void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert)
{ {
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(alert->info_hashes); const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
#else #else
const auto id = TorrentID::fromInfoHash(alert->info_hash); const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
#endif #endif
handleRemovedTorrent(torrentID);
const auto removingTorrentDataIter = m_removingTorrents.find(id);
if (removingTorrentDataIter == m_removingTorrents.end())
return;
// torrent_deleted_alert can also be posted due to deletion of partfile. Ignore it in such a case.
if (removingTorrentDataIter->deleteOption == DeleteTorrent)
return;
Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove);
LogMsg(tr("Removed torrent and deleted its content. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
m_removingTorrents.erase(removingTorrentDataIter);
} }
void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert) void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert)
{ {
#ifdef QBT_USES_LIBTORRENT2 #ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(alert->info_hashes); const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
#else #else
const auto id = TorrentID::fromInfoHash(alert->info_hash); const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
#endif #endif
const auto errorMessage = alert->error ? QString::fromLocal8Bit(alert->error.message().c_str()) : QString();
const auto removingTorrentDataIter = m_removingTorrents.find(id); handleRemovedTorrent(torrentID, errorMessage);
if (removingTorrentDataIter == m_removingTorrents.end())
return;
if (alert->error)
{
// libtorrent won't delete the directory if it contains files not listed in the torrent,
// so we remove the directory ourselves
Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove);
LogMsg(tr("Removed torrent but failed to delete its content and/or partfile. Torrent: \"%1\". Error: \"%2\"")
.arg(removingTorrentDataIter->name, QString::fromLocal8Bit(alert->error.message().c_str()))
, Log::WARNING);
}
else // torrent without metadata, hence no files on disk
{
LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
}
m_removingTorrents.erase(removingTorrentDataIter);
} }
void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert) void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert)
@ -6079,6 +6077,29 @@ void SessionImpl::handleStateUpdateAlert(const lt::state_update_alert *alert)
if (!updatedTorrents.isEmpty()) if (!updatedTorrents.isEmpty())
emit torrentsUpdated(updatedTorrents); emit torrentsUpdated(updatedTorrents);
if (!m_pendingFinishedTorrents.isEmpty())
{
for (TorrentImpl *torrent : m_pendingFinishedTorrents)
{
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
emit torrentFinished(torrent);
if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
exportTorrentFile(torrent, exportPath);
processTorrentShareLimits(torrent);
}
m_pendingFinishedTorrents.clear();
const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent)
{
return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored());
});
if (!hasUnfinishedTorrents)
emit allTorrentsFinished();
}
if (m_needSaveTorrentsQueue) if (m_needSaveTorrentsQueue)
saveTorrentsQueue(); saveTorrentsQueue();
@ -6140,7 +6161,7 @@ void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *a
if (torrent2) if (torrent2)
{ {
if (torrent1) if (torrent1)
deleteTorrent(torrentIDv1); removeTorrent(torrentIDv1);
else else
cancelDownloadMetadata(torrentIDv1); cancelDownloadMetadata(torrentIDv1);
@ -6249,3 +6270,29 @@ void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, Q
} }
}); });
} }
void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError)
{
const auto removingTorrentDataIter = m_removingTorrents.find(torrentID);
if (removingTorrentDataIter == m_removingTorrents.end())
return;
if (!partfileRemoveError.isEmpty())
{
LogMsg(tr("Failed to remove partfile. Torrent: \"%1\". Reason: \"%2\".")
.arg(removingTorrentDataIter->name, partfileRemoveError)
, Log::WARNING);
}
if ((removingTorrentDataIter->removeOption == TorrentRemoveOption::RemoveContent)
&& !removingTorrentDataIter->contentStoragePath.isEmpty())
{
QMetaObject::invokeMethod(m_torrentContentRemover, [this, jobData = *removingTorrentDataIter]
{
m_torrentContentRemover->performJob(jobData.name, jobData.contentStoragePath
, jobData.fileNames, m_torrentContentRemoveOption);
});
}
m_removingTorrents.erase(removingTorrentDataIter);
}

View file

@ -75,6 +75,7 @@ namespace BitTorrent
class InfoHash; class InfoHash;
class ResumeDataStorage; class ResumeDataStorage;
class Torrent; class Torrent;
class TorrentContentRemover;
class TorrentDescriptor; class TorrentDescriptor;
class TorrentImpl; class TorrentImpl;
class Tracker; class Tracker;
@ -402,7 +403,7 @@ namespace BitTorrent
void setExcludedFileNamesEnabled(bool enabled) override; void setExcludedFileNamesEnabled(bool enabled) override;
QStringList excludedFileNames() const override; QStringList excludedFileNames() const override;
void setExcludedFileNames(const QStringList &excludedFileNames) override; void setExcludedFileNames(const QStringList &excludedFileNames) override;
bool isFilenameExcluded(const QString &fileName) const override; void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) override;
QStringList bannedIPs() const override; QStringList bannedIPs() const override;
void setBannedIPs(const QStringList &newList) override; void setBannedIPs(const QStringList &newList) override;
ResumeDataStorageType resumeDataStorageType() const override; ResumeDataStorageType resumeDataStorageType() const override;
@ -411,6 +412,8 @@ namespace BitTorrent
void setMergeTrackersEnabled(bool enabled) override; void setMergeTrackersEnabled(bool enabled) override;
bool isStartPaused() const override; bool isStartPaused() const override;
void setStartPaused(bool value) override; void setStartPaused(bool value) override;
TorrentContentRemoveOption torrentContentRemoveOption() const override;
void setTorrentContentRemoveOption(TorrentContentRemoveOption option) override;
bool isRestored() const override; bool isRestored() const override;
@ -430,7 +433,7 @@ namespace BitTorrent
bool isKnownTorrent(const InfoHash &infoHash) const override; bool isKnownTorrent(const InfoHash &infoHash) const override;
bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams &params = {}) override; bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams &params = {}) override;
bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteTorrent) override; bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) override;
bool downloadMetadata(const TorrentDescriptor &torrentDescr) override; bool downloadMetadata(const TorrentDescriptor &torrentDescr) override;
bool cancelDownloadMetadata(const TorrentID &id) override; bool cancelDownloadMetadata(const TorrentID &id) override;
@ -487,11 +490,11 @@ namespace BitTorrent
void configureDeferred(); void configureDeferred();
void readAlerts(); void readAlerts();
void enqueueRefresh(); void enqueueRefresh();
void processShareLimits();
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 fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
private: private:
struct ResumeSessionContext; struct ResumeSessionContext;
@ -507,8 +510,9 @@ namespace BitTorrent
struct RemovingTorrentData struct RemovingTorrentData
{ {
QString name; QString name;
Path pathToRemove; Path contentStoragePath;
DeleteOption deleteOption {}; PathList fileNames;
TorrentRemoveOption removeOption {};
}; };
explicit SessionImpl(QObject *parent = nullptr); explicit SessionImpl(QObject *parent = nullptr);
@ -536,6 +540,7 @@ namespace BitTorrent
void enableIPFilter(); void enableIPFilter();
void disableIPFilter(); void disableIPFilter();
void processTrackerStatuses(); void processTrackerStatuses();
void processTorrentShareLimits(TorrentImpl *torrent);
void populateExcludedFileNamesRegExpList(); void populateExcludedFileNamesRegExpList();
void prepareStartup(); void prepareStartup();
void handleLoadedResumeData(ResumeSessionContext *context); void handleLoadedResumeData(ResumeSessionContext *context);
@ -599,13 +604,7 @@ namespace BitTorrent
void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers); void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers);
// BitTorrent void handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError = {});
lt::session *m_nativeSession = nullptr;
NativeSessionExtension *m_nativeSessionExtension = nullptr;
bool m_deferredConfigureScheduled = false;
bool m_IPFilteringConfigured = false;
mutable bool m_listenInterfaceConfigured = false;
CachedSettingValue<QString> m_DHTBootstrapNodes; CachedSettingValue<QString> m_DHTBootstrapNodes;
CachedSettingValue<bool> m_isDHTEnabled; CachedSettingValue<bool> m_isDHTEnabled;
@ -731,8 +730,16 @@ namespace BitTorrent
CachedSettingValue<int> m_I2POutboundQuantity; CachedSettingValue<int> m_I2POutboundQuantity;
CachedSettingValue<int> m_I2PInboundLength; CachedSettingValue<int> m_I2PInboundLength;
CachedSettingValue<int> m_I2POutboundLength; CachedSettingValue<int> m_I2POutboundLength;
CachedSettingValue<TorrentContentRemoveOption> m_torrentContentRemoveOption;
SettingValue<bool> m_startPaused; SettingValue<bool> m_startPaused;
lt::session *m_nativeSession = nullptr;
NativeSessionExtension *m_nativeSessionExtension = nullptr;
bool m_deferredConfigureScheduled = false;
bool m_IPFilteringConfigured = false;
mutable bool m_listenInterfaceConfigured = false;
bool m_isRestored = false; bool m_isRestored = false;
bool m_isPaused = isStartPaused(); bool m_isPaused = isStartPaused();
@ -766,6 +773,7 @@ namespace BitTorrent
QThreadPool *m_asyncWorker = nullptr; QThreadPool *m_asyncWorker = nullptr;
ResumeDataStorage *m_resumeDataStorage = nullptr; ResumeDataStorage *m_resumeDataStorage = nullptr;
FileSearcher *m_fileSearcher = nullptr; FileSearcher *m_fileSearcher = nullptr;
TorrentContentRemover *m_torrentContentRemover = nullptr;
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata; QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
@ -809,6 +817,8 @@ namespace BitTorrent
QTimer *m_wakeupCheckTimer = nullptr; QTimer *m_wakeupCheckTimer = nullptr;
QDateTime m_wakeupCheckTimestamp; QDateTime m_wakeupCheckTimestamp;
QList<TorrentImpl *> m_pendingFinishedTorrents;
friend void Session::initInstance(); friend void Session::initInstance();
friend void Session::freeInstance(); friend void Session::freeInstance();
friend Session *Session::instance(); friend Session *Session::instance();

View file

@ -228,6 +228,7 @@ namespace BitTorrent
virtual void setShareLimitAction(ShareLimitAction action) = 0; virtual void setShareLimitAction(ShareLimitAction action) = 0;
virtual PathList filePaths() const = 0; virtual PathList filePaths() const = 0;
virtual PathList actualFilePaths() const = 0;
virtual TorrentInfo info() const = 0; virtual TorrentInfo info() const = 0;
virtual bool isFinished() const = 0; virtual bool isFinished() const = 0;

View file

@ -0,0 +1,50 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* 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.
*/
#pragma once
#include <QMetaEnum>
namespace BitTorrent
{
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
inline namespace TorrentContentRemoveOptionNS
{
Q_NAMESPACE
enum class TorrentContentRemoveOption
{
Delete,
MoveToTrash
};
Q_ENUM_NS(TorrentContentRemoveOption)
}
}

View file

@ -0,0 +1,61 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "torrentcontentremover.h"
#include "base/utils/fs.h"
void BitTorrent::TorrentContentRemover::performJob(const QString &torrentName, const Path &basePath
, const PathList &fileNames, const TorrentContentRemoveOption option)
{
QString errorMessage;
if (!fileNames.isEmpty())
{
const auto removeFileFn = [&option](const Path &filePath)
{
return ((option == TorrentContentRemoveOption::MoveToTrash)
? Utils::Fs::moveFileToTrash : Utils::Fs::removeFile)(filePath);
};
for (const Path &fileName : fileNames)
{
if (const auto result = removeFileFn(basePath / fileName)
; !result && errorMessage.isEmpty())
{
errorMessage = result.error();
}
}
const Path rootPath = Path::findRootFolder(fileNames);
if (!rootPath.isEmpty())
Utils::Fs::smartRemoveEmptyFolderTree(basePath / rootPath);
}
emit jobFinished(torrentName, errorMessage);
}

View file

@ -0,0 +1,53 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* 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.
*/
#pragma once
#include <QObject>
#include "base/path.h"
#include "torrentcontentremoveoption.h"
namespace BitTorrent
{
class TorrentContentRemover final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TorrentContentRemover)
public:
using QObject::QObject;
public slots:
void performJob(const QString &torrentName, const Path &basePath
, const PathList &fileNames, TorrentContentRemoveOption option);
signals:
void jobFinished(const QString &torrentName, const QString &errorMessage);
};
}

View file

@ -77,6 +77,10 @@
#include "base/utils/os.h" #include "base/utils/os.h"
#endif // Q_OS_MACOS || Q_OS_WIN #endif // Q_OS_MACOS || Q_OS_WIN
#ifndef QBT_USES_LIBTORRENT2
#include "customstorage.h"
#endif
using namespace BitTorrent; using namespace BitTorrent;
namespace namespace
@ -982,6 +986,21 @@ PathList TorrentImpl::filePaths() const
return m_filePaths; return m_filePaths;
} }
PathList TorrentImpl::actualFilePaths() const
{
if (!hasMetadata())
return {};
PathList paths;
paths.reserve(filesCount());
const lt::file_storage files = nativeTorrentInfo()->files();
for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes()))
paths.emplaceBack(files.file_path(nativeIndex));
return paths;
}
QVector<DownloadPriority> TorrentImpl::filePriorities() const QVector<DownloadPriority> TorrentImpl::filePriorities() const
{ {
return m_filePriorities; return m_filePriorities;
@ -1447,11 +1466,13 @@ QBitArray TorrentImpl::pieces() const
QBitArray TorrentImpl::downloadingPieces() const QBitArray TorrentImpl::downloadingPieces() const
{ {
QBitArray result(piecesCount()); if (!hasMetadata())
return {};
std::vector<lt::partial_piece_info> queue; std::vector<lt::partial_piece_info> queue;
m_nativeHandle.get_download_queue(queue); m_nativeHandle.get_download_queue(queue);
QBitArray result {piecesCount()};
for (const lt::partial_piece_info &info : queue) for (const lt::partial_piece_info &info : queue)
result.setBit(LT::toUnderlyingType(info.piece_index)); result.setBit(LT::toUnderlyingType(info.piece_index));
@ -1791,12 +1812,13 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
const Path filePath = actualFilePath.removedExtension(QB_EXT); const Path filePath = actualFilePath.removedExtension(QB_EXT);
m_filePaths.append(filePath); m_filePaths.append(filePath);
lt::download_priority_t &nativePriority = p.file_priorities[LT::toUnderlyingType(nativeIndex)]; m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
if ((nativePriority != lt::dont_download) && m_session->isFilenameExcluded(filePath.filename()))
nativePriority = lt::dont_download;
const auto priority = LT::fromNative(nativePriority);
m_filePriorities.append(priority);
} }
m_session->applyFilenameFilter(fileNames, m_filePriorities);
for (int i = 0; i < m_filePriorities.size(); ++i)
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]);
p.save_path = savePath.toString().toStdString(); p.save_path = savePath.toString().toStdString();
p.ti = metadata; p.ti = metadata;
@ -1859,6 +1881,9 @@ void TorrentImpl::reload()
auto *const extensionData = new ExtensionData; auto *const extensionData = new ExtensionData;
p.userdata = LTClientData(extensionData); p.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
m_nativeHandle = m_nativeSession->add_torrent(p); m_nativeHandle = m_nativeSession->add_torrent(p);
m_nativeStatus = extensionData->status; m_nativeStatus = extensionData->status;

View file

@ -153,6 +153,7 @@ namespace BitTorrent
Path actualFilePath(int index) const override; Path actualFilePath(int index) const override;
qlonglong fileSize(int index) const override; qlonglong fileSize(int index) const override;
PathList filePaths() const override; PathList filePaths() const override;
PathList actualFilePaths() const override;
QVector<DownloadPriority> filePriorities() const override; QVector<DownloadPriority> filePriorities() const override;
TorrentInfo info() const override; TorrentInfo info() const override;

View file

@ -44,6 +44,7 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
, m_requestHandler(requestHandler) , m_requestHandler(requestHandler)
{ {
m_socket->setParent(this); m_socket->setParent(this);
connect(m_socket, &QAbstractSocket::disconnected, this, &Connection::closed);
// reserve common size for requests, don't use the max allowed size which is too big for // reserve common size for requests, don't use the max allowed size which is too big for
// memory constrained platforms // memory constrained platforms
@ -62,11 +63,6 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
}); });
} }
Connection::~Connection()
{
m_socket->close();
}
void Connection::read() void Connection::read()
{ {
// reuse existing buffer and avoid unnecessary memory allocation/relocation // reuse existing buffer and avoid unnecessary memory allocation/relocation
@ -182,11 +178,6 @@ bool Connection::hasExpired(const qint64 timeout) const
&& m_idleTimer.hasExpired(timeout); && m_idleTimer.hasExpired(timeout);
} }
bool Connection::isClosed() const
{
return (m_socket->state() == QAbstractSocket::UnconnectedState);
}
bool Connection::acceptsGzipEncoding(QString codings) bool Connection::acceptsGzipEncoding(QString codings)
{ {
// [rfc7231] 5.3.4. Accept-Encoding // [rfc7231] 5.3.4. Accept-Encoding

View file

@ -47,10 +47,11 @@ namespace Http
public: public:
Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent = nullptr); Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent = nullptr);
~Connection();
bool hasExpired(qint64 timeout) const; bool hasExpired(qint64 timeout) const;
bool isClosed() const;
signals:
void closed();
private: private:
static bool acceptsGzipEncoding(QString codings); static bool acceptsGzipEncoding(QString codings);

View file

@ -32,7 +32,10 @@
#include <algorithm> #include <algorithm>
#include <chrono> #include <chrono>
#include <memory>
#include <new>
#include <QtLogging>
#include <QNetworkProxy> #include <QNetworkProxy>
#include <QSslCipher> #include <QSslCipher>
#include <QSslConfiguration> #include <QSslConfiguration>
@ -40,7 +43,6 @@
#include <QStringList> #include <QStringList>
#include <QTimer> #include <QTimer>
#include "base/algorithm.h"
#include "base/global.h" #include "base/global.h"
#include "base/utils/net.h" #include "base/utils/net.h"
#include "base/utils/sslkey.h" #include "base/utils/sslkey.h"
@ -113,32 +115,38 @@ Server::Server(IRequestHandler *requestHandler, QObject *parent)
void Server::incomingConnection(const qintptr socketDescriptor) void Server::incomingConnection(const qintptr socketDescriptor)
{ {
if (m_connections.size() >= CONNECTIONS_LIMIT) return; std::unique_ptr<QTcpSocket> serverSocket = m_https ? std::make_unique<QSslSocket>(this) : std::make_unique<QTcpSocket>(this);
QTcpSocket *serverSocket = nullptr;
if (m_https)
serverSocket = new QSslSocket(this);
else
serverSocket = new QTcpSocket(this);
if (!serverSocket->setSocketDescriptor(socketDescriptor)) if (!serverSocket->setSocketDescriptor(socketDescriptor))
return;
if (m_connections.size() >= CONNECTIONS_LIMIT)
{ {
delete serverSocket; qWarning("Too many connections. Exceeded CONNECTIONS_LIMIT (%d). Connection closed.", CONNECTIONS_LIMIT);
return; return;
} }
try
{
if (m_https) if (m_https)
{ {
static_cast<QSslSocket *>(serverSocket)->setProtocol(QSsl::SecureProtocols); auto *sslSocket = static_cast<QSslSocket *>(serverSocket.get());
static_cast<QSslSocket *>(serverSocket)->setPrivateKey(m_key); sslSocket->setProtocol(QSsl::SecureProtocols);
static_cast<QSslSocket *>(serverSocket)->setLocalCertificateChain(m_certificates); sslSocket->setPrivateKey(m_key);
static_cast<QSslSocket *>(serverSocket)->setPeerVerifyMode(QSslSocket::VerifyNone); sslSocket->setLocalCertificateChain(m_certificates);
static_cast<QSslSocket *>(serverSocket)->startServerEncryption(); sslSocket->setPeerVerifyMode(QSslSocket::VerifyNone);
sslSocket->startServerEncryption();
} }
auto *c = new Connection(serverSocket, m_requestHandler, this); auto *connection = new Connection(serverSocket.release(), m_requestHandler, this);
m_connections.insert(c); m_connections.insert(connection);
connect(serverSocket, &QAbstractSocket::disconnected, this, [c, this]() { removeConnection(c); }); connect(connection, &Connection::closed, this, [this, connection] { removeConnection(connection); });
}
catch (const std::bad_alloc &exception)
{
// drop the connection instead of throwing exception and crash
qWarning("Failed to allocate memory for HTTP connection. Connection closed.");
return;
}
} }
void Server::removeConnection(Connection *connection) void Server::removeConnection(Connection *connection)

View file

@ -134,17 +134,17 @@ void Preferences::setCustomUIThemePath(const Path &path)
setValue(u"Preferences/General/CustomUIThemePath"_s, path); setValue(u"Preferences/General/CustomUIThemePath"_s, path);
} }
bool Preferences::deleteTorrentFilesAsDefault() const bool Preferences::removeTorrentContent() const
{ {
return value(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, false); return value(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, false);
} }
void Preferences::setDeleteTorrentFilesAsDefault(const bool del) void Preferences::setRemoveTorrentContent(const bool remove)
{ {
if (del == deleteTorrentFilesAsDefault()) if (remove == removeTorrentContent())
return; return;
setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, del); setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, remove);
} }
bool Preferences::confirmOnExit() const bool Preferences::confirmOnExit() const

View file

@ -105,8 +105,8 @@ public:
void setUseCustomUITheme(bool use); void setUseCustomUITheme(bool use);
Path customUIThemePath() const; Path customUIThemePath() const;
void setCustomUIThemePath(const Path &path); void setCustomUIThemePath(const Path &path);
bool deleteTorrentFilesAsDefault() const; bool removeTorrentContent() const;
void setDeleteTorrentFilesAsDefault(bool del); void setRemoveTorrentContent(bool remove);
bool confirmOnExit() const; bool confirmOnExit() const;
void setConfirmOnExit(bool confirm); void setConfirmOnExit(bool confirm);
bool speedInTitleBar() const; bool speedInTitleBar() const;

View file

@ -52,19 +52,21 @@ const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored);
using BitTorrent::Torrent; using BitTorrent::Torrent;
TorrentFilter::TorrentFilter(const Type type, const std::optional<TorrentIDSet> &idSet TorrentFilter::TorrentFilter(const Type type, const std::optional<TorrentIDSet> &idSet
, const std::optional<QString> &category, const std::optional<Tag> &tag) , const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
: m_type {type} : m_type {type}
, m_category {category} , m_category {category}
, m_tag {tag} , m_tag {tag}
, m_idSet {idSet} , m_idSet {idSet}
, m_private {isPrivate}
{ {
} }
TorrentFilter::TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet TorrentFilter::TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet
, const std::optional<QString> &category, const std::optional<Tag> &tag) , const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
: m_category {category} : m_category {category}
, m_tag {tag} , m_tag {tag}
, m_idSet {idSet} , m_idSet {idSet}
, m_private {isPrivate}
{ {
setTypeByName(filter); setTypeByName(filter);
} }
@ -147,11 +149,22 @@ bool TorrentFilter::setTag(const std::optional<Tag> &tag)
return false; return false;
} }
bool TorrentFilter::setPrivate(const std::optional<bool> isPrivate)
{
if (m_private != isPrivate)
{
m_private = isPrivate;
return true;
}
return false;
}
bool TorrentFilter::match(const Torrent *const torrent) const bool TorrentFilter::match(const Torrent *const torrent) const
{ {
if (!torrent) return false; if (!torrent) return false;
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent)); return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent) && matchPrivate(torrent));
} }
bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
@ -224,3 +237,11 @@ bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const
return torrent->hasTag(*m_tag); return torrent->hasTag(*m_tag);
} }
bool TorrentFilter::matchPrivate(const BitTorrent::Torrent *const torrent) const
{
if (!m_private)
return true;
return m_private == torrent->isPrivate();
}

View file

@ -87,16 +87,24 @@ public:
TorrentFilter() = default; TorrentFilter() = default;
// category & tags: pass empty string for uncategorized / untagged torrents. // category & tags: pass empty string for uncategorized / untagged torrents.
TorrentFilter(Type type, const std::optional<TorrentIDSet> &idSet = AnyID TorrentFilter(Type type
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tag = AnyTag); , const std::optional<TorrentIDSet> &idSet = AnyID
TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet = AnyID , const std::optional<QString> &category = AnyCategory
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tags = AnyTag); , const std::optional<Tag> &tag = AnyTag
, std::optional<bool> isPrivate = {});
TorrentFilter(const QString &filter
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tags = AnyTag
, std::optional<bool> isPrivate = {});
bool setType(Type type); bool setType(Type type);
bool setTypeByName(const QString &filter); bool setTypeByName(const QString &filter);
bool setTorrentIDSet(const std::optional<TorrentIDSet> &idSet); bool setTorrentIDSet(const std::optional<TorrentIDSet> &idSet);
bool setCategory(const std::optional<QString> &category); bool setCategory(const std::optional<QString> &category);
bool setTag(const std::optional<Tag> &tag); bool setTag(const std::optional<Tag> &tag);
bool setPrivate(std::optional<bool> isPrivate);
bool match(const BitTorrent::Torrent *torrent) const; bool match(const BitTorrent::Torrent *torrent) const;
@ -105,9 +113,11 @@ private:
bool matchHash(const BitTorrent::Torrent *torrent) const; bool matchHash(const BitTorrent::Torrent *torrent) const;
bool matchCategory(const BitTorrent::Torrent *torrent) const; bool matchCategory(const BitTorrent::Torrent *torrent) const;
bool matchTag(const BitTorrent::Torrent *torrent) const; bool matchTag(const BitTorrent::Torrent *torrent) const;
bool matchPrivate(const BitTorrent::Torrent *torrent) const;
Type m_type {All}; Type m_type {All};
std::optional<QString> m_category; std::optional<QString> m_category;
std::optional<Tag> m_tag; std::optional<Tag> m_tag;
std::optional<TorrentIDSet> m_idSet; std::optional<TorrentIDSet> m_idSet;
std::optional<bool> m_private;
}; };

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 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
@ -29,8 +29,6 @@
#include "fs.h" #include "fs.h"
#include <cerrno>
#include <cstring>
#include <filesystem> #include <filesystem>
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
@ -52,6 +50,7 @@
#include <unistd.h> #include <unistd.h>
#endif #endif
#include <QCoreApplication>
#include <QDateTime> #include <QDateTime>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
@ -311,20 +310,42 @@ bool Utils::Fs::renameFile(const Path &from, const Path &to)
* *
* This function will try to fix the file permissions before removing it. * This function will try to fix the file permissions before removing it.
*/ */
bool Utils::Fs::removeFile(const Path &path) nonstd::expected<void, QString> Utils::Fs::removeFile(const Path &path)
{ {
if (QFile::remove(path.data()))
return true;
QFile file {path.data()}; QFile file {path.data()};
if (file.remove())
return {};
if (!file.exists()) if (!file.exists())
return true; return {};
// Make sure we have read/write permissions // Make sure we have read/write permissions
file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser); file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
return file.remove(); if (file.remove())
return {};
return nonstd::make_unexpected(file.errorString());
} }
nonstd::expected<void, QString> Utils::Fs::moveFileToTrash(const Path &path)
{
QFile file {path.data()};
if (file.moveToTrash())
return {};
if (!file.exists())
return {};
// Make sure we have read/write permissions
file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
if (file.moveToTrash())
return {};
const QString errorMessage = file.errorString();
return nonstd::make_unexpected(!errorMessage.isEmpty() ? errorMessage : QCoreApplication::translate("fs", "Unknown error"));
}
bool Utils::Fs::isReadable(const Path &path) bool Utils::Fs::isReadable(const Path &path)
{ {
return QFileInfo(path.data()).isReadable(); return QFileInfo(path.data()).isReadable();

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 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
@ -35,6 +35,7 @@
#include <QString> #include <QString>
#include "base/3rdparty/expected.hpp"
#include "base/global.h" #include "base/global.h"
#include "base/pathfwd.h" #include "base/pathfwd.h"
@ -60,7 +61,8 @@ namespace Utils::Fs
bool copyFile(const Path &from, const Path &to); bool copyFile(const Path &from, const Path &to);
bool renameFile(const Path &from, const Path &to); bool renameFile(const Path &from, const Path &to);
bool removeFile(const Path &path); nonstd::expected<void, QString> removeFile(const Path &path);
nonstd::expected<void, QString> moveFileToTrash(const Path &path);
bool mkdir(const Path &dirPath); bool mkdir(const Path &dirPath);
bool mkpath(const Path &dirPath); bool mkpath(const Path &dirPath);
bool rmdir(const Path &dirPath); bool rmdir(const Path &dirPath);

View file

@ -52,6 +52,8 @@ add_library(qbt_gui STATIC
desktopintegration.h desktopintegration.h
downloadfromurldialog.h downloadfromurldialog.h
executionlogwidget.h executionlogwidget.h
filterpatternformat.h
filterpatternformatmenu.h
flowlayout.h flowlayout.h
fspathedit.h fspathedit.h
fspathedit_p.h fspathedit_p.h
@ -151,6 +153,7 @@ add_library(qbt_gui STATIC
desktopintegration.cpp desktopintegration.cpp
downloadfromurldialog.cpp downloadfromurldialog.cpp
executionlogwidget.cpp executionlogwidget.cpp
filterpatternformatmenu.cpp
flowlayout.cpp flowlayout.cpp
fspathedit.cpp fspathedit.cpp
fspathedit_p.cpp fspathedit_p.cpp

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-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 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
@ -64,6 +64,7 @@
#include "base/utils/fs.h" #include "base/utils/fs.h"
#include "base/utils/misc.h" #include "base/utils/misc.h"
#include "base/utils/string.h" #include "base/utils/string.h"
#include "filterpatternformatmenu.h"
#include "lineedit.h" #include "lineedit.h"
#include "torrenttagsdialog.h" #include "torrenttagsdialog.h"
@ -181,6 +182,11 @@ public:
return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index)); return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index));
} }
PathList filePaths() const
{
return (m_filePaths.isEmpty() ? m_torrentInfo.filePaths() : m_filePaths);
}
void renameFile(const int index, const Path &newFilePath) override void renameFile(const int index, const Path &newFilePath) override
{ {
Q_ASSERT((index >= 0) && (index < filesCount())); Q_ASSERT((index >= 0) && (index < filesCount()));
@ -290,6 +296,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
, m_storeRememberLastSavePath {SETTINGS_KEY(u"RememberLastSavePath"_s)} , m_storeRememberLastSavePath {SETTINGS_KEY(u"RememberLastSavePath"_s)}
, m_storeTreeHeaderState {u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_s)} , m_storeTreeHeaderState {u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_s)}
, m_storeSplitterState {u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_s)} , m_storeSplitterState {u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_s)}
, m_storeFilterPatternFormat {u"GUI/" SETTINGS_KEY(u"FilterPatternFormat"_s)}
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
@ -316,6 +323,8 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
// Torrent content filtering // Torrent content filtering
m_filterLine->setPlaceholderText(tr("Filter files...")); m_filterLine->setPlaceholderText(tr("Filter files..."));
m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
m_filterLine->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_filterLine, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::showContentFilterContextMenu);
m_ui->contentFilterLayout->insertWidget(3, m_filterLine); m_ui->contentFilterLayout->insertWidget(3, m_filterLine);
const auto *focusSearchHotkey = new QShortcut(QKeySequence::Find, this); const auto *focusSearchHotkey = new QShortcut(QKeySequence::Find, this);
connect(focusSearchHotkey, &QShortcut::activated, this, [this]() connect(focusSearchHotkey, &QShortcut::activated, this, [this]()
@ -360,7 +369,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
}); });
dlg->open(); dlg->open();
}); });
connect(m_filterLine, &LineEdit::textChanged, m_ui->contentTreeView, &TorrentContentWidget::setFilterPattern); connect(m_filterLine, &LineEdit::textChanged, this, &AddNewTorrentDialog::setContentFilterPattern);
connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll); connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll);
connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone); connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone);
connect(Preferences::instance(), &Preferences::changed, [] connect(Preferences::instance(), &Preferences::changed, []
@ -691,6 +700,28 @@ void AddNewTorrentDialog::saveTorrentFile()
} }
} }
void AddNewTorrentDialog::showContentFilterContextMenu()
{
QMenu *menu = m_filterLine->createStandardContextMenu();
auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu);
connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format)
{
m_storeFilterPatternFormat = format;
setContentFilterPattern();
});
menu->addSeparator();
menu->addMenu(formatMenu);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(QCursor::pos());
}
void AddNewTorrentDialog::setContentFilterPattern()
{
m_ui->contentTreeView->setFilterPattern(m_filterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards));
}
void AddNewTorrentDialog::populateSavePaths() void AddNewTorrentDialog::populateSavePaths()
{ {
Q_ASSERT(m_currentContext); Q_ASSERT(m_currentContext);
@ -886,15 +917,7 @@ void AddNewTorrentDialog::setupTreeview()
{ {
// Check file name blacklist for torrents that are manually added // Check file name blacklist for torrents that are manually added
QVector<BitTorrent::DownloadPriority> priorities = m_contentAdaptor->filePriorities(); QVector<BitTorrent::DownloadPriority> priorities = m_contentAdaptor->filePriorities();
for (int i = 0; i < priorities.size(); ++i) BitTorrent::Session::instance()->applyFilenameFilter(m_contentAdaptor->filePaths(), priorities);
{
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
continue;
if (BitTorrent::Session::instance()->isFilenameExcluded(torrentInfo.filePath(i).filename()))
priorities[i] = BitTorrent::DownloadPriority::Ignored;
}
m_contentAdaptor->prioritizeFiles(priorities); m_contentAdaptor->prioritizeFiles(priorities);
} }

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-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 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
@ -35,6 +35,7 @@
#include "base/path.h" #include "base/path.h"
#include "base/settingvalue.h" #include "base/settingvalue.h"
#include "filterpatternformat.h"
class LineEdit; class LineEdit;
@ -92,6 +93,8 @@ private:
void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {}); void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {});
void setupTreeview(); void setupTreeview();
void saveTorrentFile(); void saveTorrentFile();
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::AddNewTorrentDialog *m_ui = nullptr; Ui::AddNewTorrentDialog *m_ui = nullptr;
std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor; std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
@ -107,4 +110,5 @@ private:
SettingValue<bool> m_storeRememberLastSavePath; SettingValue<bool> m_storeRememberLastSavePath;
SettingValue<QByteArray> m_storeTreeHeaderState; SettingValue<QByteArray> m_storeTreeHeaderState;
SettingValue<QByteArray> m_storeSplitterState; SettingValue<QByteArray> m_storeSplitterState;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
}; };

View file

@ -63,6 +63,7 @@ namespace
// qBittorrent section // qBittorrent section
QBITTORRENT_HEADER, QBITTORRENT_HEADER,
RESUME_DATA_STORAGE, RESUME_DATA_STORAGE,
TORRENT_CONTENT_REMOVE_OPTION,
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS) #if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
MEMORY_WORKING_SET_LIMIT, MEMORY_WORKING_SET_LIMIT,
#endif #endif
@ -364,6 +365,8 @@ void AdvancedSettings::saveAdvancedSettings() const
session->setI2PInboundLength(m_spinBoxI2PInboundLength.value()); session->setI2PInboundLength(m_spinBoxI2PInboundLength.value());
session->setI2POutboundLength(m_spinBoxI2POutboundLength.value()); session->setI2POutboundLength(m_spinBoxI2POutboundLength.value());
#endif #endif
session->setTorrentContentRemoveOption(m_comboBoxTorrentContentRemoveOption.currentData().value<BitTorrent::TorrentContentRemoveOption>());
} }
#ifndef QBT_USES_LIBTORRENT2 #ifndef QBT_USES_LIBTORRENT2
@ -472,6 +475,11 @@ void AdvancedSettings::loadAdvancedSettings()
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType()))); m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage); addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage);
m_comboBoxTorrentContentRemoveOption.addItem(tr("Delete files permanently"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::Delete));
m_comboBoxTorrentContentRemoveOption.addItem(tr("Move files to trash (if possible)"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::MoveToTrash));
m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption())));
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_MACOS)
// Physical memory (RAM) usage limit // Physical memory (RAM) usage limit
m_spinBoxMemoryWorkingSetLimit.setMinimum(1); m_spinBoxMemoryWorkingSetLimit.setMinimum(1);

View file

@ -81,7 +81,7 @@ private:
m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity, m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity,
m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused; m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused;
QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxDiskIOReadMode, m_comboBoxDiskIOWriteMode, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm, QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxDiskIOReadMode, m_comboBoxDiskIOWriteMode, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm,
m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage; m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage, m_comboBoxTorrentContentRemoveOption;
QLineEdit m_lineEditAppInstanceName, m_pythonExecutablePath, m_lineEditAnnounceIP, m_lineEditDHTBootstrapNodes; QLineEdit m_lineEditAppInstanceName, m_pythonExecutablePath, m_lineEditAnnounceIP, m_lineEditDHTBootstrapNodes;
#ifndef QBT_USES_LIBTORRENT2 #ifndef QBT_USES_LIBTORRENT2

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 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
@ -30,6 +31,7 @@
#include <QPushButton> #include <QPushButton>
#include "base/bittorrent/session.h"
#include "base/global.h" #include "base/global.h"
#include "base/preferences.h" #include "base/preferences.h"
#include "uithememanager.h" #include "uithememanager.h"
@ -53,8 +55,8 @@ DeletionConfirmationDialog::DeletionConfirmationDialog(QWidget *parent, const in
m_ui->rememberBtn->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s)); m_ui->rememberBtn->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s));
m_ui->rememberBtn->setIconSize(Utils::Gui::mediumIconSize()); m_ui->rememberBtn->setIconSize(Utils::Gui::mediumIconSize());
m_ui->checkPermDelete->setChecked(defaultDeleteFiles || Preferences::instance()->deleteTorrentFilesAsDefault()); m_ui->checkRemoveContent->setChecked(defaultDeleteFiles || Preferences::instance()->removeTorrentContent());
connect(m_ui->checkPermDelete, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState); connect(m_ui->checkRemoveContent, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState);
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Remove")); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Remove"));
m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus(); m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus();
@ -67,18 +69,18 @@ DeletionConfirmationDialog::~DeletionConfirmationDialog()
delete m_ui; delete m_ui;
} }
bool DeletionConfirmationDialog::isDeleteFileSelected() const bool DeletionConfirmationDialog::isRemoveContentSelected() const
{ {
return m_ui->checkPermDelete->isChecked(); return m_ui->checkRemoveContent->isChecked();
} }
void DeletionConfirmationDialog::updateRememberButtonState() void DeletionConfirmationDialog::updateRememberButtonState()
{ {
m_ui->rememberBtn->setEnabled(m_ui->checkPermDelete->isChecked() != Preferences::instance()->deleteTorrentFilesAsDefault()); m_ui->rememberBtn->setEnabled(m_ui->checkRemoveContent->isChecked() != Preferences::instance()->removeTorrentContent());
} }
void DeletionConfirmationDialog::on_rememberBtn_clicked() void DeletionConfirmationDialog::on_rememberBtn_clicked()
{ {
Preferences::instance()->setDeleteTorrentFilesAsDefault(m_ui->checkPermDelete->isChecked()); Preferences::instance()->setRemoveTorrentContent(m_ui->checkRemoveContent->isChecked());
m_ui->rememberBtn->setEnabled(false); m_ui->rememberBtn->setEnabled(false);
} }

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 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
@ -37,16 +38,16 @@ namespace Ui
class DeletionConfirmationDialog; class DeletionConfirmationDialog;
} }
class DeletionConfirmationDialog : public QDialog class DeletionConfirmationDialog final : public QDialog
{ {
Q_OBJECT Q_OBJECT
Q_DISABLE_COPY_MOVE(DeletionConfirmationDialog) Q_DISABLE_COPY_MOVE(DeletionConfirmationDialog)
public: public:
DeletionConfirmationDialog(QWidget *parent, int size, const QString &name, bool defaultDeleteFiles); DeletionConfirmationDialog(QWidget *parent, int size, const QString &name, bool defaultDeleteFiles);
~DeletionConfirmationDialog(); ~DeletionConfirmationDialog() override;
bool isDeleteFileSelected() const; bool isRemoveContentSelected() const;
private slots: private slots:
void updateRememberButtonState(); void updateRememberButtonState();

View file

@ -75,7 +75,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QCheckBox" name="checkPermDelete"> <widget class="QCheckBox" name="checkRemoveContent">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum"> <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch> <horstretch>0</horstretch>
@ -88,7 +88,7 @@
</font> </font>
</property> </property>
<property name="text"> <property name="text">
<string>Also permanently delete the files</string> <string>Also remove the content files</string>
</property> </property>
</widget> </widget>
</item> </item>

View file

@ -0,0 +1,48 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* 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.
*/
#pragma once
#include <QMetaEnum>
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
inline namespace FilterPatternFormatNS
{
Q_NAMESPACE
enum class FilterPatternFormat
{
PlainText,
Wildcards,
Regex
};
Q_ENUM_NS(FilterPatternFormat)
}

View file

@ -0,0 +1,82 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "filterpatternformatmenu.h"
#include <QActionGroup>
FilterPatternFormatMenu::FilterPatternFormatMenu(const FilterPatternFormat format, QWidget *parent)
: QMenu(parent)
{
setTitle(tr("Pattern Format"));
auto *patternFormatGroup = new QActionGroup(this);
patternFormatGroup->setExclusive(true);
QAction *plainTextAction = addAction(tr("Plain text"));
plainTextAction->setCheckable(true);
patternFormatGroup->addAction(plainTextAction);
QAction *wildcardsAction = addAction(tr("Wildcards"));
wildcardsAction->setCheckable(true);
patternFormatGroup->addAction(wildcardsAction);
QAction *regexAction = addAction(tr("Regular expression"));
regexAction->setCheckable(true);
patternFormatGroup->addAction(regexAction);
switch (format)
{
case FilterPatternFormat::Wildcards:
default:
wildcardsAction->setChecked(true);
break;
case FilterPatternFormat::PlainText:
plainTextAction->setChecked(true);
break;
case FilterPatternFormat::Regex:
regexAction->setChecked(true);
break;
}
connect(plainTextAction, &QAction::toggled, this, [this](const bool checked)
{
if (checked)
emit patternFormatChanged(FilterPatternFormat::PlainText);
});
connect(wildcardsAction, &QAction::toggled, this, [this](const bool checked)
{
if (checked)
emit patternFormatChanged(FilterPatternFormat::Wildcards);
});
connect(regexAction, &QAction::toggled, this, [this](const bool checked)
{
if (checked)
emit patternFormatChanged(FilterPatternFormat::Regex);
});
}

View file

@ -0,0 +1,45 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* 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.
*/
#pragma once
#include <QMenu>
#include "filterpatternformat.h"
class FilterPatternFormatMenu final : public QMenu
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(FilterPatternFormatMenu)
public:
explicit FilterPatternFormatMenu(FilterPatternFormat format, QWidget *parent = nullptr);
signals:
void patternFormatChanged(FilterPatternFormat format);
};

View file

@ -411,7 +411,7 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
return; return;
// Remove I2P peers since they will be completely reloaded. // Remove I2P peers since they will be completely reloaded.
for (QStandardItem *item : asConst(m_I2PPeerItems)) for (const QStandardItem *item : asConst(m_I2PPeerItems))
m_listModel->removeRow(item->row()); m_listModel->removeRow(item->row());
m_I2PPeerItems.clear(); m_I2PPeerItems.clear();
@ -466,10 +466,14 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
{ {
QStandardItem *item = m_peerItems.take(peerEndpoint); QStandardItem *item = m_peerItems.take(peerEndpoint);
QSet<QStandardItem *> &items = m_itemsByIP[peerEndpoint.address.ip]; const auto items = m_itemsByIP.find(peerEndpoint.address.ip);
items.remove(item); Q_ASSERT(items != m_itemsByIP.end());
if (items.isEmpty()) if (items == m_itemsByIP.end()) [[unlikely]]
m_itemsByIP.remove(peerEndpoint.address.ip); continue;
items->remove(item);
if (items->isEmpty())
m_itemsByIP.erase(items);
m_listModel->removeRow(item->row()); m_listModel->removeRow(item->row());
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2024 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
@ -52,6 +52,7 @@
#include "base/utils/misc.h" #include "base/utils/misc.h"
#include "base/utils/string.h" #include "base/utils/string.h"
#include "gui/autoexpandabledialog.h" #include "gui/autoexpandabledialog.h"
#include "gui/filterpatternformatmenu.h"
#include "gui/lineedit.h" #include "gui/lineedit.h"
#include "gui/trackerlist/trackerlistwidget.h" #include "gui/trackerlist/trackerlistwidget.h"
#include "gui/uithememanager.h" #include "gui/uithememanager.h"
@ -66,6 +67,7 @@
PropertiesWidget::PropertiesWidget(QWidget *parent) PropertiesWidget::PropertiesWidget(QWidget *parent)
: QWidget(parent) : QWidget(parent)
, m_ui {new Ui::PropertiesWidget} , m_ui {new Ui::PropertiesWidget}
, m_storeFilterPatternFormat {u"GUI/PropertiesWidget/FilterPatternFormat"_s}
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
#ifndef Q_OS_MACOS #ifndef Q_OS_MACOS
@ -78,7 +80,9 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
m_contentFilterLine = new LineEdit(this); m_contentFilterLine = new LineEdit(this);
m_contentFilterLine->setPlaceholderText(tr("Filter files...")); m_contentFilterLine->setPlaceholderText(tr("Filter files..."));
m_contentFilterLine->setFixedWidth(300); m_contentFilterLine->setFixedWidth(300);
connect(m_contentFilterLine, &LineEdit::textChanged, m_ui->filesList, &TorrentContentWidget::setFilterPattern); m_contentFilterLine->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_contentFilterLine, &QWidget::customContextMenuRequested, this, &PropertiesWidget::showContentFilterContextMenu);
connect(m_contentFilterLine, &LineEdit::textChanged, this, &PropertiesWidget::setContentFilterPattern);
m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine); m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open); m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open);
@ -206,6 +210,7 @@ void PropertiesWidget::clear()
m_ui->labelSavePathVal->clear(); m_ui->labelSavePathVal->clear();
m_ui->labelCreatedOnVal->clear(); m_ui->labelCreatedOnVal->clear();
m_ui->labelTotalPiecesVal->clear(); m_ui->labelTotalPiecesVal->clear();
m_ui->labelPrivateVal->clear();
m_ui->labelInfohash1Val->clear(); m_ui->labelInfohash1Val->clear();
m_ui->labelInfohash2Val->clear(); m_ui->labelInfohash2Val->clear();
m_ui->labelCommentVal->clear(); m_ui->labelCommentVal->clear();
@ -274,6 +279,28 @@ void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent)
m_ui->labelSavePathVal->setText(m_torrent->savePath().toString()); m_ui->labelSavePathVal->setText(m_torrent->savePath().toString());
} }
void PropertiesWidget::showContentFilterContextMenu()
{
QMenu *menu = m_contentFilterLine->createStandardContextMenu();
auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu);
connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format)
{
m_storeFilterPatternFormat = format;
setContentFilterPattern();
});
menu->addSeparator();
menu->addMenu(formatMenu);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(QCursor::pos());
}
void PropertiesWidget::setContentFilterPattern()
{
m_ui->filesList->setFilterPattern(m_contentFilterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards));
}
void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent) void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent)
{ {
if (torrent == m_torrent) if (torrent == m_torrent)
@ -309,7 +336,14 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped())); m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped()));
m_ui->labelCreatedByVal->setText(m_torrent->creator()); m_ui->labelCreatedByVal->setText(m_torrent->creator());
m_ui->labelPrivateVal->setText(m_torrent->isPrivate() ? tr("Yes") : tr("No"));
} }
else
{
m_ui->labelPrivateVal->setText(tr("N/A"));
}
// Load dynamic data // Load dynamic data
loadDynamicData(); loadDynamicData();
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2024 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,7 +32,8 @@
#include <QList> #include <QList>
#include <QWidget> #include <QWidget>
#include "base/pathfwd.h" #include "base/settingvalue.h"
#include "gui/filterpatternformat.h"
class QPushButton; class QPushButton;
class QTreeView; class QTreeView;
@ -102,6 +103,8 @@ private slots:
private: private:
QPushButton *getButtonFromIndex(int index); QPushButton *getButtonFromIndex(int index);
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::PropertiesWidget *m_ui = nullptr; Ui::PropertiesWidget *m_ui = nullptr;
BitTorrent::Torrent *m_torrent = nullptr; BitTorrent::Torrent *m_torrent = nullptr;
@ -115,4 +118,6 @@ private:
PropTabBar *m_tabBar = nullptr; PropTabBar *m_tabBar = nullptr;
LineEdit *m_contentFilterLine = nullptr; LineEdit *m_contentFilterLine = nullptr;
int m_handleWidth = -1; int m_handleWidth = -1;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
}; };

View file

@ -823,6 +823,38 @@
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="2" column="0">
<widget class="QLabel" name="labelPrivate">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Private:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1" colspan="5">
<widget class="QLabel" name="labelPrivateVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelInfohash1"> <widget class="QLabel" name="labelInfohash1">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred"> <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
@ -838,71 +870,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0">
<widget class="QLabel" name="labelInfohash2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Info Hash v2:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="1" colspan="5"> <item row="3" column="1" colspan="5">
<widget class="QLabel" name="labelInfohash2Val">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelSavePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Save Path:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelComment">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Comment:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="2" column="1" colspan="5">
<widget class="QLabel" name="labelInfohash1Val"> <widget class="QLabel" name="labelInfohash1Val">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@ -918,7 +886,55 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0">
<widget class="QLabel" name="labelInfohash2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Info Hash v2:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="1" colspan="5"> <item row="4" column="1" colspan="5">
<widget class="QLabel" name="labelInfohash2Val">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelSavePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Save Path:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="5" column="1" colspan="5">
<widget class="QLabel" name="labelSavePathVal"> <widget class="QLabel" name="labelSavePathVal">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred"> <sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@ -937,7 +953,23 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="5" column="1" colspan="5"> <item row="6" column="0">
<widget class="QLabel" name="labelComment">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Comment:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="6" column="1" colspan="5">
<widget class="QLabel" name="labelCommentVal"> <widget class="QLabel" name="labelCommentVal">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">

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-2024 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
@ -37,17 +37,12 @@
#include <QPointer> #include <QPointer>
#include <QScopeGuard> #include <QScopeGuard>
#if defined(Q_OS_WIN) #if defined(Q_OS_MACOS)
#include <windows.h>
#include <shellapi.h>
#else
#include <QMimeDatabase>
#include <QMimeType>
#endif
#if defined Q_OS_WIN || defined Q_OS_MACOS
#define QBT_PIXMAP_CACHE_FOR_FILE_ICONS #define QBT_PIXMAP_CACHE_FOR_FILE_ICONS
#include <QPixmapCache> #include <QPixmapCache>
#elif !defined(Q_OS_WIN)
#include <QMimeDatabase>
#include <QMimeType>
#endif #endif
#include "base/bittorrent/downloadpriority.h" #include "base/bittorrent/downloadpriority.h"
@ -116,27 +111,8 @@ namespace
}; };
#endif // QBT_PIXMAP_CACHE_FOR_FILE_ICONS #endif // QBT_PIXMAP_CACHE_FOR_FILE_ICONS
#if defined(Q_OS_WIN) #if defined(Q_OS_MACOS)
// See QTBUG-25319 for explanation why this is required // There is a bug on macOS, to be reported to Qt
class WinShellFileIconProvider final : public CachingFileIconProvider
{
QPixmap pixmapForExtension(const QString &ext) const override
{
const std::wstring extWStr = QString(u'.' + ext).toStdWString();
SHFILEINFOW sfi {};
const HRESULT hr = ::SHGetFileInfoW(extWStr.c_str(),
FILE_ATTRIBUTE_NORMAL, &sfi, sizeof(sfi), (SHGFI_ICON | SHGFI_USEFILEATTRIBUTES));
if (FAILED(hr))
return {};
const auto iconPixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon));
::DestroyIcon(sfi.hIcon);
return iconPixmap;
}
};
#elif defined(Q_OS_MACOS)
// There is a similar bug on macOS, to be reported to Qt
// https://github.com/qbittorrent/qBittorrent/pull/6156#issuecomment-316302615 // https://github.com/qbittorrent/qBittorrent/pull/6156#issuecomment-316302615
class MacFileIconProvider final : public CachingFileIconProvider class MacFileIconProvider final : public CachingFileIconProvider
{ {
@ -145,7 +121,7 @@ namespace
return MacUtils::pixmapForExtension(ext, QSize(32, 32)); return MacUtils::pixmapForExtension(ext, QSize(32, 32));
} }
}; };
#else #elif !defined(Q_OS_WIN)
/** /**
* @brief Tests whether QFileIconProvider actually works * @brief Tests whether QFileIconProvider actually works
* *
@ -189,7 +165,7 @@ TorrentContentModel::TorrentContentModel(QObject *parent)
: QAbstractItemModel(parent) : QAbstractItemModel(parent)
, m_rootItem(new TorrentContentModelFolder(QVector<QString>({ tr("Name"), tr("Total Size"), tr("Progress"), tr("Download Priority"), tr("Remaining"), tr("Availability") }))) , m_rootItem(new TorrentContentModelFolder(QVector<QString>({ tr("Name"), tr("Total Size"), tr("Progress"), tr("Download Priority"), tr("Remaining"), tr("Availability") })))
#if defined(Q_OS_WIN) #if defined(Q_OS_WIN)
, m_fileIconProvider {new WinShellFileIconProvider} , m_fileIconProvider {new QFileIconProvider}
#elif defined(Q_OS_MACOS) #elif defined(Q_OS_MACOS)
, m_fileIconProvider {new MacFileIconProvider} , m_fileIconProvider {new MacFileIconProvider}
#else #else

View file

@ -147,10 +147,19 @@ void TorrentContentModelFolder::recalculateProgress()
tRemaining += child->remaining(); tRemaining += child->remaining();
} }
if (!isRootItem() && (tSize > 0)) if (!isRootItem())
{
if (tSize > 0)
{ {
m_progress = tProgress / tSize; m_progress = tProgress / tSize;
m_remaining = tRemaining; m_remaining = tRemaining;
}
else
{
m_progress = 1.0;
m_remaining = 0;
}
Q_ASSERT(m_progress <= 1.); Q_ASSERT(m_progress <= 1.);
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com> * Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -56,6 +56,19 @@
#include "gui/macutilities.h" #include "gui/macutilities.h"
#endif #endif
namespace
{
QList<QPersistentModelIndex> toPersistentIndexes(const QModelIndexList &indexes)
{
QList<QPersistentModelIndex> persistentIndexes;
persistentIndexes.reserve(indexes.size());
for (const QModelIndex &index : indexes)
persistentIndexes.emplaceBack(index);
return persistentIndexes;
}
}
TorrentContentWidget::TorrentContentWidget(QWidget *parent) TorrentContentWidget::TorrentContentWidget(QWidget *parent)
: QTreeView(parent) : QTreeView(parent)
{ {
@ -173,10 +186,20 @@ Path TorrentContentWidget::getItemPath(const QModelIndex &index) const
return path; return path;
} }
void TorrentContentWidget::setFilterPattern(const QString &patternText) void TorrentContentWidget::setFilterPattern(const QString &patternText, const FilterPatternFormat format)
{ {
const QString pattern = Utils::String::wildcardToRegexPattern(patternText); if (format == FilterPatternFormat::PlainText)
{
m_filterModel->setFilterFixedString(patternText);
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
}
else
{
const QString pattern = ((format == FilterPatternFormat::Regex)
? patternText : Utils::String::wildcardToRegexPattern(patternText));
m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
}
if (patternText.isEmpty()) if (patternText.isEmpty())
{ {
collapseAll(); collapseAll();
@ -219,9 +242,9 @@ void TorrentContentWidget::keyPressEvent(QKeyEvent *event)
const Qt::CheckState state = (static_cast<Qt::CheckState>(value.toInt()) == Qt::Checked) const Qt::CheckState state = (static_cast<Qt::CheckState>(value.toInt()) == Qt::Checked)
? Qt::Unchecked : Qt::Checked; ? Qt::Unchecked : Qt::Checked;
const QModelIndexList selection = selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME); const QList<QPersistentModelIndex> selection = toPersistentIndexes(selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME));
for (const QModelIndex &index : selection) for (const QPersistentModelIndex &index : selection)
model()->setData(index, state, Qt::CheckStateRole); model()->setData(index, state, Qt::CheckStateRole);
} }
@ -248,10 +271,10 @@ void TorrentContentWidget::renameSelectedFile()
void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority) void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority)
{ {
const QModelIndexList selectedRows = selectionModel()->selectedRows(0); const QList<QPersistentModelIndex> selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
for (const QModelIndex &index : selectedRows) for (const QPersistentModelIndex &index : selectedRows)
{ {
model()->setData(index.sibling(index.row(), Priority), static_cast<int>(priority)); model()->setData(index, static_cast<int>(priority));
} }
} }
@ -261,7 +284,7 @@ void TorrentContentWidget::applyPrioritiesByOrder()
// a download priority that will apply to each item. The number of groups depends on how // a download priority that will apply to each item. The number of groups depends on how
// many "download priority" are available to be assigned // many "download priority" are available to be assigned
const QModelIndexList selectedRows = selectionModel()->selectedRows(0); const QList<QPersistentModelIndex> selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
const qsizetype priorityGroups = 3; const qsizetype priorityGroups = 3;
const auto priorityGroupSize = std::max<qsizetype>((selectedRows.length() / priorityGroups), 1); const auto priorityGroupSize = std::max<qsizetype>((selectedRows.length() / priorityGroups), 1);
@ -283,8 +306,8 @@ void TorrentContentWidget::applyPrioritiesByOrder()
break; break;
} }
const QModelIndex &index = selectedRows[i]; const QPersistentModelIndex &index = selectedRows[i];
model()->setData(index.sibling(index.row(), Priority), static_cast<int>(priority)); model()->setData(index, static_cast<int>(priority));
} }
} }

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com> * Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com>
* *
* 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 "base/bittorrent/downloadpriority.h" #include "base/bittorrent/downloadpriority.h"
#include "base/pathfwd.h" #include "base/pathfwd.h"
#include "filterpatternformat.h"
class QShortcut; class QShortcut;
@ -92,7 +93,7 @@ public:
int getFileIndex(const QModelIndex &index) const; int getFileIndex(const QModelIndex &index) const;
Path getItemPath(const QModelIndex &index) const; Path getItemPath(const QModelIndex &index) const;
void setFilterPattern(const QString &patternText); void setFilterPattern(const QString &patternText, FilterPatternFormat format = FilterPatternFormat::Wildcards);
void checkAll(); void checkAll();
void checkNone(); void checkNone();

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-2024 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
@ -37,6 +37,7 @@
#include "base/global.h" #include "base/global.h"
#include "autoexpandabledialog.h" #include "autoexpandabledialog.h"
#include "flowlayout.h" #include "flowlayout.h"
#include "utils.h"
#include "ui_torrenttagsdialog.h" #include "ui_torrenttagsdialog.h"
@ -52,10 +53,10 @@ TorrentTagsDialog::TorrentTagsDialog(const TagSet &initialTags, QWidget *parent)
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *tagsLayout = new FlowLayout(m_ui->scrollArea); auto *tagsLayout = new FlowLayout(m_ui->scrollArea->widget());
for (const Tag &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags()))) for (const Tag &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags())))
{ {
auto *tagWidget = new QCheckBox(tag.toString()); auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
if (initialTags.contains(tag)) if (initialTags.contains(tag))
tagWidget->setChecked(true); tagWidget->setChecked(true);
tagsLayout->addWidget(tagWidget); tagsLayout->addWidget(tagWidget);
@ -78,12 +79,12 @@ TorrentTagsDialog::~TorrentTagsDialog()
TagSet TorrentTagsDialog::tags() const TagSet TorrentTagsDialog::tags() const
{ {
TagSet tags; TagSet tags;
auto *layout = m_ui->scrollArea->layout(); auto *layout = m_ui->scrollArea->widget()->layout();
for (int i = 0; i < (layout->count() - 1); ++i) for (int i = 0; i < (layout->count() - 1); ++i)
{ {
const auto *tagWidget = static_cast<QCheckBox *>(layout->itemAt(i)->widget()); const auto *tagWidget = static_cast<QCheckBox *>(layout->itemAt(i)->widget());
if (tagWidget->isChecked()) if (tagWidget->isChecked())
tags.insert(Tag(tagWidget->text())); tags.insert(Utils::Gui::widgetTextToTag(tagWidget->text()));
} }
return tags; return tags;
@ -111,9 +112,9 @@ void TorrentTagsDialog::addNewTag()
} }
else else
{ {
auto *layout = m_ui->scrollArea->layout(); auto *layout = m_ui->scrollArea->widget()->layout();
auto *btn = layout->takeAt(layout->count() - 1); auto *btn = layout->takeAt(layout->count() - 1);
auto *tagWidget = new QCheckBox(tag.toString()); auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
tagWidget->setChecked(true); tagWidget->setChecked(true);
layout->addWidget(tagWidget); layout->addWidget(tagWidget);
layout->addItem(btn); layout->addItem(btn);

View file

@ -488,11 +488,11 @@ QVariant TrackerListModel::headerData(const int section, const Qt::Orientation o
switch (section) switch (section)
{ {
case COL_URL: case COL_URL:
return tr("URL/Announce endpoint"); return tr("URL/Announce Endpoint");
case COL_TIER: case COL_TIER:
return tr("Tier"); return tr("Tier");
case COL_PROTOCOL: case COL_PROTOCOL:
return tr("Protocol"); return tr("BT Protocol");
case COL_STATUS: case COL_STATUS:
return tr("Status"); return tr("Status");
case COL_PEERS: case COL_PEERS:
@ -506,9 +506,9 @@ QVariant TrackerListModel::headerData(const int section, const Qt::Orientation o
case COL_MSG: case COL_MSG:
return tr("Message"); return tr("Message");
case COL_NEXT_ANNOUNCE: case COL_NEXT_ANNOUNCE:
return tr("Next announce"); return tr("Next Announce");
case COL_MIN_ANNOUNCE: case COL_MIN_ANNOUNCE:
return tr("Min announce"); return tr("Min Announce");
default: default:
return {}; return {};
} }
@ -585,7 +585,7 @@ QVariant TrackerListModel::data(const QModelIndex &index, const int role) const
case COL_TIER: case COL_TIER:
return (isEndpoint || (index.row() < STICKY_ROW_COUNT)) ? QString() : QString::number(itemPtr->tier); return (isEndpoint || (index.row() < STICKY_ROW_COUNT)) ? QString() : QString::number(itemPtr->tier);
case COL_PROTOCOL: case COL_PROTOCOL:
return isEndpoint ? tr("v%1").arg(itemPtr->btVersion) : QString(); return isEndpoint ? (u'v' + QString::number(itemPtr->btVersion)) : QString();
case COL_STATUS: case COL_STATUS:
if (isEndpoint) if (isEndpoint)
return toString(itemPtr->status); return toString(itemPtr->status);

View file

@ -39,7 +39,6 @@
#include <QUrl> #include <QUrl>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "base/algorithm.h"
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h" #include "base/bittorrent/torrent.h"
#include "base/bittorrent/trackerentrystatus.h" #include "base/bittorrent/trackerentrystatus.h"

View file

@ -193,6 +193,7 @@ QVariant TransferListModel::headerData(const int section, const Qt::Orientation
case TR_INFOHASH_V1: return tr("Info Hash v1", "i.e: torrent info hash v1"); case TR_INFOHASH_V1: return tr("Info Hash v1", "i.e: torrent info hash v1");
case TR_INFOHASH_V2: return tr("Info Hash v2", "i.e: torrent info hash v2"); case TR_INFOHASH_V2: return tr("Info Hash v2", "i.e: torrent info hash v2");
case TR_REANNOUNCE: return tr("Reannounce In", "Indicates the time until next trackers reannounce"); case TR_REANNOUNCE: return tr("Reannounce In", "Indicates the time until next trackers reannounce");
case TR_PRIVATE: return tr("Private", "Flags private torrents");
default: return {}; default: return {};
} }
} }
@ -357,6 +358,15 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
return Utils::Misc::userFriendlyDuration(time); return Utils::Misc::userFriendlyDuration(time);
}; };
const auto privateString = [hideValues](const bool isPrivate, const bool hasMetadata) -> QString
{
if (hideValues && !isPrivate)
return {};
if (hasMetadata)
return isPrivate ? tr("Yes") : tr("No");
return tr("N/A");
};
switch (column) switch (column)
{ {
case TR_NAME: case TR_NAME:
@ -431,6 +441,8 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
return hashString(torrent->infoHash().v2()); return hashString(torrent->infoHash().v2());
case TR_REANNOUNCE: case TR_REANNOUNCE:
return reannounceString(torrent->nextAnnounce()); return reannounceString(torrent->nextAnnounce());
case TR_PRIVATE:
return privateString(torrent->isPrivate(), torrent->hasMetadata());
} }
return {}; return {};
@ -512,6 +524,8 @@ QVariant TransferListModel::internalValue(const BitTorrent::Torrent *torrent, co
return QVariant::fromValue(torrent->infoHash().v2()); return QVariant::fromValue(torrent->infoHash().v2());
case TR_REANNOUNCE: case TR_REANNOUNCE:
return torrent->nextAnnounce(); return torrent->nextAnnounce();
case TR_PRIVATE:
return (torrent->hasMetadata() ? torrent->isPrivate() : QVariant());
} }
return {}; return {};

View file

@ -86,6 +86,7 @@ public:
TR_INFOHASH_V1, TR_INFOHASH_V1,
TR_INFOHASH_V2, TR_INFOHASH_V2,
TR_REANNOUNCE, TR_REANNOUNCE,
TR_PRIVATE,
NB_COLUMNS NB_COLUMNS
}; };

View file

@ -84,6 +84,17 @@ namespace
return isLeftValid ? -1 : 1; return isLeftValid ? -1 : 1;
} }
int compareAsBool(const QVariant &left, const QVariant &right)
{
const bool leftValid = left.isValid();
const bool rightValid = right.isValid();
if (leftValid && rightValid)
return threeWayCompare(left.toBool(), right.toBool());
if (!leftValid && !rightValid)
return 0;
return leftValid ? -1 : 1;
}
int adjustSubSortColumn(const int column) int adjustSubSortColumn(const int column)
{ {
return ((column >= 0) && (column < TransferListModel::NB_COLUMNS)) return ((column >= 0) && (column < TransferListModel::NB_COLUMNS))
@ -214,6 +225,9 @@ int TransferListSortModel::compare(const QModelIndex &left, const QModelIndex &r
case TransferListModel::TR_UPSPEED: case TransferListModel::TR_UPSPEED:
return customCompare(leftValue.toInt(), rightValue.toInt()); return customCompare(leftValue.toInt(), rightValue.toInt());
case TransferListModel::TR_PRIVATE:
return compareAsBool(leftValue, rightValue);
case TransferListModel::TR_PEERS: case TransferListModel::TR_PEERS:
case TransferListModel::TR_SEEDS: case TransferListModel::TR_SEEDS:
{ {

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-2024 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
@ -116,9 +116,10 @@ namespace
void removeTorrents(const QVector<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected) void removeTorrents(const QVector<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
{ {
auto *session = BitTorrent::Session::instance(); auto *session = BitTorrent::Session::instance();
const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent; const BitTorrent::TorrentRemoveOption removeOption = isDeleteFileSelected
? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
for (const BitTorrent::Torrent *torrent : torrents) for (const BitTorrent::Torrent *torrent : torrents)
session->deleteTorrent(torrent->id(), deleteOption); session->removeTorrent(torrent->id(), removeOption);
} }
} }
@ -183,6 +184,7 @@ TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow)
setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true); setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true); setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
setColumnHidden(TransferListModel::TR_REANNOUNCE, true); setColumnHidden(TransferListModel::TR_REANNOUNCE, true);
setColumnHidden(TransferListModel::TR_PRIVATE, true);
} }
//Ensure that at least one column is visible at all times //Ensure that at least one column is visible at all times
@ -442,7 +444,7 @@ void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
{ {
// Some torrents might be removed when waiting for user input, so refetch the torrent list // Some torrents might be removed when waiting for user input, so refetch the torrent list
// NOTE: this will only work when dialog is modal // NOTE: this will only work when dialog is modal
removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected()); removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected());
}); });
dialog->open(); dialog->open();
} }
@ -465,7 +467,7 @@ void TransferListWidget::deleteVisibleTorrents()
{ {
// Some torrents might be removed when waiting for user input, so refetch the torrent list // Some torrents might be removed when waiting for user input, so refetch the torrent list
// NOTE: this will only work when dialog is modal // NOTE: this will only work when dialog is modal
removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected()); removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected());
}); });
dialog->open(); dialog->open();
} }
@ -1190,7 +1192,7 @@ void TransferListWidget::displayListMenu()
const TagSet tags = BitTorrent::Session::instance()->tags(); const TagSet tags = BitTorrent::Session::instance()->tags();
for (const Tag &tag : asConst(tags)) for (const Tag &tag : asConst(tags))
{ {
auto *action = new TriStateAction(tag.toString(), tagsMenu); auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu);
action->setCloseOnInteraction(false); action->setCloseOnInteraction(false);
const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2017 Mike Tzou * Copyright (C) 2017 Mike Tzou
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -54,6 +55,7 @@
#include "base/global.h" #include "base/global.h"
#include "base/path.h" #include "base/path.h"
#include "base/tag.h"
#include "base/utils/fs.h" #include "base/utils/fs.h"
#include "base/utils/version.h" #include "base/utils/version.h"
@ -216,3 +218,29 @@ void Utils::Gui::openFolderSelect(const Path &path)
openPath(path.parentPath()); openPath(path.parentPath());
#endif #endif
} }
QString Utils::Gui::tagToWidgetText(const Tag &tag)
{
return tag.toString().replace(u'&', u"&&"_s);
}
Tag Utils::Gui::widgetTextToTag(const QString &text)
{
// replace pairs of '&' with single '&' and remove non-paired occurrences of '&'
QString cleanedText;
cleanedText.reserve(text.size());
bool amp = false;
for (const QChar c : text)
{
if (c == u'&')
{
amp = !amp;
if (amp)
continue;
}
cleanedText.append(c);
}
return Tag(cleanedText);
}

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2017 Mike Tzou * Copyright (C) 2017 Mike Tzou
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -34,8 +35,11 @@ class QIcon;
class QPixmap; class QPixmap;
class QPoint; class QPoint;
class QSize; class QSize;
class QString;
class QWidget; class QWidget;
class Tag;
namespace Utils::Gui namespace Utils::Gui
{ {
bool isDarkTheme(); bool isDarkTheme();
@ -51,4 +55,7 @@ namespace Utils::Gui
void openPath(const Path &path); void openPath(const Path &path);
void openFolderSelect(const Path &path); void openFolderSelect(const Path &path);
QString tagToWidgetText(const Tag &tag);
Tag widgetTextToTag(const QString &text);
} }

View file

@ -1,5 +1,12 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0"> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" manifestVersion="1.0">
<assemblyIdentity
name="org.qbittorrent.qBittorrent"
version="1.0.0.0"
processorArchitecture="*"
type="win32"
/>
<!-- Enable use of version 6 of the common controls (Win XP and later) --> <!-- Enable use of version 6 of the common controls (Win XP and later) -->
<dependency> <dependency>
<dependentAssembly> <dependentAssembly>
@ -28,6 +35,7 @@
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application> </application>
</compatibility> </compatibility>
<!-- Enable long paths that exceed MAX_PATH in length --> <!-- Enable long paths that exceed MAX_PATH in length -->
<asmv3:application> <asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings"> <asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">

View file

@ -1,4 +1,4 @@
#VERSION: 1.45 #VERSION: 1.47
# Author: # Author:
# Christophe DUMEZ (chris@qbittorrent.org) # Christophe DUMEZ (chris@qbittorrent.org)
@ -39,9 +39,11 @@ import tempfile
import urllib.error import urllib.error
import urllib.parse import urllib.parse
import urllib.request import urllib.request
from collections.abc import Mapping
from typing import Any, Dict, Optional
def getBrowserUserAgent(): def getBrowserUserAgent() -> str:
""" Disguise as browser to circumvent website blocking """ """ Disguise as browser to circumvent website blocking """
# Firefox release calendar # Firefox release calendar
@ -57,7 +59,7 @@ def getBrowserUserAgent():
return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{nowVersion}.0) Gecko/20100101 Firefox/{nowVersion}.0" return f"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:{nowVersion}.0) Gecko/20100101 Firefox/{nowVersion}.0"
headers = {'User-Agent': getBrowserUserAgent()} headers: Dict[str, Any] = {'User-Agent': getBrowserUserAgent()}
# SOCKS5 Proxy support # SOCKS5 Proxy support
if "sock_proxy" in os.environ and len(os.environ["sock_proxy"].strip()) > 0: if "sock_proxy" in os.environ and len(os.environ["sock_proxy"].strip()) > 0:
@ -67,13 +69,13 @@ if "sock_proxy" in os.environ and len(os.environ["sock_proxy"].strip()) > 0:
if m is not None: if m is not None:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'), socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'),
int(m.group('port')), True, m.group('username'), m.group('password')) int(m.group('port')), True, m.group('username'), m.group('password'))
socket.socket = socks.socksocket socket.socket = socks.socksocket # type: ignore[misc]
def htmlentitydecode(s): def htmlentitydecode(s: str) -> str:
# First convert alpha entities (such as &eacute;) # First convert alpha entities (such as &eacute;)
# (Inspired from http://mail.python.org/pipermail/python-list/2007-June/443813.html) # (Inspired from http://mail.python.org/pipermail/python-list/2007-June/443813.html)
def entity2char(m): def entity2char(m: re.Match[str]) -> str:
entity = m.group(1) entity = m.group(1)
if entity in html.entities.name2codepoint: if entity in html.entities.name2codepoint:
return chr(html.entities.name2codepoint[entity]) return chr(html.entities.name2codepoint[entity])
@ -87,15 +89,15 @@ def htmlentitydecode(s):
return re.sub(r'&#x(\w+);', lambda x: chr(int(x.group(1), 16)), t) return re.sub(r'&#x(\w+);', lambda x: chr(int(x.group(1), 16)), t)
def retrieve_url(url): def retrieve_url(url: str, custom_headers: Mapping[str, Any] = {}) -> str:
""" Return the content of the url page as a string """ """ Return the content of the url page as a string """
req = urllib.request.Request(url, headers=headers) req = urllib.request.Request(url, headers={**headers, **custom_headers})
try: try:
response = urllib.request.urlopen(req) response = urllib.request.urlopen(req)
except urllib.error.URLError as errno: except urllib.error.URLError as errno:
print(" ".join(("Connection error:", str(errno.reason)))) print(" ".join(("Connection error:", str(errno.reason))))
return "" return ""
dat = response.read() dat: bytes = response.read()
# Check if it is gzipped # Check if it is gzipped
if dat[:2] == b'\x1f\x8b': if dat[:2] == b'\x1f\x8b':
# Data is gzip encoded, decode it # Data is gzip encoded, decode it
@ -109,16 +111,15 @@ def retrieve_url(url):
ignore, charset = info['Content-Type'].split('charset=') ignore, charset = info['Content-Type'].split('charset=')
except Exception: except Exception:
pass pass
dat = dat.decode(charset, 'replace') datStr = dat.decode(charset, 'replace')
dat = htmlentitydecode(dat) datStr = htmlentitydecode(datStr)
# return dat.encode('utf-8', 'replace') return datStr
return dat
def download_file(url, referer=None): def download_file(url: str, referer: Optional[str] = None) -> str:
""" Download file at url and write it to a file, return the path to the file and the url """ """ Download file at url and write it to a file, return the path to the file and the url """
file, path = tempfile.mkstemp() fileHandle, path = tempfile.mkstemp()
file = os.fdopen(file, "wb") file = os.fdopen(fileHandle, "wb")
# Download url # Download url
req = urllib.request.Request(url, headers=headers) req = urllib.request.Request(url, headers=headers)
if referer is not None: if referer is not None:

View file

@ -1,4 +1,4 @@
#VERSION: 1.45 #VERSION: 1.46
# Author: # Author:
# Fabien Devaux <fab AT gnux DOT info> # Fabien Devaux <fab AT gnux DOT info>
@ -37,17 +37,21 @@ import importlib
import pathlib import pathlib
import sys import sys
import urllib.parse import urllib.parse
from collections.abc import Iterable, Iterator, Sequence
from enum import Enum
from glob import glob from glob import glob
from multiprocessing import Pool, cpu_count from multiprocessing import Pool, cpu_count
from os import path from os import path
from typing import Dict, List, Optional, Set, Tuple, Type
THREADED = True THREADED: bool = True
try: try:
MAX_THREADS = cpu_count() MAX_THREADS: int = cpu_count()
except NotImplementedError: except NotImplementedError:
MAX_THREADS = 1 MAX_THREADS = 1
CATEGORIES = {'all', 'movies', 'tv', 'music', 'games', 'anime', 'software', 'pictures', 'books'} Category = Enum('Category', ['all', 'movies', 'tv', 'music', 'games', 'anime', 'software', 'pictures', 'books'])
################################################################################ ################################################################################
# Every engine should have a "search" method taking # Every engine should have a "search" method taking
@ -58,11 +62,29 @@ CATEGORIES = {'all', 'movies', 'tv', 'music', 'games', 'anime', 'software', 'pic
################################################################################ ################################################################################
EngineName = str
class Engine:
url: str
name: EngineName
supported_categories: Dict[str, str]
def __init__(self) -> None:
pass
def search(self, what: str, cat: str = Category.all.name) -> None:
pass
def download_torrent(self, info: str) -> None:
pass
# global state # global state
engine_dict = dict() engine_dict: Dict[EngineName, Optional[Type[Engine]]] = {}
def list_engines(): def list_engines() -> List[EngineName]:
""" List all engines, """ List all engines,
including broken engines that fail on import including broken engines that fail on import
@ -81,10 +103,10 @@ def list_engines():
return found_engines return found_engines
def get_engine(engine_name): def get_engine(engine_name: EngineName) -> Optional[Type[Engine]]:
#global engine_dict
if engine_name in engine_dict: if engine_name in engine_dict:
return engine_dict[engine_name] return engine_dict[engine_name]
# when import fails, engine is None # when import fails, engine is None
engine = None engine = None
try: try:
@ -97,35 +119,37 @@ def get_engine(engine_name):
return engine return engine
def initialize_engines(found_engines): def initialize_engines(found_engines: Iterable[EngineName]) -> Set[EngineName]:
""" Import available engines """ Import available engines
Return list of available engines Return set of available engines
""" """
supported_engines = [] supported_engines = set()
for engine_name in found_engines: for engine_name in found_engines:
# import engine # import engine
engine = get_engine(engine_name) engine = get_engine(engine_name)
if engine is None: if engine is None:
continue continue
supported_engines.append(engine_name) supported_engines.add(engine_name)
return supported_engines return supported_engines
def engines_to_xml(supported_engines): def engines_to_xml(supported_engines: Iterable[EngineName]) -> Iterator[str]:
""" Generates xml for supported engines """ """ Generates xml for supported engines """
tab = " " * 4 tab = " " * 4
for engine_name in supported_engines: for engine_name in supported_engines:
search_engine = get_engine(engine_name) search_engine = get_engine(engine_name)
if search_engine is None:
continue
supported_categories = "" supported_categories = ""
if hasattr(search_engine, "supported_categories"): if hasattr(search_engine, "supported_categories"):
supported_categories = " ".join((key supported_categories = " ".join((key
for key in search_engine.supported_categories.keys() for key in search_engine.supported_categories.keys()
if key != "all")) if key != Category.all.name))
yield "".join((tab, "<", engine_name, ">\n", yield "".join((tab, "<", engine_name, ">\n",
tab, tab, "<name>", search_engine.name, "</name>\n", tab, tab, "<name>", search_engine.name, "</name>\n",
@ -134,7 +158,7 @@ def engines_to_xml(supported_engines):
tab, "</", engine_name, ">\n")) tab, "</", engine_name, ">\n"))
def displayCapabilities(supported_engines): def displayCapabilities(supported_engines: Iterable[EngineName]) -> None:
""" """
Display capabilities in XML format Display capabilities in XML format
<capabilities> <capabilities>
@ -151,21 +175,24 @@ def displayCapabilities(supported_engines):
print(xml) print(xml)
def run_search(engine_list): def run_search(engine_list: Tuple[Optional[Type[Engine]], str, Category]) -> bool:
""" Run search in engine """ Run search in engine
@param engine_list List with engine, query and category @param engine_list Tuple with engine, query and category
@retval False if any exceptions occurred @retval False if any exceptions occurred
@retval True otherwise @retval True otherwise
""" """
engine, what, cat = engine_list engine_class, what, cat = engine_list
if engine_class is None:
return False
try: try:
engine = engine() engine = engine_class()
# avoid exceptions due to invalid category # avoid exceptions due to invalid category
if hasattr(engine, 'supported_categories'): if hasattr(engine, 'supported_categories'):
if cat in engine.supported_categories: if cat.name in engine.supported_categories:
engine.search(what, cat) engine.search(what, cat.name)
else: else:
engine.search(what) engine.search(what)
@ -174,7 +201,7 @@ def run_search(engine_list):
return False return False
def main(args): def main(args: Sequence[str]) -> None:
# qbt tend to run this script in 'isolate mode' so append the current path manually # qbt tend to run this script in 'isolate mode' so append the current path manually
current_path = str(pathlib.Path(__file__).parent.resolve()) current_path = str(pathlib.Path(__file__).parent.resolve())
if current_path not in sys.path: if current_path not in sys.path:
@ -182,7 +209,7 @@ def main(args):
found_engines = list_engines() found_engines = list_engines()
def show_usage(): def show_usage() -> None:
print("./nova2.py all|engine1[,engine2]* <category> <keywords>", file=sys.stderr) print("./nova2.py all|engine1[,engine2]* <category> <keywords>", file=sys.stderr)
print("found engines: " + ','.join(found_engines), file=sys.stderr) print("found engines: " + ','.join(found_engines), file=sys.stderr)
print("to list available engines: ./nova2.py --capabilities [--names]", file=sys.stderr) print("to list available engines: ./nova2.py --capabilities [--names]", file=sys.stderr)
@ -190,7 +217,6 @@ def main(args):
if not args: if not args:
show_usage() show_usage()
sys.exit(1) sys.exit(1)
elif args[0] == "--capabilities": elif args[0] == "--capabilities":
supported_engines = initialize_engines(found_engines) supported_engines = initialize_engines(found_engines)
if "--names" in args: if "--names" in args:
@ -198,14 +224,14 @@ def main(args):
return return
displayCapabilities(supported_engines) displayCapabilities(supported_engines)
return return
elif len(args) < 3: elif len(args) < 3:
show_usage() show_usage()
sys.exit(1) sys.exit(1)
cat = args[1].lower() cat = args[1].lower()
try:
if cat not in CATEGORIES: category = Category[cat]
except KeyError:
print(" - ".join(('Invalid category', cat)), file=sys.stderr) print(" - ".join(('Invalid category', cat)), file=sys.stderr)
sys.exit(1) sys.exit(1)
@ -223,16 +249,18 @@ def main(args):
engines_list = initialize_engines(found_engines) engines_list = initialize_engines(found_engines)
else: else:
# discard not-found engines # discard not-found engines
engines_list = [engine for engine in engines_list if engine in found_engines] engines_list = {engine for engine in engines_list if engine in found_engines}
what = urllib.parse.quote(' '.join(args[2:])) what = urllib.parse.quote(' '.join(args[2:]))
params = ((get_engine(engine_name), what, category) for engine_name in engines_list)
if THREADED: if THREADED:
# child process spawning is controlled min(number of searches, number of cpu) # child process spawning is controlled min(number of searches, number of cpu)
with Pool(min(len(engines_list), MAX_THREADS)) as pool: with Pool(min(len(engines_list), MAX_THREADS)) as pool:
pool.map(run_search, ([get_engine(engine_name), what, cat] for engine_name in engines_list)) pool.map(run_search, params)
else: else:
# py3 note: map is needed to be evaluated for content to be executed # py3 note: map is needed to be evaluated for content to be executed
all(map(run_search, ([get_engine(engine_name), what, cat] for engine_name in engines_list))) all(map(run_search, params))
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -1,4 +1,4 @@
#VERSION: 1.48 #VERSION: 1.50
# Redistribution and use in source and binary forms, with or without # Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met: # modification, are permitted provided that the following conditions are met:
@ -24,8 +24,25 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE. # POSSIBILITY OF SUCH DAMAGE.
import re
from collections.abc import Mapping
from typing import Any, Union
def prettyPrinter(dictionary): # TODO: enable the following when using Python >= 3.8
#SearchResults = TypedDict('SearchResults', {
# 'link': str,
# 'name': str,
# 'size': Union[float, int, str],
# 'seeds': int,
# 'leech': int,
# 'engine_url': str,
# 'desc_link': str, # Optional # TODO: use `NotRequired[str]` when using Python >= 3.11
# 'pub_date': int # Optional # TODO: use `NotRequired[int]` when using Python >= 3.11
#})
SearchResults = Mapping[str, Any]
def prettyPrinter(dictionary: SearchResults) -> None:
outtext = "|".join(( outtext = "|".join((
dictionary["link"], dictionary["link"],
dictionary["name"].replace("|", " "), dictionary["name"].replace("|", " "),
@ -34,7 +51,7 @@ def prettyPrinter(dictionary):
str(dictionary["leech"]), str(dictionary["leech"]),
dictionary["engine_url"], dictionary["engine_url"],
dictionary.get("desc_link", ""), # Optional dictionary.get("desc_link", ""), # Optional
str(dictionary.get("pub_date", -1)), # Optional str(dictionary.get("pub_date", -1)) # Optional
)) ))
# fd 1 is stdout # fd 1 is stdout
@ -42,30 +59,32 @@ def prettyPrinter(dictionary):
print(outtext, file=utf8stdout) print(outtext, file=utf8stdout)
def anySizeToBytes(size_string): sizeUnitRegex: re.Pattern[str] = re.compile(r"^(?P<size>\d*\.?\d+) *(?P<unit>[a-z]+)?", re.IGNORECASE)
def anySizeToBytes(size_string: Union[float, int, str]) -> int:
""" """
Convert a string like '1 KB' to '1024' (bytes) Convert a string like '1 KB' to '1024' (bytes)
"""
# separate integer from unit
try:
size, unit = size_string.split()
except Exception:
try:
size = size_string.strip()
unit = ''.join([c for c in size if c.isalpha()])
if len(unit) > 0:
size = size[:-len(unit)]
except Exception:
return -1
if len(size) == 0:
return -1
size = float(size)
if len(unit) == 0:
return int(size)
short_unit = unit.upper()[0]
# convert The canonical type for `size_string` is `str`. However numeric types are also accepted in order to
units_dict = {'T': 40, 'G': 30, 'M': 20, 'K': 10} accommodate poorly written plugins.
if short_unit in units_dict: """
size = size * 2**units_dict[short_unit]
return int(size) if isinstance(size_string, int):
return size_string
if isinstance(size_string, float):
return round(size_string)
match = sizeUnitRegex.match(size_string.strip())
if match is None:
return -1
size = float(match.group('size')) # need to match decimals
unit = match.group('unit')
if unit is not None:
units_exponents = {'T': 40, 'G': 30, 'M': 20, 'K': 10}
exponent = units_exponents.get(unit[0].upper(), 0)
size *= 2**exponent
return round(size)

View file

@ -37,6 +37,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QDebug> #include <QDebug>
#include <QDir> #include <QDir>
#include <QDirIterator>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QJsonObject> #include <QJsonObject>
@ -135,7 +136,7 @@ void AppController::preferencesAction()
data[u"file_log_age"_s] = app()->fileLoggerAge(); data[u"file_log_age"_s] = app()->fileLoggerAge();
data[u"file_log_age_type"_s] = app()->fileLoggerAgeType(); data[u"file_log_age_type"_s] = app()->fileLoggerAgeType();
// Delete torrent contents files on torrent removal // Delete torrent contents files on torrent removal
data[u"delete_torrent_content_files"_s] = pref->deleteTorrentFilesAsDefault(); data[u"delete_torrent_content_files"_s] = pref->removeTorrentContent();
// Downloads // Downloads
// When adding a torrent // When adding a torrent
@ -349,6 +350,8 @@ void AppController::preferencesAction()
// qBitorrent preferences // qBitorrent preferences
// Resume data storage type // Resume data storage type
data[u"resume_data_storage_type"_s] = Utils::String::fromEnum(session->resumeDataStorageType()); data[u"resume_data_storage_type"_s] = Utils::String::fromEnum(session->resumeDataStorageType());
// Torrent content removing mode
data[u"torrent_content_remove_option"_s] = Utils::String::fromEnum(session->torrentContentRemoveOption());
// Physical memory (RAM) usage limit // Physical memory (RAM) usage limit
data[u"memory_working_set_limit"_s] = app()->memoryWorkingSetLimit(); data[u"memory_working_set_limit"_s] = app()->memoryWorkingSetLimit();
// Current network interface // Current network interface
@ -518,7 +521,7 @@ void AppController::setPreferencesAction()
app()->setFileLoggerAgeType(it.value().toInt()); app()->setFileLoggerAgeType(it.value().toInt());
// Delete torrent content files on torrent removal // Delete torrent content files on torrent removal
if (hasKey(u"delete_torrent_content_files"_s)) if (hasKey(u"delete_torrent_content_files"_s))
pref->setDeleteTorrentFilesAsDefault(it.value().toBool()); pref->setRemoveTorrentContent(it.value().toBool());
// Downloads // Downloads
// When adding a torrent // When adding a torrent
@ -930,6 +933,9 @@ void AppController::setPreferencesAction()
// Resume data storage type // Resume data storage type
if (hasKey(u"resume_data_storage_type"_s)) if (hasKey(u"resume_data_storage_type"_s))
session->setResumeDataStorageType(Utils::String::toEnum(it.value().toString(), BitTorrent::ResumeDataStorageType::Legacy)); session->setResumeDataStorageType(Utils::String::toEnum(it.value().toString(), BitTorrent::ResumeDataStorageType::Legacy));
// Torrent content removing mode
if (hasKey(u"torrent_content_remove_option"_s))
session->setTorrentContentRemoveOption(Utils::String::toEnum(it.value().toString(), BitTorrent::TorrentContentRemoveOption::MoveToTrash));
// Physical memory (RAM) usage limit // Physical memory (RAM) usage limit
if (hasKey(u"memory_working_set_limit"_s)) if (hasKey(u"memory_working_set_limit"_s))
app()->setMemoryWorkingSetLimit(it.value().toInt()); app()->setMemoryWorkingSetLimit(it.value().toInt());
@ -1159,8 +1165,11 @@ void AppController::getDirectoryContentAction()
throw APIError(APIErrorType::BadParams, tr("Invalid mode, allowed values: %1").arg(u"all, dirs, files"_s)); throw APIError(APIErrorType::BadParams, tr("Invalid mode, allowed values: %1").arg(u"all, dirs, files"_s));
}; };
const QStringList dirs = dir.entryList(QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility)); QJsonArray ret;
setResult(QJsonArray::fromStringList(dirs)); QDirIterator it {dirPath, (QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility))};
while (it.hasNext())
ret.append(it.next());
setResult(ret);
} }
void AppController::networkInterfaceListAction() void AppController::networkInterfaceListAction()

View file

@ -135,6 +135,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent)
{KEY_TORRENT_SAVE_PATH, torrent.savePath().toString()}, {KEY_TORRENT_SAVE_PATH, torrent.savePath().toString()},
{KEY_TORRENT_DOWNLOAD_PATH, torrent.downloadPath().toString()}, {KEY_TORRENT_DOWNLOAD_PATH, torrent.downloadPath().toString()},
{KEY_TORRENT_CONTENT_PATH, torrent.contentPath().toString()}, {KEY_TORRENT_CONTENT_PATH, torrent.contentPath().toString()},
{KEY_TORRENT_ROOT_PATH, torrent.rootPath().toString()},
{KEY_TORRENT_ADDED_ON, Utils::DateTime::toSecsSinceEpoch(torrent.addedTime())}, {KEY_TORRENT_ADDED_ON, Utils::DateTime::toSecsSinceEpoch(torrent.addedTime())},
{KEY_TORRENT_COMPLETION_ON, Utils::DateTime::toSecsSinceEpoch(torrent.completedTime())}, {KEY_TORRENT_COMPLETION_ON, Utils::DateTime::toSecsSinceEpoch(torrent.completedTime())},
{KEY_TORRENT_TRACKER, torrent.currentTracker()}, {KEY_TORRENT_TRACKER, torrent.currentTracker()},
@ -163,8 +164,8 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent)
{KEY_TORRENT_AVAILABILITY, torrent.distributedCopies()}, {KEY_TORRENT_AVAILABILITY, torrent.distributedCopies()},
{KEY_TORRENT_REANNOUNCE, torrent.nextAnnounce()}, {KEY_TORRENT_REANNOUNCE, torrent.nextAnnounce()},
{KEY_TORRENT_COMMENT, torrent.comment()}, {KEY_TORRENT_COMMENT, torrent.comment()},
{KEY_TORRENT_ISPRIVATE, torrent.isPrivate()}, {KEY_TORRENT_PRIVATE, (torrent.hasMetadata() ? torrent.isPrivate() : QVariant())},
{KEY_TORRENT_TOTAL_SIZE, torrent.totalSize()},
{KEY_TORRENT_TOTAL_SIZE, torrent.totalSize()} {KEY_TORRENT_HAS_METADATA, torrent.hasMetadata()}
}; };
} }

View file

@ -66,6 +66,7 @@ inline const QString KEY_TORRENT_FORCE_START = u"force_start"_s;
inline const QString KEY_TORRENT_SAVE_PATH = u"save_path"_s; inline const QString KEY_TORRENT_SAVE_PATH = u"save_path"_s;
inline const QString KEY_TORRENT_DOWNLOAD_PATH = u"download_path"_s; inline const QString KEY_TORRENT_DOWNLOAD_PATH = u"download_path"_s;
inline const QString KEY_TORRENT_CONTENT_PATH = u"content_path"_s; inline const QString KEY_TORRENT_CONTENT_PATH = u"content_path"_s;
inline const QString KEY_TORRENT_ROOT_PATH = u"root_path"_s;
inline const QString KEY_TORRENT_ADDED_ON = u"added_on"_s; inline const QString KEY_TORRENT_ADDED_ON = u"added_on"_s;
inline const QString KEY_TORRENT_COMPLETION_ON = u"completion_on"_s; inline const QString KEY_TORRENT_COMPLETION_ON = u"completion_on"_s;
inline const QString KEY_TORRENT_TRACKER = u"tracker"_s; inline const QString KEY_TORRENT_TRACKER = u"tracker"_s;
@ -93,6 +94,7 @@ inline const QString KEY_TORRENT_SEEDING_TIME = u"seeding_time"_s;
inline const QString KEY_TORRENT_AVAILABILITY = u"availability"_s; inline const QString KEY_TORRENT_AVAILABILITY = u"availability"_s;
inline const QString KEY_TORRENT_REANNOUNCE = u"reannounce"_s; inline const QString KEY_TORRENT_REANNOUNCE = u"reannounce"_s;
inline const QString KEY_TORRENT_COMMENT = u"comment"_s; inline const QString KEY_TORRENT_COMMENT = u"comment"_s;
inline const QString KEY_TORRENT_ISPRIVATE = u"is_private"_s; inline const QString KEY_TORRENT_PRIVATE = u"private"_s;
inline const QString KEY_TORRENT_HAS_METADATA = u"has_metadata"_s;
QVariantMap serialize(const BitTorrent::Torrent &torrent); QVariantMap serialize(const BitTorrent::Torrent &torrent);

View file

@ -222,6 +222,7 @@ namespace
case QMetaType::UInt: case QMetaType::UInt:
case QMetaType::QDateTime: case QMetaType::QDateTime:
case QMetaType::Nullptr: case QMetaType::Nullptr:
case QMetaType::UnknownType:
if (prevData[key] != value) if (prevData[key] != value)
syncData[key] = value; syncData[key] = value;
break; break;

View file

@ -111,10 +111,13 @@ const QString KEY_PROP_CREATION_DATE = u"creation_date"_s;
const QString KEY_PROP_SAVE_PATH = u"save_path"_s; const QString KEY_PROP_SAVE_PATH = u"save_path"_s;
const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_s; const QString KEY_PROP_DOWNLOAD_PATH = u"download_path"_s;
const QString KEY_PROP_COMMENT = u"comment"_s; const QString KEY_PROP_COMMENT = u"comment"_s;
const QString KEY_PROP_ISPRIVATE = u"is_private"_s; const QString KEY_PROP_IS_PRIVATE = u"is_private"_s; // deprecated, "private" should be used instead
const QString KEY_PROP_PRIVATE = u"private"_s;
const QString KEY_PROP_SSL_CERTIFICATE = u"ssl_certificate"_s; const QString KEY_PROP_SSL_CERTIFICATE = u"ssl_certificate"_s;
const QString KEY_PROP_SSL_PRIVATEKEY = u"ssl_private_key"_s; const QString KEY_PROP_SSL_PRIVATEKEY = u"ssl_private_key"_s;
const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s; const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s;
const QString KEY_PROP_HAS_METADATA = u"has_metadata"_s;
// File keys // File keys
const QString KEY_FILE_INDEX = u"index"_s; const QString KEY_FILE_INDEX = u"index"_s;
@ -282,6 +285,7 @@ void TorrentsController::countAction()
// - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category") // - category (string): torrent category for filtering by it (empty string means "uncategorized"; no "category" param presented means "any category")
// - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag") // - tag (string): torrent tag for filtering by it (empty string means "untagged"; no "tag" param presented means "any tag")
// - hashes (string): filter by hashes, can contain multiple hashes separated by | // - hashes (string): filter by hashes, can contain multiple hashes separated by |
// - private (bool): filter torrents that are from private trackers (true) or not (false). Empty means any torrent (no filtering)
// - sort (string): name of column for sorting by its value // - sort (string): name of column for sorting by its value
// - reverse (bool): enable reverse sorting // - reverse (bool): enable reverse sorting
// - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited) // - limit (int): set limit number of torrents returned (if greater than 0, otherwise - unlimited)
@ -296,6 +300,7 @@ void TorrentsController::infoAction()
int limit {params()[u"limit"_s].toInt()}; int limit {params()[u"limit"_s].toInt()};
int offset {params()[u"offset"_s].toInt()}; int offset {params()[u"offset"_s].toInt()};
const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)}; const QStringList hashes {params()[u"hashes"_s].split(u'|', Qt::SkipEmptyParts)};
const std::optional<bool> isPrivate = parseBool(params()[u"private"_s]);
std::optional<TorrentIDSet> idSet; std::optional<TorrentIDSet> idSet;
if (!hashes.isEmpty()) if (!hashes.isEmpty())
@ -305,7 +310,7 @@ void TorrentsController::infoAction()
idSet->insert(BitTorrent::TorrentID::fromString(hash)); idSet->insert(BitTorrent::TorrentID::fromString(hash));
} }
const TorrentFilter torrentFilter {filter, idSet, category, tag}; const TorrentFilter torrentFilter {filter, idSet, category, tag, isPrivate};
QVariantList torrentList; QVariantList torrentList;
for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents())) for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
{ {
@ -435,6 +440,8 @@ void TorrentsController::propertiesAction()
const int uploadLimit = torrent->uploadLimit(); const int uploadLimit = torrent->uploadLimit();
const qreal ratio = torrent->realRatio(); const qreal ratio = torrent->realRatio();
const qreal popularity = torrent->popularity(); const qreal popularity = torrent->popularity();
const bool hasMetadata = torrent->hasMetadata();
const bool isPrivate = torrent->isPrivate();
const QJsonObject ret const QJsonObject ret
{ {
@ -470,14 +477,16 @@ void TorrentsController::propertiesAction()
{KEY_PROP_PIECE_SIZE, torrent->pieceLength()}, {KEY_PROP_PIECE_SIZE, torrent->pieceLength()},
{KEY_PROP_PIECES_HAVE, torrent->piecesHave()}, {KEY_PROP_PIECES_HAVE, torrent->piecesHave()},
{KEY_PROP_CREATED_BY, torrent->creator()}, {KEY_PROP_CREATED_BY, torrent->creator()},
{KEY_PROP_ISPRIVATE, torrent->isPrivate()}, {KEY_PROP_IS_PRIVATE, torrent->isPrivate()}, // used for maintaining backward compatibility
{KEY_PROP_PRIVATE, (hasMetadata ? isPrivate : QJsonValue())},
{KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())}, {KEY_PROP_ADDITION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->addedTime())},
{KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())}, {KEY_PROP_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())},
{KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())}, {KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())},
{KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())}, {KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())},
{KEY_PROP_SAVE_PATH, torrent->savePath().toString()}, {KEY_PROP_SAVE_PATH, torrent->savePath().toString()},
{KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()}, {KEY_PROP_DOWNLOAD_PATH, torrent->downloadPath().toString()},
{KEY_PROP_COMMENT, torrent->comment()} {KEY_PROP_COMMENT, torrent->comment()},
{KEY_PROP_HAS_METADATA, torrent->hasMetadata()}
}; };
setResult(ret); setResult(ret);
@ -1092,11 +1101,11 @@ void TorrentsController::deleteAction()
requireParams({u"hashes"_s, u"deleteFiles"_s}); requireParams({u"hashes"_s, u"deleteFiles"_s});
const QStringList hashes {params()[u"hashes"_s].split(u'|')}; const QStringList hashes {params()[u"hashes"_s].split(u'|')};
const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false) const BitTorrent::TorrentRemoveOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false)
? DeleteTorrentAndFiles : DeleteTorrent; ? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent) applyToTorrents(hashes, [deleteOption](const BitTorrent::Torrent *torrent)
{ {
BitTorrent::Session::instance()->deleteTorrent(torrent->id(), deleteOption); BitTorrent::Session::instance()->removeTorrent(torrent->id(), deleteOption);
}); });
} }

View file

@ -737,16 +737,15 @@ void WebApplication::sessionStart()
connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace); connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace);
m_currentSession->registerAPIController(u"sync"_s, syncController); m_currentSession->registerAPIController(u"sync"_s, syncController);
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toUtf8()}; QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
cookie.setHttpOnly(true); cookie.setHttpOnly(true);
cookie.setSecure(m_isSecureCookieEnabled && m_isHttpsEnabled); cookie.setSecure(m_isSecureCookieEnabled && m_isHttpsEnabled);
cookie.setPath(u"/"_s); cookie.setPath(u"/"_s);
QByteArray cookieRawForm = cookie.toRawForm();
if (m_isCSRFProtectionEnabled) if (m_isCSRFProtectionEnabled)
cookieRawForm.append("; SameSite=Strict"); cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
else if (cookie.isSecure()) else if (cookie.isSecure())
cookieRawForm.append("; SameSite=None"); cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookieRawForm)}); setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
} }
void WebApplication::sessionEnd() void WebApplication::sessionEnd()

View file

@ -54,7 +54,7 @@
#include "base/utils/version.h" #include "base/utils/version.h"
#include "api/isessionmanager.h" #include "api/isessionmanager.h"
inline const Utils::Version<3, 2> API_VERSION {2, 11, 0}; inline const Utils::Version<3, 2> API_VERSION {2, 11, 2};
class QTimer; class QTimer;

View file

@ -1,8 +1,8 @@
import Globals from 'globals'; import Globals from "globals";
import Html from 'eslint-plugin-html'; import Html from "eslint-plugin-html";
import Js from '@eslint/js'; import Js from "@eslint/js";
import Stylistic from '@stylistic/eslint-plugin'; import Stylistic from "@stylistic/eslint-plugin";
import * as RegexpPlugin from 'eslint-plugin-regexp'; import * as RegexpPlugin from "eslint-plugin-regexp";
export default [ export default [
Js.configs.recommended, Js.configs.recommended,
@ -26,9 +26,16 @@ export default [
Stylistic Stylistic
}, },
rules: { rules: {
"curly": ["error", "multi-or-nest", "consistent"],
"eqeqeq": "error", "eqeqeq": "error",
"guard-for-in": "error",
"no-undef": "off", "no-undef": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"no-var": "error",
"operator-assignment": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"radix": "error",
"Stylistic/no-mixed-operators": [ "Stylistic/no-mixed-operators": [
"error", "error",
{ {
@ -38,7 +45,16 @@ export default [
} }
], ],
"Stylistic/nonblock-statement-body-position": ["error", "below"], "Stylistic/nonblock-statement-body-position": ["error", "below"],
"Stylistic/semi": "error" "Stylistic/quotes": [
"error",
"double",
{
"avoidEscape": true,
"allowTemplateLiterals": true
}
],
"Stylistic/semi": "error",
"Stylistic/spaced-comment": ["error", "always", { "exceptions": ["*"] }]
} }
} }
]; ];

View file

@ -6,8 +6,8 @@
"url": "https://github.com/qbittorrent/qBittorrent.git" "url": "https://github.com/qbittorrent/qBittorrent.git"
}, },
"scripts": { "scripts": {
"format": "js-beautify -r private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css", "format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css",
"lint": "eslint private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public" "lint": "eslint *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "*", "@stylistic/eslint-plugin": "*",

View file

@ -8,42 +8,42 @@
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', function() { window.addEvent("domready", () => {
const hash = new URI().getData('hash'); const hash = new URI().getData("hash");
if (!hash) if (!hash)
return false; return false;
$('peers').focus(); $("peers").focus();
$('addPeersOk').addEvent('click', function(e) { $("addPeersOk").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
const peers = $('peers').get('value').trim().split(/[\r\n]+/); const peers = $("peers").get("value").trim().split(/[\r\n]+/);
if (peers.length === 0) if (peers.length === 0)
return; return;
new Request({ new Request({
url: 'api/v2/torrents/addPeers', url: "api/v2/torrents/addPeers",
method: 'post', method: "post",
data: { data: {
hashes: hash, hashes: hash,
peers: peers.join('|') peers: peers.join("|")
}, },
onFailure: function() { onFailure: function() {
alert("QBT_TR(Unable to add peers. Please ensure you are adhering to the IP:port format.)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Unable to add peers. Please ensure you are adhering to the IP:port format.)QBT_TR[CONTEXT=HttpServer]");

View file

@ -8,33 +8,33 @@
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
window.addEvent('domready', function() { window.addEvent("domready", () => {
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
$('trackersUrls').focus(); $("trackersUrls").focus();
$('addTrackersButton').addEvent('click', function(e) { $("addTrackersButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
const hash = new URI().getData('hash'); const hash = new URI().getData("hash");
new Request({ new Request({
url: 'api/v2/torrents/addTrackers', url: "api/v2/torrents/addTrackers",
method: 'post', method: "post",
data: { data: {
hash: hash, hash: hash,
urls: $('trackersUrls').value urls: $("trackersUrls").value
}, },
onComplete: function() { onComplete: function() {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();

View file

@ -8,73 +8,73 @@
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
function setRememberBtnEnabled(enable) { function setRememberBtnEnabled(enable) {
const btn = $('rememberBtn'); const btn = $("rememberBtn");
btn.disabled = !enable; btn.disabled = !enable;
const icon = btn.getElementsByTagName('path')[0]; const icon = btn.getElementsByTagName("path")[0];
if (enable) if (enable)
icon.style.removeProperty('fill'); icon.style.removeProperty("fill");
else else
icon.style.fill = "var(--color-border-default)"; icon.style.fill = "var(--color-border-default)";
} }
window.addEvent('domready', function() { window.addEvent("domready", () => {
new Request({ new Request({
url: 'images/object-locked.svg', url: "images/object-locked.svg",
method: 'get', method: "get",
onSuccess: function(text, xml) { onSuccess: function(text, xml) {
const newIcon = xml.childNodes[0]; const newIcon = xml.childNodes[0];
newIcon.style.height = '24px'; newIcon.style.height = "24px";
newIcon.style.width = '24px'; newIcon.style.width = "24px";
$('rememberBtn').appendChild(newIcon); $("rememberBtn").appendChild(newIcon);
setRememberBtnEnabled(false); setRememberBtnEnabled(false);
} }
}).send(); }).send();
const isDeletingFiles = (new URI().getData('deleteFiles') === "true"); const isDeletingFiles = (new URI().getData("deleteFiles") === "true");
$('deleteFromDiskCB').checked = isDeletingFiles; $("deleteFromDiskCB").checked = isDeletingFiles;
const prefCache = window.parent.qBittorrent.Cache.preferences.get(); const prefCache = window.parent.qBittorrent.Cache.preferences.get();
let prefDeleteContentFiles = prefCache.delete_torrent_content_files; let prefDeleteContentFiles = prefCache.delete_torrent_content_files;
$('deleteFromDiskCB').checked ||= prefDeleteContentFiles; $("deleteFromDiskCB").checked ||= prefDeleteContentFiles;
$('deleteFromDiskCB').addEvent('click', function(e) { $("deleteFromDiskCB").addEvent("click", (e) => {
setRememberBtnEnabled($('deleteFromDiskCB').checked !== prefDeleteContentFiles); setRememberBtnEnabled($("deleteFromDiskCB").checked !== prefDeleteContentFiles);
}); });
// Set current "Delete files" choice as the default // Set current "Delete files" choice as the default
$('rememberBtn').addEvent('click', function(e) { $("rememberBtn").addEvent("click", (e) => {
window.parent.qBittorrent.Cache.preferences.set({ window.parent.qBittorrent.Cache.preferences.set({
data: { data: {
'delete_torrent_content_files': $('deleteFromDiskCB').checked "delete_torrent_content_files": $("deleteFromDiskCB").checked
}, },
onSuccess: function() { onSuccess: function() {
prefDeleteContentFiles = $('deleteFromDiskCB').checked; prefDeleteContentFiles = $("deleteFromDiskCB").checked;
setRememberBtnEnabled(false); setRememberBtnEnabled(false);
} }
}); });
}); });
const hashes = new URI().getData('hashes').split('|'); const hashes = new URI().getData("hashes").split("|");
$('cancelBtn').focus(); $("cancelBtn").focus();
$('cancelBtn').addEvent('click', function(e) { $("cancelBtn").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
}); });
$('confirmBtn').addEvent('click', function(e) { $("confirmBtn").addEvent("click", (e) => {
parent.torrentsTable.deselectAll(); parent.torrentsTable.deselectAll();
new Event(e).stop(); new Event(e).stop();
const cmd = 'api/v2/torrents/delete'; const cmd = "api/v2/torrents/delete";
const deleteFiles = $('deleteFromDiskCB').get('checked'); const deleteFiles = $("deleteFromDiskCB").get("checked");
new Request({ new Request({
url: cmd, url: cmd,
method: 'post', method: "post",
data: { data: {
'hashes': hashes.join('|'), "hashes": hashes.join("|"),
'deleteFiles': deleteFiles "deleteFiles": deleteFiles
}, },
onComplete: function() { onComplete: function() {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
@ -91,7 +91,7 @@
<p>&nbsp;&nbsp;QBT_TR(Are you sure you want to remove the selected torrents from the transfer list?)QBT_TR[CONTEXT=HttpServer]</p> <p>&nbsp;&nbsp;QBT_TR(Are you sure you want to remove the selected torrents from the transfer list?)QBT_TR[CONTEXT=HttpServer]</p>
&nbsp;&nbsp;&nbsp;&nbsp;<button id="rememberBtn" type="button" title="Remember choice" style="vertical-align: middle; padding: 4px 6px;"> &nbsp;&nbsp;&nbsp;&nbsp;<button id="rememberBtn" type="button" title="Remember choice" style="vertical-align: middle; padding: 4px 6px;">
</button> </button>
<input type="checkbox" id="deleteFromDiskCB" /> <label for="deleteFromDiskCB"><i>QBT_TR(Also permanently delete the files)QBT_TR[CONTEXT=confirmDeletionDlg]</i></label><br /><br /> <input type="checkbox" id="deleteFromDiskCB" /> <label for="deleteFromDiskCB"><i>QBT_TR(Also remove the content files)QBT_TR[CONTEXT=confirmDeletionDlg]</i></label><br /><br />
<div style="text-align: right;"> <div style="text-align: right;">
<input type="button" id="cancelBtn" value="QBT_TR(Cancel)QBT_TR[CONTEXT=MainWindow]" />&nbsp;&nbsp;<input type="button" id="confirmBtn" value="QBT_TR(Remove)QBT_TR[CONTEXT=MainWindow]" />&nbsp;&nbsp; <input type="button" id="cancelBtn" value="QBT_TR(Cancel)QBT_TR[CONTEXT=MainWindow]" />&nbsp;&nbsp;<input type="button" id="confirmBtn" value="QBT_TR(Remove)QBT_TR[CONTEXT=MainWindow]" />&nbsp;&nbsp;
</div> </div>

View file

@ -8,22 +8,22 @@
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
window.addEvent('domready', () => { window.addEvent("domready", () => {
const paths = new URI().getData('paths').split('|'); const paths = new URI().getData("paths").split("|");
$('cancelBtn').focus(); $("cancelBtn").focus();
$('cancelBtn').addEvent('click', (e) => { $("cancelBtn").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
}); });
$('confirmBtn').addEvent('click', (e) => { $("confirmBtn").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
let completionCount = 0; let completionCount = 0;
paths.forEach((path) => { paths.forEach((path) => {
new Request({ new Request({
url: 'api/v2/rss/removeItem', url: "api/v2/rss/removeItem",
method: 'post', method: "post",
data: { data: {
path: decodeURIComponent(path) path: decodeURIComponent(path)
}, },

View file

@ -8,25 +8,25 @@
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
window.addEvent('domready', () => { window.addEvent("domready", () => {
const rules = new URI().getData('rules').split('|'); const rules = new URI().getData("rules").split("|");
$('cancelBtn').focus(); $("cancelBtn").focus();
$('cancelBtn').addEvent('click', (e) => { $("cancelBtn").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
window.parent.MochaUI.closeWindow(window.parent.$('clearRulesPage')); window.parent.MochaUI.closeWindow(window.parent.$("clearRulesPage"));
}); });
$('confirmBtn').addEvent('click', (e) => { $("confirmBtn").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
let completionCount = 0; let completionCount = 0;
rules.forEach((rule) => { rules.forEach((rule) => {
window.parent.qBittorrent.RssDownloader.modifyRuleState(decodeURIComponent(rule), 'previouslyMatchedEpisodes', [], () => { window.parent.qBittorrent.RssDownloader.modifyRuleState(decodeURIComponent(rule), "previouslyMatchedEpisodes", [], () => {
++completionCount; ++completionCount;
if (completionCount === rules.length) { if (completionCount === rules.length) {
window.parent.qBittorrent.RssDownloader.updateRulesList(); window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('clearRulesPage')); window.parent.MochaUI.closeWindow(window.parent.$("clearRulesPage"));
} }
}); });
}); });

View file

@ -8,23 +8,23 @@
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
window.addEvent('domready', () => { window.addEvent("domready", () => {
const rules = new URI().getData('rules').split('|'); const rules = new URI().getData("rules").split("|");
$('cancelBtn').focus(); $("cancelBtn").focus();
$('cancelBtn').addEvent('click', (e) => { $("cancelBtn").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
window.parent.MochaUI.closeWindow(window.parent.$('removeRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("removeRulePage"));
}); });
$('confirmBtn').addEvent('click', (e) => { $("confirmBtn").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
let completionCount = 0; let completionCount = 0;
rules.forEach((rule) => { rules.forEach((rule) => {
new Request({ new Request({
url: 'api/v2/rss/removeRule', url: "api/v2/rss/removeRule",
method: 'post', method: "post",
data: { data: {
ruleName: decodeURIComponent(rule) ruleName: decodeURIComponent(rule)
}, },
@ -32,7 +32,7 @@
++completionCount; ++completionCount;
if (completionCount === rules.length) { if (completionCount === rules.length) {
window.parent.qBittorrent.RssDownloader.updateRulesList(); window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('removeRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("removeRulePage"));
} }
} }
}).send(); }).send();

View file

@ -163,31 +163,31 @@
<div id="download_spinner" class="mochaSpinner"></div> <div id="download_spinner" class="mochaSpinner"></div>
<script> <script>
'use strict'; "use strict";
const encodedUrls = new URI().getData('urls'); const encodedUrls = new URI().getData("urls");
if (encodedUrls) { if (encodedUrls) {
const urls = encodedUrls.split('|').map(function(url) { const urls = encodedUrls.split("|").map((url) => {
return decodeURIComponent(url); return decodeURIComponent(url);
}); });
if (urls.length) if (urls.length)
$('urls').set('value', urls.join("\n")); $("urls").set("value", urls.join("\n"));
} }
let submitted = false; let submitted = false;
$('downloadForm').addEventListener("submit", function() { $("downloadForm").addEventListener("submit", () => {
$('startTorrentHidden').value = $('startTorrent').checked ? 'false' : 'true'; $("startTorrentHidden").value = $("startTorrent").checked ? "false" : "true";
$('dlLimitHidden').value = $('dlLimitText').value.toInt() * 1024; $("dlLimitHidden").value = $("dlLimitText").value.toInt() * 1024;
$('upLimitHidden').value = $('upLimitText').value.toInt() * 1024; $("upLimitHidden").value = $("upLimitText").value.toInt() * 1024;
$('download_spinner').style.display = "block"; $("download_spinner").style.display = "block";
submitted = true; submitted = true;
}); });
$('download_frame').addEventListener("load", function() { $("download_frame").addEventListener("load", () => {
if (submitted) if (submitted)
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
}); });

View file

@ -25,17 +25,17 @@
</div> </div>
<script> <script>
'use strict'; "use strict";
const hashes = new URI().getData('hashes').split('|'); const hashes = new URI().getData("hashes").split("|");
const setDlLimit = function() { const setDlLimit = function() {
const limit = $("dllimitUpdatevalue").value.toInt() * 1024; const limit = $("dllimitUpdatevalue").value.toInt() * 1024;
if (hashes[0] === "global") { if (hashes[0] === "global") {
new Request({ new Request({
url: 'api/v2/transfer/setDownloadLimit', url: "api/v2/transfer/setDownloadLimit",
method: 'post', method: "post",
data: { data: {
'limit': limit "limit": limit
}, },
onComplete: function() { onComplete: function() {
window.parent.updateMainData(); window.parent.updateMainData();
@ -45,11 +45,11 @@
} }
else { else {
new Request({ new Request({
url: 'api/v2/torrents/setDownloadLimit', url: "api/v2/torrents/setDownloadLimit",
method: 'post', method: "post",
data: { data: {
'hashes': hashes.join('|'), "hashes": hashes.join("|"),
'limit': limit "limit": limit
}, },
onComplete: function() { onComplete: function() {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
@ -59,24 +59,24 @@
}; };
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': function(event) { "Enter": function(event) {
$('applyButton').click(); $("applyButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
$('dllimitUpdatevalue').focus(); $("dllimitUpdatevalue").focus();
MochaUI.addDlLimitSlider(hashes); MochaUI.addDlLimitSlider(hashes);
</script> </script>

View file

@ -8,44 +8,44 @@
<script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
window.addEvent('domready', function() { window.addEvent("domready", () => {
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': function(event) { "Enter": function(event) {
$('editTrackerButton').click(); $("editTrackerButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
const currentUrl = new URI().getData('url'); const currentUrl = new URI().getData("url");
if (!currentUrl) if (!currentUrl)
return false; return false;
$('trackerUrl').value = currentUrl; $("trackerUrl").value = currentUrl;
$('trackerUrl').focus(); $("trackerUrl").focus();
$('editTrackerButton').addEvent('click', function(e) { $("editTrackerButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
const hash = new URI().getData('hash'); const hash = new URI().getData("hash");
new Request({ new Request({
url: 'api/v2/torrents/editTracker', url: "api/v2/torrents/editTracker",
method: 'post', method: "post",
data: { data: {
hash: hash, hash: hash,
origUrl: currentUrl, origUrl: currentUrl,
newUrl: $('trackerUrl').value newUrl: $("trackerUrl").value
}, },
onComplete: function() { onComplete: function() {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();

View file

@ -9,54 +9,54 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': function(event) { "Enter": function(event) {
$('categoryNameButton').click(); $("categoryNameButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', function() { window.addEvent("domready", () => {
const uriAction = window.qBittorrent.Misc.safeTrim(new URI().getData('action')); const uriAction = window.qBittorrent.Misc.safeTrim(new URI().getData("action"));
const uriHashes = window.qBittorrent.Misc.safeTrim(new URI().getData('hashes')); const uriHashes = window.qBittorrent.Misc.safeTrim(new URI().getData("hashes"));
const uriCategoryName = window.qBittorrent.Misc.safeTrim(new URI().getData('categoryName')); const uriCategoryName = window.qBittorrent.Misc.safeTrim(new URI().getData("categoryName"));
const uriSavePath = window.qBittorrent.Misc.safeTrim(new URI().getData('savePath')); const uriSavePath = window.qBittorrent.Misc.safeTrim(new URI().getData("savePath"));
if (uriAction === "edit") { if (uriAction === "edit") {
if (!uriCategoryName) if (!uriCategoryName)
return false; return false;
$('categoryName').set('disabled', true); $("categoryName").set("disabled", true);
$('categoryName').set('value', window.qBittorrent.Misc.escapeHtml(uriCategoryName)); $("categoryName").set("value", window.qBittorrent.Misc.escapeHtml(uriCategoryName));
$('savePath').set('value', window.qBittorrent.Misc.escapeHtml(uriSavePath)); $("savePath").set("value", window.qBittorrent.Misc.escapeHtml(uriSavePath));
$('savePath').focus(); $("savePath").focus();
} }
else if (uriAction === "createSubcategory") { else if (uriAction === "createSubcategory") {
$('categoryName').set('value', window.qBittorrent.Misc.escapeHtml(uriCategoryName)); $("categoryName").set("value", window.qBittorrent.Misc.escapeHtml(uriCategoryName));
$('categoryName').focus(); $("categoryName").focus();
} }
else { else {
$('categoryName').focus(); $("categoryName").focus();
} }
$('categoryNameButton').addEvent('click', function(e) { $("categoryNameButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
const savePath = $('savePath').value.trim(); const savePath = $("savePath").value.trim();
const categoryName = $('categoryName').value.trim(); const categoryName = $("categoryName").value.trim();
const verifyCategoryName = function(name) { const verifyCategoryName = function(name) {
if ((name === null) || (name === "")) if ((name === null) || (name === ""))
@ -74,16 +74,16 @@
return; return;
new Request({ new Request({
url: 'api/v2/torrents/createCategory', url: "api/v2/torrents/createCategory",
method: 'post', method: "post",
data: { data: {
category: categoryName, category: categoryName,
savePath: savePath savePath: savePath
}, },
onSuccess: function() { onSuccess: function() {
new Request({ new Request({
url: 'api/v2/torrents/setCategory', url: "api/v2/torrents/setCategory",
method: 'post', method: "post",
data: { data: {
hashes: uriHashes, hashes: uriHashes,
category: categoryName category: categoryName
@ -104,8 +104,8 @@
return; return;
new Request({ new Request({
url: 'api/v2/torrents/createCategory', url: "api/v2/torrents/createCategory",
method: 'post', method: "post",
data: { data: {
category: categoryName, category: categoryName,
savePath: savePath savePath: savePath
@ -117,8 +117,8 @@
break; break;
case "edit": case "edit":
new Request({ new Request({
url: 'api/v2/torrents/editCategory', url: "api/v2/torrents/editCategory",
method: 'post', method: "post",
data: { data: {
category: uriCategoryName, // category name can't be changed category: uriCategoryName, // category name can't be changed
savePath: savePath savePath: savePath

View file

@ -9,45 +9,45 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': (event) => { "Enter": (event) => {
$('submitButton').click(); $("submitButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': (event) => { "Escape": (event) => {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': (event) => { "Esc": (event) => {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', () => { window.addEvent("domready", () => {
$('feedURL').focus(); $("feedURL").focus();
const path = new URI().getData('path'); const path = new URI().getData("path");
$('submitButton').addEvent('click', (e) => { $("submitButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
// check field // check field
const feedURL = $('feedURL').value.trim(); const feedURL = $("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; $("submitButton").disabled = true;
new Request({ new Request({
url: 'api/v2/rss/addFeed', url: "api/v2/rss/addFeed",
method: 'post', method: "post",
data: { data: {
url: feedURL, url: feedURL,
path: path ? (path + '\\' + feedURL) : '' path: path ? (path + "\\" + feedURL) : ""
}, },
onSuccess: (response) => { onSuccess: (response) => {
window.parent.qBittorrent.Rss.updateRssFeedList(); window.parent.qBittorrent.Rss.updateRssFeedList();
@ -56,7 +56,7 @@
onFailure: (response) => { onFailure: (response) => {
if (response.status === 409) if (response.status === 409)
alert(response.responseText); alert(response.responseText);
$('submitButton').disabled = false; $("submitButton").disabled = false;
} }
}).send(); }).send();
}); });

View file

@ -9,44 +9,44 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': (event) => { "Enter": (event) => {
$('submitButton').click(); $("submitButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': (event) => { "Escape": (event) => {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': (event) => { "Esc": (event) => {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', () => { window.addEvent("domready", () => {
$('folderName').focus(); $("folderName").focus();
const path = new URI().getData('path'); const path = new URI().getData("path");
$('submitButton').addEvent('click', (e) => { $("submitButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
// check field // check field
const folderName = $('folderName').value.trim(); const folderName = $("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; $("submitButton").disabled = true;
new Request({ new Request({
url: 'api/v2/rss/addFolder', url: "api/v2/rss/addFolder",
method: 'post', method: "post",
data: { data: {
path: path ? (path + '\\' + folderName) : folderName path: path ? (path + "\\" + folderName) : folderName
}, },
onSuccess: (response) => { onSuccess: (response) => {
window.parent.qBittorrent.Rss.updateRssFeedList(); window.parent.qBittorrent.Rss.updateRssFeedList();
@ -55,7 +55,7 @@
onFailure: (response) => { onFailure: (response) => {
if (response.status === 409) if (response.status === 409)
alert(response.responseText); alert(response.responseText);
$('submitButton').disabled = false; $("submitButton").disabled = false;
} }
}).send(); }).send();
}); });

View file

@ -9,46 +9,46 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': (event) => { "Enter": (event) => {
$('submitButton').click(); $("submitButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': (event) => { "Escape": (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('newRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("newRulePage"));
event.preventDefault(); event.preventDefault();
}, },
'Esc': (event) => { "Esc": (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('newRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("newRulePage"));
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', () => { window.addEvent("domready", () => {
$('name').focus(); $("name").focus();
$('submitButton').addEvent('click', (e) => { $("submitButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
// check field // check field
const name = $('name').value.trim(); const name = $("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; $("submitButton").disabled = true;
new Request({ new Request({
url: 'api/v2/rss/setRule', url: "api/v2/rss/setRule",
method: 'post', method: "post",
data: { data: {
ruleName: name, ruleName: name,
ruleDef: '{}' ruleDef: "{}"
}, },
onSuccess: (response) => { onSuccess: (response) => {
window.parent.qBittorrent.RssDownloader.updateRulesList(); window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('newRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("newRulePage"));
} }
}).send(); }).send();
}); });

View file

@ -9,39 +9,39 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': function(event) { "Enter": function(event) {
$('tagNameButton').click(); $("tagNameButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', function() { window.addEvent("domready", () => {
const uriAction = window.qBittorrent.Misc.safeTrim(new URI().getData('action')); const uriAction = window.qBittorrent.Misc.safeTrim(new URI().getData("action"));
const uriHashes = window.qBittorrent.Misc.safeTrim(new URI().getData('hashes')); const uriHashes = window.qBittorrent.Misc.safeTrim(new URI().getData("hashes"));
if (uriAction === 'create') if (uriAction === "create")
$('legendText').innerText = 'QBT_TR(Tag:)QBT_TR[CONTEXT=TagFilterWidget]'; $("legendText").innerText = "QBT_TR(Tag:)QBT_TR[CONTEXT=TagFilterWidget]";
$('tagName').focus(); $("tagName").focus();
$('tagNameButton').addEvent('click', function(e) { $("tagNameButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
const tagName = $('tagName').value.trim(); const tagName = $("tagName").value.trim();
const verifyTagName = function(name) { const verifyTagName = function(name) {
if ((name === null) || (name === "")) if ((name === null) || (name === ""))
@ -59,8 +59,8 @@
return; return;
new Request({ new Request({
url: 'api/v2/torrents/addTags', url: "api/v2/torrents/addTags",
method: 'post', method: "post",
data: { data: {
hashes: uriHashes, hashes: uriHashes,
tags: tagName, tags: tagName,
@ -76,8 +76,8 @@
return; return;
new Request({ new Request({
url: 'api/v2/torrents/createTags', url: "api/v2/torrents/createTags",
method: 'post', method: "post",
data: { data: {
tags: tagName, tags: tagName,
}, },

View file

@ -9,45 +9,45 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': function(event) { "Enter": function(event) {
$('renameButton').click(); $("renameButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', function() { window.addEvent("domready", () => {
const name = new URI().getData('name'); const name = new URI().getData("name");
// set text field to current value // set text field to current value
if (name) if (name)
$('rename').value = name; $("rename").value = name;
$('rename').focus(); $("rename").focus();
$('renameButton').addEvent('click', function(e) { $("renameButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
// check field // check field
const name = $('rename').value.trim(); const name = $("rename").value.trim();
if ((name === null) || (name === "")) if ((name === null) || (name === ""))
return false; return false;
const hash = new URI().getData('hash'); const hash = new URI().getData("hash");
if (hash) { if (hash) {
new Request({ new Request({
url: 'api/v2/torrents/rename', url: "api/v2/torrents/rename",
method: 'post', method: "post",
data: { data: {
hash: hash, hash: hash,
name: name name: name

View file

@ -9,51 +9,51 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': (event) => { "Enter": (event) => {
$('renameButton').click(); $("renameButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': (event) => { "Escape": (event) => {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': (event) => { "Esc": (event) => {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', () => { window.addEvent("domready", () => {
const oldPath = new URI().getData('oldPath'); const oldPath = new URI().getData("oldPath");
$('rename').value = oldPath; $("rename").value = oldPath;
$('rename').focus(); $("rename").focus();
$('rename').setSelectionRange(0, oldPath.length); $("rename").setSelectionRange(0, oldPath.length);
$('renameButton').addEvent('click', (e) => { $("renameButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
// check field // check field
const newPath = $('rename').value.trim(); const newPath = $("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;
} }
if (newPath === oldPath) { if (newPath === oldPath) {
alert('QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]'); alert("QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]");
return; return;
} }
$('renameButton').disabled = true; $("renameButton").disabled = true;
new Request({ new Request({
url: 'api/v2/rss/moveItem', url: "api/v2/rss/moveItem",
method: 'post', method: "post",
data: { data: {
itemPath: oldPath, itemPath: oldPath,
destPath: newPath destPath: newPath
@ -63,10 +63,9 @@
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
}, },
onFailure: (response) => { onFailure: (response) => {
if (response.status === 409) { if (response.status === 409)
alert(response.responseText); alert(response.responseText);
} $("renameButton").disabled = false;
$('renameButton').disabled = false;
} }
}).send(); }).send();
}); });

View file

@ -10,60 +10,60 @@
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script src="scripts/filesystem.js?v=${CACHEID}"></script> <script src="scripts/filesystem.js?v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': function(event) { "Enter": function(event) {
$('renameButton').click(); $("renameButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', function() { window.addEvent("domready", () => {
const hash = new URI().getData('hash'); const hash = new URI().getData("hash");
const oldPath = new URI().getData('path'); const oldPath = new URI().getData("path");
const isFolder = ((new URI().getData('isFolder')) === 'true'); const isFolder = ((new URI().getData("isFolder")) === "true");
const oldName = window.qBittorrent.Filesystem.fileName(oldPath); const oldName = window.qBittorrent.Filesystem.fileName(oldPath);
$('rename').value = oldName; $("rename").value = oldName;
$('rename').focus(); $("rename").focus();
if (!isFolder) if (!isFolder)
$('rename').setSelectionRange(0, oldName.lastIndexOf('.')); $("rename").setSelectionRange(0, oldName.lastIndexOf("."));
$('renameButton').addEvent('click', function(e) { $("renameButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
// check field // check field
const newName = $('rename').value.trim(); const newName = $("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;
} }
if (newName === oldName) { if (newName === oldName) {
alert('QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]'); alert("QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]");
return; return;
} }
$('renameButton').disabled = true; $("renameButton").disabled = true;
const parentPath = window.qBittorrent.Filesystem.folderName(oldPath); const parentPath = window.qBittorrent.Filesystem.folderName(oldPath);
const newPath = parentPath const newPath = parentPath
? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName ? parentPath + window.qBittorrent.Filesystem.PathSeparator + newName
: newName; : newName;
new Request({ new Request({
url: isFolder ? 'api/v2/torrents/renameFolder' : 'api/v2/torrents/renameFile', url: isFolder ? "api/v2/torrents/renameFolder" : "api/v2/torrents/renameFile",
method: 'post', method: "post",
data: { data: {
hash: hash, hash: hash,
oldPath: oldPath, oldPath: oldPath,
@ -73,8 +73,8 @@
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
}, },
onFailure: function() { onFailure: function() {
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; $("renameButton").disabled = false;
} }
}).send(); }).send();
}); });

View file

@ -12,23 +12,22 @@
<script src="scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/dynamicTable.js?locale=${LANG}&v=${CACHEID}"></script>
<script src="scripts/rename-files.js?v=${CACHEID}"></script> <script src="scripts/rename-files.js?v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
if (window.parent.qBittorrent !== undefined) { if (window.parent.qBittorrent !== undefined)
window.qBittorrent = window.parent.qBittorrent; window.qBittorrent = window.parent.qBittorrent;
}
window.qBittorrent = window.parent.qBittorrent; window.qBittorrent = window.parent.qBittorrent;
var TriState = window.qBittorrent.FileTree.TriState; const TriState = window.qBittorrent.FileTree.TriState;
var data = window.MUI.Windows.instances['multiRenamePage'].options.data; const data = window.MUI.Windows.instances["multiRenamePage"].options.data;
var bulkRenameFilesContextMenu; let bulkRenameFilesContextMenu;
if (!bulkRenameFilesContextMenu) { if (!bulkRenameFilesContextMenu) {
bulkRenameFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ bulkRenameFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: '#bulkRenameFilesTableDiv tr', targets: "#bulkRenameFilesTableDiv tr",
menu: 'multiRenameFilesMenu', menu: "multiRenameFilesMenu",
actions: { actions: {
ToggleSelection: function(element, ref) { ToggleSelection: function(element, ref) {
const rowId = parseInt(element.get('data-row-id')); const rowId = parseInt(element.get("data-row-id"), 10);
const row = bulkRenameFilesTable.getNode(rowId); const row = bulkRenameFilesTable.getNode(rowId);
const checkState = (row.checked === 1) ? 0 : 1; const checkState = (row.checked === 1) ? 0 : 1;
bulkRenameFilesTable.toggleNodeTreeCheckbox(rowId, checkState); bulkRenameFilesTable.toggleNodeTreeCheckbox(rowId, checkState);
@ -44,20 +43,19 @@
} }
// Setup the dynamic table for bulk renaming // Setup the dynamic table for bulk renaming
var bulkRenameFilesTable = new window.qBittorrent.DynamicTable.BulkRenameTorrentFilesTable(); const bulkRenameFilesTable = new window.qBittorrent.DynamicTable.BulkRenameTorrentFilesTable();
bulkRenameFilesTable.setup('bulkRenameFilesTableDiv', 'bulkRenameFilesTableFixedHeaderDiv', bulkRenameFilesContextMenu); bulkRenameFilesTable.setup("bulkRenameFilesTableDiv", "bulkRenameFilesTableFixedHeaderDiv", bulkRenameFilesContextMenu);
// Inject checkbox into the first column of the table header // Inject checkbox into the first column of the table header
var tableHeaders = $$('#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th'); const tableHeaders = $$("#bulkRenameFilesTableFixedHeaderDiv .dynamicTableHeader th");
var checkboxHeader; let checkboxHeader;
if (tableHeaders.length > 0) { if (tableHeaders.length > 0) {
if (checkboxHeader) { if (checkboxHeader)
checkboxHeader.remove(); checkboxHeader.remove();
} checkboxHeader = new Element("input");
checkboxHeader = new Element('input'); checkboxHeader.set("type", "checkbox");
checkboxHeader.set('type', 'checkbox'); checkboxHeader.set("id", "rootMultiRename_cb");
checkboxHeader.set('id', 'rootMultiRename_cb'); checkboxHeader.addEvent("click", (e) => {
checkboxHeader.addEvent('click', function(e) {
bulkRenameFilesTable.toggleGlobalCheckbox(); bulkRenameFilesTable.toggleGlobalCheckbox();
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows(); fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows();
fileRenamer.update(); fileRenamer.update();
@ -69,16 +67,16 @@
// Register keyboard events to modal window // Register keyboard events to modal window
// https://github.com/qbittorrent/qBittorrent/pull/18687#discussion_r1135045726 // https://github.com/qbittorrent/qBittorrent/pull/18687#discussion_r1135045726
var keyboard; let keyboard;
if (!keyboard) { if (!keyboard) {
keyboard = new Keyboard({ keyboard = new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
@ -87,55 +85,55 @@
keyboard.activate(); keyboard.activate();
} }
var fileRenamer = new window.qBittorrent.MultiRename.RenameFiles(); const fileRenamer = new window.qBittorrent.MultiRename.RenameFiles();
fileRenamer.hash = data.hash; fileRenamer.hash = data.hash;
// Load Multi Rename Preferences // Load Multi Rename Preferences
var multiRenamePrefChecked = LocalPreferences.get('multirename_rememberPreferences', "true") === "true"; const multiRenamePrefChecked = LocalPreferences.get("multirename_rememberPreferences", "true") === "true";
$('multirename_rememberprefs_checkbox').setProperty('checked', multiRenamePrefChecked); $("multirename_rememberprefs_checkbox").setProperty("checked", multiRenamePrefChecked);
if (multiRenamePrefChecked) { if (multiRenamePrefChecked) {
var multirename_search = LocalPreferences.get('multirename_search', ''); const multirename_search = LocalPreferences.get("multirename_search", "");
fileRenamer.setSearch(multirename_search); fileRenamer.setSearch(multirename_search);
$('multiRenameSearch').set('value', multirename_search); $("multiRenameSearch").set("value", multirename_search);
var 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; $("use_regex_search").checked = fileRenamer.useRegex;
var 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; $("match_all_occurrences").checked = fileRenamer.matchAllOccurrences;
var 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; $("case_sensitive").checked = fileRenamer.caseSensitive;
var multirename_replace = LocalPreferences.get('multirename_replace', ''); const multirename_replace = LocalPreferences.get("multirename_replace", "");
fileRenamer.setReplacement(multirename_replace); fileRenamer.setReplacement(multirename_replace);
$('multiRenameReplace').set('value', multirename_replace); $("multiRenameReplace").set("value", multirename_replace);
var 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').set('value', fileRenamer.appliesTo); $("applies_to_option").set("value", fileRenamer.appliesTo);
var 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; $("include_files").checked = fileRenamer.includeFiles;
var 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; $("include_folders").checked = fileRenamer.includeFolders;
var multirename_fileEnumerationStart = LocalPreferences.get('multirename_fileEnumerationStart', 0); const multirename_fileEnumerationStart = LocalPreferences.get("multirename_fileEnumerationStart", 0);
fileRenamer.fileEnumerationStart = parseInt(multirename_fileEnumerationStart); fileRenamer.fileEnumerationStart = parseInt(multirename_fileEnumerationStart, 10);
$('file_counter').set('value', fileRenamer.fileEnumerationStart); $("file_counter").set("value", fileRenamer.fileEnumerationStart);
var 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";
var renameButtonValue = fileRenamer.replaceAll ? 'Replace All' : 'Replace'; const renameButtonValue = fileRenamer.replaceAll ? "Replace All" : "Replace";
$('renameOptions').set('value', renameButtonValue); $("renameOptions").set("value", renameButtonValue);
$('renameButton').set('value', renameButtonValue); $("renameButton").set("value", renameButtonValue);
} }
// Fires every time a row's selection changes // Fires every time a row's selection changes
@ -145,28 +143,28 @@
}; };
// Setup Search Events that control renaming // Setup Search Events that control renaming
$('multiRenameSearch').addEvent('input', function(e) { $("multiRenameSearch").addEvent("input", (e) => {
let sanitized = e.target.value.replace(/\n/g, ''); const sanitized = e.target.value.replace(/\n/g, "");
$('multiRenameSearch').set('value', sanitized); $("multiRenameSearch").set("value", sanitized);
// Search input has changed // Search input has changed
$('multiRenameSearch').style['border-color'] = ''; $("multiRenameSearch").style["border-color"] = "";
LocalPreferences.set('multirename_search', sanitized); LocalPreferences.set("multirename_search", sanitized);
fileRenamer.setSearch(sanitized); fileRenamer.setSearch(sanitized);
}); });
$('use_regex_search').addEvent('change', function(e) { $("use_regex_search").addEvent("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').addEvent('change', function(e) { $("match_all_occurrences").addEvent("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').addEvent('change', function(e) { $("case_sensitive").addEvent("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();
}); });
@ -177,138 +175,136 @@
// Clear renamed column // Clear renamed column
document document
.querySelectorAll("span[id^='filesTablefileRenamed']") .querySelectorAll("span[id^='filesTablefileRenamed']")
.forEach(function(span) { .forEach((span) => {
span.set('text', ""); span.set("text", "");
}); });
// Update renamed column for matched rows // Update renamed column for matched rows
for (let i = 0; i < matchedRows.length; ++i) { for (let i = 0; i < matchedRows.length; ++i) {
const row = matchedRows[i]; const row = matchedRows[i];
$('filesTablefileRenamed' + row.rowId).set('text', row.renamed); $("filesTablefileRenamed" + row.rowId).set("text", row.renamed);
} }
}; };
fileRenamer.onInvalidRegex = function(err) { fileRenamer.onInvalidRegex = function(err) {
$('multiRenameSearch').style['border-color'] = '#CC0033'; $("multiRenameSearch").style["border-color"] = "#CC0033";
}; };
// Setup Replace Events that control renaming // Setup Replace Events that control renaming
$('multiRenameReplace').addEvent('input', function(e) { $("multiRenameReplace").addEvent("input", (e) => {
let sanitized = e.target.value.replace(/\n/g, ''); const sanitized = e.target.value.replace(/\n/g, "");
$('multiRenameReplace').set('value', sanitized); $("multiRenameReplace").set("value", sanitized);
// Replace input has changed // Replace input has changed
$('multiRenameReplace').style['border-color'] = ''; $("multiRenameReplace").style["border-color"] = "";
LocalPreferences.set('multirename_replace', sanitized); LocalPreferences.set("multirename_replace", sanitized);
fileRenamer.setReplacement(sanitized); fileRenamer.setReplacement(sanitized);
}); });
$('applies_to_option').addEvent('change', function(e) { $("applies_to_option").addEvent("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').addEvent('change', function(e) { $("include_files").addEvent("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').addEvent('change', function(e) { $("include_folders").addEvent("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').addEvent('input', function(e) { $("file_counter").addEvent("input", (e) => {
let value = e.target.valueAsNumber; let value = e.target.valueAsNumber;
if (!value) { value = 0; } if (!value)
if (value < 0) { value = 0; } value = 0;
if (value > 99999999) { value = 99999999; } if (value < 0)
value = 0;
if (value > 99999999)
value = 99999999;
fileRenamer.fileEnumerationStart = value; fileRenamer.fileEnumerationStart = value;
$('file_counter').set('value', value); $("file_counter").set("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').addEvent('click', function(e) { $("renameButton").addEvent("click", (e) => {
// Disable Search Options // Disable Search Options
$('multiRenameSearch').disabled = true; $("multiRenameSearch").disabled = true;
$('use_regex_search').disabled = true; $("use_regex_search").disabled = true;
$('match_all_occurrences').disabled = true; $("match_all_occurrences").disabled = true;
$('case_sensitive').disabled = true; $("case_sensitive").disabled = true;
// Disable Replace Options // Disable Replace Options
$('multiRenameReplace').disabled = true; $("multiRenameReplace").disabled = true;
$('applies_to_option').disabled = true; $("applies_to_option").disabled = true;
$('include_files').disabled = true; $("include_files").disabled = true;
$('include_folders').disabled = true; $("include_folders").disabled = true;
$('file_counter').disabled = true; $("file_counter").disabled = true;
// Disable Rename Buttons // Disable Rename Buttons
$('renameButton').disabled = true; $("renameButton").disabled = true;
$('renameOptions').disabled = true; $("renameOptions").disabled = true;
// Clear error text // Clear error text
$('rename_error').set('text', ''); $("rename_error").set("text", "");
fileRenamer.rename(); fileRenamer.rename();
}); });
fileRenamer.onRenamed = function(rows) { fileRenamer.onRenamed = function(rows) {
// Disable Search Options // Disable Search Options
$('multiRenameSearch').disabled = false; $("multiRenameSearch").disabled = false;
$('use_regex_search').disabled = false; $("use_regex_search").disabled = false;
$('match_all_occurrences').disabled = false; $("match_all_occurrences").disabled = false;
$('case_sensitive').disabled = false; $("case_sensitive").disabled = false;
// Disable Replace Options // Disable Replace Options
$('multiRenameReplace').disabled = false; $("multiRenameReplace").disabled = false;
$('applies_to_option').disabled = false; $("applies_to_option").disabled = false;
$('include_files').disabled = false; $("include_files").disabled = false;
$('include_folders').disabled = false; $("include_folders").disabled = false;
$('file_counter').disabled = false; $("file_counter").disabled = false;
// Disable Rename Buttons // Disable Rename Buttons
$('renameButton').disabled = false; $("renameButton").disabled = false;
$('renameOptions').disabled = false; $("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());
for (let renamedRow of rows) { for (const renamedRow of rows)
selectedRows = selectedRows.filter(selectedRow => selectedRow !== renamedRow.rowId.toString()); selectedRows = selectedRows.filter(selectedRow => selectedRow !== renamedRow.rowId.toString());
}
bulkRenameFilesTable.clear(); bulkRenameFilesTable.clear();
// 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').set('value', fileRenamer.fileEnumerationStart); $("file_counter").set("value", fileRenamer.fileEnumerationStart);
} }
setupTable(selectedRows); setupTable(selectedRows);
}; };
fileRenamer.onRenameError = function(err, row) { fileRenamer.onRenameError = function(err, row) {
if (err.xhr.status === 409) { if (err.xhr.status === 409)
$('rename_error').set('text', `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}\``); $("rename_error").set("text", `QBT_TR(Rename failed: file or folder already exists)QBT_TR[CONTEXT=PropertiesWidget] \`${row.renamed}\``);
}
}; };
$('renameOptions').addEvent('change', function(e) { $("renameOptions").addEvent("change", (e) => {
const combobox = e.target; const combobox = e.target;
const replaceOperation = combobox.value; const replaceOperation = combobox.value;
if (replaceOperation === "Replace") { if (replaceOperation === "Replace")
fileRenamer.replaceAll = false; fileRenamer.replaceAll = false;
} else if (replaceOperation === "Replace All")
else if (replaceOperation === "Replace All") {
fileRenamer.replaceAll = true; fileRenamer.replaceAll = true;
} else
else {
fileRenamer.replaceAll = false; fileRenamer.replaceAll = false;
} LocalPreferences.set("multirename_replaceAll", fileRenamer.replaceAll);
LocalPreferences.set('multirename_replaceAll', fileRenamer.replaceAll); $("renameButton").set("value", replaceOperation);
$('renameButton').set('value', replaceOperation);
}); });
$('closeButton').addEvent('click', function() { $("closeButton").addEvent("click", () => {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}); });
// synchronize header scrolling to table body // synchronize header scrolling to table body
$('bulkRenameFilesTableDiv').onscroll = function() { $("bulkRenameFilesTableDiv").onscroll = function() {
const length = $(this).scrollLeft; const length = $(this).scrollLeft;
$('bulkRenameFilesTableFixedHeaderDiv').scrollLeft = length; $("bulkRenameFilesTableFixedHeaderDiv").scrollLeft = length;
}; };
var handleTorrentFiles = function(files, selectedRows) { const handleTorrentFiles = function(files, selectedRows) {
const rows = files.map(function(file, index) { const rows = files.map((file, index) => {
const row = { const row = {
fileId: index, fileId: index,
@ -325,20 +321,19 @@
addRowsToTable(rows, selectedRows); addRowsToTable(rows, selectedRows);
}; };
var addRowsToTable = function(rows, selectedRows) { const addRowsToTable = function(rows, selectedRows) {
let rowId = 0; let rowId = 0;
const rootNode = new window.qBittorrent.FileTree.FolderNode(); const rootNode = new window.qBittorrent.FileTree.FolderNode();
rootNode.autoCheckFolders = false; rootNode.autoCheckFolders = false;
rows.forEach(function(row) { rows.forEach((row) => {
const pathItems = row.path.split(window.qBittorrent.Filesystem.PathSeparator); const pathItems = row.path.split(window.qBittorrent.Filesystem.PathSeparator);
pathItems.pop(); // remove last item (i.e. file name) pathItems.pop(); // remove last item (i.e. file name)
let parent = rootNode; let parent = rootNode;
pathItems.forEach(function(folderName) { pathItems.forEach((folderName) => {
if (folderName === '.unwanted') { if (folderName === ".unwanted")
return; return;
}
let folderNode = null; let folderNode = null;
if (parent.children !== null) { if (parent.children !== null) {
@ -387,26 +382,24 @@
bulkRenameFilesTable.updateTable(false); bulkRenameFilesTable.updateTable(false);
bulkRenameFilesTable.altRow(); bulkRenameFilesTable.altRow();
if (selectedRows !== undefined) { if (selectedRows !== undefined)
bulkRenameFilesTable.reselectRows(selectedRows); bulkRenameFilesTable.reselectRows(selectedRows);
}
fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows(); fileRenamer.selectedFiles = bulkRenameFilesTable.getSelectedRows();
fileRenamer.update(); fileRenamer.update();
}; };
var setupTable = function(selectedRows) { const setupTable = function(selectedRows) {
new Request.JSON({ new Request.JSON({
url: new URI('api/v2/torrents/files?hash=' + data.hash), url: new URI("api/v2/torrents/files?hash=" + data.hash),
noCache: true, noCache: true,
method: 'get', method: "get",
onSuccess: function(files) { onSuccess: function(files) {
if (files.length === 0) { if (files.length === 0)
bulkRenameFilesTable.clear(); bulkRenameFilesTable.clear();
} else
else {
handleTorrentFiles(files, selectedRows); handleTorrentFiles(files, selectedRows);
} }
}
}).send(); }).send();
}; };
setupTable(data.selectedRows); setupTable(data.selectedRows);

View file

@ -9,57 +9,57 @@
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script> <script src="scripts/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Enter': (event) => { "Enter": (event) => {
$('renameButton').click(); $("renameButton").click();
event.preventDefault(); event.preventDefault();
}, },
'Escape': (event) => { "Escape": (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('renameRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("renameRulePage"));
event.preventDefault(); event.preventDefault();
}, },
'Esc': (event) => { "Esc": (event) => {
window.parent.MochaUI.closeWindow(window.parent.$('renameRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("renameRulePage"));
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', () => { window.addEvent("domready", () => {
const oldName = new URI().getData('rule'); const oldName = new URI().getData("rule");
$('rename').value = oldName; $("rename").value = oldName;
$('rename').focus(); $("rename").focus();
$('rename').setSelectionRange(0, oldName.length); $("rename").setSelectionRange(0, oldName.length);
$('renameButton').addEvent('click', (e) => { $("renameButton").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
// check field // check field
const newName = $('rename').value.trim(); const newName = $("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;
} }
if (newName === oldName) { if (newName === oldName) {
alert('QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]'); alert("QBT_TR(Name is unchanged)QBT_TR[CONTEXT=HttpServer]");
return; return;
} }
$('renameButton').disabled = true; $("renameButton").disabled = true;
new Request({ new Request({
url: 'api/v2/rss/renameRule', url: "api/v2/rss/renameRule",
method: 'post', method: "post",
data: { data: {
ruleName: oldName, ruleName: oldName,
newRuleName: newName newRuleName: newName
}, },
onSuccess: (response) => { onSuccess: (response) => {
window.parent.qBittorrent.RssDownloader.updateRulesList(); window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('renameRulePage')); window.parent.MochaUI.closeWindow(window.parent.$("renameRulePage"));
} }
}).send(); }).send();
}); });

View file

@ -26,7 +26,7 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
@ -46,7 +46,7 @@ window.qBittorrent.Cache = (() => {
const keys = Reflect.ownKeys(obj); const keys = Reflect.ownKeys(obj);
for (const key of keys) { for (const key of keys) {
const value = obj[key]; const value = obj[key];
if ((value && (typeof value === 'object')) || (typeof value === 'function')) if ((value && (typeof value === "object")) || (typeof value === "function"))
deepFreeze(value); deepFreeze(value);
} }
Object.freeze(obj); Object.freeze(obj);
@ -57,8 +57,8 @@ window.qBittorrent.Cache = (() => {
init() { init() {
new Request.JSON({ new Request.JSON({
url: 'api/v2/app/buildInfo', url: "api/v2/app/buildInfo",
method: 'get', method: "get",
noCache: true, noCache: true,
onSuccess: (responseJSON) => { onSuccess: (responseJSON) => {
if (!responseJSON) if (!responseJSON)
@ -84,11 +84,11 @@ window.qBittorrent.Cache = (() => {
// } // }
init(obj = {}) { init(obj = {}) {
new Request.JSON({ new Request.JSON({
url: 'api/v2/app/preferences', url: "api/v2/app/preferences",
method: 'get', method: "get",
noCache: true, noCache: true,
onFailure: (xhr) => { onFailure: (xhr) => {
if (typeof obj.onFailure === 'function') if (typeof obj.onFailure === "function")
obj.onFailure(xhr); obj.onFailure(xhr);
}, },
onSuccess: (responseJSON, responseText) => { onSuccess: (responseJSON, responseText) => {
@ -98,7 +98,7 @@ window.qBittorrent.Cache = (() => {
deepFreeze(responseJSON); deepFreeze(responseJSON);
this.#m_store = responseJSON; this.#m_store = responseJSON;
if (typeof obj.onSuccess === 'function') if (typeof obj.onSuccess === "function")
obj.onSuccess(responseJSON, responseText); obj.onSuccess(responseJSON, responseText);
} }
}).send(); }).send();
@ -114,19 +114,19 @@ window.qBittorrent.Cache = (() => {
// onSuccess: () => {} // onSuccess: () => {}
// } // }
set(obj) { set(obj) {
if (typeof obj !== 'object') if (typeof obj !== "object")
throw new Error('`obj` is not an object.'); throw new Error("`obj` is not an object.");
if (typeof obj.data !== 'object') if (typeof obj.data !== "object")
throw new Error('`data` is not an object.'); throw new Error("`data` is not an object.");
new Request({ new Request({
url: 'api/v2/app/setPreferences', url: "api/v2/app/setPreferences",
method: 'post', method: "post",
data: { data: {
'json': JSON.stringify(obj.data) "json": JSON.stringify(obj.data)
}, },
onFailure: (xhr) => { onFailure: (xhr) => {
if (typeof obj.onFailure === 'function') if (typeof obj.onFailure === "function")
obj.onFailure(xhr); obj.onFailure(xhr);
}, },
onSuccess: (responseText, responseXML) => { onSuccess: (responseText, responseXML) => {
@ -140,7 +140,7 @@ window.qBittorrent.Cache = (() => {
} }
deepFreeze(this.#m_store); deepFreeze(this.#m_store);
if (typeof obj.onSuccess === 'function') if (typeof obj.onSuccess === "function")
obj.onSuccess(responseText, responseXML); obj.onSuccess(responseText, responseXML);
} }
}).send(); }).send();
@ -148,12 +148,12 @@ window.qBittorrent.Cache = (() => {
} }
class QbtVersionCache { class QbtVersionCache {
#m_store = ''; #m_store = "";
init() { init() {
new Request({ new Request({
url: 'api/v2/app/version', url: "api/v2/app/version",
method: 'get', method: "get",
noCache: true, noCache: true,
onSuccess: (responseText) => { onSuccess: (responseText) => {
if (!responseText) if (!responseText)

File diff suppressed because it is too large Load diff

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.ContextMenu = (function() { window.qBittorrent.ContextMenu = (function() {
const exports = function() { const exports = function() {
@ -48,15 +47,15 @@ window.qBittorrent.ContextMenu = (function() {
let lastShownContextMenu = null; let lastShownContextMenu = null;
const ContextMenu = new Class({ const ContextMenu = new Class({
//implements // implements
Implements: [Options, Events], Implements: [Options, Events],
//options // options
options: { options: {
actions: {}, actions: {},
menu: 'menu_id', menu: "menu_id",
stopEvent: true, stopEvent: true,
targets: 'body', targets: "body",
offsets: { offsets: {
x: 0, x: 0,
y: 0 y: 0
@ -68,37 +67,35 @@ window.qBittorrent.ContextMenu = (function() {
touchTimer: 600 touchTimer: 600
}, },
//initialization // initialization
initialize: function(options) { initialize: function(options) {
//set options // set options
this.setOptions(options); this.setOptions(options);
//option diffs menu // option diffs menu
this.menu = $(this.options.menu); this.menu = $(this.options.menu);
this.targets = $$(this.options.targets); this.targets = $$(this.options.targets);
//fx // fx
this.fx = new Fx.Tween(this.menu, { this.fx = new Fx.Tween(this.menu, {
property: 'opacity', property: "opacity",
duration: this.options.fadeSpeed, duration: this.options.fadeSpeed,
onComplete: function() { onComplete: function() {
if (this.getStyle('opacity')) { if (this.getStyle("opacity"))
this.setStyle('visibility', 'visible'); this.setStyle("visibility", "visible");
} else
else { this.setStyle("visibility", "hidden");
this.setStyle('visibility', 'hidden');
}
}.bind(this.menu) }.bind(this.menu)
}); });
//hide and begin the listener // hide and begin the listener
this.hide().startListener(); this.hide().startListener();
//hide the menu // hide the menu
this.menu.setStyles({ this.menu.setStyles({
'position': 'absolute', "position": "absolute",
'top': '-900000px', "top": "-900000px",
'display': 'block' "display": "block"
}); });
}, },
@ -107,13 +104,13 @@ window.qBittorrent.ContextMenu = (function() {
const scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75; const scrollableMenuMaxHeight = document.documentElement.clientHeight * 0.75;
if (this.menu.hasClass('scrollableMenu')) if (this.menu.hasClass("scrollableMenu"))
this.menu.setStyle('max-height', scrollableMenuMaxHeight); this.menu.setStyle("max-height", scrollableMenuMaxHeight);
// draw the menu off-screen to know the menu dimensions // draw the menu off-screen to know the menu dimensions
this.menu.setStyles({ this.menu.setStyles({
left: '-999em', left: "-999em",
top: '-999em' top: "-999em"
}); });
// position the menu // position the menu
@ -130,16 +127,16 @@ window.qBittorrent.ContextMenu = (function() {
this.menu.setStyles({ this.menu.setStyles({
left: xPosMenu, left: xPosMenu,
top: yPosMenu, top: yPosMenu,
position: 'absolute', position: "absolute",
'z-index': '2000' "z-index": "2000"
}); });
// position the sub-menu // position the sub-menu
const uls = this.menu.getElementsByTagName('ul'); const uls = this.menu.getElementsByTagName("ul");
for (let i = 0; i < uls.length; ++i) { for (let i = 0; i < uls.length; ++i) {
const ul = uls[i]; const ul = uls[i];
if (ul.hasClass('scrollableMenu')) if (ul.hasClass("scrollableMenu"))
ul.setStyle('max-height', scrollableMenuMaxHeight); ul.setStyle("max-height", scrollableMenuMaxHeight);
const rectParent = ul.parentNode.getBoundingClientRect(); const rectParent = ul.parentNode.getBoundingClientRect();
const xPosOrigin = rectParent.left; const xPosOrigin = rectParent.left;
const yPosOrigin = rectParent.bottom; const yPosOrigin = rectParent.bottom;
@ -154,26 +151,26 @@ window.qBittorrent.ContextMenu = (function() {
if (yPos < 0) if (yPos < 0)
yPos = 0; yPos = 0;
ul.setStyles({ ul.setStyles({
'margin-left': xPos - xPosOrigin, "margin-left": xPos - xPosOrigin,
'margin-top': yPos - yPosOrigin "margin-top": yPos - yPosOrigin
}); });
} }
}, },
setupEventListeners: function(elem) { setupEventListeners: function(elem) {
elem.addEvent('contextmenu', function(e) { elem.addEvent("contextmenu", (e) => {
this.triggerMenu(e, elem); this.triggerMenu(e, elem);
}.bind(this)); });
elem.addEvent('click', function(e) { elem.addEvent("click", (e) => {
this.hide(); this.hide();
}.bind(this)); });
elem.addEvent('touchstart', function(e) { elem.addEvent("touchstart", (e) => {
this.hide(); this.hide();
this.touchStartAt = performance.now(); this.touchStartAt = performance.now();
this.touchStartEvent = e; this.touchStartEvent = e;
}.bind(this)); });
elem.addEvent('touchend', function(e) { elem.addEvent("touchend", (e) => {
const now = performance.now(); const now = performance.now();
const touchStartAt = this.touchStartAt; const touchStartAt = this.touchStartAt;
const touchStartEvent = this.touchStartEvent; const touchStartEvent = this.touchStartEvent;
@ -182,16 +179,15 @@ window.qBittorrent.ContextMenu = (function() {
this.touchStartEvent = null; this.touchStartEvent = null;
const isTargetUnchanged = (Math.abs(e.event.pageX - touchStartEvent.event.pageX) <= 10) && (Math.abs(e.event.pageY - touchStartEvent.event.pageY) <= 10); const isTargetUnchanged = (Math.abs(e.event.pageX - touchStartEvent.event.pageX) <= 10) && (Math.abs(e.event.pageY - touchStartEvent.event.pageY) <= 10);
if (((now - touchStartAt) >= this.options.touchTimer) && isTargetUnchanged) { if (((now - touchStartAt) >= this.options.touchTimer) && isTargetUnchanged)
this.triggerMenu(touchStartEvent, elem); this.triggerMenu(touchStartEvent, elem);
} });
}.bind(this));
}, },
addTarget: function(t) { addTarget: function(t) {
// prevent long press from selecting this text // prevent long press from selecting this text
t.style.setProperty('user-select', 'none'); t.style.setProperty("user-select", "none");
t.style.setProperty('-webkit-user-select', 'none'); t.style.setProperty("-webkit-user-select", "none");
this.targets[this.targets.length] = t; this.targets[this.targets.length] = t;
this.setupEventListeners(t); this.setupEventListeners(t);
@ -201,102 +197,100 @@ window.qBittorrent.ContextMenu = (function() {
if (this.options.disabled) if (this.options.disabled)
return; return;
//prevent default, if told to // prevent default, if told to
if (this.options.stopEvent) { if (this.options.stopEvent)
e.stop(); e.stop();
} // 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();
}, },
//get things started // get things started
startListener: function() { startListener: function() {
/* all elements */ /* all elements */
this.targets.each(function(el) { this.targets.each((el) => {
this.setupEventListeners(el); this.setupEventListeners(el);
}.bind(this), this);
/* menu items */
this.menu.getElements('a').each(function(item) {
item.addEvent('click', function(e) {
e.preventDefault();
if (!item.hasClass('disabled')) {
this.execute(item.get('href').split('#')[1], $(this.options.element));
this.fireEvent('click', [item, e]);
}
}.bind(this));
}, this); }, this);
//hide on body click /* menu items */
$(document.body).addEvent('click', function() { this.menu.getElements("a").each(function(item) {
item.addEvent("click", (e) => {
e.preventDefault();
if (!item.hasClass("disabled")) {
this.execute(item.get("href").split("#")[1], $(this.options.element));
this.fireEvent("click", [item, e]);
}
});
}, this);
// hide on body click
$(document.body).addEvent("click", () => {
this.hide(); this.hide();
}.bind(this)); });
}, },
updateMenuItems: function() {}, updateMenuItems: function() {},
//show menu // show menu
show: function(trigger) { show: function(trigger) {
if (lastShownContextMenu && (lastShownContextMenu !== this)) if (lastShownContextMenu && (lastShownContextMenu !== this))
lastShownContextMenu.hide(); lastShownContextMenu.hide();
this.fx.start(1); this.fx.start(1);
this.fireEvent('show'); this.fireEvent("show");
lastShownContextMenu = this; lastShownContextMenu = this;
return this; return this;
}, },
//hide the menu // hide the menu
hide: function(trigger) { hide: function(trigger) {
if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== 'hidden')) { if (lastShownContextMenu && (lastShownContextMenu.menu.style.visibility !== "hidden")) {
this.fx.start(0); this.fx.start(0);
//this.menu.fade('out'); // this.menu.fade('out');
this.fireEvent('hide'); this.fireEvent("hide");
} }
return this; return this;
}, },
setItemChecked: function(item, checked) { setItemChecked: function(item, checked) {
this.menu.getElement('a[href$=' + item + ']').firstChild.style.opacity = this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity =
checked ? '1' : '0'; checked ? "1" : "0";
return this; return this;
}, },
getItemChecked: function(item) { getItemChecked: function(item) {
return this.menu.getElement('a[href$=' + item + ']').firstChild.style.opacity !== '0'; return this.menu.getElement("a[href$=" + item + "]").firstChild.style.opacity !== "0";
}, },
//hide an item // hide an item
hideItem: function(item) { hideItem: function(item) {
this.menu.getElement('a[href$=' + item + ']').parentNode.addClass('invisible'); this.menu.getElement("a[href$=" + item + "]").parentNode.addClass("invisible");
return this; return this;
}, },
//show an item // show an item
showItem: function(item) { showItem: function(item) {
this.menu.getElement('a[href$=' + item + ']').parentNode.removeClass('invisible'); this.menu.getElement("a[href$=" + item + "]").parentNode.removeClass("invisible");
return this; return this;
}, },
//disable the entire menu // disable the entire menu
disable: function() { disable: function() {
this.options.disabled = true; this.options.disabled = true;
return this; return this;
}, },
//enable the entire menu // enable the entire menu
enable: function() { enable: function() {
this.options.disabled = false; this.options.disabled = false;
return this; return this;
}, },
//execute an action // execute an action
execute: function(action, element) { execute: function(action, element) {
if (this.options.actions[action]) { if (this.options.actions[action])
this.options.actions[action](element, this, action); this.options.actions[action](element, this, action);
}
return this; return this;
} }
}); });
@ -323,37 +317,37 @@ window.qBittorrent.ContextMenu = (function() {
selectedRows.forEach((item, index) => { selectedRows.forEach((item, index) => {
const data = torrentsTable.rows.get(item).full_data; const data = torrentsTable.rows.get(item).full_data;
if (data['seq_dl'] !== true) if (data["seq_dl"] !== true)
all_are_seq_dl = false; all_are_seq_dl = false;
else else
there_are_seq_dl = true; there_are_seq_dl = true;
if (data['f_l_piece_prio'] !== true) if (data["f_l_piece_prio"] !== true)
all_are_f_l_piece_prio = false; all_are_f_l_piece_prio = false;
else else
there_are_f_l_piece_prio = true; there_are_f_l_piece_prio = true;
if (data['progress'] !== 1.0) // not downloaded if (data["progress"] !== 1.0) // not downloaded
all_are_downloaded = false; all_are_downloaded = false;
else if (data['super_seeding'] !== true) else if (data["super_seeding"] !== true)
all_are_super_seeding = false; all_are_super_seeding = false;
if ((data['state'] !== 'stoppedUP') && (data['state'] !== 'stoppedDL')) if ((data["state"] !== "stoppedUP") && (data["state"] !== "stoppedDL"))
all_are_stopped = false; all_are_stopped = false;
else else
there_are_stopped = true; there_are_stopped = true;
if (data['force_start'] !== true) if (data["force_start"] !== true)
all_are_force_start = false; all_are_force_start = false;
else else
there_are_force_start = true; there_are_force_start = true;
if (data['auto_tmm'] === true) if (data["auto_tmm"] === true)
there_are_auto_tmm = true; there_are_auto_tmm = true;
else else
all_are_auto_tmm = false; all_are_auto_tmm = false;
const torrentTags = data['tags'].split(', '); const torrentTags = data["tags"].split(", ");
for (const tag of torrentTags) { for (const tag of torrentTags) {
const count = tagCount.get(tag); const count = tagCount.get(tag);
tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1)); tagCount.set(tag, ((count !== undefined) ? (count + 1) : 1));
@ -363,71 +357,71 @@ window.qBittorrent.ContextMenu = (function() {
// hide renameFiles when more than 1 torrent is selected // hide renameFiles when more than 1 torrent is selected
if (selectedRows.length === 1) { if (selectedRows.length === 1) {
const data = torrentsTable.rows.get(selectedRows[0]).full_data; const data = torrentsTable.rows.get(selectedRows[0]).full_data;
let metadata_downloaded = !((data['state'] === 'metaDL') || (data['state'] === 'forcedMetaDL') || (data['total_size'] === -1)); const metadata_downloaded = !((data["state"] === "metaDL") || (data["state"] === "forcedMetaDL") || (data["total_size"] === -1));
// hide renameFiles when metadata hasn't been downloaded yet // hide renameFiles when metadata hasn't been downloaded yet
metadata_downloaded metadata_downloaded
? this.showItem('renameFiles') ? this.showItem("renameFiles")
: this.hideItem('renameFiles'); : this.hideItem("renameFiles");
} }
else { else {
this.hideItem('renameFiles'); this.hideItem("renameFiles");
} }
if (all_are_downloaded) { if (all_are_downloaded) {
this.hideItem('downloadLimit'); this.hideItem("downloadLimit");
this.menu.getElement('a[href$=uploadLimit]').parentNode.addClass('separator'); this.menu.getElement("a[href$=uploadLimit]").parentNode.addClass("separator");
this.hideItem('sequentialDownload'); this.hideItem("sequentialDownload");
this.hideItem('firstLastPiecePrio'); this.hideItem("firstLastPiecePrio");
this.showItem('superSeeding'); this.showItem("superSeeding");
this.setItemChecked('superSeeding', all_are_super_seeding); this.setItemChecked("superSeeding", all_are_super_seeding);
} }
else { else {
const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl); const show_seq_dl = (all_are_seq_dl || !there_are_seq_dl);
const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio); const show_f_l_piece_prio = (all_are_f_l_piece_prio || !there_are_f_l_piece_prio);
if (!show_seq_dl && show_f_l_piece_prio) if (!show_seq_dl && show_f_l_piece_prio)
this.menu.getElement('a[href$=firstLastPiecePrio]').parentNode.addClass('separator'); this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.addClass("separator");
else else
this.menu.getElement('a[href$=firstLastPiecePrio]').parentNode.removeClass('separator'); this.menu.getElement("a[href$=firstLastPiecePrio]").parentNode.removeClass("separator");
if (show_seq_dl) if (show_seq_dl)
this.showItem('sequentialDownload'); this.showItem("sequentialDownload");
else else
this.hideItem('sequentialDownload'); this.hideItem("sequentialDownload");
if (show_f_l_piece_prio) if (show_f_l_piece_prio)
this.showItem('firstLastPiecePrio'); this.showItem("firstLastPiecePrio");
else else
this.hideItem('firstLastPiecePrio'); this.hideItem("firstLastPiecePrio");
this.setItemChecked('sequentialDownload', all_are_seq_dl); this.setItemChecked("sequentialDownload", all_are_seq_dl);
this.setItemChecked('firstLastPiecePrio', all_are_f_l_piece_prio); this.setItemChecked("firstLastPiecePrio", all_are_f_l_piece_prio);
this.showItem('downloadLimit'); this.showItem("downloadLimit");
this.menu.getElement('a[href$=uploadLimit]').parentNode.removeClass('separator'); this.menu.getElement("a[href$=uploadLimit]").parentNode.removeClass("separator");
this.hideItem('superSeeding'); this.hideItem("superSeeding");
} }
this.showItem('start'); this.showItem("start");
this.showItem('stop'); this.showItem("stop");
this.showItem('forceStart'); this.showItem("forceStart");
if (all_are_stopped) if (all_are_stopped)
this.hideItem('stop'); this.hideItem("stop");
else if (all_are_force_start) else if (all_are_force_start)
this.hideItem('forceStart'); this.hideItem("forceStart");
else if (!there_are_stopped && !there_are_force_start) else if (!there_are_stopped && !there_are_force_start)
this.hideItem('start'); this.hideItem("start");
if (!all_are_auto_tmm && there_are_auto_tmm) { if (!all_are_auto_tmm && there_are_auto_tmm) {
this.hideItem('autoTorrentManagement'); this.hideItem("autoTorrentManagement");
} }
else { else {
this.showItem('autoTorrentManagement'); this.showItem("autoTorrentManagement");
this.setItemChecked('autoTorrentManagement', all_are_auto_tmm); this.setItemChecked("autoTorrentManagement", all_are_auto_tmm);
} }
const contextTagList = $('contextTagList'); const contextTagList = $("contextTagList");
tagList.forEach((tag, tagHash) => { tagList.forEach((tag, tagHash) => {
const checkbox = contextTagList.getElement(`a[href="#Tag/${tagHash}"] input[type="checkbox"]`); const checkbox = contextTagList.getElement(`a[href="#Tag/${tagHash}"] input[type="checkbox"]`);
const count = tagCount.get(tag.name); const count = tagCount.get(tag.name);
@ -439,12 +433,12 @@ window.qBittorrent.ContextMenu = (function() {
}, },
updateCategoriesSubMenu: function(categoryList) { updateCategoriesSubMenu: function(categoryList) {
const contextCategoryList = $('contextCategoryList'); const contextCategoryList = $("contextCategoryList");
contextCategoryList.getChildren().each(c => c.destroy()); contextCategoryList.getChildren().each(c => c.destroy());
contextCategoryList.appendChild(new Element('li', { contextCategoryList.appendChild(new Element("li", {
html: '<a href="javascript:torrentNewCategoryFN();"><img src="images/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>' html: '<a href="javascript:torrentNewCategoryFN();"><img src="images/list-add.svg" alt="QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(New...)QBT_TR[CONTEXT=TransferListWidget]</a>'
})); }));
contextCategoryList.appendChild(new Element('li', { contextCategoryList.appendChild(new Element("li", {
html: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>' html: '<a href="javascript:torrentSetCategoryFN(0);"><img src="images/edit-clear.svg" alt="QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]"/> QBT_TR(Reset)QBT_TR[CONTEXT=TransferListWidget]</a>'
})); }));
@ -458,11 +452,11 @@ window.qBittorrent.ContextMenu = (function() {
let first = true; let first = true;
for (const { categoryName, categoryHash } of sortedCategories) { for (const { categoryName, categoryHash } of sortedCategories) {
const el = new Element('li', { const el = new Element("li", {
html: `<a href="javascript:torrentSetCategoryFN(${categoryHash});"><img src="images/view-categories.svg"/>${window.qBittorrent.Misc.escapeHtml(categoryName)}</a>` html: `<a href="javascript:torrentSetCategoryFN(${categoryHash});"><img src="images/view-categories.svg"/>${window.qBittorrent.Misc.escapeHtml(categoryName)}</a>`
}); });
if (first) { if (first) {
el.addClass('separator'); el.addClass("separator");
first = false; first = false;
} }
contextCategoryList.appendChild(el); contextCategoryList.appendChild(el);
@ -470,21 +464,21 @@ window.qBittorrent.ContextMenu = (function() {
}, },
updateTagsSubMenu: function(tagList) { updateTagsSubMenu: function(tagList) {
const contextTagList = $('contextTagList'); const contextTagList = $("contextTagList");
while (contextTagList.firstChild !== null) while (contextTagList.firstChild !== null)
contextTagList.removeChild(contextTagList.firstChild); contextTagList.removeChild(contextTagList.firstChild);
contextTagList.appendChild(new Element('li', { contextTagList.appendChild(new Element("li", {
html: '<a href="javascript:torrentAddTagsFN();">' html: '<a href="javascript:torrentAddTagsFN();">'
+ '<img src="images/list-add.svg" alt="QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"/>' + '<img src="images/list-add.svg" alt="QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"/>'
+ ' QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]' + " QBT_TR(Add...)QBT_TR[CONTEXT=TransferListWidget]"
+ '</a>' + "</a>"
})); }));
contextTagList.appendChild(new Element('li', { contextTagList.appendChild(new Element("li", {
html: '<a href="javascript:torrentRemoveAllTagsFN();">' html: '<a href="javascript:torrentRemoveAllTagsFN();">'
+ '<img src="images/edit-clear.svg" alt="QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"/>' + '<img src="images/edit-clear.svg" alt="QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"/>'
+ ' QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]' + " QBT_TR(Remove All)QBT_TR[CONTEXT=TransferListWidget]"
+ '</a>' + "</a>"
})); }));
const sortedTags = []; const sortedTags = [];
@ -496,13 +490,13 @@ window.qBittorrent.ContextMenu = (function() {
for (let i = 0; i < sortedTags.length; ++i) { for (let i = 0; i < sortedTags.length; ++i) {
const { tagName, tagHash } = sortedTags[i]; const { tagName, tagHash } = sortedTags[i];
const el = new Element('li', { const el = new Element("li", {
html: `<a href="#Tag/${tagHash}" onclick="event.preventDefault(); torrentSetTagsFN(${tagHash}, !event.currentTarget.getElement('input[type=checkbox]').checked);">` html: `<a href="#Tag/${tagHash}" onclick="event.preventDefault(); torrentSetTagsFN(${tagHash}, !event.currentTarget.getElement('input[type=checkbox]').checked);">`
+ '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + window.qBittorrent.Misc.escapeHtml(tagName) + '<input type="checkbox" onclick="this.checked = !this.checked;"> ' + window.qBittorrent.Misc.escapeHtml(tagName)
+ '</a>' + "</a>"
}); });
if (i === 0) if (i === 0)
el.addClass('separator'); el.addClass("separator");
contextTagList.appendChild(el); contextTagList.appendChild(el);
} }
} }
@ -513,19 +507,17 @@ window.qBittorrent.ContextMenu = (function() {
updateMenuItems: function() { updateMenuItems: function() {
const id = Number(this.options.element.id); const id = Number(this.options.element.id);
if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) { if ((id !== CATEGORIES_ALL) && (id !== CATEGORIES_UNCATEGORIZED)) {
this.showItem('editCategory'); this.showItem("editCategory");
this.showItem('deleteCategory'); this.showItem("deleteCategory");
if (useSubcategories) { if (useSubcategories)
this.showItem('createSubcategory'); this.showItem("createSubcategory");
else
this.hideItem("createSubcategory");
} }
else { else {
this.hideItem('createSubcategory'); this.hideItem("editCategory");
} this.hideItem("deleteCategory");
} this.hideItem("createSubcategory");
else {
this.hideItem('editCategory');
this.hideItem('deleteCategory');
this.hideItem('createSubcategory');
} }
} }
}); });
@ -535,9 +527,9 @@ window.qBittorrent.ContextMenu = (function() {
updateMenuItems: function() { updateMenuItems: function() {
const id = Number(this.options.element.id); const id = Number(this.options.element.id);
if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED)) if ((id !== TAGS_ALL) && (id !== TAGS_UNTAGGED))
this.showItem('deleteTag'); this.showItem("deleteTag");
else else
this.hideItem('deleteTag'); this.hideItem("deleteTag");
} }
}); });
@ -547,82 +539,83 @@ window.qBittorrent.ContextMenu = (function() {
updateMenuItems: function() { updateMenuItems: function() {
const enabledColumnIndex = function(text) { const enabledColumnIndex = function(text) {
const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th"); const columns = $("searchPluginsTableFixedHeaderRow").getChildren("th");
for (let i = 0; i < columns.length; ++i) for (let i = 0; i < columns.length; ++i) {
if (columns[i].get("html") === "Enabled") if (columns[i].get("html") === "Enabled")
return i; return i;
}
}; };
this.showItem('Enabled'); this.showItem("Enabled");
this.setItemChecked('Enabled', this.options.element.getChildren("td")[enabledColumnIndex()].get("html") === "Yes"); this.setItemChecked("Enabled", this.options.element.getChildren("td")[enabledColumnIndex()].get("html") === "Yes");
this.showItem('Uninstall'); this.showItem("Uninstall");
} }
}); });
const RssFeedContextMenu = new Class({ const RssFeedContextMenu = new Class({
Extends: ContextMenu, Extends: ContextMenu,
updateMenuItems: function() { updateMenuItems: function() {
let selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds(); const selectedRows = window.qBittorrent.Rss.rssFeedTable.selectedRowsIds();
this.menu.getElement('a[href$=newSubscription]').parentNode.addClass('separator'); this.menu.getElement("a[href$=newSubscription]").parentNode.addClass("separator");
switch (selectedRows.length) { switch (selectedRows.length) {
case 0: case 0:
// remove separator on top of newSubscription entry to avoid double line // remove separator on top of newSubscription entry to avoid double line
this.menu.getElement('a[href$=newSubscription]').parentNode.removeClass('separator'); this.menu.getElement("a[href$=newSubscription]").parentNode.removeClass("separator");
// menu when nothing selected // menu when nothing selected
this.hideItem('update'); this.hideItem("update");
this.hideItem('markRead'); this.hideItem("markRead");
this.hideItem('rename'); this.hideItem("rename");
this.hideItem('delete'); this.hideItem("delete");
this.showItem('newSubscription'); this.showItem("newSubscription");
this.showItem('newFolder'); this.showItem("newFolder");
this.showItem('updateAll'); this.showItem("updateAll");
this.hideItem('copyFeedURL'); this.hideItem("copyFeedURL");
break; break;
case 1: case 1:
if (selectedRows[0] === 0) { if (selectedRows[0] === 0) {
// menu when "unread" feed selected // menu when "unread" feed selected
this.showItem('update'); this.showItem("update");
this.showItem('markRead'); this.showItem("markRead");
this.hideItem('rename'); this.hideItem("rename");
this.hideItem('delete'); this.hideItem("delete");
this.showItem('newSubscription'); this.showItem("newSubscription");
this.hideItem('newFolder'); this.hideItem("newFolder");
this.hideItem('updateAll'); this.hideItem("updateAll");
this.hideItem('copyFeedURL'); this.hideItem("copyFeedURL");
} }
else if (window.qBittorrent.Rss.rssFeedTable.rows[selectedRows[0]].full_data.dataUid === '') { else if (window.qBittorrent.Rss.rssFeedTable.rows[selectedRows[0]].full_data.dataUid === "") {
// menu when single folder selected // menu when single folder selected
this.showItem('update'); this.showItem("update");
this.showItem('markRead'); this.showItem("markRead");
this.showItem('rename'); this.showItem("rename");
this.showItem('delete'); this.showItem("delete");
this.showItem('newSubscription'); this.showItem("newSubscription");
this.showItem('newFolder'); this.showItem("newFolder");
this.hideItem('updateAll'); this.hideItem("updateAll");
this.hideItem('copyFeedURL'); this.hideItem("copyFeedURL");
} }
else { else {
// menu when single feed selected // menu when single feed selected
this.showItem('update'); this.showItem("update");
this.showItem('markRead'); this.showItem("markRead");
this.showItem('rename'); this.showItem("rename");
this.showItem('delete'); this.showItem("delete");
this.showItem('newSubscription'); this.showItem("newSubscription");
this.hideItem('newFolder'); this.hideItem("newFolder");
this.hideItem('updateAll'); this.hideItem("updateAll");
this.showItem('copyFeedURL'); this.showItem("copyFeedURL");
} }
break; break;
default: default:
// menu when multiple items selected // menu when multiple items selected
this.showItem('update'); this.showItem("update");
this.showItem('markRead'); this.showItem("markRead");
this.hideItem('rename'); this.hideItem("rename");
this.showItem('delete'); this.showItem("delete");
this.hideItem('newSubscription'); this.hideItem("newSubscription");
this.hideItem('newFolder'); this.hideItem("newFolder");
this.hideItem('updateAll'); this.hideItem("updateAll");
this.showItem('copyFeedURL'); this.showItem("copyFeedURL");
break; break;
} }
} }
@ -639,12 +632,12 @@ window.qBittorrent.ContextMenu = (function() {
// draw the menu off-screen to know the menu dimensions // draw the menu off-screen to know the menu dimensions
this.menu.setStyles({ this.menu.setStyles({
left: '-999em', left: "-999em",
top: '-999em' top: "-999em"
}); });
// position the menu // position the menu
let xPosMenu = e.page.x + this.options.offsets.x - $('rssdownloaderpage').offsetLeft; let xPosMenu = e.page.x + this.options.offsets.x - $("rssdownloaderpage").offsetLeft;
let yPosMenu = e.page.y + this.options.offsets.y - $('rssdownloaderpage').offsetTop; let yPosMenu = e.page.y + this.options.offsets.y - $("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)
@ -655,31 +648,31 @@ window.qBittorrent.ContextMenu = (function() {
this.menu.setStyles({ this.menu.setStyles({
left: xPosMenu, left: xPosMenu,
top: yPosMenu, top: yPosMenu,
position: 'absolute', position: "absolute",
'z-index': '2000' "z-index": "2000"
}); });
}, },
updateMenuItems: function() { updateMenuItems: function() {
let selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds(); const selectedRows = window.qBittorrent.RssDownloader.rssDownloaderRulesTable.selectedRowsIds();
this.showItem('addRule'); this.showItem("addRule");
switch (selectedRows.length) { switch (selectedRows.length) {
case 0: case 0:
// menu when nothing selected // menu when nothing selected
this.hideItem('deleteRule'); this.hideItem("deleteRule");
this.hideItem('renameRule'); this.hideItem("renameRule");
this.hideItem('clearDownloadedEpisodes'); this.hideItem("clearDownloadedEpisodes");
break; break;
case 1: case 1:
// menu when single item selected // menu when single item selected
this.showItem('deleteRule'); this.showItem("deleteRule");
this.showItem('renameRule'); this.showItem("renameRule");
this.showItem('clearDownloadedEpisodes'); this.showItem("clearDownloadedEpisodes");
break; break;
default: default:
// menu when multiple items selected // menu when multiple items selected
this.showItem('deleteRule'); this.showItem("deleteRule");
this.hideItem('renameRule'); this.hideItem("renameRule");
this.showItem('clearDownloadedEpisodes'); this.showItem("clearDownloadedEpisodes");
break; break;
} }
} }

View file

@ -21,11 +21,10 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.Download = (function() { window.qBittorrent.Download = (function() {
const exports = function() { const exports = function() {
@ -40,18 +39,21 @@ window.qBittorrent.Download = (function() {
const getCategories = function() { const getCategories = function() {
new Request.JSON({ new Request.JSON({
url: 'api/v2/torrents/categories', url: "api/v2/torrents/categories",
method: 'get', method: "get",
noCache: true, noCache: true,
onSuccess: function(data) { onSuccess: function(data) {
if (data) { if (data) {
categories = data; categories = data;
for (const i in data) { for (const i in data) {
if (!Object.hasOwn(data, i))
continue;
const category = data[i]; const category = data[i];
const option = new Element("option"); const option = new Element("option");
option.set('value', category.name); option.set("value", category.name);
option.set('html', category.name); option.set("html", category.name);
$('categorySelect').appendChild(option); $("categorySelect").appendChild(option);
} }
} }
} }
@ -62,37 +64,31 @@ window.qBittorrent.Download = (function() {
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').setProperty('value', defaultSavePath); $("savepath").setProperty("value", defaultSavePath);
$('startTorrent').checked = !pref.add_stopped_enabled; $("startTorrent").checked = !pref.add_stopped_enabled;
$('addToTopOfQueue').checked = pref.add_to_top_of_queue; $("addToTopOfQueue").checked = pref.add_to_top_of_queue;
if (pref.auto_tmm_enabled === 1) { if (pref.auto_tmm_enabled) {
$('autoTMM').selectedIndex = 1; $("autoTMM").selectedIndex = 1;
$('savepath').disabled = true; $("savepath").disabled = true;
} }
else { else {
$('autoTMM').selectedIndex = 0; $("autoTMM").selectedIndex = 0;
} }
if (pref.torrent_stop_condition === "MetadataReceived") { if (pref.torrent_stop_condition === "MetadataReceived")
$('stopCondition').selectedIndex = 1; $("stopCondition").selectedIndex = 1;
} else if (pref.torrent_stop_condition === "FilesChecked")
else if (pref.torrent_stop_condition === "FilesChecked") { $("stopCondition").selectedIndex = 2;
$('stopCondition').selectedIndex = 2; else
} $("stopCondition").selectedIndex = 0;
else {
$('stopCondition').selectedIndex = 0;
}
if (pref.torrent_content_layout === "Subfolder") { if (pref.torrent_content_layout === "Subfolder")
$('contentLayout').selectedIndex = 1; $("contentLayout").selectedIndex = 1;
} else if (pref.torrent_content_layout === "NoSubfolder")
else if (pref.torrent_content_layout === "NoSubfolder") { $("contentLayout").selectedIndex = 2;
$('contentLayout').selectedIndex = 2; else
} $("contentLayout").selectedIndex = 0;
else {
$('contentLayout').selectedIndex = 0;
}
}; };
const changeCategorySelect = function(item) { const changeCategorySelect = function(item) {
@ -101,41 +97,41 @@ window.qBittorrent.Download = (function() {
item.nextElementSibling.value = ""; item.nextElementSibling.value = "";
item.nextElementSibling.select(); item.nextElementSibling.select();
if ($('autoTMM').selectedIndex === 1) if ($("autoTMM").selectedIndex === 1)
$('savepath').value = defaultSavePath; $("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 ($("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; $("savepath").value = savePath;
} }
} }
}; };
const changeTMM = function(item) { const changeTMM = function(item) {
if (item.selectedIndex === 1) { if (item.selectedIndex === 1) {
$('savepath').disabled = true; $("savepath").disabled = true;
const categorySelect = $('categorySelect'); const categorySelect = $("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']; $("savepath").value = (category === undefined) ? "" : category["savePath"];
} }
else { else {
$('savepath').disabled = false; $("savepath").disabled = false;
$('savepath').value = defaultSavePath; $("savepath").value = defaultSavePath;
} }
}; };
$(window).addEventListener("load", function() { $(window).addEventListener("load", () => {
getPreferences(); getPreferences();
getCategories(); getCategories();
}); });

File diff suppressed because it is too large Load diff

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.FileTree = (function() { window.qBittorrent.FileTree = (function() {
const exports = function() { const exports = function() {
@ -77,13 +76,12 @@ window.qBittorrent.FileTree = (function() {
generateNodeMap: function(node) { generateNodeMap: function(node) {
// 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;
}
node.children.each(function(child) { node.children.each((child) => {
this.generateNodeMap(child); this.generateNodeMap(child);
}.bind(this)); });
}, },
getNode: function(rowId) { getNode: function(rowId) {
@ -101,17 +99,17 @@ window.qBittorrent.FileTree = (function() {
*/ */
toArray: function() { toArray: function() {
const nodes = []; const nodes = [];
this.root.children.each(function(node) { this.root.children.each((node) => {
this._getArrayOfNodes(node, nodes); this._getArrayOfNodes(node, nodes);
}.bind(this)); });
return nodes; return nodes;
}, },
_getArrayOfNodes: function(node, array) { _getArrayOfNodes: function(node, array) {
array.push(node); array.push(node);
node.children.each(function(child) { node.children.each((child) => {
this._getArrayOfNodes(child, array); this._getArrayOfNodes(child, array);
}.bind(this)); });
} }
}); });
@ -161,7 +159,7 @@ window.qBittorrent.FileTree = (function() {
let isFirstFile = true; let isFirstFile = true;
this.children.each(function(node) { this.children.each((node) => {
if (node.isFolder) if (node.isFolder)
node.calculateSize(); node.calculateSize();
@ -185,7 +183,7 @@ window.qBittorrent.FileTree = (function() {
progress += (node.progress * node.size); progress += (node.progress * node.size);
availability += (node.availability * node.size); availability += (node.availability * node.size);
} }
}.bind(this)); });
this.size = size; this.size = size;
this.remaining = remaining; this.remaining = remaining;

View file

@ -26,13 +26,12 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
// This file is the JavaScript implementation of base/utils/fs.cpp // This file is the JavaScript implementation of base/utils/fs.cpp
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.Filesystem = (function() { window.qBittorrent.Filesystem = (function() {
const exports = function() { const exports = function() {
@ -44,15 +43,15 @@ window.qBittorrent.Filesystem = (function() {
}; };
}; };
const PathSeparator = '/'; const PathSeparator = "/";
/** /**
* Returns the file extension part of a file name. * Returns the file extension part of a file name.
*/ */
const fileExtension = function(filename) { const fileExtension = function(filename) {
const pointIndex = filename.lastIndexOf('.'); const pointIndex = filename.lastIndexOf(".");
if (pointIndex === -1) if (pointIndex === -1)
return ''; return "";
return filename.substring(pointIndex + 1); return filename.substring(pointIndex + 1);
}; };
@ -66,7 +65,7 @@ window.qBittorrent.Filesystem = (function() {
const folderName = function(filepath) { const folderName = function(filepath) {
const slashIndex = filepath.lastIndexOf(PathSeparator); const slashIndex = filepath.lastIndexOf(PathSeparator);
if (slashIndex === -1) if (slashIndex === -1)
return ''; return "";
return filepath.substring(0, slashIndex); return filepath.substring(0, slashIndex);
}; };

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.LocalPreferences = (function() { window.qBittorrent.LocalPreferences = (function() {
const exports = function() { const exports = function() {

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.Misc = (function() { window.qBittorrent.Misc = (function() {
const exports = function() { const exports = function() {
@ -86,8 +85,9 @@ window.qBittorrent.Misc = (function() {
} }
let ret; let ret;
if (i === 0) if (i === 0) {
ret = value + " " + units[i]; ret = value + " " + units[i];
}
else { else {
const precision = friendlyUnitPrecision(i); const precision = friendlyUnitPrecision(i);
const offset = Math.pow(10, precision); const offset = Math.pow(10, precision);
@ -112,18 +112,18 @@ window.qBittorrent.Misc = (function() {
return "QBT_TR(< 1m)QBT_TR[CONTEXT=misc]"; return "QBT_TR(< 1m)QBT_TR[CONTEXT=misc]";
let minutes = seconds / 60; let minutes = seconds / 60;
if (minutes < 60) if (minutes < 60)
return "QBT_TR(%1m)QBT_TR[CONTEXT=misc]".replace("%1", parseInt(minutes)); return "QBT_TR(%1m)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(minutes));
let hours = minutes / 60; let hours = minutes / 60;
minutes = minutes % 60; minutes %= 60;
if (hours < 24) if (hours < 24)
return "QBT_TR(%1h %2m)QBT_TR[CONTEXT=misc]".replace("%1", parseInt(hours)).replace("%2", parseInt(minutes)); return "QBT_TR(%1h %2m)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(hours)).replace("%2", Math.floor(minutes));
let days = hours / 24; let days = hours / 24;
hours = hours % 24; hours %= 24;
if (days < 365) if (days < 365)
return "QBT_TR(%1d %2h)QBT_TR[CONTEXT=misc]".replace("%1", parseInt(days)).replace("%2", parseInt(hours)); return "QBT_TR(%1d %2h)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(days)).replace("%2", Math.floor(hours));
const years = days / 365; const years = days / 365;
days = days % 365; days %= 365;
return "QBT_TR(%1y %2d)QBT_TR[CONTEXT=misc]".replace("%1", parseInt(years)).replace("%2", parseInt(days)); return "QBT_TR(%1y %2d)QBT_TR[CONTEXT=misc]".replace("%1", Math.floor(years)).replace("%2", Math.floor(days));
}; };
const friendlyPercentage = function(value) { const friendlyPercentage = function(value) {
@ -152,7 +152,7 @@ window.qBittorrent.Misc = (function() {
valid: false valid: false
}; };
if (typeof versionString !== 'string') if (typeof versionString !== "string")
return failure; return failure;
const tryToNumber = (str) => { const tryToNumber = (str) => {
@ -160,7 +160,7 @@ window.qBittorrent.Misc = (function() {
return (isNaN(num) ? str : num); return (isNaN(num) ? str : num);
}; };
const ver = versionString.split('.', 4).map(val => tryToNumber(val)); const ver = versionString.split(".", 4).map(val => tryToNumber(val));
return { return {
valid: true, valid: true,
major: ver[0], major: ver[0],
@ -171,7 +171,7 @@ window.qBittorrent.Misc = (function() {
}; };
const escapeHtml = function(str) { const escapeHtml = function(str) {
const div = document.createElement('div'); const div = document.createElement("div");
div.appendChild(document.createTextNode(str)); div.appendChild(document.createTextNode(str));
const escapedString = div.innerHTML; const escapedString = div.innerHTML;
div.remove(); div.remove();
@ -179,7 +179,7 @@ window.qBittorrent.Misc = (function() {
}; };
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#parameters // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#parameters
const naturalSortCollator = new Intl.Collator(undefined, { numeric: true, usage: 'sort' }); const naturalSortCollator = new Intl.Collator(undefined, { numeric: true, usage: "sort" });
const safeTrim = function(value) { const safeTrim = function(value) {
try { try {
@ -206,9 +206,9 @@ window.qBittorrent.Misc = (function() {
*/ */
const containsAllTerms = function(text, terms) { const containsAllTerms = function(text, terms) {
const textToSearch = text.toLowerCase(); const textToSearch = text.toLowerCase();
return terms.every(function(term) { return terms.every((term) => {
const isTermRequired = (term[0] === '+'); const isTermRequired = (term[0] === "+");
const isTermExcluded = (term[0] === '-'); const isTermExcluded = (term[0] === "-");
if (isTermRequired || isTermExcluded) { if (isTermRequired || isTermExcluded) {
// ignore lonely +/- // ignore lonely +/-
if (term.length === 1) if (term.length === 1)

View file

@ -36,7 +36,7 @@
it in the onContentLoaded function of the new window. it in the onContentLoaded function of the new window.
----------------------------------------------------------------- */ ----------------------------------------------------------------- */
'use strict'; "use strict";
const LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferencesClass(); const LocalPreferences = new window.qBittorrent.LocalPreferences.LocalPreferencesClass();
@ -96,45 +96,43 @@ let exportTorrentFN = function() {};
const initializeWindows = function() { const initializeWindows = function() {
saveWindowSize = function(windowId) { saveWindowSize = function(windowId) {
const size = $(windowId).getSize(); const size = $(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);
}; };
loadWindowWidth = function(windowId, defaultValue) { loadWindowWidth = function(windowId, defaultValue) {
return LocalPreferences.get('window_' + windowId + '_width', defaultValue); return LocalPreferences.get("window_" + windowId + "_width", defaultValue);
}; };
loadWindowHeight = function(windowId, defaultValue) { loadWindowHeight = function(windowId, defaultValue) {
return LocalPreferences.get('window_' + windowId + '_height', defaultValue); return LocalPreferences.get("window_" + windowId + "_height", defaultValue);
}; };
function addClickEvent(el, fn) { function addClickEvent(el, fn) {
['Link', 'Button'].each(function(item) { ["Link", "Button"].each((item) => {
if ($(el + item)) { if ($(el + item))
$(el + item).addEvent('click', fn); $(el + item).addEvent("click", fn);
}
}); });
} }
addClickEvent('download', function(e) { addClickEvent("download", (e) => {
new Event(e).stop(); new Event(e).stop();
showDownloadPage(); showDownloadPage();
}); });
showDownloadPage = function(urls) { showDownloadPage = function(urls) {
const id = 'downloadPage'; const id = "downloadPage";
let contentUri = new URI('download.html'); const contentUri = new URI("download.html");
if (urls && (urls.length > 0)) { if (urls && (urls.length > 0))
contentUri.setData("urls", urls.map(encodeURIComponent).join("|")); contentUri.setData("urls", urls.map(encodeURIComponent).join("|"));
}
new MochaUI.Window({ new MochaUI.Window({
id: id, id: id,
title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]", title: "QBT_TR(Download from URLs)QBT_TR[CONTEXT=downloadFromURL]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: contentUri.toString(), contentURL: contentUri.toString(),
addClass: 'windowFrame', // fixes iframe scrolling on iOS Safari addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
scrollbars: true, scrollbars: true,
maximizable: false, maximizable: false,
closable: true, closable: true,
@ -149,19 +147,19 @@ const initializeWindows = function() {
updateMainData(); updateMainData();
}; };
addClickEvent('preferences', function(e) { addClickEvent("preferences", (e) => {
new Event(e).stop(); new Event(e).stop();
const id = 'preferencesPage'; const id = "preferencesPage";
new MochaUI.Window({ new MochaUI.Window({
id: id, id: id,
title: "QBT_TR(Options)QBT_TR[CONTEXT=OptionsDialog]", title: "QBT_TR(Options)QBT_TR[CONTEXT=OptionsDialog]",
loadMethod: 'xhr', loadMethod: "xhr",
toolbar: true, toolbar: true,
contentURL: new URI("views/preferences.html").toString(), contentURL: new URI("views/preferences.html").toString(),
require: { require: {
css: ['css/Tabs.css'] css: ["css/Tabs.css"]
}, },
toolbarURL: 'views/preferencesToolbar.html', toolbarURL: "views/preferencesToolbar.html",
maximizable: false, maximizable: false,
closable: true, closable: true,
paddingVertical: 0, paddingVertical: 0,
@ -174,15 +172,15 @@ const initializeWindows = function() {
}); });
}); });
addClickEvent('upload', function(e) { addClickEvent("upload", (e) => {
new Event(e).stop(); new Event(e).stop();
const id = 'uploadPage'; const id = "uploadPage";
new MochaUI.Window({ new MochaUI.Window({
id: id, id: id,
title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]", title: "QBT_TR(Upload local torrent)QBT_TR[CONTEXT=HttpServer]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("upload.html").toString(), contentURL: new URI("upload.html").toString(),
addClass: 'windowFrame', // fixes iframe scrolling on iOS Safari addClass: "windowFrame", // fixes iframe scrolling on iOS Safari
scrollbars: true, scrollbars: true,
maximizable: false, maximizable: false,
paddingVertical: 0, paddingVertical: 0,
@ -198,9 +196,9 @@ const initializeWindows = function() {
globalUploadLimitFN = function() { globalUploadLimitFN = function() {
new MochaUI.Window({ new MochaUI.Window({
id: 'uploadLimitPage', id: "uploadLimitPage",
title: "QBT_TR(Global Upload Speed Limit)QBT_TR[CONTEXT=MainWindow]", title: "QBT_TR(Global Upload Speed Limit)QBT_TR[CONTEXT=MainWindow]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("uploadlimit.html").setData("hashes", "global").toString(), contentURL: new URI("uploadlimit.html").setData("hashes", "global").toString(),
scrollbars: false, scrollbars: false,
resizable: false, resizable: false,
@ -216,9 +214,9 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'uploadLimitPage', id: "uploadLimitPage",
title: "QBT_TR(Torrent Upload Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Torrent Upload Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("uploadlimit.html").setData("hashes", hashes.join("|")).toString(), contentURL: new URI("uploadlimit.html").setData("hashes", hashes.join("|")).toString(),
scrollbars: false, scrollbars: false,
resizable: false, resizable: false,
@ -257,16 +255,16 @@ const initializeWindows = function() {
// if all torrents have same share ratio, display that share ratio. else use the default // if all torrents have same share ratio, display that share ratio. else use the default
const orig = torrentsHaveSameShareRatio ? shareRatio : ""; const orig = torrentsHaveSameShareRatio ? shareRatio : "";
new MochaUI.Window({ new MochaUI.Window({
id: 'shareRatioPage', id: "shareRatioPage",
title: "QBT_TR(Torrent Upload/Download Ratio Limiting)QBT_TR[CONTEXT=UpDownRatioDialog]", title: "QBT_TR(Torrent Upload/Download Ratio Limiting)QBT_TR[CONTEXT=UpDownRatioDialog]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("shareratio.html").setData("hashes", hashes.join("|")).setData("orig", orig).toString(), contentURL: new URI("shareratio.html").setData("hashes", hashes.join("|")).setData("orig", orig).toString(),
scrollbars: false, scrollbars: false,
maximizable: false, maximizable: false,
paddingVertical: 0, paddingVertical: 0,
paddingHorizontal: 0, paddingHorizontal: 0,
width: 424, width: 424,
height: 175 height: 200
}); });
} }
}; };
@ -275,8 +273,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/toggleSequentialDownload', url: "api/v2/torrents/toggleSequentialDownload",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -289,8 +287,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/toggleFirstLastPiecePrio', url: "api/v2/torrents/toggleFirstLastPiecePrio",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -303,8 +301,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/setSuperSeeding', url: "api/v2/torrents/setSuperSeeding",
method: 'post', method: "post",
data: { data: {
value: val, value: val,
hashes: hashes.join("|") hashes: hashes.join("|")
@ -318,10 +316,10 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/setForceStart', url: "api/v2/torrents/setForceStart",
method: 'post', method: "post",
data: { data: {
value: 'true', value: "true",
hashes: hashes.join("|") hashes: hashes.join("|")
} }
}).send(); }).send();
@ -331,9 +329,9 @@ const initializeWindows = function() {
globalDownloadLimitFN = function() { globalDownloadLimitFN = function() {
new MochaUI.Window({ new MochaUI.Window({
id: 'downloadLimitPage', id: "downloadLimitPage",
title: "QBT_TR(Global Download Speed Limit)QBT_TR[CONTEXT=MainWindow]", title: "QBT_TR(Global Download Speed Limit)QBT_TR[CONTEXT=MainWindow]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("downloadlimit.html").setData("hashes", "global").toString(), contentURL: new URI("downloadlimit.html").setData("hashes", "global").toString(),
scrollbars: false, scrollbars: false,
resizable: false, resizable: false,
@ -346,11 +344,11 @@ const initializeWindows = function() {
}; };
StatisticsLinkFN = function() { StatisticsLinkFN = function() {
const id = 'statisticspage'; const id = "statisticspage";
new MochaUI.Window({ new MochaUI.Window({
id: id, id: id,
title: 'QBT_TR(Statistics)QBT_TR[CONTEXT=StatsDialog]', title: "QBT_TR(Statistics)QBT_TR[CONTEXT=StatsDialog]",
loadMethod: 'xhr', loadMethod: "xhr",
contentURL: new URI("views/statistics.html").toString(), contentURL: new URI("views/statistics.html").toString(),
maximizable: false, maximizable: false,
padding: 10, padding: 10,
@ -366,9 +364,9 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'downloadLimitPage', id: "downloadLimitPage",
title: "QBT_TR(Torrent Download Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Torrent Download Speed Limiting)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("downloadlimit.html").setData("hashes", hashes.join("|")).toString(), contentURL: new URI("downloadlimit.html").setData("hashes", hashes.join("|")).toString(),
scrollbars: false, scrollbars: false,
resizable: false, resizable: false,
@ -385,9 +383,9 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'confirmDeletionPage', id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]", title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).setData("deleteFiles", deleteFiles).toString(), contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).setData("deleteFiles", deleteFiles).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -400,7 +398,7 @@ const initializeWindows = function() {
} }
}; };
addClickEvent('delete', function(e) { addClickEvent("delete", (e) => {
new Event(e).stop(); new Event(e).stop();
deleteFN(); deleteFN();
}); });
@ -409,8 +407,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/stop', url: "api/v2/torrents/stop",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -423,8 +421,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/start', url: "api/v2/torrents/start",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -437,14 +435,14 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
let enable = false; let enable = false;
hashes.each(function(hash, index) { hashes.each((hash, index) => {
const row = torrentsTable.rows[hash]; const row = torrentsTable.rows[hash];
if (!row.full_data.auto_tmm) if (!row.full_data.auto_tmm)
enable = true; enable = true;
}); });
new Request({ new Request({
url: 'api/v2/torrents/setAutoManagement', url: "api/v2/torrents/setAutoManagement",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|"), hashes: hashes.join("|"),
enable: enable enable: enable
@ -458,8 +456,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/recheck', url: "api/v2/torrents/recheck",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|"), hashes: hashes.join("|"),
} }
@ -472,8 +470,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/reannounce', url: "api/v2/torrents/reannounce",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|"), hashes: hashes.join("|"),
} }
@ -489,10 +487,10 @@ const initializeWindows = function() {
const row = torrentsTable.rows[hash]; const row = torrentsTable.rows[hash];
new MochaUI.Window({ new MochaUI.Window({
id: 'setLocationPage', id: "setLocationPage",
title: "QBT_TR(Set location)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Set location)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("setlocation.html").setData("hashes", hashes.join('|')).setData("path", encodeURIComponent(row.full_data.save_path)).toString(), contentURL: new URI("setlocation.html").setData("hashes", hashes.join("|")).setData("path", encodeURIComponent(row.full_data.save_path)).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
maximizable: false, maximizable: false,
@ -511,9 +509,9 @@ const initializeWindows = function() {
const row = torrentsTable.rows[hash]; const row = torrentsTable.rows[hash];
if (row) { if (row) {
new MochaUI.Window({ new MochaUI.Window({
id: 'renamePage', id: "renamePage",
title: "QBT_TR(Rename)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Rename)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("rename.html").setData("hash", hash).setData("name", row.full_data.name).toString(), contentURL: new URI("rename.html").setData("hash", hash).setData("name", row.full_data.name).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -534,11 +532,11 @@ const initializeWindows = function() {
const row = torrentsTable.rows[hash]; const row = torrentsTable.rows[hash];
if (row) { if (row) {
new MochaUI.Window({ new MochaUI.Window({
id: 'multiRenamePage', id: "multiRenamePage",
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TransferListWidget]",
data: { hash: hash, selectedRows: [] }, data: { hash: hash, selectedRows: [] },
loadMethod: 'xhr', loadMethod: "xhr",
contentURL: 'rename_files.html', contentURL: "rename_files.html",
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
maximizable: false, maximizable: false,
@ -546,7 +544,7 @@ const initializeWindows = function() {
paddingHorizontal: 0, paddingHorizontal: 0,
width: 800, width: 800,
height: 420, height: 420,
resizeLimit: { 'x': [800], 'y': [420] } resizeLimit: { "x": [800], "y": [420] }
}); });
} }
} }
@ -557,10 +555,10 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'newCategoryPage', id: "newCategoryPage",
title: "QBT_TR(New Category)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(New Category)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("newcategory.html").setData("action", action).setData("hashes", hashes.join('|')).toString(), contentURL: new URI("newcategory.html").setData("action", action).setData("hashes", hashes.join("|")).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
maximizable: false, maximizable: false,
@ -579,10 +577,10 @@ const initializeWindows = function() {
const categoryName = category_list.has(categoryHash) const categoryName = category_list.has(categoryHash)
? category_list.get(categoryHash).name ? category_list.get(categoryHash).name
: ''; : "";
new Request({ new Request({
url: 'api/v2/torrents/setCategory', url: "api/v2/torrents/setCategory",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|"), hashes: hashes.join("|"),
category: categoryName category: categoryName
@ -593,9 +591,9 @@ const initializeWindows = function() {
createCategoryFN = function() { createCategoryFN = function() {
const action = "create"; const action = "create";
new MochaUI.Window({ new MochaUI.Window({
id: 'newCategoryPage', id: "newCategoryPage",
title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]", title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("newcategory.html").setData("action", action).toString(), contentURL: new URI("newcategory.html").setData("action", action).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -612,9 +610,9 @@ const initializeWindows = function() {
const action = "createSubcategory"; const action = "createSubcategory";
const categoryName = category_list.get(categoryHash).name + "/"; const categoryName = category_list.get(categoryHash).name + "/";
new MochaUI.Window({ new MochaUI.Window({
id: 'newSubcategoryPage', id: "newSubcategoryPage",
title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]", title: "QBT_TR(New Category)QBT_TR[CONTEXT=CategoryFilterWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("newcategory.html").setData("action", action).setData("categoryName", categoryName).toString(), contentURL: new URI("newcategory.html").setData("action", action).setData("categoryName", categoryName).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -631,10 +629,10 @@ const initializeWindows = function() {
const action = "edit"; const action = "edit";
const category = category_list.get(categoryHash); const category = category_list.get(categoryHash);
new MochaUI.Window({ new MochaUI.Window({
id: 'editCategoryPage', id: "editCategoryPage",
title: "QBT_TR(Edit Category)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Edit Category)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI('newcategory.html').setData("action", action).setData("categoryName", category.name).setData("savePath", category.savePath).toString(), contentURL: new URI("newcategory.html").setData("action", action).setData("categoryName", category.name).setData("savePath", category.savePath).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
maximizable: false, maximizable: false,
@ -649,8 +647,8 @@ const initializeWindows = function() {
removeCategoryFN = function(categoryHash) { removeCategoryFN = function(categoryHash) {
const categoryName = category_list.get(categoryHash).name; const categoryName = category_list.get(categoryHash).name;
new Request({ new Request({
url: 'api/v2/torrents/removeCategories', url: "api/v2/torrents/removeCategories",
method: 'post', method: "post",
data: { data: {
categories: categoryName categories: categoryName
} }
@ -661,26 +659,26 @@ const initializeWindows = function() {
deleteUnusedCategoriesFN = function() { deleteUnusedCategoriesFN = function() {
const categories = []; const categories = [];
category_list.forEach((category, hash) => { category_list.forEach((category, hash) => {
if (torrentsTable.getFilteredTorrentsNumber('all', hash, TAGS_ALL, TRACKERS_ALL) === 0) if (torrentsTable.getFilteredTorrentsNumber("all", hash, TAGS_ALL, TRACKERS_ALL) === 0)
categories.push(category.name); categories.push(category.name);
}); });
new Request({ new Request({
url: 'api/v2/torrents/removeCategories', url: "api/v2/torrents/removeCategories",
method: 'post', method: "post",
data: { data: {
categories: categories.join('\n') categories: categories.join("\n")
} }
}).send(); }).send();
setCategoryFilter(CATEGORIES_ALL); setCategoryFilter(CATEGORIES_ALL);
}; };
startTorrentsByCategoryFN = function(categoryHash) { startTorrentsByCategoryFN = function(categoryHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash, TAGS_ALL, TRACKERS_ALL); const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/start', url: "api/v2/torrents/start",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -690,11 +688,11 @@ const initializeWindows = function() {
}; };
stopTorrentsByCategoryFN = function(categoryHash) { stopTorrentsByCategoryFN = function(categoryHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash, TAGS_ALL, TRACKERS_ALL); const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/stop', url: "api/v2/torrents/stop",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -704,12 +702,12 @@ const initializeWindows = function() {
}; };
deleteTorrentsByCategoryFN = function(categoryHash) { deleteTorrentsByCategoryFN = function(categoryHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', categoryHash, TAGS_ALL, TRACKERS_ALL); const hashes = torrentsTable.getFilteredTorrentsHashes("all", categoryHash, TAGS_ALL, TRACKERS_ALL);
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'confirmDeletionPage', id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]", title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).toString(), contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -727,9 +725,9 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'newTagPage', id: "newTagPage",
title: "QBT_TR(Add Tags)QBT_TR[CONTEXT=TransferListWidget]", title: "QBT_TR(Add Tags)QBT_TR[CONTEXT=TransferListWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("newtag.html").setData("action", action).setData("hashes", hashes.join("|")).toString(), contentURL: new URI("newtag.html").setData("action", action).setData("hashes", hashes.join("|")).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -747,10 +745,10 @@ const initializeWindows = function() {
if (hashes.length <= 0) if (hashes.length <= 0)
return; return;
const tagName = tagList.has(tagHash) ? tagList.get(tagHash).name : ''; const tagName = tagList.has(tagHash) ? tagList.get(tagHash).name : "";
new Request({ new Request({
url: (isSet ? 'api/v2/torrents/addTags' : 'api/v2/torrents/removeTags'), url: (isSet ? "api/v2/torrents/addTags" : "api/v2/torrents/removeTags"),
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|"), hashes: hashes.join("|"),
tags: tagName, tags: tagName,
@ -762,8 +760,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: ('api/v2/torrents/removeTags'), url: ("api/v2/torrents/removeTags"),
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|"), hashes: hashes.join("|"),
} }
@ -774,9 +772,9 @@ const initializeWindows = function() {
createTagFN = function() { createTagFN = function() {
const action = "create"; const action = "create";
new MochaUI.Window({ new MochaUI.Window({
id: 'newTagPage', id: "newTagPage",
title: "QBT_TR(New Tag)QBT_TR[CONTEXT=TagFilterWidget]", title: "QBT_TR(New Tag)QBT_TR[CONTEXT=TagFilterWidget]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("newtag.html").setData("action", action).toString(), contentURL: new URI("newtag.html").setData("action", action).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -792,8 +790,8 @@ const initializeWindows = function() {
removeTagFN = function(tagHash) { removeTagFN = function(tagHash) {
const tagName = tagList.get(tagHash).name; const tagName = tagList.get(tagHash).name;
new Request({ new Request({
url: 'api/v2/torrents/deleteTags', url: "api/v2/torrents/deleteTags",
method: 'post', method: "post",
data: { data: {
tags: tagName tags: tagName
} }
@ -804,25 +802,25 @@ const initializeWindows = function() {
deleteUnusedTagsFN = function() { deleteUnusedTagsFN = function() {
const tags = []; const tags = [];
tagList.forEach((tag, hash) => { tagList.forEach((tag, hash) => {
if (torrentsTable.getFilteredTorrentsNumber('all', CATEGORIES_ALL, hash, TRACKERS_ALL) === 0) if (torrentsTable.getFilteredTorrentsNumber("all", CATEGORIES_ALL, hash, TRACKERS_ALL) === 0)
tags.push(tag.name); tags.push(tag.name);
}); });
new Request({ new Request({
url: 'api/v2/torrents/deleteTags', url: "api/v2/torrents/deleteTags",
method: 'post', method: "post",
data: { data: {
tags: tags.join(',') tags: tags.join(",")
} }
}).send(); }).send();
setTagFilter(TAGS_ALL); setTagFilter(TAGS_ALL);
}; };
startTorrentsByTagFN = function(tagHash) { startTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, tagHash, TRACKERS_ALL); const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/start', url: "api/v2/torrents/start",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -832,11 +830,11 @@ const initializeWindows = function() {
}; };
stopTorrentsByTagFN = function(tagHash) { stopTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, tagHash, TRACKERS_ALL); const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/stop', url: "api/v2/torrents/stop",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -846,12 +844,12 @@ const initializeWindows = function() {
}; };
deleteTorrentsByTagFN = function(tagHash) { deleteTorrentsByTagFN = function(tagHash) {
const hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, tagHash, TRACKERS_ALL); const hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, tagHash, TRACKERS_ALL);
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'confirmDeletionPage', id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]", title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).toString(), contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -869,18 +867,17 @@ const initializeWindows = function() {
let hashes = []; let hashes = [];
switch (trackerHashInt) { switch (trackerHashInt) {
case TRACKERS_ALL: case TRACKERS_ALL:
hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL); hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
break; break;
case TRACKERS_TRACKERLESS: case TRACKERS_TRACKERLESS:
hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS); hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
break; break;
default: { default: {
const uniqueTorrents = new Set(); const uniqueTorrents = new Set();
for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) { for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
for (const torrent of torrents) { for (const torrent of torrents)
uniqueTorrents.add(torrent); uniqueTorrents.add(torrent);
} }
}
hashes = [...uniqueTorrents]; hashes = [...uniqueTorrents];
break; break;
} }
@ -888,8 +885,8 @@ const initializeWindows = function() {
if (hashes.length > 0) { if (hashes.length > 0) {
new Request({ new Request({
url: 'api/v2/torrents/start', url: "api/v2/torrents/start",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -903,18 +900,17 @@ const initializeWindows = function() {
let hashes = []; let hashes = [];
switch (trackerHashInt) { switch (trackerHashInt) {
case TRACKERS_ALL: case TRACKERS_ALL:
hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL); hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
break; break;
case TRACKERS_TRACKERLESS: case TRACKERS_TRACKERLESS:
hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS); hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
break; break;
default: { default: {
const uniqueTorrents = new Set(); const uniqueTorrents = new Set();
for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) { for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
for (const torrent of torrents) { for (const torrent of torrents)
uniqueTorrents.add(torrent); uniqueTorrents.add(torrent);
} }
}
hashes = [...uniqueTorrents]; hashes = [...uniqueTorrents];
break; break;
} }
@ -922,8 +918,8 @@ const initializeWindows = function() {
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/stop', url: "api/v2/torrents/stop",
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -937,18 +933,17 @@ const initializeWindows = function() {
let hashes = []; let hashes = [];
switch (trackerHashInt) { switch (trackerHashInt) {
case TRACKERS_ALL: case TRACKERS_ALL:
hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL); hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_ALL);
break; break;
case TRACKERS_TRACKERLESS: case TRACKERS_TRACKERLESS:
hashes = torrentsTable.getFilteredTorrentsHashes('all', CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS); hashes = torrentsTable.getFilteredTorrentsHashes("all", CATEGORIES_ALL, TAGS_ALL, TRACKERS_TRACKERLESS);
break; break;
default: { default: {
const uniqueTorrents = new Set(); const uniqueTorrents = new Set();
for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) { for (const torrents of trackerList.get(trackerHashInt).trackerTorrentMap.values()) {
for (const torrent of torrents) { for (const torrent of torrents)
uniqueTorrents.add(torrent); uniqueTorrents.add(torrent);
} }
}
hashes = [...uniqueTorrents]; hashes = [...uniqueTorrents];
break; break;
} }
@ -956,9 +951,9 @@ const initializeWindows = function() {
if (hashes.length) { if (hashes.length) {
new MochaUI.Window({ new MochaUI.Window({
id: 'confirmDeletionPage', id: "confirmDeletionPage",
title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]", title: "QBT_TR(Remove torrent(s))QBT_TR[CONTEXT=confirmDeletionDlg]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).toString(), contentURL: new URI("confirmdeletion.html").setData("hashes", hashes.join("|")).toString(),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
@ -1068,13 +1063,13 @@ const initializeWindows = function() {
} }
}; };
addClickEvent('stopAll', (e) => { addClickEvent("stopAll", (e) => {
new Event(e).stop(); new Event(e).stop();
if (confirm('QBT_TR(Would you like to stop all torrents?)QBT_TR[CONTEXT=MainWindow]')) { if (confirm("QBT_TR(Would you like to stop all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
new Request({ new Request({
url: 'api/v2/torrents/stop', url: "api/v2/torrents/stop",
method: 'post', method: "post",
data: { data: {
hashes: "all" hashes: "all"
} }
@ -1083,13 +1078,13 @@ const initializeWindows = function() {
} }
}); });
addClickEvent('startAll', (e) => { addClickEvent("startAll", (e) => {
new Event(e).stop(); new Event(e).stop();
if (confirm('QBT_TR(Would you like to start all torrents?)QBT_TR[CONTEXT=MainWindow]')) { if (confirm("QBT_TR(Would you like to start all torrents?)QBT_TR[CONTEXT=MainWindow]")) {
new Request({ new Request({
url: 'api/v2/torrents/start', url: "api/v2/torrents/start",
method: 'post', method: "post",
data: { data: {
hashes: "all" hashes: "all"
} }
@ -1098,15 +1093,15 @@ const initializeWindows = function() {
} }
}); });
['stop', 'start', 'recheck'].each(function(item) { ["stop", "start", "recheck"].each((item) => {
addClickEvent(item, function(e) { addClickEvent(item, (e) => {
new Event(e).stop(); new Event(e).stop();
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
hashes.each(function(hash, index) { hashes.each((hash, index) => {
new Request({ new Request({
url: 'api/v2/torrents/' + item, url: "api/v2/torrents/" + item,
method: 'post', method: "post",
data: { data: {
hashes: hash hashes: hash
} }
@ -1117,8 +1112,8 @@ const initializeWindows = function() {
}); });
}); });
['decreasePrio', 'increasePrio', 'topPrio', 'bottomPrio'].each(function(item) { ["decreasePrio", "increasePrio", "topPrio", "bottomPrio"].each((item) => {
addClickEvent(item, function(e) { addClickEvent(item, (e) => {
new Event(e).stop(); new Event(e).stop();
setQueuePositionFN(item); setQueuePositionFN(item);
}); });
@ -1128,8 +1123,8 @@ const initializeWindows = function() {
const hashes = torrentsTable.selectedRowsIds(); const hashes = torrentsTable.selectedRowsIds();
if (hashes.length) { if (hashes.length) {
new Request({ new Request({
url: 'api/v2/torrents/' + cmd, url: "api/v2/torrents/" + cmd,
method: 'post', method: "post",
data: { data: {
hashes: hashes.join("|") hashes: hashes.join("|")
} }
@ -1138,19 +1133,19 @@ const initializeWindows = function() {
} }
}; };
addClickEvent('about', function(e) { addClickEvent("about", (e) => {
new Event(e).stop(); new Event(e).stop();
const id = 'aboutpage'; const id = "aboutpage";
new MochaUI.Window({ new MochaUI.Window({
id: id, id: id,
title: 'QBT_TR(About qBittorrent)QBT_TR[CONTEXT=AboutDialog]', title: "QBT_TR(About qBittorrent)QBT_TR[CONTEXT=AboutDialog]",
loadMethod: 'xhr', loadMethod: "xhr",
contentURL: new URI("views/about.html").toString(), contentURL: new URI("views/about.html").toString(),
require: { require: {
css: ['css/Tabs.css'] css: ["css/Tabs.css"]
}, },
toolbar: true, toolbar: true,
toolbarURL: 'views/aboutToolbar.html', toolbarURL: "views/aboutToolbar.html",
padding: 10, padding: 10,
width: loadWindowWidth(id, 550), width: loadWindowWidth(id, 550),
height: loadWindowHeight(id, 360), height: loadWindowHeight(id, 360),
@ -1160,25 +1155,25 @@ const initializeWindows = function() {
}); });
}); });
addClickEvent('logout', function(e) { addClickEvent("logout", (e) => {
new Event(e).stop(); new Event(e).stop();
new Request({ new Request({
url: 'api/v2/auth/logout', url: "api/v2/auth/logout",
method: 'post', method: "post",
onSuccess: function() { onSuccess: function() {
window.location.reload(true); window.location.reload(true);
} }
}).send(); }).send();
}); });
addClickEvent('shutdown', function(e) { addClickEvent("shutdown", (e) => {
new Event(e).stop(); new Event(e).stop();
if (confirm('QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]')) { if (confirm("QBT_TR(Are you sure you want to quit qBittorrent?)QBT_TR[CONTEXT=MainWindow]")) {
new Request({ new Request({
url: 'api/v2/app/shutdown', url: "api/v2/app/shutdown",
method: 'post', method: "post",
onSuccess: function() { onSuccess: function() {
const shutdownMessage = 'QBT_TR(%1 has been shutdown)QBT_TR[CONTEXT=HttpServer]'.replace("%1", window.qBittorrent.Client.mainTitle()); const shutdownMessage = "QBT_TR(%1 has been shutdown)QBT_TR[CONTEXT=HttpServer]".replace("%1", window.qBittorrent.Client.mainTitle());
document.write(`<!doctype html><html lang="${LANG}"><head> <meta charset="UTF-8"> <meta name="color-scheme" content="light dark"> <title>${shutdownMessage}</title> <style>* {font-family: Arial, Helvetica, sans-serif;}</style></head><body> <h1 style="text-align: center;">${shutdownMessage}</h1></body></html>`); document.write(`<!doctype html><html lang="${LANG}"><head> <meta charset="UTF-8"> <meta name="color-scheme" content="light dark"> <title>${shutdownMessage}</title> <style>* {font-family: Arial, Helvetica, sans-serif;}</style></head><body> <h1 style="text-align: center;">${shutdownMessage}</h1></body></html>`);
document.close(); document.close();
window.stop(); window.stop();
@ -1189,8 +1184,8 @@ const initializeWindows = function() {
}); });
// Deactivate menu header links // Deactivate menu header links
$$('a.returnFalse').each(function(el) { $$("a.returnFalse").each((el) => {
el.addEvent('click', function(e) { el.addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
}); });
}); });

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.PiecesBar = (() => { window.qBittorrent.PiecesBar = (() => {
const exports = () => { const exports = () => {
@ -51,35 +50,35 @@ window.qBittorrent.PiecesBar = (() => {
const PiecesBar = new Class({ const PiecesBar = new Class({
initialize(pieces, parameters) { initialize(pieces, parameters) {
const vals = { const vals = {
'id': 'piecesbar_' + (piecesBarUniqueId++), "id": "piecesbar_" + (piecesBarUniqueId++),
'width': 0, "width": 0,
'height': 0, "height": 0,
'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)"
}; };
if (parameters && (typeOf(parameters) === 'object')) if (parameters && (typeOf(parameters) === "object"))
Object.append(vals, parameters); Object.append(vals, parameters);
vals.height = Math.max(vals.height, 12); vals.height = Math.max(vals.height, 12);
const obj = new Element('div', { const obj = new Element("div", {
'id': vals.id, "id": vals.id,
'class': 'piecesbarWrapper', "class": "piecesbarWrapper",
'styles': { "styles": {
'border': vals.borderSize.toString() + 'px solid ' + vals.borderColor, "border": vals.borderSize.toString() + "px solid " + vals.borderColor,
'height': vals.height.toString() + 'px', "height": vals.height.toString() + "px",
} }
}); });
obj.vals = vals; obj.vals = vals;
obj.vals.pieces = [pieces, []].pick(); obj.vals.pieces = [pieces, []].pick();
obj.vals.canvas = new Element('canvas', { obj.vals.canvas = new Element("canvas", {
'id': vals.id + '_canvas', "id": vals.id + "_canvas",
'class': 'piecesbarCanvas', "class": "piecesbarCanvas",
'width': (vals.width - (2 * vals.borderSize)).toString(), "width": (vals.width - (2 * vals.borderSize)).toString(),
'height': '1' // will stretch vertically to take up the height of the parent "height": "1" // will stretch vertically to take up the height of the parent
}); });
obj.appendChild(obj.vals.canvas); obj.appendChild(obj.vals.canvas);
@ -124,7 +123,7 @@ window.qBittorrent.PiecesBar = (() => {
this.vals.canvas.width = width - (2 * this.vals.borderSize); this.vals.canvas.width = width - (2 * this.vals.borderSize);
const canvas = this.vals.canvas; const canvas = this.vals.canvas;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
const imageWidth = canvas.width; const imageWidth = canvas.width;
@ -136,14 +135,11 @@ window.qBittorrent.PiecesBar = (() => {
let maxStatus = 0; let maxStatus = 0;
for (const status of pieces) { for (const status of pieces) {
if (status > maxStatus) { if (status > maxStatus)
maxStatus = status; maxStatus = status;
} if (status < minStatus)
if (status < minStatus) {
minStatus = status; minStatus = status;
} }
}
// if no progress then don't do anything // if no progress then don't do anything
if (maxStatus === 0) if (maxStatus === 0)
@ -220,15 +216,13 @@ window.qBittorrent.PiecesBar = (() => {
statusValues[STATUS_DOWNLOADING] = Math.min(statusValues[STATUS_DOWNLOADING], 1); statusValues[STATUS_DOWNLOADING] = Math.min(statusValues[STATUS_DOWNLOADING], 1);
statusValues[STATUS_DOWNLOADED] = Math.min(statusValues[STATUS_DOWNLOADED], 1); statusValues[STATUS_DOWNLOADED] = Math.min(statusValues[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[STATUS_DOWNLOADING] === statusValues[STATUS_DOWNLOADING])
&& (lastValue[STATUS_DOWNLOADED] === statusValues[STATUS_DOWNLOADED])) { && (lastValue[STATUS_DOWNLOADED] === statusValues[STATUS_DOWNLOADED]))
continue; continue;
}
const rectangleWidth = x - rectangleStart; const rectangleWidth = x - rectangleStart;
this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue); this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue);
@ -246,7 +240,7 @@ window.qBittorrent.PiecesBar = (() => {
function drawStatus(ctx, start, width, statusValues) { function drawStatus(ctx, 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'; ctx.globalCompositeOperation = "lighten";
if (statusValues[STATUS_DOWNLOADING]) { if (statusValues[STATUS_DOWNLOADING]) {
ctx.globalAlpha = statusValues[STATUS_DOWNLOADING]; ctx.globalAlpha = statusValues[STATUS_DOWNLOADING];
@ -266,7 +260,7 @@ window.qBittorrent.PiecesBar = (() => {
if (!obj) if (!obj)
return; return;
if (!obj.parentNode) if (!obj.parentNode)
return setTimeout(function() { checkForParent(id); }, 1); return setTimeout(() => { checkForParent(id); }, 1);
obj.refresh(); obj.refresh();
} }

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.ProgressBar = (function() { window.qBittorrent.ProgressBar = (function() {
const exports = function() { const exports = function() {
@ -43,60 +42,60 @@ window.qBittorrent.ProgressBar = (function() {
const ProgressBar = new Class({ const ProgressBar = new Class({
initialize: function(value, parameters) { initialize: function(value, parameters) {
const vals = { const vals = {
'id': 'progressbar_' + (ProgressBars++), "id": "progressbar_" + (ProgressBars++),
'value': [value, 0].pick(), "value": [value, 0].pick(),
'width': 0, "width": 0,
'height': 0, "height": 0,
'darkbg': 'var(--color-background-blue)', "darkbg": "var(--color-background-blue)",
'darkfg': 'var(--color-text-white)', "darkfg": "var(--color-text-white)",
'lightbg': 'var(--color-background-default)', "lightbg": "var(--color-background-default)",
'lightfg': 'var(--color-text-default)' "lightfg": "var(--color-text-default)"
}; };
if (parameters && (typeOf(parameters) === 'object')) if (parameters && (typeOf(parameters) === "object"))
Object.append(vals, parameters); Object.append(vals, parameters);
if (vals.height < 12) if (vals.height < 12)
vals.height = 12; vals.height = 12;
const obj = new Element('div', { const obj = new Element("div", {
'id': vals.id, "id": vals.id,
'class': 'progressbar_wrapper', "class": "progressbar_wrapper",
'styles': { "styles": {
'border': '1px solid var(--color-border-default)', "border": "1px solid var(--color-border-default)",
'width': vals.width, "width": vals.width,
'height': vals.height, "height": vals.height,
'position': 'relative', "position": "relative",
'margin': '0 auto' "margin": "0 auto"
} }
}); });
obj.vals = vals; obj.vals = vals;
obj.vals.value = [value, 0].pick(); obj.vals.value = [value, 0].pick();
obj.vals.dark = new Element('div', { obj.vals.dark = new Element("div", {
'id': vals.id + '_dark', "id": vals.id + "_dark",
'class': 'progressbar_dark', "class": "progressbar_dark",
'styles': { "styles": {
'width': vals.width, "width": vals.width,
'height': vals.height, "height": vals.height,
'background': vals.darkbg, "background": vals.darkbg,
'color': vals.darkfg, "color": vals.darkfg,
'position': 'absolute', "position": "absolute",
'text-align': 'center', "text-align": "center",
'left': 0, "left": 0,
'top': 0, "top": 0,
'line-height': vals.height "line-height": vals.height
} }
}); });
obj.vals.light = new Element('div', { obj.vals.light = new Element("div", {
'id': vals.id + '_light', "id": vals.id + "_light",
'class': 'progressbar_light', "class": "progressbar_light",
'styles': { "styles": {
'width': vals.width, "width": vals.width,
'height': vals.height, "height": vals.height,
'background': vals.lightbg, "background": vals.lightbg,
'color': vals.lightfg, "color": vals.lightfg,
'position': 'absolute', "position": "absolute",
'text-align': 'center', "text-align": "center",
'left': 0, "left": 0,
'top': 0, "top": 0,
'line-height': vals.height "line-height": vals.height
} }
}); });
obj.appendChild(obj.vals.dark); obj.appendChild(obj.vals.dark);
@ -120,26 +119,24 @@ window.qBittorrent.ProgressBar = (function() {
value = parseFloat(value); value = parseFloat(value);
if (isNaN(value)) if (isNaN(value))
value = 0; value = 0;
if (value > 100) value = Math.min(Math.max(value, 0), 100);
value = 100;
if (value < 0)
value = 0;
this.vals.value = value; this.vals.value = value;
this.vals.dark.empty();
this.vals.light.empty(); const displayedValue = `${value.round(1).toFixed(1)}%`;
this.vals.dark.appendText(value.round(1).toFixed(1) + '%'); this.vals.dark.textContent = displayedValue;
this.vals.light.appendText(value.round(1).toFixed(1) + '%'); this.vals.light.textContent = displayedValue;
const r = parseInt(this.vals.width * (value / 100));
this.vals.dark.setStyle('clip', 'rect(0,' + r + 'px,' + this.vals.height + 'px,0)'); const r = parseInt((this.vals.width * (value / 100)), 10);
this.vals.light.setStyle('clip', 'rect(0,' + this.vals.width + 'px,' + this.vals.height + 'px,' + r + 'px)'); this.vals.dark.setStyle("clip", `rect(0, ${r}px, ${this.vals.height}px, 0)`);
this.vals.light.setStyle("clip", `rect(0, ${this.vals.width}px, ${this.vals.height}px, ${r}px)`);
} }
function ProgressBar_setWidth(value) { function ProgressBar_setWidth(value) {
if (this.vals.width !== value) { if (this.vals.width !== value) {
this.vals.width = value; this.vals.width = value;
this.setStyle('width', value); this.setStyle("width", value);
this.vals.dark.setStyle('width', value); this.vals.dark.setStyle("width", value);
this.vals.light.setStyle('width', value); this.vals.light.setStyle("width", value);
this.setValue(this.vals.value); this.setValue(this.vals.value);
} }
} }
@ -150,10 +147,10 @@ window.qBittorrent.ProgressBar = (function() {
return; return;
if (!obj.parentNode) if (!obj.parentNode)
return setTimeout('ProgressBar_checkForParent("' + id + '")', 1); return setTimeout('ProgressBar_checkForParent("' + id + '")', 1);
obj.setStyle('width', '100%'); obj.setStyle("width", "100%");
const w = obj.offsetWidth; const w = obj.offsetWidth;
obj.vals.dark.setStyle('width', w); obj.vals.dark.setStyle("width", w);
obj.vals.light.setStyle('width', w); obj.vals.light.setStyle("width", w);
obj.vals.width = w; obj.vals.width = w;
obj.setValue(obj.vals.value); obj.setValue(obj.vals.value);
} }

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.PropFiles = (function() { window.qBittorrent.PropFiles = (function() {
const exports = function() { const exports = function() {
@ -82,7 +81,7 @@ window.qBittorrent.PropFiles = (function() {
const getChildFiles = function(node) { const getChildFiles = function(node) {
if (node.isFolder) { if (node.isFolder) {
node.children.each(function(child) { node.children.each((child) => {
getChildFiles(child); getChildFiles(child);
}); });
} }
@ -92,7 +91,7 @@ window.qBittorrent.PropFiles = (function() {
} }
}; };
node.children.each(function(child) { node.children.each((child) => {
getChildFiles(child); getChildFiles(child);
}); });
@ -107,8 +106,8 @@ window.qBittorrent.PropFiles = (function() {
const checkbox = e.target; const checkbox = e.target;
const priority = checkbox.checked ? FilePriority.Normal : FilePriority.Ignored; const priority = checkbox.checked ? FilePriority.Normal : FilePriority.Ignored;
const id = checkbox.get('data-id'); const id = checkbox.get("data-id");
const fileId = checkbox.get('data-file-id'); const fileId = checkbox.get("data-file-id");
const rows = getAllChildren(id, fileId); const rows = getAllChildren(id, fileId);
@ -119,8 +118,8 @@ window.qBittorrent.PropFiles = (function() {
const fileComboboxChanged = function(e) { const fileComboboxChanged = function(e) {
const combobox = e.target; const combobox = e.target;
const priority = combobox.value; const priority = combobox.value;
const id = combobox.get('data-id'); const id = combobox.get("data-id");
const fileId = combobox.get('data-file-id'); const fileId = combobox.get("data-file-id");
const rows = getAllChildren(id, fileId); const rows = getAllChildren(id, fileId);
@ -129,24 +128,24 @@ window.qBittorrent.PropFiles = (function() {
}; };
const isDownloadCheckboxExists = function(id) { const isDownloadCheckboxExists = function(id) {
return ($('cbPrio' + id) !== null); return ($("cbPrio" + id) !== null);
}; };
const createDownloadCheckbox = function(id, fileId, checked) { const createDownloadCheckbox = function(id, fileId, checked) {
const checkbox = new Element('input'); const checkbox = new Element("input");
checkbox.set('type', 'checkbox'); checkbox.set("type", "checkbox");
checkbox.set('id', 'cbPrio' + id); checkbox.set("id", "cbPrio" + id);
checkbox.set('data-id', id); checkbox.set("data-id", id);
checkbox.set('data-file-id', fileId); checkbox.set("data-file-id", fileId);
checkbox.set('class', 'DownloadedCB'); checkbox.set("class", "DownloadedCB");
checkbox.addEvent('click', fileCheckboxClicked); checkbox.addEvent("click", fileCheckboxClicked);
updateCheckbox(checkbox, checked); updateCheckbox(checkbox, checked);
return checkbox; return checkbox;
}; };
const updateDownloadCheckbox = function(id, checked) { const updateDownloadCheckbox = function(id, checked) {
const checkbox = $('cbPrio' + id); const checkbox = $("cbPrio" + id);
updateCheckbox(checkbox, checked); updateCheckbox(checkbox, checked);
}; };
@ -165,43 +164,42 @@ window.qBittorrent.PropFiles = (function() {
}; };
const isPriorityComboExists = function(id) { const isPriorityComboExists = function(id) {
return ($('comboPrio' + id) !== null); return ($("comboPrio" + id) !== null);
}; };
const createPriorityOptionElement = function(priority, selected, html) { const createPriorityOptionElement = function(priority, selected, html) {
const elem = new Element('option'); const elem = new Element("option");
elem.set('value', priority.toString()); elem.set("value", priority.toString());
elem.set('html', html); elem.set("html", html);
if (selected) if (selected)
elem.selected = true; elem.selected = true;
return elem; return elem;
}; };
const createPriorityCombo = function(id, fileId, selectedPriority) { const createPriorityCombo = function(id, fileId, selectedPriority) {
const select = new Element('select'); const select = new Element("select");
select.set('id', 'comboPrio' + id); select.set("id", "comboPrio" + id);
select.set('data-id', id); select.set("data-id", id);
select.set('data-file-id', fileId); select.set("data-file-id", fileId);
select.addClass('combo_priority'); select.addClass("combo_priority");
select.addEvent('change', fileComboboxChanged); select.addEvent("change", fileComboboxChanged);
createPriorityOptionElement(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), 'QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select); createPriorityOptionElement(FilePriority.Ignored, (FilePriority.Ignored === selectedPriority), "QBT_TR(Do not download)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select);
createPriorityOptionElement(FilePriority.Normal, (FilePriority.Normal === selectedPriority), 'QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select); createPriorityOptionElement(FilePriority.Normal, (FilePriority.Normal === selectedPriority), "QBT_TR(Normal)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select);
createPriorityOptionElement(FilePriority.High, (FilePriority.High === selectedPriority), 'QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select); createPriorityOptionElement(FilePriority.High, (FilePriority.High === selectedPriority), "QBT_TR(High)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select);
createPriorityOptionElement(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), 'QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]').injectInside(select); createPriorityOptionElement(FilePriority.Maximum, (FilePriority.Maximum === selectedPriority), "QBT_TR(Maximum)QBT_TR[CONTEXT=PropListDelegate]").injectInside(select);
// "Mixed" priority is for display only; it shouldn't be selectable // "Mixed" priority is for display only; it shouldn't be selectable
const mixedPriorityOption = createPriorityOptionElement(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), 'QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]'); const mixedPriorityOption = createPriorityOptionElement(FilePriority.Mixed, (FilePriority.Mixed === selectedPriority), "QBT_TR(Mixed)QBT_TR[CONTEXT=PropListDelegate]");
mixedPriorityOption.set('disabled', true); mixedPriorityOption.set("disabled", true);
mixedPriorityOption.injectInside(select); mixedPriorityOption.injectInside(select);
return select; return select;
}; };
const updatePriorityCombo = function(id, selectedPriority) { const updatePriorityCombo = function(id, selectedPriority) {
const combobox = $('comboPrio' + id); const combobox = $("comboPrio" + id);
if (parseInt(combobox.value, 10) !== selectedPriority)
if (parseInt(combobox.value) !== selectedPriority)
selectComboboxPriority(combobox, selectedPriority); selectComboboxPriority(combobox, selectedPriority);
}; };
@ -209,7 +207,7 @@ window.qBittorrent.PropFiles = (function() {
const options = combobox.options; const options = combobox.options;
for (let i = 0; i < options.length; ++i) { for (let i = 0; i < options.length; ++i) {
const option = options[i]; const option = options[i];
if (parseInt(option.value) === priority) if (parseInt(option.value, 10) === priority)
option.selected = true; option.selected = true;
else else
option.selected = false; option.selected = false;
@ -224,12 +222,12 @@ window.qBittorrent.PropFiles = (function() {
const rowIds = []; const rowIds = [];
const fileIds = []; const fileIds = [];
let priority = FilePriority.Ignored; let priority = FilePriority.Ignored;
const checkbox = $('tristate_cb'); const checkbox = $("tristate_cb");
if (checkbox.state === "checked") { if (checkbox.state === "checked") {
setCheckboxUnchecked(checkbox); setCheckboxUnchecked(checkbox);
// set file priority for all checked to Ignored // set file priority for all checked to Ignored
torrentFilesTable.getFilteredAndSortedRows().forEach(function(row) { torrentFilesTable.getFilteredAndSortedRows().forEach((row) => {
const rowId = row.rowId; const rowId = row.rowId;
const fileId = row.full_data.fileId; const fileId = row.full_data.fileId;
const isChecked = (row.full_data.checked === TriState.Checked); const isChecked = (row.full_data.checked === TriState.Checked);
@ -244,7 +242,7 @@ window.qBittorrent.PropFiles = (function() {
setCheckboxChecked(checkbox); setCheckboxChecked(checkbox);
priority = FilePriority.Normal; priority = FilePriority.Normal;
// set file priority for all unchecked to Normal // set file priority for all unchecked to Normal
torrentFilesTable.getFilteredAndSortedRows().forEach(function(row) { torrentFilesTable.getFilteredAndSortedRows().forEach((row) => {
const rowId = row.rowId; const rowId = row.rowId;
const fileId = row.full_data.fileId; const fileId = row.full_data.fileId;
const isUnchecked = (row.full_data.checked === TriState.Unchecked); const isUnchecked = (row.full_data.checked === TriState.Unchecked);
@ -261,7 +259,7 @@ window.qBittorrent.PropFiles = (function() {
}; };
const updateGlobalCheckbox = function() { const updateGlobalCheckbox = function() {
const checkbox = $('tristate_cb'); const checkbox = $("tristate_cb");
if (isAllCheckboxesChecked()) if (isAllCheckboxesChecked())
setCheckboxChecked(checkbox); setCheckboxChecked(checkbox);
else if (isAllCheckboxesUnchecked()) else if (isAllCheckboxesUnchecked())
@ -288,7 +286,7 @@ window.qBittorrent.PropFiles = (function() {
}; };
const isAllCheckboxesChecked = function() { const isAllCheckboxesChecked = function() {
const checkboxes = $$('input.DownloadedCB'); const checkboxes = $$("input.DownloadedCB");
for (let i = 0; i < checkboxes.length; ++i) { for (let i = 0; i < checkboxes.length; ++i) {
if (!checkboxes[i].checked) if (!checkboxes[i].checked)
return false; return false;
@ -297,7 +295,7 @@ window.qBittorrent.PropFiles = (function() {
}; };
const isAllCheckboxesUnchecked = function() { const isAllCheckboxesUnchecked = function() {
const checkboxes = $$('input.DownloadedCB'); const checkboxes = $$("input.DownloadedCB");
for (let i = 0; i < checkboxes.length; ++i) { for (let i = 0; i < checkboxes.length; ++i) {
if (checkboxes[i].checked) if (checkboxes[i].checked)
return false; return false;
@ -311,12 +309,12 @@ window.qBittorrent.PropFiles = (function() {
clearTimeout(loadTorrentFilesDataTimer); clearTimeout(loadTorrentFilesDataTimer);
new Request({ new Request({
url: 'api/v2/torrents/filePrio', url: "api/v2/torrents/filePrio",
method: 'post', method: "post",
data: { data: {
'hash': current_hash, "hash": current_hash,
'id': fileIds.join('|'), "id": fileIds.join("|"),
'priority': priority "priority": priority
}, },
onComplete: function() { onComplete: function() {
loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000); loadTorrentFilesDataTimer = loadTorrentFilesData.delay(1000);
@ -324,10 +322,10 @@ window.qBittorrent.PropFiles = (function() {
}).send(); }).send();
const ignore = (priority === FilePriority.Ignored); const ignore = (priority === FilePriority.Ignored);
ids.forEach(function(_id) { ids.forEach((_id) => {
torrentFilesTable.setIgnored(_id, ignore); torrentFilesTable.setIgnored(_id, ignore);
const combobox = $('comboPrio' + _id); const combobox = $("comboPrio" + _id);
if (combobox !== null) if (combobox !== null)
selectComboboxPriority(combobox, priority); selectComboboxPriority(combobox, priority);
}); });
@ -337,8 +335,8 @@ window.qBittorrent.PropFiles = (function() {
let loadTorrentFilesDataTimer; let loadTorrentFilesDataTimer;
const loadTorrentFilesData = function() { const loadTorrentFilesData = function() {
if ($('prop_files').hasClass('invisible') if ($("prop_files").hasClass("invisible")
|| $('propertiesPanel_collapseToggle').hasClass('panel-expand')) { || $("propertiesPanel_collapseToggle").hasClass("panel-expand")) {
// Tab changed, don't do anything // Tab changed, don't do anything
return; return;
} }
@ -355,10 +353,10 @@ window.qBittorrent.PropFiles = (function() {
current_hash = new_hash; current_hash = new_hash;
loadedNewTorrent = true; loadedNewTorrent = true;
} }
const url = new URI('api/v2/torrents/files?hash=' + current_hash); const url = new URI("api/v2/torrents/files?hash=" + current_hash);
new Request.JSON({ new Request.JSON({
url: url, url: url,
method: 'get', method: "get",
noCache: true, noCache: true,
onComplete: function() { onComplete: function() {
clearTimeout(loadTorrentFilesDataTimer); clearTimeout(loadTorrentFilesDataTimer);
@ -388,7 +386,7 @@ window.qBittorrent.PropFiles = (function() {
const handleNewTorrentFiles = function(files) { const handleNewTorrentFiles = function(files) {
is_seed = (files.length > 0) ? files[0].is_seed : true; is_seed = (files.length > 0) ? files[0].is_seed : true;
const rows = files.map(function(file, index) { const rows = files.map((file, index) => {
let progress = (file.progress * 100).round(1); let progress = (file.progress * 100).round(1);
if ((progress === 100) && (file.progress < 1)) if ((progress === 100) && (file.progress < 1))
progress = 99.9; progress = 99.9;
@ -421,13 +419,13 @@ window.qBittorrent.PropFiles = (function() {
const rootNode = new window.qBittorrent.FileTree.FolderNode(); const rootNode = new window.qBittorrent.FileTree.FolderNode();
rows.forEach(function(row) { rows.forEach((row) => {
const pathItems = row.fileName.split(window.qBittorrent.Filesystem.PathSeparator); const pathItems = row.fileName.split(window.qBittorrent.Filesystem.PathSeparator);
pathItems.pop(); // remove last item (i.e. file name) pathItems.pop(); // remove last item (i.e. file name)
let parent = rootNode; let parent = rootNode;
pathItems.forEach(function(folderName) { pathItems.forEach((folderName) => {
if (folderName === '.unwanted') if (folderName === ".unwanted")
return; return;
let folderNode = null; let folderNode = null;
@ -474,7 +472,7 @@ window.qBittorrent.PropFiles = (function() {
parent.addChild(childNode); parent.addChild(childNode);
++rowId; ++rowId;
}.bind(this)); });
torrentFilesTable.populateTable(rootNode); torrentFilesTable.populateTable(rootNode);
torrentFilesTable.updateTable(false); torrentFilesTable.updateTable(false);
@ -497,16 +495,14 @@ window.qBittorrent.PropFiles = (function() {
const expandFolder = function(id) { const expandFolder = function(id) {
const node = torrentFilesTable.getNode(id); const node = torrentFilesTable.getNode(id);
if (node.isFolder) { if (node.isFolder)
expandNode(node); expandNode(node);
}
}; };
const collapseFolder = function(id) { const collapseFolder = function(id) {
const node = torrentFilesTable.getNode(id); const node = torrentFilesTable.getNode(id);
if (node.isFolder) { if (node.isFolder)
collapseNode(node); collapseNode(node);
}
}; };
const filesPriorityMenuClicked = function(priority) { const filesPriorityMenuClicked = function(priority) {
@ -516,8 +512,8 @@ window.qBittorrent.PropFiles = (function() {
const rowIds = []; const rowIds = [];
const fileIds = []; const fileIds = [];
selectedRows.forEach(function(rowId) { selectedRows.forEach((rowId) => {
const elem = $('comboPrio' + rowId); const elem = $("comboPrio" + rowId);
rowIds.push(rowId); rowIds.push(rowId);
fileIds.push(elem.get("data-file-id")); fileIds.push(elem.get("data-file-id"));
}); });
@ -526,10 +522,10 @@ window.qBittorrent.PropFiles = (function() {
const uniqueFileIds = {}; const uniqueFileIds = {};
for (let i = 0; i < rowIds.length; ++i) { for (let i = 0; i < rowIds.length; ++i) {
const rows = getAllChildren(rowIds[i], fileIds[i]); const rows = getAllChildren(rowIds[i], fileIds[i]);
rows.rowIds.forEach(function(rowId) { rows.rowIds.forEach((rowId) => {
uniqueRowIds[rowId] = true; uniqueRowIds[rowId] = true;
}); });
rows.fileIds.forEach(function(fileId) { rows.fileIds.forEach((fileId) => {
uniqueFileIds[fileId] = true; uniqueFileIds[fileId] = true;
}); });
} }
@ -549,11 +545,11 @@ window.qBittorrent.PropFiles = (function() {
const path = node.path; const path = node.path;
new MochaUI.Window({ new MochaUI.Window({
id: 'renamePage', id: "renamePage",
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]", title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
loadMethod: 'iframe', loadMethod: "iframe",
contentURL: 'rename_file.html?hash=' + hash + '&isFolder=' + node.isFolder contentURL: "rename_file.html?hash=" + hash + "&isFolder=" + node.isFolder
+ '&path=' + encodeURIComponent(path), + "&path=" + encodeURIComponent(path),
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
maximizable: false, maximizable: false,
@ -566,11 +562,11 @@ window.qBittorrent.PropFiles = (function() {
const multiFileRename = function(hash) { const multiFileRename = function(hash) {
new MochaUI.Window({ new MochaUI.Window({
id: 'multiRenamePage', id: "multiRenamePage",
title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]", title: "QBT_TR(Renaming)QBT_TR[CONTEXT=TorrentContentTreeView]",
data: { hash: hash, selectedRows: torrentFilesTable.selectedRows }, data: { hash: hash, selectedRows: torrentFilesTable.selectedRows },
loadMethod: 'xhr', loadMethod: "xhr",
contentURL: 'rename_files.html', contentURL: "rename_files.html",
scrollbars: false, scrollbars: false,
resizable: true, resizable: true,
maximizable: false, maximizable: false,
@ -578,25 +574,23 @@ window.qBittorrent.PropFiles = (function() {
paddingHorizontal: 0, paddingHorizontal: 0,
width: 800, width: 800,
height: 420, height: 420,
resizeLimit: { 'x': [800], 'y': [420] } resizeLimit: { "x": [800], "y": [420] }
}); });
}; };
const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: '#torrentFilesTableDiv tr', targets: "#torrentFilesTableDiv tr",
menu: 'torrentFilesMenu', menu: "torrentFilesMenu",
actions: { actions: {
Rename: function(element, ref) { Rename: function(element, ref) {
const hash = torrentsTable.getCurrentTorrentID(); const hash = torrentsTable.getCurrentTorrentID();
if (!hash) if (!hash)
return; return;
if (torrentFilesTable.selectedRowsIds().length > 1) { if (torrentFilesTable.selectedRowsIds().length > 1)
multiFileRename(hash); multiFileRename(hash);
} else
else {
singleFileRename(hash); singleFileRename(hash);
}
}, },
FilePrioIgnore: function(element, ref) { FilePrioIgnore: function(element, ref) {
@ -618,20 +612,20 @@ window.qBittorrent.PropFiles = (function() {
}, },
onShow: function() { onShow: function() {
if (is_seed) if (is_seed)
this.hideItem('FilePrio'); this.hideItem("FilePrio");
else else
this.showItem('FilePrio'); this.showItem("FilePrio");
} }
}); });
torrentFilesTable.setup('torrentFilesTableDiv', 'torrentFilesTableFixedHeaderDiv', torrentFilesContextMenu); torrentFilesTable.setup("torrentFilesTableDiv", "torrentFilesTableFixedHeaderDiv", torrentFilesContextMenu);
// inject checkbox into table header // inject checkbox into table header
const tableHeaders = $$('#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th'); const tableHeaders = $$("#torrentFilesTableFixedHeaderDiv .dynamicTableHeader th");
if (tableHeaders.length > 0) { if (tableHeaders.length > 0) {
const checkbox = new Element('input'); const checkbox = new Element("input");
checkbox.set('type', 'checkbox'); checkbox.set("type", "checkbox");
checkbox.set('id', 'tristate_cb'); checkbox.set("id", "tristate_cb");
checkbox.addEvent('click', switchCheckboxState); checkbox.addEvent("click", switchCheckboxState);
const checkboxTH = tableHeaders[0]; const checkboxTH = tableHeaders[0];
checkbox.injectInside(checkboxTH); checkbox.injectInside(checkboxTH);
@ -639,14 +633,14 @@ window.qBittorrent.PropFiles = (function() {
// default sort by name column // default sort by name column
if (torrentFilesTable.getSortedColumn() === null) if (torrentFilesTable.getSortedColumn() === null)
torrentFilesTable.setSortedColumn('name'); torrentFilesTable.setSortedColumn("name");
// listen for changes to torrentFilesFilterInput // listen for changes to torrentFilesFilterInput
let torrentFilesFilterInputTimer = -1; let torrentFilesFilterInputTimer = -1;
$('torrentFilesFilterInput').addEvent('input', () => { $("torrentFilesFilterInput").addEvent("input", () => {
clearTimeout(torrentFilesFilterInputTimer); clearTimeout(torrentFilesFilterInputTimer);
const value = $('torrentFilesFilterInput').get("value"); const value = $("torrentFilesFilterInput").get("value");
torrentFilesTable.setFilter(value); torrentFilesTable.setFilter(value);
torrentFilesFilterInputTimer = setTimeout(() => { torrentFilesFilterInputTimer = setTimeout(() => {
@ -668,7 +662,7 @@ window.qBittorrent.PropFiles = (function() {
* Show/hide a node's row * Show/hide a node's row
*/ */
const _hideNode = function(node, shouldHide) { const _hideNode = function(node, shouldHide) {
const span = $('filesTablefileName' + node.rowId); const span = $("filesTablefileName" + node.rowId);
// span won't exist if row has been filtered out // span won't exist if row has been filtered out
if (span === null) if (span === null)
return; return;
@ -683,7 +677,7 @@ window.qBittorrent.PropFiles = (function() {
* Update a node's collapsed state and icon * Update a node's collapsed state and icon
*/ */
const _updateNodeState = function(node, isCollapsed) { const _updateNodeState = function(node, isCollapsed) {
const span = $('filesTablefileName' + node.rowId); const span = $("filesTablefileName" + node.rowId);
// span won't exist if row has been filtered out // span won't exist if row has been filtered out
if (span === null) if (span === null)
return; return;
@ -701,7 +695,7 @@ window.qBittorrent.PropFiles = (function() {
}; };
const _isCollapsed = function(node) { const _isCollapsed = function(node) {
const span = $('filesTablefileName' + node.rowId); const span = $("filesTablefileName" + node.rowId);
if (span === null) if (span === null)
return true; return true;
@ -721,8 +715,8 @@ window.qBittorrent.PropFiles = (function() {
const expandAllNodes = function() { const expandAllNodes = function() {
const root = torrentFilesTable.getRoot(); const root = torrentFilesTable.getRoot();
root.children.each(function(node) { root.children.each((node) => {
node.children.each(function(child) { node.children.each((child) => {
_collapseNode(child, false, true, false); _collapseNode(child, false, true, false);
}); });
}); });
@ -731,8 +725,8 @@ window.qBittorrent.PropFiles = (function() {
const collapseAllNodes = function() { const collapseAllNodes = function() {
const root = torrentFilesTable.getRoot(); const root = torrentFilesTable.getRoot();
root.children.each(function(node) { root.children.each((node) => {
node.children.each(function(child) { node.children.each((child) => {
_collapseNode(child, true, true, false); _collapseNode(child, true, true, false);
}); });
}); });
@ -757,7 +751,7 @@ window.qBittorrent.PropFiles = (function() {
if (!isChildNode || applyToChildren || !canSkipNode) if (!isChildNode || applyToChildren || !canSkipNode)
_updateNodeState(node, shouldCollapse); _updateNodeState(node, shouldCollapse);
node.children.each(function(child) { node.children.each((child) => {
_hideNode(child, shouldCollapse); _hideNode(child, shouldCollapse);
if (!child.isFolder) if (!child.isFolder)

View file

@ -26,11 +26,10 @@
* exception statement from your version. * exception statement from your version.
*/ */
'use strict'; "use strict";
if (window.qBittorrent === undefined) { if (window.qBittorrent === undefined)
window.qBittorrent = {}; window.qBittorrent = {};
}
window.qBittorrent.PropGeneral = (function() { window.qBittorrent.PropGeneral = (function() {
const exports = function() { const exports = function() {
@ -42,42 +41,43 @@ window.qBittorrent.PropGeneral = (function() {
const piecesBar = new window.qBittorrent.PiecesBar.PiecesBar([], { const piecesBar = new window.qBittorrent.PiecesBar.PiecesBar([], {
height: 16 height: 16
}); });
$('progress').appendChild(piecesBar); $("progress").appendChild(piecesBar);
const clearData = function() { const clearData = function() {
$('time_elapsed').set('html', ''); $("time_elapsed").set("html", "");
$('eta').set('html', ''); $("eta").set("html", "");
$('nb_connections').set('html', ''); $("nb_connections").set("html", "");
$('total_downloaded').set('html', ''); $("total_downloaded").set("html", "");
$('total_uploaded').set('html', ''); $("total_uploaded").set("html", "");
$('dl_speed').set('html', ''); $("dl_speed").set("html", "");
$('up_speed').set('html', ''); $("up_speed").set("html", "");
$('dl_limit').set('html', ''); $("dl_limit").set("html", "");
$('up_limit').set('html', ''); $("up_limit").set("html", "");
$('total_wasted').set('html', ''); $("total_wasted").set("html", "");
$('seeds').set('html', ''); $("seeds").set("html", "");
$('peers').set('html', ''); $("peers").set("html", "");
$('share_ratio').set('html', ''); $("share_ratio").set("html", "");
$('popularity').set('html', ''); $("popularity").set("html", "");
$('reannounce').set('html', ''); $("reannounce").set("html", "");
$('last_seen').set('html', ''); $("last_seen").set("html", "");
$('total_size').set('html', ''); $("total_size").set("html", "");
$('pieces').set('html', ''); $("pieces").set("html", "");
$('created_by').set('html', ''); $("created_by").set("html", "");
$('addition_date').set('html', ''); $("addition_date").set("html", "");
$('completion_date').set('html', ''); $("completion_date").set("html", "");
$('creation_date').set('html', ''); $("creation_date").set("html", "");
$('torrent_hash_v1').set('html', ''); $("torrent_hash_v1").set("html", "");
$('torrent_hash_v2').set('html', ''); $("torrent_hash_v2").set("html", "");
$('save_path').set('html', ''); $("save_path").set("html", "");
$('comment').set('html', ''); $("comment").set("html", "");
$("private").set("html", "");
piecesBar.clear(); piecesBar.clear();
}; };
let loadTorrentDataTimer; let loadTorrentDataTimer;
const loadTorrentData = function() { const loadTorrentData = function() {
if ($('prop_general').hasClass('invisible') if ($("prop_general").hasClass("invisible")
|| $('propertiesPanel_collapseToggle').hasClass('panel-expand')) { || $("propertiesPanel_collapseToggle").hasClass("panel-expand")) {
// Tab changed, don't do anything // Tab changed, don't do anything
return; return;
} }
@ -88,18 +88,18 @@ window.qBittorrent.PropGeneral = (function() {
loadTorrentDataTimer = loadTorrentData.delay(5000); loadTorrentDataTimer = loadTorrentData.delay(5000);
return; return;
} }
const url = new URI('api/v2/torrents/properties?hash=' + current_id); const url = new URI("api/v2/torrents/properties?hash=" + current_id);
new Request.JSON({ new Request.JSON({
url: url, url: url,
method: 'get', method: "get",
noCache: true, noCache: true,
onFailure: function() { onFailure: function() {
$('error_div').set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]'); $("error_div").set("html", "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]");
clearTimeout(loadTorrentDataTimer); clearTimeout(loadTorrentDataTimer);
loadTorrentDataTimer = loadTorrentData.delay(10000); loadTorrentDataTimer = loadTorrentData.delay(10000);
}, },
onSuccess: function(data) { onSuccess: function(data) {
$('error_div').set('html', ''); $("error_div").set("html", "");
if (data) { if (data) {
// Update Torrent data // Update Torrent data
@ -108,109 +108,118 @@ window.qBittorrent.PropGeneral = (function() {
.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').set('html', timeElapsed); $("time_elapsed").set("html", timeElapsed);
$('eta').set('html', window.qBittorrent.Misc.friendlyDuration(data.eta, window.qBittorrent.Misc.MAX_ETA)); $("eta").set("html", 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').set('html', nbConnections); $("nb_connections").set("html", 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').set('html', totalDownloaded); $("total_downloaded").set("html", 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').set('html', totalUploaded); $("total_uploaded").set("html", 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').set('html', dlSpeed); $("dl_speed").set("html", 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').set('html', upSpeed); $("up_speed").set("html", 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').set('html', dlLimit); $("dl_limit").set("html", 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').set('html', upLimit); $("up_limit").set("html", upLimit);
$('total_wasted').set('html', window.qBittorrent.Misc.friendlyUnit(data.total_wasted)); $("total_wasted").set("html", 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').set('html', seeds); $("seeds").set("html", 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').set('html', peers); $("peers").set("html", peers);
$('share_ratio').set('html', data.share_ratio.toFixed(2)); $("share_ratio").set("html", data.share_ratio.toFixed(2));
$('popularity').set('html', data.popularity.toFixed(2)); $("popularity").set("html", data.popularity.toFixed(2));
$('reannounce').set('html', window.qBittorrent.Misc.friendlyDuration(data.reannounce)); $("reannounce").set("html", 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').set('html', lastSeen); $("last_seen").set("html", 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').set('html', totalSize); $("total_size").set("html", 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]"
.replace("%1", data.pieces_num) .replace("%1", data.pieces_num)
.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').set('html', pieces); $("pieces").set("html", pieces);
$('created_by').set('text', data.created_by); $("created_by").set("text", 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').set('html', additionDate); $("addition_date").set("html", 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').set('html', completionDate); $("completion_date").set("html", 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').set('html', creationDate); $("creation_date").set("html", 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').set('html', torrentHashV1); $("torrent_hash_v1").set("html", 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').set('html', torrentHashV2); $("torrent_hash_v2").set("html", torrentHashV2);
$('save_path').set('html', data.save_path); $("save_path").set("html", data.save_path);
$('comment').set('html', window.qBittorrent.Misc.parseHtmlLinks(window.qBittorrent.Misc.escapeHtml(data.comment))); $("comment").set("html", window.qBittorrent.Misc.parseHtmlLinks(window.qBittorrent.Misc.escapeHtml(data.comment)));
if (data.has_metadata) {
$("private").set("text", (data.private
? "QBT_TR(Yes)QBT_TR[CONTEXT=PropertiesWidget]"
: "QBT_TR(No)QBT_TR[CONTEXT=PropertiesWidget]"));
}
else {
$("private").set("text", "QBT_TR(N/A)QBT_TR[CONTEXT=PropertiesWidget]");
}
} }
else { else {
clearData(); clearData();
@ -220,25 +229,24 @@ window.qBittorrent.PropGeneral = (function() {
} }
}).send(); }).send();
const piecesUrl = new URI('api/v2/torrents/pieceStates?hash=' + current_id); const piecesUrl = new URI("api/v2/torrents/pieceStates?hash=" + current_id);
new Request.JSON({ new Request.JSON({
url: piecesUrl, url: piecesUrl,
method: 'get', method: "get",
noCache: true, noCache: true,
onFailure: function() { onFailure: function() {
$('error_div').set('html', 'QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]'); $("error_div").set("html", "QBT_TR(qBittorrent client is not reachable)QBT_TR[CONTEXT=HttpServer]");
clearTimeout(loadTorrentDataTimer); clearTimeout(loadTorrentDataTimer);
loadTorrentDataTimer = loadTorrentData.delay(10000); loadTorrentDataTimer = loadTorrentData.delay(10000);
}, },
onSuccess: function(data) { onSuccess: function(data) {
$('error_div').set('html', ''); $("error_div").set("html", "");
if (data) { if (data)
piecesBar.setPieces(data); piecesBar.setPieces(data);
} else
else {
clearData(); clearData();
}
clearTimeout(loadTorrentDataTimer); clearTimeout(loadTorrentDataTimer);
loadTorrentDataTimer = loadTorrentData.delay(5000); loadTorrentDataTimer = loadTorrentData.delay(5000);
} }

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