mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-08-19 21:03:30 -07:00
Compare commits
60 commits
master
...
release-5.
Author | SHA1 | Date | |
---|---|---|---|
|
2d67729617 |
||
|
878ebbed41 |
||
|
c61c3d7cd8 |
||
|
978fbbdc0d |
||
|
63689cf763 |
||
|
cebc72d3cf |
||
|
a67bd271c6 |
||
|
a8cffbb205 |
||
|
7dfb0110d4 |
||
|
3ad8fcbdd2 |
||
|
195eae5f3d |
||
|
920ae26f7b |
||
|
09ed0d6b66 |
||
|
4f0cc8aa11 |
||
|
4d490c84e7 |
||
|
96607ce874 |
||
|
418edc7471 |
||
|
bd01b7c4df |
||
|
b0ac763048 |
||
|
127d2d6f0b |
||
|
4149609e78 |
||
|
78c549f83e |
||
|
a3a53e2e0e |
||
|
5aaa43e01d |
||
|
86745d7b07 |
||
|
210650a5ee |
||
|
fe93b6d0d8 |
||
|
e8b585acd8 |
||
|
cea20141a9 |
||
|
0f5a27ed50 |
||
|
c2cf898ccd |
||
|
5e5aa8a563 |
||
|
12a4c3fda2 |
||
|
5f50b701d2 |
||
|
9f20d9c3aa |
||
|
05e3130baa |
||
|
683492648f |
||
|
2f2e158877 |
||
|
e60e96cb0e |
||
|
5f31208bf1 |
||
|
fa58e58e70 |
||
|
671943a9a6 |
||
|
8bad80bcdd |
||
|
c44e300507 |
||
|
318a677e8f |
||
|
0246df790a |
||
|
782fbc1425 |
||
|
7deccd5592 |
||
|
4a36fe7278 |
||
|
1c5af96ad8 |
||
|
3bb47a5410 |
||
|
d7abeb4bf0 |
||
|
a19d623ead |
||
|
1ef21bc2b7 |
||
|
4687b4e8e4 |
||
|
d2e5163861 |
||
|
8a15ea8026 |
||
|
2b99554813 |
||
|
e6638f9c19 |
||
|
ec6eac2ba1 |
253 changed files with 16285 additions and 7827 deletions
7
.github/workflows/ci_macos.yaml
vendored
7
.github/workflows/ci_macos.yaml
vendored
|
@ -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 }}
|
||||
|
|
12
.github/workflows/ci_python.yaml
vendored
12
.github/workflows/ci_python.yaml
vendored
|
@ -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
|
||||
|
|
14
.github/workflows/ci_ubuntu.yaml
vendored
14
.github/workflows/ci_ubuntu.yaml
vendored
|
@ -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
|
||||
|
|
30
.github/workflows/ci_windows.yaml
vendored
30
.github/workflows/ci_windows.yaml
vendored
|
@ -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
|
||||
|
|
2
.github/workflows/coverity-scan.yaml
vendored
2
.github/workflows/coverity-scan.yaml
vendored
|
@ -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
|
||||
|
|
|
@ -78,11 +78,7 @@ repos:
|
|||
m4/.* |
|
||||
src/base/3rdparty/.* |
|
||||
src/searchengine/nova3/socks.py |
|
||||
src/webui/www/private/lang/.* |
|
||||
src/webui/www/private/scripts/lib/.* |
|
||||
src/webui/www/public/lang/.* |
|
||||
src/webui/www/public/scripts/lib/.* |
|
||||
src/webui/www/transifex/.*
|
||||
src/webui/www/private/scripts/lib/.*
|
||||
)$
|
||||
exclude_types:
|
||||
- ts
|
||||
|
@ -106,11 +102,7 @@ repos:
|
|||
m4/.* |
|
||||
src/base/3rdparty/.* |
|
||||
src/searchengine/nova3/socks.py |
|
||||
src/webui/www/private/lang/.* |
|
||||
src/webui/www/private/scripts/lib/.* |
|
||||
src/webui/www/public/lang/.* |
|
||||
src/webui/www/public/scripts/lib/.* |
|
||||
src/webui/www/transifex/.*
|
||||
src/webui/www/private/scripts/lib/.*
|
||||
)$
|
||||
exclude_types:
|
||||
- svg
|
||||
|
|
|
@ -17,14 +17,6 @@ type = QT
|
|||
minimum_perc = 23
|
||||
lang_map = pt: pt_PT, zh: zh_CN
|
||||
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui_json]
|
||||
file_filter = src/webui/www/transifex/<lang>.json
|
||||
source_file = src/webui/www/transifex/en.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
minimum_perc = 23
|
||||
lang_map = pt: pt_PT, zh: zh_CN
|
||||
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrentdesktop_master]
|
||||
source_file = dist/unix/org.qbittorrent.qBittorrent.desktop
|
||||
source_lang = en
|
||||
|
|
37
Changelog
37
Changelog
|
@ -12,14 +12,28 @@ Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
|
|||
- FEATURE: Enable Ctrl+F hotkey for more inputs (thalieht)
|
||||
- FEATURE: Add seeding limits to RSS and Watched folders options UI (glassez)
|
||||
- FEATURE: Subcategories implicitly follow the parent category options (glassez)
|
||||
- FEATURE: Add support for SSL torrents (Chocobo1, Radu Carpa)
|
||||
- FEATURE: Add option to name each qbittorrent instance (Chocobo1)
|
||||
- FEATURE: Add button for sending test email (Thomas Piccirello)
|
||||
- FEATURE: Allow torrents to override default share limit action (glassez)
|
||||
- FEATURE: Use Start/Stop instead of Resume/Pause (thalieht)
|
||||
- FEATURE: Add the Popularity metric (Aliaksei Urbanski)
|
||||
- FEATURE: Focus on Download button if torrent link retrieved from the clipboard (glassez)
|
||||
- FEATURE: Add ability to pause/resume entire BitTorrent session (glassez)
|
||||
- FEATURE: Add an option to set BitTorrent session shutdown timeout (glassez)
|
||||
- FEATURE: Apply "Excluded file names" to folder names as well (glassez)
|
||||
- FEATURE: Allow to use regular expression to filter torrent content (glassez)
|
||||
- FEATURE: Allow to move content files to Trash instead of deleting them (glassez)
|
||||
- FEATURE: Add ability to display torrent "privateness" in UI (ManiMatter)
|
||||
- FEATURE: Add a flag in `Peers` tab denoting a connection using NAT hole punching (stalkerok)
|
||||
- BUGFIX: Display error message when unrecoverable error occurred (glassez)
|
||||
- BUGFIX: Update size of selected files when selection is changed (glassez)
|
||||
- BUGFIX: Normalize tags by trimming leading/trailing whitespace (glassez)
|
||||
- BUGFIX: Correctly handle share limits in torrent options dialog (glassez)
|
||||
- BUGFIX: Adjust tracker tier when adding additional trackers (Chocobo1)
|
||||
- BUGFIX: Fix inconsistent naming between `Done/Progress` column (luzpaz)
|
||||
- BUGFIX: Sanitize peer client names (Hanabishi)
|
||||
- BUGFIX: Apply share limits immediately when torrent downloading is finished (glassez)
|
||||
- BUGFIX: Show download progress for folders with zero byte size as 100 instead of 0 (vikas_c)
|
||||
- WEBUI: Improve WebUI responsiveness (Chocobo1)
|
||||
- WEBUI: Do not exit the app when WebUI has failed to start (Hanabishi)
|
||||
- WEBUI: Add `Moving` filter to side panel (xavier2k6)
|
||||
|
@ -28,14 +42,35 @@ Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
|
|||
- WEBUI: Leave the fields empty when value is invalid (Chocobo1)
|
||||
- WEBUI: Use natural sorting (Chocobo1)
|
||||
- WEBUI: Improve WebUI login behavior (JayRet)
|
||||
- WEBUI: Conditionally show filters sidebar (Thomas Piccirello)
|
||||
- WEBUI: Add support for running concurrent searches (Thomas Piccirello)
|
||||
- WEBUI: Improve accuracy of trackers list (Thomas Piccirello)
|
||||
- WEBUI: Fix error when category doesn't exist (Thomas Piccirello)
|
||||
- WEBUI: Improve table scrolling and selection on mobile (Thomas Piccirello)
|
||||
- WEBUI: Restore search tabs on load (Thomas Piccirello)
|
||||
- WEBUI: Restore previously used tab on load (Thomas Piccirello)
|
||||
- WEBUI: Increase default height of 'Share ratio limit' dialog (thalieht)
|
||||
- WEBUI: Use enabled search plugins by default (Thomas Piccirello)
|
||||
- WEBUI: Add columns `Incomplete Save Path`, `Info Hash v1`, `Info Hash v2` (thalieht)
|
||||
- WEBUI: Always create generic filter items (skomerko)
|
||||
- WEBAPI: Fix wrong timestamp values (Chocobo1)
|
||||
- WEBAPI: Send binary data with filename and mime type specified (glassez)
|
||||
- WEBAPI: Expose API for the torrent creator (glassez, Radu Carpa)
|
||||
- WEBAPI: Add support for SSL torrents (Chocobo1, Radu Carpa)
|
||||
- WEBAPI: Provide endpoint for listing directory content (Paweł Kotiuk)
|
||||
- WEBAPI: Provide "private" flag via "torrents/info" endpoint (ManiMatter)
|
||||
- WEBAPI: Add a way to download .torrent file using search plugin (glassez)
|
||||
- WEBAPI: Add "private" filter for "torrents/info" endpoint (ManiMatter)
|
||||
- WEBAPI: Add root_path to "torrents/info" result (David Newhall)
|
||||
- RSS: Show RSS feed title in HTML browser (Jay)
|
||||
- RSS: Allow to set delay between requests to the same host (jNullj)
|
||||
- SEARCH: Allow users to specify Python executable path (Chocobo1)
|
||||
- SEARCH: Lazy load search plugins (milahu)
|
||||
- SEARCH: Add date column to the built-in search engine (ducalex)
|
||||
- SEARCH: Allow to rearrange search tabs (glassez)
|
||||
- WINDOWS: Use Fusion style on Windows 10+. It has better compatibility with dark mode (glassez)
|
||||
- WINDOWS: Allow to set qBittorrent as default program (glassez)
|
||||
- WINDOWS: Don't access "Favorites" folder unexpectedly (glassez)
|
||||
- LINUX: Add support for systemd power management (Chocobo1)
|
||||
- LINUX: Add support for localized man pages (Victor Chernyakin)
|
||||
- LINUX: Specify a locale if none is set (Chocobo1)
|
||||
|
|
|
@ -62,6 +62,6 @@
|
|||
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="5.0.0~beta1" date="2024-03-19"/>
|
||||
<release version="5.0.0~rc1" date="2024-08-18"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
#include <QRegularExpression>
|
||||
#include <QThread>
|
||||
|
||||
#include "base/algorithm.h"
|
||||
#include "base/exceptions.h"
|
||||
#include "base/global.h"
|
||||
#include "base/logger.h"
|
||||
|
|
|
@ -240,11 +240,11 @@ void CustomDiskIOThread::handleCompleteFiles(lt::storage_index_t storage, const
|
|||
|
||||
lt::storage_interface *customStorageConstructor(const lt::storage_params ¶ms, lt::file_pool &pool)
|
||||
{
|
||||
return new CustomStorage {params, pool};
|
||||
return new CustomStorage(params, pool);
|
||||
}
|
||||
|
||||
CustomStorage::CustomStorage(const lt::storage_params ¶ms, lt::file_pool &filePool)
|
||||
: lt::default_storage {params, filePool}
|
||||
: lt::default_storage(params, filePool)
|
||||
, m_savePath {params.path}
|
||||
{
|
||||
}
|
||||
|
|
|
@ -367,6 +367,10 @@ void PeerInfo::determineFlags()
|
|||
if (useUTPSocket())
|
||||
updateFlags(u'P', C_UTP);
|
||||
|
||||
// h = Peer is using NAT hole punching
|
||||
if (isHolepunched())
|
||||
updateFlags(u'h', tr("Peer is using NAT hole punching"));
|
||||
|
||||
m_flags.chop(1);
|
||||
m_flagsDescription.chop(1);
|
||||
}
|
||||
|
|
|
@ -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 ¶ms = {}) = 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;
|
||||
|
||||
|
|
|
@ -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,72 +2250,66 @@ 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())
|
||||
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());
|
||||
|
||||
bool reached = false;
|
||||
QString description;
|
||||
|
||||
if (const qreal ratio = torrent->realRatio();
|
||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
||||
{
|
||||
if (!torrent->isFinished() || torrent->isForced())
|
||||
continue;
|
||||
reached = true;
|
||||
description = tr("Torrent reached the share ratio limit.");
|
||||
}
|
||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the seeding time limit.");
|
||||
}
|
||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the inactive seeding time limit.");
|
||||
}
|
||||
|
||||
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());
|
||||
if (reached)
|
||||
{
|
||||
const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
|
||||
const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction();
|
||||
|
||||
bool reached = false;
|
||||
QString description;
|
||||
|
||||
if (const qreal ratio = torrent->realRatio();
|
||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
||||
if (shareLimitAction == ShareLimitAction::Remove)
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the share ratio limit.");
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
|
||||
removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent);
|
||||
}
|
||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the seeding time limit.");
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
|
||||
removeTorrent(torrent->id(), TorrentRemoveOption::RemoveContent);
|
||||
}
|
||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the inactive seeding time limit.");
|
||||
torrent->stop();
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName));
|
||||
}
|
||||
|
||||
if (reached)
|
||||
else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding())
|
||||
{
|
||||
const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
|
||||
const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction();
|
||||
|
||||
if (shareLimitAction == ShareLimitAction::Remove)
|
||||
{
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
|
||||
deleteTorrent(torrentID);
|
||||
}
|
||||
else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
|
||||
{
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
|
||||
deleteTorrent(torrentID, DeleteTorrentAndFiles);
|
||||
}
|
||||
else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
|
||||
{
|
||||
torrent->stop();
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName));
|
||||
}
|
||||
else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding())
|
||||
{
|
||||
torrent->setSuperSeeding(true);
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
|
||||
}
|
||||
torrent->setSuperSeeding(true);
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,26 +2398,29 @@ 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()
|
||||
, [&nativeHandle](const MoveStorageJob &job)
|
||||
, [&nativeHandle](const MoveStorageJob &job)
|
||||
{
|
||||
return job.torrentHandle == nativeHandle;
|
||||
});
|
||||
|
@ -2414,14 +2438,14 @@ 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)
|
||||
{
|
||||
// Delete "move storage job" for the deleted torrent
|
||||
// (note: we shouldn't delete active job)
|
||||
const auto iter = std::find_if((m_moveStorageQueue.begin() + 1), m_moveStorageQueue.end()
|
||||
, [torrent](const MoveStorageJob &job)
|
||||
, [torrent](const MoveStorageJob &job)
|
||||
{
|
||||
return job.torrentHandle == torrent->nativeHandle();
|
||||
});
|
||||
|
@ -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;
|
||||
|
@ -4890,7 +4941,7 @@ void SessionImpl::updateSeedingLimitTimer()
|
|||
if ((globalMaxRatio() == Torrent::NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit()
|
||||
&& (globalMaxSeedingMinutes() == Torrent::NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit()
|
||||
&& (globalMaxInactiveSeedingMinutes() == Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit())
|
||||
{
|
||||
{
|
||||
if (m_seedingLimitTimer->isActive())
|
||||
m_seedingLimitTimer->stop();
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 ¶ms = {}) 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();
|
||||
|
|
|
@ -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;
|
||||
|
|
50
src/base/bittorrent/torrentcontentremoveoption.h
Normal file
50
src/base/bittorrent/torrentcontentremoveoption.h
Normal 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)
|
||||
}
|
||||
}
|
61
src/base/bittorrent/torrentcontentremover.cpp
Normal file
61
src/base/bittorrent/torrentcontentremover.cpp
Normal 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);
|
||||
}
|
53
src/base/bittorrent/torrentcontentremover.h
Normal file
53
src/base/bittorrent/torrentcontentremover.h
Normal 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);
|
||||
};
|
||||
}
|
|
@ -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
|
||||
|
@ -456,6 +460,8 @@ Path TorrentImpl::savePath() const
|
|||
void TorrentImpl::setSavePath(const Path &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
if (isAutoTMMEnabled()) [[unlikely]]
|
||||
return;
|
||||
|
||||
const Path basePath = m_session->useCategoryPathsInManualMode()
|
||||
? m_session->categorySavePath(category()) : m_session->savePath();
|
||||
|
@ -483,6 +489,8 @@ Path TorrentImpl::downloadPath() const
|
|||
void TorrentImpl::setDownloadPath(const Path &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
if (isAutoTMMEnabled()) [[unlikely]]
|
||||
return;
|
||||
|
||||
const Path basePath = m_session->useCategoryPathsInManualMode()
|
||||
? m_session->categoryDownloadPath(category()) : m_session->downloadPath();
|
||||
|
@ -982,6 +990,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 +1470,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 +1816,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 +1885,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;
|
||||
|
@ -1933,8 +1962,17 @@ void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext cont
|
|||
{
|
||||
if (!hasMetadata())
|
||||
{
|
||||
m_savePath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
if (context == MoveStorageContext::ChangeSavePath)
|
||||
{
|
||||
m_savePath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
else if (context == MoveStorageContext::ChangeDownloadPath)
|
||||
{
|
||||
m_downloadPath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
if (m_https)
|
||||
try
|
||||
{
|
||||
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();
|
||||
}
|
||||
if (m_https)
|
||||
{
|
||||
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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
#define QBT_VERSION_MINOR 0
|
||||
#define QBT_VERSION_BUGFIX 0
|
||||
#define QBT_VERSION_BUILD 0
|
||||
#define QBT_VERSION_STATUS "beta1" // Should be empty for stable releases!
|
||||
#define QBT_VERSION_STATUS "rc1" // Should be empty for stable releases!
|
||||
|
||||
#define QBT__STRINGIFY(x) #x
|
||||
#define QBT_STRINGIFY(x) QBT__STRINGIFY(x)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>
|
||||
|
|
48
src/gui/filterpatternformat.h
Normal file
48
src/gui/filterpatternformat.h
Normal 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)
|
||||
}
|
82
src/gui/filterpatternformatmenu.cpp
Normal file
82
src/gui/filterpatternformatmenu.cpp
Normal 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);
|
||||
});
|
||||
}
|
45
src/gui/filterpatternformatmenu.h
Normal file
45
src/gui/filterpatternformatmenu.h
Normal 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);
|
||||
};
|
|
@ -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
|
||||
|
@ -46,9 +47,9 @@ namespace
|
|||
}
|
||||
|
||||
DownloadedPiecesBar::DownloadedPiecesBar(QWidget *parent)
|
||||
: base {parent}
|
||||
, m_dlPieceColor {dlPieceColor(pieceColor())}
|
||||
: base(parent)
|
||||
{
|
||||
updateColorsImpl();
|
||||
}
|
||||
|
||||
QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin, int reqSize)
|
||||
|
@ -128,25 +129,24 @@ QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin
|
|||
return result;
|
||||
}
|
||||
|
||||
bool DownloadedPiecesBar::updateImage(QImage &image)
|
||||
QImage DownloadedPiecesBar::renderImage()
|
||||
{
|
||||
// qDebug() << "updateImage";
|
||||
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888);
|
||||
if (image2.isNull())
|
||||
QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
|
||||
if (image.isNull())
|
||||
{
|
||||
qDebug() << "QImage image2() allocation failed, width():" << width();
|
||||
return false;
|
||||
qDebug() << "QImage allocation failed, width():" << width();
|
||||
return image;
|
||||
}
|
||||
|
||||
if (m_pieces.isEmpty())
|
||||
{
|
||||
image2.fill(backgroundColor());
|
||||
image = image2;
|
||||
return true;
|
||||
image.fill(backgroundColor());
|
||||
return image;
|
||||
}
|
||||
|
||||
QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image2.width());
|
||||
QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image2.width());
|
||||
QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image.width());
|
||||
QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image.width());
|
||||
|
||||
// filling image
|
||||
for (int x = 0; x < scaledPieces.size(); ++x)
|
||||
|
@ -161,15 +161,15 @@ bool DownloadedPiecesBar::updateImage(QImage &image)
|
|||
QRgb mixedColor = mixTwoColors(pieceColor().rgb(), m_dlPieceColor.rgb(), ratio);
|
||||
mixedColor = mixTwoColors(backgroundColor().rgb(), mixedColor, fillRatio);
|
||||
|
||||
image2.setPixel(x, 0, mixedColor);
|
||||
image.setPixel(x, 0, mixedColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
image2.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
image.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
}
|
||||
}
|
||||
image = image2;
|
||||
return true;
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::setProgress(const QBitArray &pieces, const QBitArray &downloadedPieces)
|
||||
|
@ -177,7 +177,7 @@ void DownloadedPiecesBar::setProgress(const QBitArray &pieces, const QBitArray &
|
|||
m_pieces = pieces;
|
||||
m_downloadedPieces = downloadedPieces;
|
||||
|
||||
requestImageUpdate();
|
||||
redraw();
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::clear()
|
||||
|
@ -198,3 +198,14 @@ QString DownloadedPiecesBar::simpleToolTipText() const
|
|||
+ u"</table>";
|
||||
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::updateColors()
|
||||
{
|
||||
PiecesBar::updateColors();
|
||||
updateColorsImpl();
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::updateColorsImpl()
|
||||
{
|
||||
m_dlPieceColor = dlPieceColor(pieceColor());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -52,11 +53,13 @@ public:
|
|||
private:
|
||||
// scale bitfield vector to float vector
|
||||
QVector<float> bitfieldToFloatVector(const QBitArray &vecin, int reqSize);
|
||||
bool updateImage(QImage &image) override;
|
||||
QImage renderImage() override;
|
||||
QString simpleToolTipText() const override;
|
||||
void updateColors() override;
|
||||
void updateColorsImpl();
|
||||
|
||||
// incomplete piece color
|
||||
const QColor m_dlPieceColor;
|
||||
QColor m_dlPieceColor;
|
||||
// last used bitfields, uses to better resize redraw
|
||||
// TODO: make a diff pieces to new pieces and update only changed pixels, speedup when update > 20x faster
|
||||
QBitArray m_pieces;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -126,39 +127,38 @@ QVector<float> PieceAvailabilityBar::intToFloatVector(const QVector<int> &vecin,
|
|||
return result;
|
||||
}
|
||||
|
||||
bool PieceAvailabilityBar::updateImage(QImage &image)
|
||||
QImage PieceAvailabilityBar::renderImage()
|
||||
{
|
||||
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888);
|
||||
if (image2.isNull())
|
||||
QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
|
||||
if (image.isNull())
|
||||
{
|
||||
qDebug() << "QImage image2() allocation failed, width():" << width();
|
||||
return false;
|
||||
qDebug() << "QImage allocation failed, width():" << width();
|
||||
return image;
|
||||
}
|
||||
|
||||
if (m_pieces.empty())
|
||||
{
|
||||
image2.fill(backgroundColor());
|
||||
image = image2;
|
||||
return true;
|
||||
image.fill(backgroundColor());
|
||||
return image;
|
||||
}
|
||||
|
||||
QVector<float> scaledPieces = intToFloatVector(m_pieces, image2.width());
|
||||
QVector<float> scaledPieces = intToFloatVector(m_pieces, image.width());
|
||||
|
||||
// filling image
|
||||
for (int x = 0; x < scaledPieces.size(); ++x)
|
||||
{
|
||||
float piecesToValue = scaledPieces.at(x);
|
||||
image2.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
image.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
}
|
||||
image = image2;
|
||||
return true;
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void PieceAvailabilityBar::setAvailability(const QVector<int> &avail)
|
||||
{
|
||||
m_pieces = avail;
|
||||
|
||||
requestImageUpdate();
|
||||
redraw();
|
||||
}
|
||||
|
||||
void PieceAvailabilityBar::clear()
|
||||
|
|
|
@ -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
|
||||
|
@ -46,7 +47,7 @@ public:
|
|||
void clear() override;
|
||||
|
||||
private:
|
||||
bool updateImage(QImage &image) override;
|
||||
QImage renderImage() override;
|
||||
QString simpleToolTipText() const override;
|
||||
|
||||
// last used int vector, uses to better resize redraw
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2016 Eugene Shalygin
|
||||
* Copyright (C) 2006 Christophe Dumez
|
||||
*
|
||||
|
@ -41,6 +42,7 @@
|
|||
#include "base/indexrange.h"
|
||||
#include "base/path.h"
|
||||
#include "base/utils/misc.h"
|
||||
#include "gui/uithememanager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
@ -114,10 +116,16 @@ namespace
|
|||
}
|
||||
|
||||
PiecesBar::PiecesBar(QWidget *parent)
|
||||
: QWidget {parent}
|
||||
: QWidget(parent)
|
||||
{
|
||||
updatePieceColors();
|
||||
setMouseTracking(true);
|
||||
|
||||
updateColorsImpl();
|
||||
connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, [this]
|
||||
{
|
||||
updateColors();
|
||||
redraw();
|
||||
});
|
||||
}
|
||||
|
||||
void PiecesBar::setTorrent(const BitTorrent::Torrent *torrent)
|
||||
|
@ -154,7 +162,7 @@ void PiecesBar::leaveEvent(QEvent *e)
|
|||
{
|
||||
m_hovered = false;
|
||||
m_highlightedRegion = {};
|
||||
requestImageUpdate();
|
||||
redraw();
|
||||
base::leaveEvent(e);
|
||||
}
|
||||
|
||||
|
@ -178,7 +186,10 @@ void PiecesBar::paintEvent(QPaintEvent *)
|
|||
else
|
||||
{
|
||||
if (m_image.width() != imageRect.width())
|
||||
updateImage(m_image);
|
||||
{
|
||||
if (const QImage image = renderImage(); !image.isNull())
|
||||
m_image = image;
|
||||
}
|
||||
painter.drawImage(imageRect, m_image);
|
||||
}
|
||||
|
||||
|
@ -196,30 +207,33 @@ void PiecesBar::paintEvent(QPaintEvent *)
|
|||
painter.drawPath(border);
|
||||
}
|
||||
|
||||
void PiecesBar::requestImageUpdate()
|
||||
void PiecesBar::redraw()
|
||||
{
|
||||
if (updateImage(m_image))
|
||||
if (const QImage image = renderImage(); !image.isNull())
|
||||
{
|
||||
m_image = image;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
QColor PiecesBar::backgroundColor() const
|
||||
{
|
||||
return palette().color(QPalette::Base);
|
||||
return palette().color(QPalette::Active, QPalette::Base);
|
||||
}
|
||||
|
||||
QColor PiecesBar::borderColor() const
|
||||
{
|
||||
return palette().color(QPalette::Dark);
|
||||
return palette().color(QPalette::Active, QPalette::Dark);
|
||||
}
|
||||
|
||||
QColor PiecesBar::pieceColor() const
|
||||
{
|
||||
return palette().color(QPalette::Highlight);
|
||||
return palette().color(QPalette::Active, QPalette::Highlight);
|
||||
}
|
||||
|
||||
QColor PiecesBar::colorBoxBorderColor() const
|
||||
{
|
||||
return palette().color(QPalette::ToolTipText);
|
||||
return palette().color(QPalette::Active, QPalette::ToolTipText);
|
||||
}
|
||||
|
||||
const QVector<QRgb> &PiecesBar::pieceColors() const
|
||||
|
@ -325,12 +339,17 @@ void PiecesBar::highlightFile(int imagePos)
|
|||
}
|
||||
}
|
||||
|
||||
void PiecesBar::updatePieceColors()
|
||||
void PiecesBar::updateColors()
|
||||
{
|
||||
updateColorsImpl();
|
||||
}
|
||||
|
||||
void PiecesBar::updateColorsImpl()
|
||||
{
|
||||
m_pieceColors = QVector<QRgb>(256);
|
||||
for (int i = 0; i < 256; ++i)
|
||||
{
|
||||
float ratio = (i / 255.0);
|
||||
const float ratio = (i / 255.0);
|
||||
m_pieceColors[i] = mixTwoColors(backgroundColor().rgb(), pieceColor().rgb(), ratio);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2016 Eugene Shalygin
|
||||
* Copyright (C) 2006 Christophe Dumez
|
||||
*
|
||||
|
@ -54,17 +55,15 @@ public:
|
|||
|
||||
virtual void clear();
|
||||
|
||||
// QObject interface
|
||||
bool event(QEvent *e) override;
|
||||
|
||||
protected:
|
||||
// QWidget interface
|
||||
bool event(QEvent *e) override;
|
||||
void enterEvent(QEnterEvent *e) override;
|
||||
void leaveEvent(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void requestImageUpdate();
|
||||
|
||||
virtual void updateColors();
|
||||
void redraw();
|
||||
|
||||
QColor backgroundColor() const;
|
||||
QColor borderColor() const;
|
||||
|
@ -82,11 +81,9 @@ private:
|
|||
void highlightFile(int imagePos);
|
||||
|
||||
virtual QString simpleToolTipText() const = 0;
|
||||
virtual QImage renderImage() = 0;
|
||||
|
||||
// draw new image to replace the actual image
|
||||
// returns true if image was successfully updated
|
||||
virtual bool updateImage(QImage &image) = 0;
|
||||
void updatePieceColors();
|
||||
void updateColorsImpl();
|
||||
|
||||
const BitTorrent::Torrent *m_torrent = nullptr;
|
||||
QImage m_image;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -147,10 +147,19 @@ void TorrentContentModelFolder::recalculateProgress()
|
|||
tRemaining += child->remaining();
|
||||
}
|
||||
|
||||
if (!isRootItem() && (tSize > 0))
|
||||
if (!isRootItem())
|
||||
{
|
||||
m_progress = tProgress / tSize;
|
||||
m_remaining = tRemaining;
|
||||
if (tSize > 0)
|
||||
{
|
||||
m_progress = tProgress / tSize;
|
||||
m_remaining = tRemaining;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_progress = 1.0;
|
||||
m_remaining = 0;
|
||||
}
|
||||
|
||||
Q_ASSERT(m_progress <= 1.);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -235,10 +235,7 @@ void StatusFilterWidget::applyFilter(int row)
|
|||
|
||||
void StatusFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
|
||||
{
|
||||
for (const BitTorrent::Torrent *torrent : torrents)
|
||||
updateTorrentStatus(torrent);
|
||||
|
||||
updateTexts();
|
||||
update(torrents);
|
||||
}
|
||||
|
||||
void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
|
||||
|
@ -273,6 +270,12 @@ void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torr
|
|||
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
|
||||
|
||||
updateTexts();
|
||||
|
||||
if (Preferences::instance()->getHideZeroStatusFilters())
|
||||
{
|
||||
hideZeroItems();
|
||||
updateGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
void StatusFilterWidget::configure()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 {};
|
||||
|
|
|
@ -86,6 +86,7 @@ public:
|
|||
TR_INFOHASH_V1,
|
||||
TR_INFOHASH_V2,
|
||||
TR_REANNOUNCE,
|
||||
TR_PRIVATE,
|
||||
|
||||
NB_COLUMNS
|
||||
};
|
||||
|
|
|
@ -59,8 +59,8 @@ namespace
|
|||
int customCompare(const TagSet &left, const TagSet &right, const Utils::Compare::NaturalCompare<Qt::CaseInsensitive> &compare)
|
||||
{
|
||||
for (auto leftIter = left.cbegin(), rightIter = right.cbegin();
|
||||
(leftIter != left.cend()) && (rightIter != right.cend());
|
||||
++leftIter, ++rightIter)
|
||||
(leftIter != left.cend()) && (rightIter != right.cend());
|
||||
++leftIter, ++rightIter)
|
||||
{
|
||||
const int result = compare(leftIter->toString(), rightIter->toString());
|
||||
if (result != 0)
|
||||
|
@ -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:
|
||||
{
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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 é)
|
||||
# (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:
|
||||
|
|
|
@ -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__":
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -533,15 +533,12 @@ void WebApplication::sendFile(const Path &path)
|
|||
const QDateTime lastModified = Utils::Fs::lastModified(path);
|
||||
|
||||
// find translated file in cache
|
||||
if (!m_isAltUIUsed)
|
||||
if (const auto it = m_translatedFiles.constFind(path);
|
||||
(it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified))
|
||||
{
|
||||
if (const auto it = m_translatedFiles.constFind(path);
|
||||
(it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified))
|
||||
{
|
||||
print(it->data, it->mimeType);
|
||||
setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(it->mimeType)});
|
||||
return;
|
||||
}
|
||||
print(it->data, it->mimeType);
|
||||
setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(it->mimeType)});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto readResult = Utils::IO::readFile(path, MAX_ALLOWED_FILESIZE);
|
||||
|
@ -572,7 +569,7 @@ void WebApplication::sendFile(const Path &path)
|
|||
|
||||
QByteArray data = readResult.value();
|
||||
const QMimeType mimeType = QMimeDatabase().mimeTypeForFileNameAndData(path.data(), data);
|
||||
const bool isTranslatable = !m_isAltUIUsed && mimeType.inherits(u"text/plain"_s);
|
||||
const bool isTranslatable = mimeType.inherits(u"text/plain"_s);
|
||||
|
||||
if (isTranslatable)
|
||||
{
|
||||
|
@ -740,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()
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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": ["*"] }]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
|
|
@ -6,9 +6,8 @@
|
|||
"url": "https://github.com/qbittorrent/qBittorrent.git"
|
||||
},
|
||||
"scripts": {
|
||||
"extract_translation": "i18next -c public/i18next-parser.config.mjs public/index.html public/scripts/login.js",
|
||||
"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": "*",
|
||||
|
@ -16,7 +15,6 @@
|
|||
"eslint-plugin-html": "*",
|
||||
"eslint-plugin-regexp": "*",
|
||||
"html-validate": "*",
|
||||
"i18next-parser": "*",
|
||||
"js-beautify": "*",
|
||||
"prettier": "*",
|
||||
"stylelint": "*",
|
||||
|
|
|
@ -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]");
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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> QBT_TR(Are you sure you want to remove the selected torrents from the transfer list?)QBT_TR[CONTEXT=HttpServer]</p>
|
||||
<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]" /> <input type="button" id="confirmBtn" value="QBT_TR(Remove)QBT_TR[CONTEXT=MainWindow]" />
|
||||
</div>
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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,25 +382,23 @@
|
|||
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();
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue