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

View file

@ -53,7 +53,7 @@ jobs:
python-version: '3.7'
- name: Install tools (search engine)
run: pip install bandit pycodestyle pyflakes
run: pip install bandit mypy pycodestyle pyflakes pyright
- name: Gather files (search engine)
run: |
@ -61,6 +61,16 @@ jobs:
echo $PY_FILES
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)
run: |
pyflakes $PY_FILES

View file

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

View file

@ -93,7 +93,7 @@ jobs:
move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}"
- name: Install Qt
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
version: "6.7.0"
archives: qtbase qtsvg qttools
@ -153,26 +153,26 @@ jobs:
copy build/qbittorrent.pdb upload/qBittorrent
copy dist/windows/qt.conf upload/qBittorrent
# runtimes
copy "${{ env.Qt6_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
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
copy "${{ env.Qt6_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/qico.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
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
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
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
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
mkdir upload/cmake
copy build/compile_commands.json upload/cmake

View file

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

View file

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

View file

@ -40,7 +40,6 @@
#include <QRegularExpression>
#include <QThread>
#include "base/algorithm.h"
#include "base/exceptions.h"
#include "base/global.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)
{
return new CustomStorage {params, pool};
return new CustomStorage(params, pool);
}
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}
{
}

View file

@ -37,17 +37,12 @@
#include "addtorrentparams.h"
#include "categoryoptions.h"
#include "sharelimitaction.h"
#include "torrentcontentremoveoption.h"
#include "trackerentry.h"
#include "trackerentrystatus.h"
class QString;
enum DeleteOption
{
DeleteTorrent,
DeleteTorrentAndFiles
};
namespace BitTorrent
{
class InfoHash;
@ -58,6 +53,12 @@ namespace BitTorrent
struct CacheStatus;
struct SessionStatus;
enum class TorrentRemoveOption
{
KeepContent,
RemoveContent
};
// 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
@ -425,7 +426,7 @@ namespace BitTorrent
virtual void setExcludedFileNamesEnabled(bool enabled) = 0;
virtual QStringList excludedFileNames() const = 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 void setBannedIPs(const QStringList &newList) = 0;
virtual ResumeDataStorageType resumeDataStorageType() const = 0;
@ -434,6 +435,8 @@ namespace BitTorrent
virtual void setMergeTrackersEnabled(bool enabled) = 0;
virtual bool isStartPaused() const = 0;
virtual void setStartPaused(bool value) = 0;
virtual TorrentContentRemoveOption torrentContentRemoveOption() const = 0;
virtual void setTorrentContentRemoveOption(TorrentContentRemoveOption option) = 0;
virtual bool isRestored() const = 0;
@ -453,7 +456,7 @@ namespace BitTorrent
virtual bool isKnownTorrent(const InfoHash &infoHash) const = 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 cancelDownloadMetadata(const TorrentID &id) = 0;

View file

@ -101,6 +101,7 @@
#include "nativesessionextension.h"
#include "portforwarderimpl.h"
#include "resumedatastorage.h"
#include "torrentcontentremover.h"
#include "torrentdescriptor.h"
#include "torrentimpl.h"
#include "tracker.h"
@ -525,6 +526,7 @@ SessionImpl::SessionImpl(QObject *parent)
, m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3}
, m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_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_seedingLimitTimer {new QTimer(this)}
, m_resumeDataTimer {new QTimer(this)}
@ -550,7 +552,14 @@ SessionImpl::SessionImpl(QObject *parent)
, this, [this]() { m_recentErroredTorrents.clear(); });
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();
configureComponents();
@ -586,6 +595,11 @@ SessionImpl::SessionImpl(QObject *parent)
connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater);
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();
initMetrics();
@ -604,7 +618,7 @@ SessionImpl::~SessionImpl()
{
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};
if (m_torrentsQueueChanged)
@ -2236,21 +2250,16 @@ void SessionImpl::populateAdditionalTrackers()
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
{
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 int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes());
const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes());
@ -2285,12 +2294,12 @@ void SessionImpl::processShareLimits()
if (shareLimitAction == ShareLimitAction::Remove)
{
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
deleteTorrent(torrentID);
removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent);
}
else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
{
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())
{
@ -2303,7 +2312,6 @@ void SessionImpl::processShareLimits()
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)
@ -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
{
return m_torrents.value(id);
@ -2377,22 +2398,25 @@ void SessionImpl::banIP(const QString &ip)
// Delete a torrent from the session, given its hash
// 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);
if (!torrent)
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);
if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid())
m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1()));
// 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 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
{
m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption};
m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), torrent->actualFilePaths(), deleteOption};
if (m_moveStorageQueue.size() > 1)
{
@ -2429,12 +2453,13 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp
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
m_resumeDataStorage->remove(torrent->id());
m_resumeDataStorage->remove(torrentID);
LogMsg(tr("Torrent removed. Torrent: \"%1\"").arg(torrentName));
delete torrent;
return true;
}
@ -2462,7 +2487,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
}
#endif
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files);
m_nativeSession->remove_torrent(nativeHandle);
return true;
}
@ -2769,26 +2794,22 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
Q_ASSERT(p.file_priorities.empty());
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
// Use qBittorrent default priority rather than libtorrent's (4)
p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal));
if (addTorrentParams.filePriorities.isEmpty())
if (!filePriorities.isEmpty())
{
if (isExcludedFileNamesEnabled())
{
// 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]);
for (int i = 0; i < filePriorities.size(); ++i)
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(filePriorities[i]);
}
Q_ASSERT(p.ti);
@ -3874,21 +3895,41 @@ void SessionImpl::populateExcludedFileNamesRegExpList()
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};
m_excludedFileNamesRegExpList.append(re);
}
}
bool SessionImpl::isFilenameExcluded(const QString &fileName) const
void SessionImpl::applyFilenameFilter(const PathList &files, QList<DownloadPriority> &priorities)
{
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)
@ -3957,6 +3998,16 @@ void SessionImpl::setStartPaused(const bool value)
m_startPaused = value;
}
TorrentContentRemoveOption SessionImpl::torrentContentRemoveOption() const
{
return m_torrentContentRemoveOption;
}
void SessionImpl::setTorrentContentRemoveOption(const TorrentContentRemoveOption option)
{
m_torrentContentRemoveOption = option;
}
QStringList SessionImpl::bannedIPs() const
{
return m_bannedIPs;
@ -5002,18 +5053,7 @@ void SessionImpl::handleTorrentChecked(TorrentImpl *const torrent)
void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent)
{
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
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();
m_pendingFinishedTorrents.append(torrent);
}
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
const lt::torrent_handle nativeHandle {finishedJob.torrentHandle};
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);
}
}
@ -5660,74 +5700,32 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle,
return torrent;
}
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert *alert)
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/)
{
#ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
#else
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);
}
}
// We cannot consider `torrent_removed_alert` as a starting point for removing content,
// because it has an inconsistent posting time between different versions of libtorrent,
// so files may still be in use in some cases.
}
void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert)
{
#ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
#else
const auto id = TorrentID::fromInfoHash(alert->info_hash);
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
#endif
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);
handleRemovedTorrent(torrentID);
}
void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert)
{
#ifdef QBT_USES_LIBTORRENT2
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
#else
const auto id = TorrentID::fromInfoHash(alert->info_hash);
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
#endif
const auto removingTorrentDataIter = m_removingTorrents.find(id);
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);
const auto errorMessage = alert->error ? QString::fromLocal8Bit(alert->error.message().c_str()) : QString();
handleRemovedTorrent(torrentID, errorMessage);
}
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())
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)
saveTorrentsQueue();
@ -6140,7 +6161,7 @@ void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *a
if (torrent2)
{
if (torrent1)
deleteTorrent(torrentIDv1);
removeTorrent(torrentIDv1);
else
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 ResumeDataStorage;
class Torrent;
class TorrentContentRemover;
class TorrentDescriptor;
class TorrentImpl;
class Tracker;
@ -402,7 +403,7 @@ namespace BitTorrent
void setExcludedFileNamesEnabled(bool enabled) override;
QStringList excludedFileNames() const 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;
void setBannedIPs(const QStringList &newList) override;
ResumeDataStorageType resumeDataStorageType() const override;
@ -411,6 +412,8 @@ namespace BitTorrent
void setMergeTrackersEnabled(bool enabled) override;
bool isStartPaused() const override;
void setStartPaused(bool value) override;
TorrentContentRemoveOption torrentContentRemoveOption() const override;
void setTorrentContentRemoveOption(TorrentContentRemoveOption option) override;
bool isRestored() const override;
@ -430,7 +433,7 @@ namespace BitTorrent
bool isKnownTorrent(const InfoHash &infoHash) const 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 cancelDownloadMetadata(const TorrentID &id) override;
@ -487,11 +490,11 @@ namespace BitTorrent
void configureDeferred();
void readAlerts();
void enqueueRefresh();
void processShareLimits();
void generateResumeData();
void handleIPFilterParsed(int ruleCount);
void handleIPFilterError();
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
private:
struct ResumeSessionContext;
@ -507,8 +510,9 @@ namespace BitTorrent
struct RemovingTorrentData
{
QString name;
Path pathToRemove;
DeleteOption deleteOption {};
Path contentStoragePath;
PathList fileNames;
TorrentRemoveOption removeOption {};
};
explicit SessionImpl(QObject *parent = nullptr);
@ -536,6 +540,7 @@ namespace BitTorrent
void enableIPFilter();
void disableIPFilter();
void processTrackerStatuses();
void processTorrentShareLimits(TorrentImpl *torrent);
void populateExcludedFileNamesRegExpList();
void prepareStartup();
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);
// BitTorrent
lt::session *m_nativeSession = nullptr;
NativeSessionExtension *m_nativeSessionExtension = nullptr;
bool m_deferredConfigureScheduled = false;
bool m_IPFilteringConfigured = false;
mutable bool m_listenInterfaceConfigured = false;
void handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError = {});
CachedSettingValue<QString> m_DHTBootstrapNodes;
CachedSettingValue<bool> m_isDHTEnabled;
@ -731,8 +730,16 @@ namespace BitTorrent
CachedSettingValue<int> m_I2POutboundQuantity;
CachedSettingValue<int> m_I2PInboundLength;
CachedSettingValue<int> m_I2POutboundLength;
CachedSettingValue<TorrentContentRemoveOption> m_torrentContentRemoveOption;
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_isPaused = isStartPaused();
@ -766,6 +773,7 @@ namespace BitTorrent
QThreadPool *m_asyncWorker = nullptr;
ResumeDataStorage *m_resumeDataStorage = nullptr;
FileSearcher *m_fileSearcher = nullptr;
TorrentContentRemover *m_torrentContentRemover = nullptr;
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
@ -809,6 +817,8 @@ namespace BitTorrent
QTimer *m_wakeupCheckTimer = nullptr;
QDateTime m_wakeupCheckTimestamp;
QList<TorrentImpl *> m_pendingFinishedTorrents;
friend void Session::initInstance();
friend void Session::freeInstance();
friend Session *Session::instance();

View file

@ -228,6 +228,7 @@ namespace BitTorrent
virtual void setShareLimitAction(ShareLimitAction action) = 0;
virtual PathList filePaths() const = 0;
virtual PathList actualFilePaths() const = 0;
virtual TorrentInfo info() 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"
#endif // Q_OS_MACOS || Q_OS_WIN
#ifndef QBT_USES_LIBTORRENT2
#include "customstorage.h"
#endif
using namespace BitTorrent;
namespace
@ -982,6 +986,21 @@ PathList TorrentImpl::filePaths() const
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
{
return m_filePriorities;
@ -1447,11 +1466,13 @@ QBitArray TorrentImpl::pieces() const
QBitArray TorrentImpl::downloadingPieces() const
{
QBitArray result(piecesCount());
if (!hasMetadata())
return {};
std::vector<lt::partial_piece_info> queue;
m_nativeHandle.get_download_queue(queue);
QBitArray result {piecesCount()};
for (const lt::partial_piece_info &info : queue)
result.setBit(LT::toUnderlyingType(info.piece_index));
@ -1791,12 +1812,13 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
const Path filePath = actualFilePath.removedExtension(QB_EXT);
m_filePaths.append(filePath);
lt::download_priority_t &nativePriority = 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_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
}
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.ti = metadata;
@ -1859,6 +1881,9 @@ void TorrentImpl::reload()
auto *const extensionData = new ExtensionData;
p.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
m_nativeHandle = m_nativeSession->add_torrent(p);
m_nativeStatus = extensionData->status;

View file

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

View file

@ -44,6 +44,7 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
, m_requestHandler(requestHandler)
{
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
// memory constrained platforms
@ -62,11 +63,6 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
});
}
Connection::~Connection()
{
m_socket->close();
}
void Connection::read()
{
// reuse existing buffer and avoid unnecessary memory allocation/relocation
@ -182,11 +178,6 @@ bool Connection::hasExpired(const qint64 timeout) const
&& m_idleTimer.hasExpired(timeout);
}
bool Connection::isClosed() const
{
return (m_socket->state() == QAbstractSocket::UnconnectedState);
}
bool Connection::acceptsGzipEncoding(QString codings)
{
// [rfc7231] 5.3.4. Accept-Encoding

View file

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

View file

@ -32,7 +32,10 @@
#include <algorithm>
#include <chrono>
#include <memory>
#include <new>
#include <QtLogging>
#include <QNetworkProxy>
#include <QSslCipher>
#include <QSslConfiguration>
@ -40,7 +43,6 @@
#include <QStringList>
#include <QTimer>
#include "base/algorithm.h"
#include "base/global.h"
#include "base/utils/net.h"
#include "base/utils/sslkey.h"
@ -113,32 +115,38 @@ Server::Server(IRequestHandler *requestHandler, QObject *parent)
void Server::incomingConnection(const qintptr socketDescriptor)
{
if (m_connections.size() >= CONNECTIONS_LIMIT) return;
QTcpSocket *serverSocket = nullptr;
if (m_https)
serverSocket = new QSslSocket(this);
else
serverSocket = new QTcpSocket(this);
std::unique_ptr<QTcpSocket> serverSocket = m_https ? std::make_unique<QSslSocket>(this) : std::make_unique<QTcpSocket>(this);
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;
}
try
{
if (m_https)
{
static_cast<QSslSocket *>(serverSocket)->setProtocol(QSsl::SecureProtocols);
static_cast<QSslSocket *>(serverSocket)->setPrivateKey(m_key);
static_cast<QSslSocket *>(serverSocket)->setLocalCertificateChain(m_certificates);
static_cast<QSslSocket *>(serverSocket)->setPeerVerifyMode(QSslSocket::VerifyNone);
static_cast<QSslSocket *>(serverSocket)->startServerEncryption();
auto *sslSocket = static_cast<QSslSocket *>(serverSocket.get());
sslSocket->setProtocol(QSsl::SecureProtocols);
sslSocket->setPrivateKey(m_key);
sslSocket->setLocalCertificateChain(m_certificates);
sslSocket->setPeerVerifyMode(QSslSocket::VerifyNone);
sslSocket->startServerEncryption();
}
auto *c = new Connection(serverSocket, m_requestHandler, this);
m_connections.insert(c);
connect(serverSocket, &QAbstractSocket::disconnected, this, [c, this]() { removeConnection(c); });
auto *connection = new Connection(serverSocket.release(), m_requestHandler, this);
m_connections.insert(connection);
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)

View file

@ -134,17 +134,17 @@ void Preferences::setCustomUIThemePath(const Path &path)
setValue(u"Preferences/General/CustomUIThemePath"_s, path);
}
bool Preferences::deleteTorrentFilesAsDefault() const
bool Preferences::removeTorrentContent() const
{
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;
setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, del);
setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, remove);
}
bool Preferences::confirmOnExit() const

View file

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

View file

@ -52,19 +52,21 @@ const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored);
using BitTorrent::Torrent;
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_category {category}
, m_tag {tag}
, m_idSet {idSet}
, m_private {isPrivate}
{
}
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_tag {tag}
, m_idSet {idSet}
, m_private {isPrivate}
{
setTypeByName(filter);
}
@ -147,11 +149,22 @@ bool TorrentFilter::setTag(const std::optional<Tag> &tag)
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
{
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
@ -224,3 +237,11 @@ bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const
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;
// category & tags: pass empty string for uncategorized / untagged torrents.
TorrentFilter(Type type, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tag = AnyTag);
TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tags = AnyTag);
TorrentFilter(Type type
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, 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 setTypeByName(const QString &filter);
bool setTorrentIDSet(const std::optional<TorrentIDSet> &idSet);
bool setCategory(const std::optional<QString> &category);
bool setTag(const std::optional<Tag> &tag);
bool setPrivate(std::optional<bool> isPrivate);
bool match(const BitTorrent::Torrent *torrent) const;
@ -105,9 +113,11 @@ private:
bool matchHash(const BitTorrent::Torrent *torrent) const;
bool matchCategory(const BitTorrent::Torrent *torrent) const;
bool matchTag(const BitTorrent::Torrent *torrent) const;
bool matchPrivate(const BitTorrent::Torrent *torrent) const;
Type m_type {All};
std::optional<QString> m_category;
std::optional<Tag> m_tag;
std::optional<TorrentIDSet> m_idSet;
std::optional<bool> m_private;
};

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -29,8 +29,6 @@
#include "fs.h"
#include <cerrno>
#include <cstring>
#include <filesystem>
#if defined(Q_OS_WIN)
@ -52,6 +50,7 @@
#include <unistd.h>
#endif
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#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.
*/
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()};
if (file.remove())
return {};
if (!file.exists())
return true;
return {};
// Make sure we have read/write permissions
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)
{
return QFileInfo(path.data()).isReadable();

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -35,6 +35,7 @@
#include <QString>
#include "base/3rdparty/expected.hpp"
#include "base/global.h"
#include "base/pathfwd.h"
@ -60,7 +61,8 @@ namespace Utils::Fs
bool copyFile(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 mkpath(const Path &dirPath);
bool rmdir(const Path &dirPath);

View file

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

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -64,6 +64,7 @@
#include "base/utils/fs.h"
#include "base/utils/misc.h"
#include "base/utils/string.h"
#include "filterpatternformatmenu.h"
#include "lineedit.h"
#include "torrenttagsdialog.h"
@ -181,6 +182,11 @@ public:
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
{
Q_ASSERT((index >= 0) && (index < filesCount()));
@ -290,6 +296,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
, m_storeRememberLastSavePath {SETTINGS_KEY(u"RememberLastSavePath"_s)}
, m_storeTreeHeaderState {u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_s)}
, m_storeSplitterState {u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_s)}
, m_storeFilterPatternFormat {u"GUI/" SETTINGS_KEY(u"FilterPatternFormat"_s)}
{
m_ui->setupUi(this);
@ -316,6 +323,8 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
// Torrent content filtering
m_filterLine->setPlaceholderText(tr("Filter files..."));
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);
const auto *focusSearchHotkey = new QShortcut(QKeySequence::Find, this);
connect(focusSearchHotkey, &QShortcut::activated, this, [this]()
@ -360,7 +369,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
});
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->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone);
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()
{
Q_ASSERT(m_currentContext);
@ -886,15 +917,7 @@ void AddNewTorrentDialog::setupTreeview()
{
// Check file name blacklist for torrents that are manually added
QVector<BitTorrent::DownloadPriority> priorities = m_contentAdaptor->filePriorities();
for (int i = 0; i < priorities.size(); ++i)
{
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
continue;
if (BitTorrent::Session::instance()->isFilenameExcluded(torrentInfo.filePath(i).filename()))
priorities[i] = BitTorrent::DownloadPriority::Ignored;
}
BitTorrent::Session::instance()->applyFilenameFilter(m_contentAdaptor->filePaths(), priorities);
m_contentAdaptor->prioritizeFiles(priorities);
}

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -35,6 +35,7 @@
#include "base/path.h"
#include "base/settingvalue.h"
#include "filterpatternformat.h"
class LineEdit;
@ -92,6 +93,8 @@ private:
void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {});
void setupTreeview();
void saveTorrentFile();
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::AddNewTorrentDialog *m_ui = nullptr;
std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
@ -107,4 +110,5 @@ private:
SettingValue<bool> m_storeRememberLastSavePath;
SettingValue<QByteArray> m_storeTreeHeaderState;
SettingValue<QByteArray> m_storeSplitterState;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
};

View file

@ -63,6 +63,7 @@ namespace
// qBittorrent section
QBITTORRENT_HEADER,
RESUME_DATA_STORAGE,
TORRENT_CONTENT_REMOVE_OPTION,
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
MEMORY_WORKING_SET_LIMIT,
#endif
@ -364,6 +365,8 @@ void AdvancedSettings::saveAdvancedSettings() const
session->setI2PInboundLength(m_spinBoxI2PInboundLength.value());
session->setI2POutboundLength(m_spinBoxI2POutboundLength.value());
#endif
session->setTorrentContentRemoveOption(m_comboBoxTorrentContentRemoveOption.currentData().value<BitTorrent::TorrentContentRemoveOption>());
}
#ifndef QBT_USES_LIBTORRENT2
@ -472,6 +475,11 @@ void AdvancedSettings::loadAdvancedSettings()
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
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)
// Physical memory (RAM) usage limit
m_spinBoxMemoryWorkingSetLimit.setMinimum(1);

View file

@ -81,7 +81,7 @@ private:
m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity,
m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused;
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;
#ifndef QBT_USES_LIBTORRENT2

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -30,6 +31,7 @@
#include <QPushButton>
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "base/preferences.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->setIconSize(Utils::Gui::mediumIconSize());
m_ui->checkPermDelete->setChecked(defaultDeleteFiles || Preferences::instance()->deleteTorrentFilesAsDefault());
connect(m_ui->checkPermDelete, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState);
m_ui->checkRemoveContent->setChecked(defaultDeleteFiles || Preferences::instance()->removeTorrentContent());
connect(m_ui->checkRemoveContent, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState);
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Remove"));
m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus();
@ -67,18 +69,18 @@ DeletionConfirmationDialog::~DeletionConfirmationDialog()
delete m_ui;
}
bool DeletionConfirmationDialog::isDeleteFileSelected() const
bool DeletionConfirmationDialog::isRemoveContentSelected() const
{
return m_ui->checkPermDelete->isChecked();
return m_ui->checkRemoveContent->isChecked();
}
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()
{
Preferences::instance()->setDeleteTorrentFilesAsDefault(m_ui->checkPermDelete->isChecked());
Preferences::instance()->setRemoveTorrentContent(m_ui->checkRemoveContent->isChecked());
m_ui->rememberBtn->setEnabled(false);
}

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -37,16 +38,16 @@ namespace Ui
class DeletionConfirmationDialog;
}
class DeletionConfirmationDialog : public QDialog
class DeletionConfirmationDialog final : public QDialog
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(DeletionConfirmationDialog)
public:
DeletionConfirmationDialog(QWidget *parent, int size, const QString &name, bool defaultDeleteFiles);
~DeletionConfirmationDialog();
~DeletionConfirmationDialog() override;
bool isDeleteFileSelected() const;
bool isRemoveContentSelected() const;
private slots:
void updateRememberButtonState();

View file

@ -75,7 +75,7 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkPermDelete">
<widget class="QCheckBox" name="checkRemoveContent">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
@ -88,7 +88,7 @@
</font>
</property>
<property name="text">
<string>Also permanently delete the files</string>
<string>Also remove the content files</string>
</property>
</widget>
</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;
// 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_I2PPeerItems.clear();
@ -466,10 +466,14 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
{
QStandardItem *item = m_peerItems.take(peerEndpoint);
QSet<QStandardItem *> &items = m_itemsByIP[peerEndpoint.address.ip];
items.remove(item);
if (items.isEmpty())
m_itemsByIP.remove(peerEndpoint.address.ip);
const auto items = m_itemsByIP.find(peerEndpoint.address.ip);
Q_ASSERT(items != m_itemsByIP.end());
if (items == m_itemsByIP.end()) [[unlikely]]
continue;
items->remove(item);
if (items->isEmpty())
m_itemsByIP.erase(items);
m_listModel->removeRow(item->row());
}

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -52,6 +52,7 @@
#include "base/utils/misc.h"
#include "base/utils/string.h"
#include "gui/autoexpandabledialog.h"
#include "gui/filterpatternformatmenu.h"
#include "gui/lineedit.h"
#include "gui/trackerlist/trackerlistwidget.h"
#include "gui/uithememanager.h"
@ -66,6 +67,7 @@
PropertiesWidget::PropertiesWidget(QWidget *parent)
: QWidget(parent)
, m_ui {new Ui::PropertiesWidget}
, m_storeFilterPatternFormat {u"GUI/PropertiesWidget/FilterPatternFormat"_s}
{
m_ui->setupUi(this);
#ifndef Q_OS_MACOS
@ -78,7 +80,9 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
m_contentFilterLine = new LineEdit(this);
m_contentFilterLine->setPlaceholderText(tr("Filter files..."));
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->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open);
@ -206,6 +210,7 @@ void PropertiesWidget::clear()
m_ui->labelSavePathVal->clear();
m_ui->labelCreatedOnVal->clear();
m_ui->labelTotalPiecesVal->clear();
m_ui->labelPrivateVal->clear();
m_ui->labelInfohash1Val->clear();
m_ui->labelInfohash2Val->clear();
m_ui->labelCommentVal->clear();
@ -274,6 +279,28 @@ void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent)
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)
{
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->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
loadDynamicData();
}

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -32,7 +32,8 @@
#include <QList>
#include <QWidget>
#include "base/pathfwd.h"
#include "base/settingvalue.h"
#include "gui/filterpatternformat.h"
class QPushButton;
class QTreeView;
@ -102,6 +103,8 @@ private slots:
private:
QPushButton *getButtonFromIndex(int index);
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::PropertiesWidget *m_ui = nullptr;
BitTorrent::Torrent *m_torrent = nullptr;
@ -115,4 +118,6 @@ private:
PropTabBar *m_tabBar = nullptr;
LineEdit *m_contentFilterLine = nullptr;
int m_handleWidth = -1;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
};

View file

@ -823,6 +823,38 @@
</widget>
</item>
<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">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
@ -838,71 +870,7 @@
</property>
</widget>
</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">
<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">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@ -918,7 +886,55 @@
</property>
</widget>
</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">
<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">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@ -937,7 +953,23 @@
</property>
</widget>
</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">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -37,17 +37,12 @@
#include <QPointer>
#include <QScopeGuard>
#if defined(Q_OS_WIN)
#include <windows.h>
#include <shellapi.h>
#else
#include <QMimeDatabase>
#include <QMimeType>
#endif
#if defined Q_OS_WIN || defined Q_OS_MACOS
#if defined(Q_OS_MACOS)
#define QBT_PIXMAP_CACHE_FOR_FILE_ICONS
#include <QPixmapCache>
#elif !defined(Q_OS_WIN)
#include <QMimeDatabase>
#include <QMimeType>
#endif
#include "base/bittorrent/downloadpriority.h"
@ -116,27 +111,8 @@ namespace
};
#endif // QBT_PIXMAP_CACHE_FOR_FILE_ICONS
#if defined(Q_OS_WIN)
// See QTBUG-25319 for explanation why this is required
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
#if defined(Q_OS_MACOS)
// There is a bug on macOS, to be reported to Qt
// https://github.com/qbittorrent/qBittorrent/pull/6156#issuecomment-316302615
class MacFileIconProvider final : public CachingFileIconProvider
{
@ -145,7 +121,7 @@ namespace
return MacUtils::pixmapForExtension(ext, QSize(32, 32));
}
};
#else
#elif !defined(Q_OS_WIN)
/**
* @brief Tests whether QFileIconProvider actually works
*
@ -189,7 +165,7 @@ TorrentContentModel::TorrentContentModel(QObject *parent)
: QAbstractItemModel(parent)
, 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)
, m_fileIconProvider {new WinShellFileIconProvider}
, m_fileIconProvider {new QFileIconProvider}
#elif defined(Q_OS_MACOS)
, m_fileIconProvider {new MacFileIconProvider}
#else

View file

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

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -56,6 +56,19 @@
#include "gui/macutilities.h"
#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)
: QTreeView(parent)
{
@ -173,10 +186,20 @@ Path TorrentContentWidget::getItemPath(const QModelIndex &index) const
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));
}
if (patternText.isEmpty())
{
collapseAll();
@ -219,9 +242,9 @@ void TorrentContentWidget::keyPressEvent(QKeyEvent *event)
const Qt::CheckState state = (static_cast<Qt::CheckState>(value.toInt()) == 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);
}
@ -248,10 +271,10 @@ void TorrentContentWidget::renameSelectedFile()
void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority)
{
const QModelIndexList selectedRows = selectionModel()->selectedRows(0);
for (const QModelIndex &index : selectedRows)
const QList<QPersistentModelIndex> selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
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
// 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 auto priorityGroupSize = std::max<qsizetype>((selectedRows.length() / priorityGroups), 1);
@ -283,8 +306,8 @@ void TorrentContentWidget::applyPrioritiesByOrder()
break;
}
const QModelIndex &index = selectedRows[i];
model()->setData(index.sibling(index.row(), Priority), static_cast<int>(priority));
const QPersistentModelIndex &index = selectedRows[i];
model()->setData(index, static_cast<int>(priority));
}
}

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@ -33,6 +33,7 @@
#include "base/bittorrent/downloadpriority.h"
#include "base/pathfwd.h"
#include "filterpatternformat.h"
class QShortcut;
@ -92,7 +93,7 @@ public:
int getFileIndex(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 checkNone();

View file

@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@ -37,6 +37,7 @@
#include "base/global.h"
#include "autoexpandabledialog.h"
#include "flowlayout.h"
#include "utils.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::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())))
{
auto *tagWidget = new QCheckBox(tag.toString());
auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
if (initialTags.contains(tag))
tagWidget->setChecked(true);
tagsLayout->addWidget(tagWidget);
@ -78,12 +79,12 @@ TorrentTagsDialog::~TorrentTagsDialog()
TagSet TorrentTagsDialog::tags() const
{
TagSet tags;
auto *layout = m_ui->scrollArea->layout();
auto *layout = m_ui->scrollArea->widget()->layout();
for (int i = 0; i < (layout->count() - 1); ++i)
{
const auto *tagWidget = static_cast<QCheckBox *>(layout->itemAt(i)->widget());
if (tagWidget->isChecked())
tags.insert(Tag(tagWidget->text()));
tags.insert(Utils::Gui::widgetTextToTag(tagWidget->text()));
}
return tags;
@ -111,9 +112,9 @@ void TorrentTagsDialog::addNewTag()
}
else
{
auto *layout = m_ui->scrollArea->layout();
auto *layout = m_ui->scrollArea->widget()->layout();
auto *btn = layout->takeAt(layout->count() - 1);
auto *tagWidget = new QCheckBox(tag.toString());
auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
tagWidget->setChecked(true);
layout->addWidget(tagWidget);
layout->addItem(btn);

View file

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

View file

@ -39,7 +39,6 @@
#include <QUrl>
#include <QVBoxLayout>
#include "base/algorithm.h"
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.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_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_PRIVATE: return tr("Private", "Flags private torrents");
default: return {};
}
}
@ -357,6 +358,15 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
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)
{
case TR_NAME:
@ -431,6 +441,8 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
return hashString(torrent->infoHash().v2());
case TR_REANNOUNCE:
return reannounceString(torrent->nextAnnounce());
case TR_PRIVATE:
return privateString(torrent->isPrivate(), torrent->hasMetadata());
}
return {};
@ -512,6 +524,8 @@ QVariant TransferListModel::internalValue(const BitTorrent::Torrent *torrent, co
return QVariant::fromValue(torrent->infoHash().v2());
case TR_REANNOUNCE:
return torrent->nextAnnounce();
case TR_PRIVATE:
return (torrent->hasMetadata() ? torrent->isPrivate() : QVariant());
}
return {};

View file

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

View file

@ -84,6 +84,17 @@ namespace
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)
{
return ((column >= 0) && (column < TransferListModel::NB_COLUMNS))
@ -214,6 +225,9 @@ int TransferListSortModel::compare(const QModelIndex &left, const QModelIndex &r
case TransferListModel::TR_UPSPEED:
return customCompare(leftValue.toInt(), rightValue.toInt());
case TransferListModel::TR_PRIVATE:
return compareAsBool(leftValue, rightValue);
case TransferListModel::TR_PEERS:
case TransferListModel::TR_SEEDS:
{

View file

@ -1,6 +1,6 @@
/*
* 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>
*
* 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)
{
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)
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_TOTAL_SIZE, true);
setColumnHidden(TransferListModel::TR_REANNOUNCE, true);
setColumnHidden(TransferListModel::TR_PRIVATE, true);
}
//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
// NOTE: this will only work when dialog is modal
removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected());
removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected());
});
dialog->open();
}
@ -465,7 +467,7 @@ void TransferListWidget::deleteVisibleTorrents()
{
// Some torrents might be removed when waiting for user input, so refetch the torrent list
// NOTE: this will only work when dialog is modal
removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected());
removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected());
});
dialog->open();
}
@ -1190,7 +1192,7 @@ void TransferListWidget::displayListMenu()
const TagSet tags = BitTorrent::Session::instance()->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);
const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2017 Mike Tzou
*
* This program is free software; you can redistribute it and/or
@ -54,6 +55,7 @@
#include "base/global.h"
#include "base/path.h"
#include "base/tag.h"
#include "base/utils/fs.h"
#include "base/utils/version.h"
@ -216,3 +218,29 @@ void Utils::Gui::openFolderSelect(const Path &path)
openPath(path.parentPath());
#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.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2017 Mike Tzou
*
* This program is free software; you can redistribute it and/or
@ -34,8 +35,11 @@ class QIcon;
class QPixmap;
class QPoint;
class QSize;
class QString;
class QWidget;
class Tag;
namespace Utils::Gui
{
bool isDarkTheme();
@ -51,4 +55,7 @@ namespace Utils::Gui
void openPath(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"?>
<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) -->
<dependency>
<dependentAssembly>
@ -28,6 +35,7 @@
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<!-- Enable long paths that exceed MAX_PATH in length -->
<asmv3:application>
<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">

View file

@ -1,4 +1,4 @@
#VERSION: 1.45
#VERSION: 1.47
# Author:
# Christophe DUMEZ (chris@qbittorrent.org)
@ -39,9 +39,11 @@ import tempfile
import urllib.error
import urllib.parse
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 """
# 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"
headers = {'User-Agent': getBrowserUserAgent()}
headers: Dict[str, Any] = {'User-Agent': getBrowserUserAgent()}
# SOCKS5 Proxy support
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:
socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, m.group('host'),
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;)
# (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)
if entity in html.entities.name2codepoint:
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)
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 """
req = urllib.request.Request(url, headers=headers)
req = urllib.request.Request(url, headers={**headers, **custom_headers})
try:
response = urllib.request.urlopen(req)
except urllib.error.URLError as errno:
print(" ".join(("Connection error:", str(errno.reason))))
return ""
dat = response.read()
dat: bytes = response.read()
# Check if it is gzipped
if dat[:2] == b'\x1f\x8b':
# Data is gzip encoded, decode it
@ -109,16 +111,15 @@ def retrieve_url(url):
ignore, charset = info['Content-Type'].split('charset=')
except Exception:
pass
dat = dat.decode(charset, 'replace')
dat = htmlentitydecode(dat)
# return dat.encode('utf-8', 'replace')
return dat
datStr = dat.decode(charset, 'replace')
datStr = htmlentitydecode(datStr)
return datStr
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 """
file, path = tempfile.mkstemp()
file = os.fdopen(file, "wb")
fileHandle, path = tempfile.mkstemp()
file = os.fdopen(fileHandle, "wb")
# Download url
req = urllib.request.Request(url, headers=headers)
if referer is not None:

View file

@ -1,4 +1,4 @@
#VERSION: 1.45
#VERSION: 1.46
# Author:
# Fabien Devaux <fab AT gnux DOT info>
@ -37,17 +37,21 @@ import importlib
import pathlib
import sys
import urllib.parse
from collections.abc import Iterable, Iterator, Sequence
from enum import Enum
from glob import glob
from multiprocessing import Pool, cpu_count
from os import path
from typing import Dict, List, Optional, Set, Tuple, Type
THREADED = True
THREADED: bool = True
try:
MAX_THREADS = cpu_count()
MAX_THREADS: int = cpu_count()
except NotImplementedError:
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
@ -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
engine_dict = dict()
engine_dict: Dict[EngineName, Optional[Type[Engine]]] = {}
def list_engines():
def list_engines() -> List[EngineName]:
""" List all engines,
including broken engines that fail on import
@ -81,10 +103,10 @@ def list_engines():
return found_engines
def get_engine(engine_name):
#global engine_dict
def get_engine(engine_name: EngineName) -> Optional[Type[Engine]]:
if engine_name in engine_dict:
return engine_dict[engine_name]
# when import fails, engine is None
engine = None
try:
@ -97,35 +119,37 @@ def get_engine(engine_name):
return engine
def initialize_engines(found_engines):
def initialize_engines(found_engines: Iterable[EngineName]) -> Set[EngineName]:
""" Import available engines
Return list of available engines
Return set of available engines
"""
supported_engines = []
supported_engines = set()
for engine_name in found_engines:
# import engine
engine = get_engine(engine_name)
if engine is None:
continue
supported_engines.append(engine_name)
supported_engines.add(engine_name)
return supported_engines
def engines_to_xml(supported_engines):
def engines_to_xml(supported_engines: Iterable[EngineName]) -> Iterator[str]:
""" Generates xml for supported engines """
tab = " " * 4
for engine_name in supported_engines:
search_engine = get_engine(engine_name)
if search_engine is None:
continue
supported_categories = ""
if hasattr(search_engine, "supported_categories"):
supported_categories = " ".join((key
for key in search_engine.supported_categories.keys()
if key != "all"))
if key != Category.all.name))
yield "".join((tab, "<", engine_name, ">\n",
tab, tab, "<name>", search_engine.name, "</name>\n",
@ -134,7 +158,7 @@ def engines_to_xml(supported_engines):
tab, "</", engine_name, ">\n"))
def displayCapabilities(supported_engines):
def displayCapabilities(supported_engines: Iterable[EngineName]) -> None:
"""
Display capabilities in XML format
<capabilities>
@ -151,21 +175,24 @@ def displayCapabilities(supported_engines):
print(xml)
def run_search(engine_list):
def run_search(engine_list: Tuple[Optional[Type[Engine]], str, Category]) -> bool:
""" 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 True otherwise
"""
engine, what, cat = engine_list
engine_class, what, cat = engine_list
if engine_class is None:
return False
try:
engine = engine()
engine = engine_class()
# avoid exceptions due to invalid category
if hasattr(engine, 'supported_categories'):
if cat in engine.supported_categories:
engine.search(what, cat)
if cat.name in engine.supported_categories:
engine.search(what, cat.name)
else:
engine.search(what)
@ -174,7 +201,7 @@ def run_search(engine_list):
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
current_path = str(pathlib.Path(__file__).parent.resolve())
if current_path not in sys.path:
@ -182,7 +209,7 @@ def main(args):
found_engines = list_engines()
def show_usage():
def show_usage() -> None:
print("./nova2.py all|engine1[,engine2]* <category> <keywords>", file=sys.stderr)
print("found engines: " + ','.join(found_engines), 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:
show_usage()
sys.exit(1)
elif args[0] == "--capabilities":
supported_engines = initialize_engines(found_engines)
if "--names" in args:
@ -198,14 +224,14 @@ def main(args):
return
displayCapabilities(supported_engines)
return
elif len(args) < 3:
show_usage()
sys.exit(1)
cat = args[1].lower()
if cat not in CATEGORIES:
try:
category = Category[cat]
except KeyError:
print(" - ".join(('Invalid category', cat)), file=sys.stderr)
sys.exit(1)
@ -223,16 +249,18 @@ def main(args):
engines_list = initialize_engines(found_engines)
else:
# 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:]))
params = ((get_engine(engine_name), what, category) for engine_name in engines_list)
if THREADED:
# child process spawning is controlled min(number of searches, number of cpu)
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:
# 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__":

View file

@ -1,4 +1,4 @@
#VERSION: 1.48
#VERSION: 1.50
# Redistribution and use in source and binary forms, with or without
# 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
# 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((
dictionary["link"],
dictionary["name"].replace("|", " "),
@ -34,7 +51,7 @@ def prettyPrinter(dictionary):
str(dictionary["leech"]),
dictionary["engine_url"],
dictionary.get("desc_link", ""), # Optional
str(dictionary.get("pub_date", -1)), # Optional
str(dictionary.get("pub_date", -1)) # Optional
))
# fd 1 is stdout
@ -42,30 +59,32 @@ def prettyPrinter(dictionary):
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)
"""
# 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
units_dict = {'T': 40, 'G': 30, 'M': 20, 'K': 10}
if short_unit in units_dict:
size = size * 2**units_dict[short_unit]
return int(size)
The canonical type for `size_string` is `str`. However numeric types are also accepted in order to
accommodate poorly written plugins.
"""
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 <QDebug>
#include <QDir>
#include <QDirIterator>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
@ -135,7 +136,7 @@ void AppController::preferencesAction()
data[u"file_log_age"_s] = app()->fileLoggerAge();
data[u"file_log_age_type"_s] = app()->fileLoggerAgeType();
// 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
// When adding a torrent
@ -349,6 +350,8 @@ void AppController::preferencesAction()
// qBitorrent preferences
// Resume data storage type
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
data[u"memory_working_set_limit"_s] = app()->memoryWorkingSetLimit();
// Current network interface
@ -518,7 +521,7 @@ void AppController::setPreferencesAction()
app()->setFileLoggerAgeType(it.value().toInt());
// Delete torrent content files on torrent removal
if (hasKey(u"delete_torrent_content_files"_s))
pref->setDeleteTorrentFilesAsDefault(it.value().toBool());
pref->setRemoveTorrentContent(it.value().toBool());
// Downloads
// When adding a torrent
@ -930,6 +933,9 @@ void AppController::setPreferencesAction()
// Resume data storage type
if (hasKey(u"resume_data_storage_type"_s))
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
if (hasKey(u"memory_working_set_limit"_s))
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));
};
const QStringList dirs = dir.entryList(QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility));
setResult(QJsonArray::fromStringList(dirs));
QJsonArray ret;
QDirIterator it {dirPath, (QDir::NoDotAndDotDot | parseDirectoryContentMode(visibility))};
while (it.hasNext())
ret.append(it.next());
setResult(ret);
}
void AppController::networkInterfaceListAction()

View file

@ -135,6 +135,7 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent)
{KEY_TORRENT_SAVE_PATH, torrent.savePath().toString()},
{KEY_TORRENT_DOWNLOAD_PATH, torrent.downloadPath().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_COMPLETION_ON, Utils::DateTime::toSecsSinceEpoch(torrent.completedTime())},
{KEY_TORRENT_TRACKER, torrent.currentTracker()},
@ -163,8 +164,8 @@ QVariantMap serialize(const BitTorrent::Torrent &torrent)
{KEY_TORRENT_AVAILABILITY, torrent.distributedCopies()},
{KEY_TORRENT_REANNOUNCE, torrent.nextAnnounce()},
{KEY_TORRENT_COMMENT, torrent.comment()},
{KEY_TORRENT_ISPRIVATE, torrent.isPrivate()},
{KEY_TORRENT_TOTAL_SIZE, torrent.totalSize()}
{KEY_TORRENT_PRIVATE, (torrent.hasMetadata() ? torrent.isPrivate() : QVariant())},
{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_DOWNLOAD_PATH = u"download_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_COMPLETION_ON = u"completion_on"_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_REANNOUNCE = u"reannounce"_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);

View file

@ -222,6 +222,7 @@ namespace
case QMetaType::UInt:
case QMetaType::QDateTime:
case QMetaType::Nullptr:
case QMetaType::UnknownType:
if (prevData[key] != value)
syncData[key] = value;
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_DOWNLOAD_PATH = u"download_path"_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_PRIVATEKEY = u"ssl_private_key"_s;
const QString KEY_PROP_SSL_DHPARAMS = u"ssl_dh_params"_s;
const QString KEY_PROP_HAS_METADATA = u"has_metadata"_s;
// File keys
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")
// - 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 |
// - 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
// - reverse (bool): enable reverse sorting
// - 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 offset {params()[u"offset"_s].toInt()};
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;
if (!hashes.isEmpty())
@ -305,7 +310,7 @@ void TorrentsController::infoAction()
idSet->insert(BitTorrent::TorrentID::fromString(hash));
}
const TorrentFilter torrentFilter {filter, idSet, category, tag};
const TorrentFilter torrentFilter {filter, idSet, category, tag, isPrivate};
QVariantList torrentList;
for (const BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
{
@ -435,6 +440,8 @@ void TorrentsController::propertiesAction()
const int uploadLimit = torrent->uploadLimit();
const qreal ratio = torrent->realRatio();
const qreal popularity = torrent->popularity();
const bool hasMetadata = torrent->hasMetadata();
const bool isPrivate = torrent->isPrivate();
const QJsonObject ret
{
@ -470,14 +477,16 @@ void TorrentsController::propertiesAction()
{KEY_PROP_PIECE_SIZE, torrent->pieceLength()},
{KEY_PROP_PIECES_HAVE, torrent->piecesHave()},
{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_LAST_SEEN, Utils::DateTime::toSecsSinceEpoch(torrent->lastSeenComplete())},
{KEY_PROP_COMPLETION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->completedTime())},
{KEY_PROP_CREATION_DATE, Utils::DateTime::toSecsSinceEpoch(torrent->creationDate())},
{KEY_PROP_SAVE_PATH, torrent->savePath().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);
@ -1092,11 +1101,11 @@ void TorrentsController::deleteAction()
requireParams({u"hashes"_s, u"deleteFiles"_s});
const QStringList hashes {params()[u"hashes"_s].split(u'|')};
const DeleteOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false)
? DeleteTorrentAndFiles : DeleteTorrent;
const BitTorrent::TorrentRemoveOption deleteOption = parseBool(params()[u"deleteFiles"_s]).value_or(false)
? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
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);
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.setSecure(m_isSecureCookieEnabled && m_isHttpsEnabled);
cookie.setPath(u"/"_s);
QByteArray cookieRawForm = cookie.toRawForm();
if (m_isCSRFProtectionEnabled)
cookieRawForm.append("; SameSite=Strict");
cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
else if (cookie.isSecure())
cookieRawForm.append("; SameSite=None");
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookieRawForm)});
cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
}
void WebApplication::sessionEnd()

View file

@ -54,7 +54,7 @@
#include "base/utils/version.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;

View file

@ -1,8 +1,8 @@
import Globals from 'globals';
import Html from 'eslint-plugin-html';
import Js from '@eslint/js';
import Stylistic from '@stylistic/eslint-plugin';
import * as RegexpPlugin from 'eslint-plugin-regexp';
import Globals from "globals";
import Html from "eslint-plugin-html";
import Js from "@eslint/js";
import Stylistic from "@stylistic/eslint-plugin";
import * as RegexpPlugin from "eslint-plugin-regexp";
export default [
Js.configs.recommended,
@ -26,9 +26,16 @@ export default [
Stylistic
},
rules: {
"curly": ["error", "multi-or-nest", "consistent"],
"eqeqeq": "error",
"guard-for-in": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-var": "error",
"operator-assignment": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"radix": "error",
"Stylistic/no-mixed-operators": [
"error",
{
@ -38,7 +45,16 @@ export default [
}
],
"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"
},
"scripts": {
"format": "js-beautify -r 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"
"format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css",
"lint": "eslint *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public"
},
"devDependencies": {
"@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-More-1.6.0-compat-compressed.js"></script>
<script>
'use strict';
"use strict";
new Keyboard({
defaultEventType: 'keydown',
defaultEventType: "keydown",
events: {
'Escape': function(event) {
"Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
},
'Esc': function(event) {
"Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', function() {
const hash = new URI().getData('hash');
window.addEvent("domready", () => {
const hash = new URI().getData("hash");
if (!hash)
return false;
$('peers').focus();
$("peers").focus();
$('addPeersOk').addEvent('click', function(e) {
$("addPeersOk").addEvent("click", (e) => {
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)
return;
new Request({
url: 'api/v2/torrents/addPeers',
method: 'post',
url: "api/v2/torrents/addPeers",
method: "post",
data: {
hashes: hash,
peers: peers.join('|')
peers: peers.join("|")
},
onFailure: function() {
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-More-1.6.0-compat-compressed.js"></script>
<script>
'use strict';
"use strict";
window.addEvent('domready', function() {
window.addEvent("domready", () => {
new Keyboard({
defaultEventType: 'keydown',
defaultEventType: "keydown",
events: {
'Escape': function(event) {
"Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
},
'Esc': function(event) {
"Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
}
}
}).activate();
$('trackersUrls').focus();
$('addTrackersButton').addEvent('click', function(e) {
$("trackersUrls").focus();
$("addTrackersButton").addEvent("click", (e) => {
new Event(e).stop();
const hash = new URI().getData('hash');
const hash = new URI().getData("hash");
new Request({
url: 'api/v2/torrents/addTrackers',
method: 'post',
url: "api/v2/torrents/addTrackers",
method: "post",
data: {
hash: hash,
urls: $('trackersUrls').value
urls: $("trackersUrls").value
},
onComplete: function() {
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-More-1.6.0-compat-compressed.js"></script>
<script>
'use strict';
"use strict";
function setRememberBtnEnabled(enable) {
const btn = $('rememberBtn');
const btn = $("rememberBtn");
btn.disabled = !enable;
const icon = btn.getElementsByTagName('path')[0];
const icon = btn.getElementsByTagName("path")[0];
if (enable)
icon.style.removeProperty('fill');
icon.style.removeProperty("fill");
else
icon.style.fill = "var(--color-border-default)";
}
window.addEvent('domready', function() {
window.addEvent("domready", () => {
new Request({
url: 'images/object-locked.svg',
method: 'get',
url: "images/object-locked.svg",
method: "get",
onSuccess: function(text, xml) {
const newIcon = xml.childNodes[0];
newIcon.style.height = '24px';
newIcon.style.width = '24px';
$('rememberBtn').appendChild(newIcon);
newIcon.style.height = "24px";
newIcon.style.width = "24px";
$("rememberBtn").appendChild(newIcon);
setRememberBtnEnabled(false);
}
}).send();
const isDeletingFiles = (new URI().getData('deleteFiles') === "true");
$('deleteFromDiskCB').checked = isDeletingFiles;
const isDeletingFiles = (new URI().getData("deleteFiles") === "true");
$("deleteFromDiskCB").checked = isDeletingFiles;
const prefCache = window.parent.qBittorrent.Cache.preferences.get();
let prefDeleteContentFiles = prefCache.delete_torrent_content_files;
$('deleteFromDiskCB').checked ||= prefDeleteContentFiles;
$('deleteFromDiskCB').addEvent('click', function(e) {
setRememberBtnEnabled($('deleteFromDiskCB').checked !== prefDeleteContentFiles);
$("deleteFromDiskCB").checked ||= prefDeleteContentFiles;
$("deleteFromDiskCB").addEvent("click", (e) => {
setRememberBtnEnabled($("deleteFromDiskCB").checked !== prefDeleteContentFiles);
});
// Set current "Delete files" choice as the default
$('rememberBtn').addEvent('click', function(e) {
$("rememberBtn").addEvent("click", (e) => {
window.parent.qBittorrent.Cache.preferences.set({
data: {
'delete_torrent_content_files': $('deleteFromDiskCB').checked
"delete_torrent_content_files": $("deleteFromDiskCB").checked
},
onSuccess: function() {
prefDeleteContentFiles = $('deleteFromDiskCB').checked;
prefDeleteContentFiles = $("deleteFromDiskCB").checked;
setRememberBtnEnabled(false);
}
});
});
const hashes = new URI().getData('hashes').split('|');
$('cancelBtn').focus();
$('cancelBtn').addEvent('click', function(e) {
const hashes = new URI().getData("hashes").split("|");
$("cancelBtn").focus();
$("cancelBtn").addEvent("click", (e) => {
new Event(e).stop();
window.parent.qBittorrent.Client.closeWindows();
});
$('confirmBtn').addEvent('click', function(e) {
$("confirmBtn").addEvent("click", (e) => {
parent.torrentsTable.deselectAll();
new Event(e).stop();
const cmd = 'api/v2/torrents/delete';
const deleteFiles = $('deleteFromDiskCB').get('checked');
const cmd = "api/v2/torrents/delete";
const deleteFiles = $("deleteFromDiskCB").get("checked");
new Request({
url: cmd,
method: 'post',
method: "post",
data: {
'hashes': hashes.join('|'),
'deleteFiles': deleteFiles
"hashes": hashes.join("|"),
"deleteFiles": deleteFiles
},
onComplete: function() {
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>
&nbsp;&nbsp;&nbsp;&nbsp;<button id="rememberBtn" type="button" title="Remember choice" style="vertical-align: middle; padding: 4px 6px;">
</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;">
<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>

View file

@ -8,22 +8,22 @@
<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>
'use strict';
"use strict";
window.addEvent('domready', () => {
const paths = new URI().getData('paths').split('|');
$('cancelBtn').focus();
$('cancelBtn').addEvent('click', (e) => {
window.addEvent("domready", () => {
const paths = new URI().getData("paths").split("|");
$("cancelBtn").focus();
$("cancelBtn").addEvent("click", (e) => {
new Event(e).stop();
window.parent.qBittorrent.Client.closeWindows();
});
$('confirmBtn').addEvent('click', (e) => {
$("confirmBtn").addEvent("click", (e) => {
new Event(e).stop();
let completionCount = 0;
paths.forEach((path) => {
new Request({
url: 'api/v2/rss/removeItem',
method: 'post',
url: "api/v2/rss/removeItem",
method: "post",
data: {
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-More-1.6.0-compat-compressed.js"></script>
<script>
'use strict';
"use strict";
window.addEvent('domready', () => {
const rules = new URI().getData('rules').split('|');
window.addEvent("domready", () => {
const rules = new URI().getData("rules").split("|");
$('cancelBtn').focus();
$('cancelBtn').addEvent('click', (e) => {
$("cancelBtn").focus();
$("cancelBtn").addEvent("click", (e) => {
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();
let completionCount = 0;
rules.forEach((rule) => {
window.parent.qBittorrent.RssDownloader.modifyRuleState(decodeURIComponent(rule), 'previouslyMatchedEpisodes', [], () => {
window.parent.qBittorrent.RssDownloader.modifyRuleState(decodeURIComponent(rule), "previouslyMatchedEpisodes", [], () => {
++completionCount;
if (completionCount === rules.length) {
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-More-1.6.0-compat-compressed.js"></script>
<script>
'use strict';
"use strict";
window.addEvent('domready', () => {
const rules = new URI().getData('rules').split('|');
window.addEvent("domready", () => {
const rules = new URI().getData("rules").split("|");
$('cancelBtn').focus();
$('cancelBtn').addEvent('click', (e) => {
$("cancelBtn").focus();
$("cancelBtn").addEvent("click", (e) => {
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();
let completionCount = 0;
rules.forEach((rule) => {
new Request({
url: 'api/v2/rss/removeRule',
method: 'post',
url: "api/v2/rss/removeRule",
method: "post",
data: {
ruleName: decodeURIComponent(rule)
},
@ -32,7 +32,7 @@
++completionCount;
if (completionCount === rules.length) {
window.parent.qBittorrent.RssDownloader.updateRulesList();
window.parent.MochaUI.closeWindow(window.parent.$('removeRulePage'));
window.parent.MochaUI.closeWindow(window.parent.$("removeRulePage"));
}
}
}).send();

View file

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

View file

@ -25,17 +25,17 @@
</div>
<script>
'use strict';
"use strict";
const hashes = new URI().getData('hashes').split('|');
const hashes = new URI().getData("hashes").split("|");
const setDlLimit = function() {
const limit = $("dllimitUpdatevalue").value.toInt() * 1024;
if (hashes[0] === "global") {
new Request({
url: 'api/v2/transfer/setDownloadLimit',
method: 'post',
url: "api/v2/transfer/setDownloadLimit",
method: "post",
data: {
'limit': limit
"limit": limit
},
onComplete: function() {
window.parent.updateMainData();
@ -45,11 +45,11 @@
}
else {
new Request({
url: 'api/v2/torrents/setDownloadLimit',
method: 'post',
url: "api/v2/torrents/setDownloadLimit",
method: "post",
data: {
'hashes': hashes.join('|'),
'limit': limit
"hashes": hashes.join("|"),
"limit": limit
},
onComplete: function() {
window.parent.qBittorrent.Client.closeWindows();
@ -59,24 +59,24 @@
};
new Keyboard({
defaultEventType: 'keydown',
defaultEventType: "keydown",
events: {
'Enter': function(event) {
$('applyButton').click();
"Enter": function(event) {
$("applyButton").click();
event.preventDefault();
},
'Escape': function(event) {
"Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
},
'Esc': function(event) {
"Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
}
}
}).activate();
$('dllimitUpdatevalue').focus();
$("dllimitUpdatevalue").focus();
MochaUI.addDlLimitSlider(hashes);
</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-More-1.6.0-compat-compressed.js"></script>
<script>
'use strict';
"use strict";
window.addEvent('domready', function() {
window.addEvent("domready", () => {
new Keyboard({
defaultEventType: 'keydown',
defaultEventType: "keydown",
events: {
'Enter': function(event) {
$('editTrackerButton').click();
"Enter": function(event) {
$("editTrackerButton").click();
event.preventDefault();
},
'Escape': function(event) {
"Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
},
'Esc': function(event) {
"Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
}
}
}).activate();
const currentUrl = new URI().getData('url');
const currentUrl = new URI().getData("url");
if (!currentUrl)
return false;
$('trackerUrl').value = currentUrl;
$('trackerUrl').focus();
$("trackerUrl").value = currentUrl;
$("trackerUrl").focus();
$('editTrackerButton').addEvent('click', function(e) {
$("editTrackerButton").addEvent("click", (e) => {
new Event(e).stop();
const hash = new URI().getData('hash');
const hash = new URI().getData("hash");
new Request({
url: 'api/v2/torrents/editTracker',
method: 'post',
url: "api/v2/torrents/editTracker",
method: "post",
data: {
hash: hash,
origUrl: currentUrl,
newUrl: $('trackerUrl').value
newUrl: $("trackerUrl").value
},
onComplete: function() {
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/misc.js?locale=${LANG}&v=${CACHEID}"></script>
<script>
'use strict';
"use strict";
new Keyboard({
defaultEventType: 'keydown',
defaultEventType: "keydown",
events: {
'Enter': function(event) {
$('categoryNameButton').click();
"Enter": function(event) {
$("categoryNameButton").click();
event.preventDefault();
},
'Escape': function(event) {
"Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
},
'Esc': function(event) {
"Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows();
event.preventDefault();
}
}
}).activate();
window.addEvent('domready', function() {
const uriAction = window.qBittorrent.Misc.safeTrim(new URI().getData('action'));
const uriHashes = window.qBittorrent.Misc.safeTrim(new URI().getData('hashes'));
const uriCategoryName = window.qBittorrent.Misc.safeTrim(new URI().getData('categoryName'));
const uriSavePath = window.qBittorrent.Misc.safeTrim(new URI().getData('savePath'));
window.addEvent("domready", () => {
const uriAction = window.qBittorrent.Misc.safeTrim(new URI().getData("action"));
const uriHashes = window.qBittorrent.Misc.safeTrim(new URI().getData("hashes"));
const uriCategoryName = window.qBittorrent.Misc.safeTrim(new URI().getData("categoryName"));
const uriSavePath = window.qBittorrent.Misc.safeTrim(new URI().getData("savePath"));
if (uriAction === "edit") {
if (!uriCategoryName)
return false;
$('categoryName').set('disabled', true);
$('categoryName').set('value', window.qBittorrent.Misc.escapeHtml(uriCategoryName));
$('savePath').set('value', window.qBittorrent.Misc.escapeHtml(uriSavePath));
$('savePath').focus();
$("categoryName").set("disabled", true);
$("categoryName").set("value", window.qBittorrent.Misc.escapeHtml(uriCategoryName));
$("savePath").set("value", window.qBittorrent.Misc.escapeHtml(uriSavePath));
$("savePath").focus();
}
else if (uriAction === "createSubcategory") {
$('categoryName').set('value', window.qBittorrent.Misc.escapeHtml(uriCategoryName));
$('categoryName').focus();
$("categoryName").set("value", window.qBittorrent.Misc.escapeHtml(uriCategoryName));
$("categoryName").focus();
}
else {
$('categoryName').focus();
$("categoryName").focus();
}
$('categoryNameButton').addEvent('click', function(e) {
$("categoryNameButton").addEvent("click", (e) => {
new Event(e).stop();
const savePath = $('savePath').value.trim();
const categoryName = $('categoryName').value.trim();
const savePath = $("savePath").value.trim();
const categoryName = $("categoryName").value.trim();
const verifyCategoryName = function(name) {
if ((name === null) || (name === ""))
@ -74,16 +74,16 @@
return;
new Request({
url: 'api/v2/torrents/createCategory',
method: 'post',
url: "api/v2/torrents/createCategory",
method: "post",
data: {
category: categoryName,
savePath: savePath
},
onSuccess: function() {
new Request({
url: 'api/v2/torrents/setCategory',
method: 'post',
url: "api/v2/torrents/setCategory",
method: "post",
data: {
hashes: uriHashes,
category: categoryName
@ -104,8 +104,8 @@
return;
new Request({
url: 'api/v2/torrents/createCategory',
method: 'post',
url: "api/v2/torrents/createCategory",
method: "post",
data: {
category: categoryName,
savePath: savePath
@ -117,8 +117,8 @@
break;
case "edit":
new Request({
url: 'api/v2/torrents/editCategory',
method: 'post',
url: "api/v2/torrents/editCategory",
method: "post",
data: {
category: uriCategoryName, // category name can't be changed
savePath: savePath

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

@ -26,13 +26,12 @@
* exception statement from your version.
*/
'use strict';
"use strict";
// This file is the JavaScript implementation of base/utils/fs.cpp
if (window.qBittorrent === undefined) {
if (window.qBittorrent === undefined)
window.qBittorrent = {};
}
window.qBittorrent.Filesystem = (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.
*/
const fileExtension = function(filename) {
const pointIndex = filename.lastIndexOf('.');
const pointIndex = filename.lastIndexOf(".");
if (pointIndex === -1)
return '';
return "";
return filename.substring(pointIndex + 1);
};
@ -66,7 +65,7 @@ window.qBittorrent.Filesystem = (function() {
const folderName = function(filepath) {
const slashIndex = filepath.lastIndexOf(PathSeparator);
if (slashIndex === -1)
return '';
return "";
return filepath.substring(0, slashIndex);
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -26,11 +26,10 @@
* exception statement from your version.
*/
'use strict';
"use strict";
if (window.qBittorrent === undefined) {
if (window.qBittorrent === undefined)
window.qBittorrent = {};
}
window.qBittorrent.PropGeneral = (function() {
const exports = function() {
@ -42,42 +41,43 @@ window.qBittorrent.PropGeneral = (function() {
const piecesBar = new window.qBittorrent.PiecesBar.PiecesBar([], {
height: 16
});
$('progress').appendChild(piecesBar);
$("progress").appendChild(piecesBar);
const clearData = function() {
$('time_elapsed').set('html', '');
$('eta').set('html', '');
$('nb_connections').set('html', '');
$('total_downloaded').set('html', '');
$('total_uploaded').set('html', '');
$('dl_speed').set('html', '');
$('up_speed').set('html', '');
$('dl_limit').set('html', '');
$('up_limit').set('html', '');
$('total_wasted').set('html', '');
$('seeds').set('html', '');
$('peers').set('html', '');
$('share_ratio').set('html', '');
$('popularity').set('html', '');
$('reannounce').set('html', '');
$('last_seen').set('html', '');
$('total_size').set('html', '');
$('pieces').set('html', '');
$('created_by').set('html', '');
$('addition_date').set('html', '');
$('completion_date').set('html', '');
$('creation_date').set('html', '');
$('torrent_hash_v1').set('html', '');
$('torrent_hash_v2').set('html', '');
$('save_path').set('html', '');
$('comment').set('html', '');
$("time_elapsed").set("html", "");
$("eta").set("html", "");
$("nb_connections").set("html", "");
$("total_downloaded").set("html", "");
$("total_uploaded").set("html", "");
$("dl_speed").set("html", "");
$("up_speed").set("html", "");
$("dl_limit").set("html", "");
$("up_limit").set("html", "");
$("total_wasted").set("html", "");
$("seeds").set("html", "");
$("peers").set("html", "");
$("share_ratio").set("html", "");
$("popularity").set("html", "");
$("reannounce").set("html", "");
$("last_seen").set("html", "");
$("total_size").set("html", "");
$("pieces").set("html", "");
$("created_by").set("html", "");
$("addition_date").set("html", "");
$("completion_date").set("html", "");
$("creation_date").set("html", "");
$("torrent_hash_v1").set("html", "");
$("torrent_hash_v2").set("html", "");
$("save_path").set("html", "");
$("comment").set("html", "");
$("private").set("html", "");
piecesBar.clear();
};
let loadTorrentDataTimer;
const loadTorrentData = function() {
if ($('prop_general').hasClass('invisible')
|| $('propertiesPanel_collapseToggle').hasClass('panel-expand')) {
if ($("prop_general").hasClass("invisible")
|| $("propertiesPanel_collapseToggle").hasClass("panel-expand")) {
// Tab changed, don't do anything
return;
}
@ -88,18 +88,18 @@ window.qBittorrent.PropGeneral = (function() {
loadTorrentDataTimer = loadTorrentData.delay(5000);
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({
url: url,
method: 'get',
method: "get",
noCache: true,
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);
loadTorrentDataTimer = loadTorrentData.delay(10000);
},
onSuccess: function(data) {
$('error_div').set('html', '');
$("error_div").set("html", "");
if (data) {
// Update Torrent data
@ -108,109 +108,118 @@ window.qBittorrent.PropGeneral = (function() {
.replace("%1", window.qBittorrent.Misc.friendlyDuration(data.time_elapsed))
.replace("%2", window.qBittorrent.Misc.friendlyDuration(data.seeding_time))
: 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]"
.replace("%1", data.nb_connections)
.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]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.total_downloaded))
.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]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.total_uploaded))
.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]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.dl_speed, 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]"
.replace("%1", window.qBittorrent.Misc.friendlyUnit(data.up_speed, 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)
? "∞"
: window.qBittorrent.Misc.friendlyUnit(data.dl_limit, true);
$('dl_limit').set('html', dlLimit);
$("dl_limit").set("html", dlLimit);
const upLimit = (data.up_limit === -1)
? "∞"
: 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]"
.replace("%1", data.seeds)
.replace("%2", data.seeds_total);
$('seeds').set('html', seeds);
$("seeds").set("html", seeds);
const peers = "QBT_TR(%1 (%2 total))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", data.peers)
.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)
? new Date(data.last_seen * 1000).toLocaleString()
: "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) : "";
$('total_size').set('html', totalSize);
$("total_size").set("html", totalSize);
const pieces = (data.pieces_num >= 0)
? "QBT_TR(%1 x %2 (have %3))QBT_TR[CONTEXT=PropertiesWidget]"
.replace("%1", data.pieces_num)
.replace("%2", window.qBittorrent.Misc.friendlyUnit(data.piece_size))
.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)
? new Date(data.addition_date * 1000).toLocaleString()
: "QBT_TR(Unknown)QBT_TR[CONTEXT=HttpServer]";
$('addition_date').set('html', additionDate);
$("addition_date").set("html", additionDate);
const completionDate = (data.completion_date >= 0)
? new Date(data.completion_date * 1000).toLocaleString()
: "";
$('completion_date').set('html', completionDate);
$("completion_date").set("html", completionDate);
const creationDate = (data.creation_date >= 0)
? new Date(data.creation_date * 1000).toLocaleString()
: "";
$('creation_date').set('html', creationDate);
$("creation_date").set("html", creationDate);
const torrentHashV1 = (data.infohash_v1 !== "")
? data.infohash_v1
: "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 !== "")
? data.infohash_v2
: "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 {
clearData();
@ -220,25 +229,24 @@ window.qBittorrent.PropGeneral = (function() {
}
}).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({
url: piecesUrl,
method: 'get',
method: "get",
noCache: true,
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);
loadTorrentDataTimer = loadTorrentData.delay(10000);
},
onSuccess: function(data) {
$('error_div').set('html', '');
$("error_div").set("html", "");
if (data) {
if (data)
piecesBar.setPieces(data);
}
else {
else
clearData();
}
clearTimeout(loadTorrentDataTimer);
loadTorrentDataTimer = loadTorrentData.delay(5000);
}

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