Compare commits

...

60 commits

Author SHA1 Message Date
sledgehammer999
2d67729617
Bump to v5.0.0rc1 2024-08-18 23:21:21 +03:00
sledgehammer999
878ebbed41
Update Changelog 2024-08-18 23:17:25 +03:00
Vladimir Golovnev
c61c3d7cd8
Backport changes to v5.0.x branch
PR #21164.
2024-08-16 07:17:21 +03:00
skomerko
978fbbdc0d
WebUI: Always create generic filter items
PR #21188.
2024-08-15 20:37:19 +03:00
stalkerok
63689cf763
Add a flag about the connection peers are using NAT hole punching
PR #21052.
2024-08-15 20:33:45 +03:00
thalieht
cebc72d3cf
WebUI: Add missing columns in transfer list
* Incomplete Save Path
* Info Hash v1
* Info Hash v2

PR #21158.
2024-08-15 20:32:40 +03:00
Vladimir Golovnev
a67bd271c6
Refresh pieces bar colors once color scheme is changed
PR #21183.
Closes #21155.
2024-08-13 09:37:47 +03:00
skomerko
a8cffbb205
WebUI: Clear trackerList on full update
Like other similar data structures, trackerList also need to be cleared in the event of a full sync update.

PR #21148.
2024-08-11 14:20:45 +03:00
Vladimir Golovnev
7dfb0110d4
Fix Incomplete Save Path cannot be changed for torrents without metadata
PR #21152.
Closes #21140.
2024-08-08 08:22:54 +03:00
Vladimir Golovnev
3ad8fcbdd2
Hide zero status filters when torrents removed
PR #21150.
Closes #21146.
2024-08-08 08:22:51 +03:00
Vladimir Golovnev
195eae5f3d
Backport changes to v5.0.x branch
PR #20996.
2024-08-02 21:22:49 +03:00
Hanabishi
920ae26f7b
WebUI: Fix Torrent Management Mode selector
PR #21053.
2024-07-15 17:40:17 +03:00
David Newhall
09ed0d6b66
WebAPI: Add root_path to torrent/info result
PR #21066.
Closes #21057.
2024-07-15 08:52:52 +03:00
Vladimir Golovnev
4f0cc8aa11
Fix incorrect sorting by "private" column
PR #21041.
2024-07-15 08:52:42 +03:00
ManiMatter
4d490c84e7
Add ability to display torrent "privateness" in UI
PR #20951.

---------

Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
Co-authored-by: Vladimir Golovnev <glassez@yandex.ru>
Co-authored-by: thalieht <thalieht@users.noreply.github.com>
2024-07-15 08:52:23 +03:00
Vladimir Golovnev
96607ce874
Prevent incorrect size from being used for creating array
PR #21050.
2024-07-12 08:51:08 +03:00
Vladimir Golovnev
418edc7471
Apply bulk changes to correct content widget items
PR #21006.
Closes #21001.
2024-07-08 16:51:33 +03:00
Vladimir Golovnev
bd01b7c4df
WebUI: Correctly apply changed "save path" of RSS rules
PR #21030.
Closes #20141.
2024-07-08 10:18:02 +03:00
Vladimir Golovnev
b0ac763048
Show scroll bar in Torrent Tags dialog
PR #21026.
Closes #21022.
2024-07-07 16:10:07 +03:00
Vladimir Golovnev
127d2d6f0b
Fix handling of tags containing '&' character
PR #21024.
Closes #20773.
2024-07-07 16:10:05 +03:00
Vladimir Golovnev
4149609e78
Allow to move content files to Trash instead of deleting them
PR #20252.
2024-07-07 16:09:48 +03:00
Vladimir Golovnev
78c549f83e
Use custom storage when reloading torrent
PR #20998.
2024-07-07 16:07:22 +03:00
Thomas Piccirello
a3a53e2e0e
WebUI: Fix preference name conflict
PR #20990.
2024-07-07 16:06:55 +03:00
Vladimir Golovnev
5aaa43e01d
Restore ability to use server-side translation by custom WebUI
PR #20968.
2024-06-29 21:59:22 +03:00
Chocobo1
86745d7b07
GHA CI: use static versions of AppImage builder
It does not affect the produced artifacts. The only difference is the
tool itself won't depend on some specific OS image or library version.

PR #20983.
2024-06-25 21:13:20 +03:00
Thomas Piccirello
210650a5ee
Use enabled search plugins by default in WebUI
PR #20969.
Closes #20558.
2024-06-25 21:13:20 +03:00
Chocobo1
fe93b6d0d8
Use proper casting
Previously `m_shutdownTimeout * 1000` was calculated in `int` and now it
is `qint64`.

PR #20982.
2024-06-25 21:13:19 +03:00
Chocobo1
e8b585acd8
Allow numeric types
The canonical type for `size_string` is `str`. However numeric types are also accepted in order
to accommodate poorly written plugins.

PR #20976.
2024-06-25 21:13:19 +03:00
vikas_c
cea20141a9
Show download progress for folders with zero byte size as 100 instead of 0
Fixes the download progress calculation for folders with zero size.
Previously, the progress would be Zero. Now, folders with zero size
show 100% progress.

PR #20567.
2024-06-25 21:13:19 +03:00
Chocobo1
0f5a27ed50
Improve connection handling
1. Previously unhandled connections will stay in pending state. It won't
be closed until timeout happened. This may lead to wasting system
resources. Now the (over-limit) connection is actively rejected.

2. When out-of-memory occurs here, reject the new connection instead of
throwing exception and crash.

3. Also clean up some unused bits.

PR #20961.
2024-06-25 21:13:18 +03:00
Vladimir Golovnev
c2cf898ccd
Allow to use regular expression to filter torrent content
PR #20944.
Closes #19934.
2024-06-25 21:13:18 +03:00
Chocobo1
5e5aa8a563
Add type annotations
A few code are revised because the type checker (mypy) doesn't allow
changing types on a variable.

PR #20935.
2024-06-25 21:13:18 +03:00
ManiMatter
12a4c3fda2
WebAPI: Add "private" filter for 'info' endpoint
PR #20833.

---------

Co-authored-by: Vladimir Golovnev <glassez@yandex.ru>
Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
2024-06-25 21:13:17 +03:00
Vladimir Golovnev
5f50b701d2
Don't use custom "file icon provider" on Windows
PR #20936.
Closes #20908.
2024-06-25 21:13:17 +03:00
Chocobo1
9f20d9c3aa
Revise Protocol column
Add "BT" (BitTorrent) to avoid confusion about which protocol it is referring to.
Also its value doesn't need to be translated.

PR #20897.
2024-06-25 21:13:17 +03:00
Vladimir Golovnev
05e3130baa
Apply share limits when torrent downloading is finished
PR #20917.
Closes #20874.
2024-06-25 21:13:17 +03:00
Vladimir Golovnev
683492648f
Apply filename filter to subfolder names as well
PR #20902.
Closes #14480.
2024-06-25 21:13:17 +03:00
Chocobo1
2f2e158877
WebUI: unify comment format 2024-06-25 21:13:16 +03:00
BurningMop
e60e96cb0e
Add optional headers to search request
PR #20923.
2024-06-25 21:13:16 +03:00
Chocobo1
5f31208bf1
Add required manifest field
https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests#assemblyidentity

PR #20907.
2024-06-25 21:13:16 +03:00
Chocobo1
fa58e58e70
WebUI: unify curly bracket usage 2024-06-25 21:13:16 +03:00
dependabot[bot]
671943a9a6
GHA CI: Bump Github Actions versions
PR #20913.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
2024-06-25 21:13:16 +03:00
Chocobo1
8bad80bcdd
Avoid redundant lookup
PR #20890.
2024-06-25 21:13:15 +03:00
thalieht
c44e300507
Increase default height of 'Share ratio limit' dialog in WebUI
PR #20866.
2024-06-25 21:13:15 +03:00
Chocobo1
318a677e8f
Avoid creating redundant temporary file list
PR #20863.
2024-06-25 21:13:15 +03:00
Chocobo1
0246df790a
Use Qt built-in methods 2024-06-25 21:13:15 +03:00
Chocobo1
782fbc1425
Use simpler conversion
The cookie value can only contain ASCII characters.
2024-06-25 21:13:15 +03:00
Chocobo1
7deccd5592
WebUI: add missing break 2024-06-25 21:13:14 +03:00
Chocobo1
4a36fe7278
WebUI: don't auto infer radix parameter 2024-06-25 21:13:14 +03:00
Chocobo1
1c5af96ad8
WebUI: simplify code 2024-06-25 21:13:14 +03:00
Chocobo1
3bb47a5410
WebUI: iterate over own properties only 2024-06-25 21:13:14 +03:00
Chocobo1
d7abeb4bf0
WebUI: use assignment operator shorthand 2024-06-25 21:13:14 +03:00
Chocobo1
a19d623ead
WebUI: prefer arrow function in callbacks 2024-06-25 21:13:13 +03:00
Chocobo1
1ef21bc2b7
WebUI: enforce usage of const whenever possible 2024-06-25 21:13:13 +03:00
Chocobo1
4687b4e8e4
WebUI: enforce string quotes coding style 2024-06-25 19:33:20 +03:00
Thomas Piccirello
d2e5163861
WebUI: Restore previously used tab on load
This PR restores the users previously used tab (Transfer, Search, RSS, etc.) when the WebUI is reloaded.

PR #20705.
2024-06-25 19:30:10 +03:00
sledgehammer999
8a15ea8026
Merge pull request #20963 from sledgehammer999/revert_webui_i18n
Revert i18next
2024-06-25 03:02:24 +03:00
sledgehammer999
2b99554813
Update WebUI translation files 2024-06-17 02:07:15 +03:00
sledgehammer999
e6638f9c19
Revert "Use client side translation for public login page"
This reverts commit ac91c1348b.
2024-06-16 23:31:19 +03:00
sledgehammer999
ec6eac2ba1
Revert "Avoid leaking user locale preference to the web"
This reverts commit 66c34ddb6e.
2024-06-16 23:14:21 +03:00
253 changed files with 16285 additions and 7827 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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>

View file

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

View file

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

View file

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

View file

@ -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);
}

View file

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

View file

@ -101,6 +101,7 @@
#include "nativesessionextension.h"
#include "portforwarderimpl.h"
#include "resumedatastorage.h"
#include "torrentcontentremover.h"
#include "torrentdescriptor.h"
#include "torrentimpl.h"
#include "tracker.h"
@ -525,6 +526,7 @@ SessionImpl::SessionImpl(QObject *parent)
, m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3}
, m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3}
, m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3}
, m_torrentContentRemoveOption {BITTORRENT_SESSION_KEY(u"TorrentContentRemoveOption"_s), TorrentContentRemoveOption::MoveToTrash}
, m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)}
, m_seedingLimitTimer {new QTimer(this)}
, m_resumeDataTimer {new QTimer(this)}
@ -550,7 +552,14 @@ SessionImpl::SessionImpl(QObject *parent)
, this, [this]() { m_recentErroredTorrents.clear(); });
m_seedingLimitTimer->setInterval(10s);
connect(m_seedingLimitTimer, &QTimer::timeout, this, &SessionImpl::processShareLimits);
connect(m_seedingLimitTimer, &QTimer::timeout, this, [this]
{
// We shouldn't iterate over `m_torrents` in the loop below
// since `deleteTorrent()` modifies it indirectly
const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
for (TorrentImpl *torrent : torrents)
processTorrentShareLimits(torrent);
});
initializeNativeSession();
configureComponents();
@ -586,6 +595,11 @@ SessionImpl::SessionImpl(QObject *parent)
connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater);
connect(m_fileSearcher, &FileSearcher::searchFinished, this, &SessionImpl::fileSearchFinished);
m_torrentContentRemover = new TorrentContentRemover;
m_torrentContentRemover->moveToThread(m_ioThread.get());
connect(m_ioThread.get(), &QThread::finished, m_torrentContentRemover, &QObject::deleteLater);
connect(m_torrentContentRemover, &TorrentContentRemover::jobFinished, this, &SessionImpl::torrentContentRemovingFinished);
m_ioThread->start();
initMetrics();
@ -604,7 +618,7 @@ SessionImpl::~SessionImpl()
{
m_nativeSession->pause();
const qint64 timeout = (m_shutdownTimeout >= 0) ? (m_shutdownTimeout * 1000) : -1;
const auto timeout = (m_shutdownTimeout >= 0) ? (static_cast<qint64>(m_shutdownTimeout) * 1000) : -1;
const QDeadlineTimer shutdownDeadlineTimer {timeout};
if (m_torrentsQueueChanged)
@ -2236,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);
}

View file

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

View file

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

View file

@ -0,0 +1,50 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QMetaEnum>
namespace BitTorrent
{
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
inline namespace TorrentContentRemoveOptionNS
{
Q_NAMESPACE
enum class TorrentContentRemoveOption
{
Delete,
MoveToTrash
};
Q_ENUM_NS(TorrentContentRemoveOption)
}
}

View file

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

View file

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

View file

@ -77,6 +77,10 @@
#include "base/utils/os.h"
#endif // Q_OS_MACOS || Q_OS_WIN
#ifndef QBT_USES_LIBTORRENT2
#include "customstorage.h"
#endif
using namespace BitTorrent;
namespace
@ -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;
}

View file

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

View file

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

View file

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

View file

@ -32,7 +32,10 @@
#include <algorithm>
#include <chrono>
#include <memory>
#include <new>
#include <QtLogging>
#include <QNetworkProxy>
#include <QSslCipher>
#include <QSslConfiguration>
@ -40,7 +43,6 @@
#include <QStringList>
#include <QTimer>
#include "base/algorithm.h"
#include "base/global.h"
#include "base/utils/net.h"
#include "base/utils/sslkey.h"
@ -113,32 +115,38 @@ Server::Server(IRequestHandler *requestHandler, QObject *parent)
void Server::incomingConnection(const qintptr socketDescriptor)
{
if (m_connections.size() >= CONNECTIONS_LIMIT) return;
QTcpSocket *serverSocket = nullptr;
if (m_https)
serverSocket = new QSslSocket(this);
else
serverSocket = new QTcpSocket(this);
std::unique_ptr<QTcpSocket> serverSocket = m_https ? std::make_unique<QSslSocket>(this) : std::make_unique<QTcpSocket>(this);
if (!serverSocket->setSocketDescriptor(socketDescriptor))
return;
if (m_connections.size() >= CONNECTIONS_LIMIT)
{
delete serverSocket;
qWarning("Too many connections. Exceeded CONNECTIONS_LIMIT (%d). Connection closed.", CONNECTIONS_LIMIT);
return;
}
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)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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)

View file

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

View file

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

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -35,6 +35,7 @@
#include "base/path.h"
#include "base/settingvalue.h"
#include "filterpatternformat.h"
class LineEdit;
@ -92,6 +93,8 @@ private:
void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {});
void setupTreeview();
void saveTorrentFile();
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::AddNewTorrentDialog *m_ui = nullptr;
std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
@ -107,4 +110,5 @@ private:
SettingValue<bool> m_storeRememberLastSavePath;
SettingValue<QByteArray> m_storeTreeHeaderState;
SettingValue<QByteArray> m_storeSplitterState;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,48 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QMetaEnum>
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
inline namespace FilterPatternFormatNS
{
Q_NAMESPACE
enum class FilterPatternFormat
{
PlainText,
Wildcards,
Regex
};
Q_ENUM_NS(FilterPatternFormat)
}

View file

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

View file

@ -0,0 +1,45 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QMenu>
#include "filterpatternformat.h"
class FilterPatternFormatMenu final : public QMenu
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(FilterPatternFormatMenu)
public:
explicit FilterPatternFormatMenu(FilterPatternFormat format, QWidget *parent = nullptr);
signals:
void patternFormatChanged(FilterPatternFormat format);
};

View file

@ -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());
}

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -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;

View file

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

View file

@ -1,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()

View file

@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -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

View file

@ -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);
}
}

View file

@ -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;

View file

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

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -32,7 +32,8 @@
#include <QList>
#include <QWidget>
#include "base/pathfwd.h"
#include "base/settingvalue.h"
#include "gui/filterpatternformat.h"
class QPushButton;
class QTreeView;
@ -102,6 +103,8 @@ private slots:
private:
QPushButton *getButtonFromIndex(int index);
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::PropertiesWidget *m_ui = nullptr;
BitTorrent::Torrent *m_torrent = nullptr;
@ -115,4 +118,6 @@ private:
PropTabBar *m_tabBar = nullptr;
LineEdit *m_contentFilterLine = nullptr;
int m_handleWidth = -1;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
};

View file

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

View file

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

View file

@ -147,10 +147,19 @@ void TorrentContentModelFolder::recalculateProgress()
tRemaining += child->remaining();
}
if (!isRootItem() && (tSize > 0))
if (!isRootItem())
{
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.);
}
}

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com>
*
* This program is free software; you can redistribute it and/or
@ -56,6 +56,19 @@
#include "gui/macutilities.h"
#endif
namespace
{
QList<QPersistentModelIndex> toPersistentIndexes(const QModelIndexList &indexes)
{
QList<QPersistentModelIndex> persistentIndexes;
persistentIndexes.reserve(indexes.size());
for (const QModelIndex &index : indexes)
persistentIndexes.emplaceBack(index);
return persistentIndexes;
}
}
TorrentContentWidget::TorrentContentWidget(QWidget *parent)
: QTreeView(parent)
{
@ -173,10 +186,20 @@ Path TorrentContentWidget::getItemPath(const QModelIndex &index) const
return path;
}
void TorrentContentWidget::setFilterPattern(const QString &patternText)
void TorrentContentWidget::setFilterPattern(const QString &patternText, const FilterPatternFormat format)
{
const QString pattern = Utils::String::wildcardToRegexPattern(patternText);
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));
}
}

View file

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

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@ -37,6 +37,7 @@
#include "base/global.h"
#include "autoexpandabledialog.h"
#include "flowlayout.h"
#include "utils.h"
#include "ui_torrenttagsdialog.h"
@ -52,10 +53,10 @@ TorrentTagsDialog::TorrentTagsDialog(const TagSet &initialTags, QWidget *parent)
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *tagsLayout = new FlowLayout(m_ui->scrollArea);
auto *tagsLayout = new FlowLayout(m_ui->scrollArea->widget());
for (const Tag &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags())))
{
auto *tagWidget = new QCheckBox(tag.toString());
auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
if (initialTags.contains(tag))
tagWidget->setChecked(true);
tagsLayout->addWidget(tagWidget);
@ -78,12 +79,12 @@ TorrentTagsDialog::~TorrentTagsDialog()
TagSet TorrentTagsDialog::tags() const
{
TagSet tags;
auto *layout = m_ui->scrollArea->layout();
auto *layout = m_ui->scrollArea->widget()->layout();
for (int i = 0; i < (layout->count() - 1); ++i)
{
const auto *tagWidget = static_cast<QCheckBox *>(layout->itemAt(i)->widget());
if (tagWidget->isChecked())
tags.insert(Tag(tagWidget->text()));
tags.insert(Utils::Gui::widgetTextToTag(tagWidget->text()));
}
return tags;
@ -111,9 +112,9 @@ void TorrentTagsDialog::addNewTag()
}
else
{
auto *layout = m_ui->scrollArea->layout();
auto *layout = m_ui->scrollArea->widget()->layout();
auto *btn = layout->takeAt(layout->count() - 1);
auto *tagWidget = new QCheckBox(tag.toString());
auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
tagWidget->setChecked(true);
layout->addWidget(tagWidget);
layout->addItem(btn);

View file

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

View file

@ -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()

View file

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

View file

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

View file

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

View file

@ -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:
{

View file

@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@ -116,9 +116,10 @@ namespace
void removeTorrents(const QVector<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
{
auto *session = BitTorrent::Session::instance();
const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent;
const BitTorrent::TorrentRemoveOption removeOption = isDeleteFileSelected
? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
for (const BitTorrent::Torrent *torrent : torrents)
session->deleteTorrent(torrent->id(), deleteOption);
session->removeTorrent(torrent->id(), removeOption);
}
}
@ -183,6 +184,7 @@ TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow)
setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
setColumnHidden(TransferListModel::TR_REANNOUNCE, true);
setColumnHidden(TransferListModel::TR_PRIVATE, true);
}
//Ensure that at least one column is visible at all times
@ -442,7 +444,7 @@ void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
{
// Some torrents might be removed when waiting for user input, so refetch the torrent list
// NOTE: this will only work when dialog is modal
removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected());
removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected());
});
dialog->open();
}
@ -465,7 +467,7 @@ void TransferListWidget::deleteVisibleTorrents()
{
// Some torrents might be removed when waiting for user input, so refetch the torrent list
// NOTE: this will only work when dialog is modal
removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected());
removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected());
});
dialog->open();
}
@ -1190,7 +1192,7 @@ void TransferListWidget::displayListMenu()
const TagSet tags = BitTorrent::Session::instance()->tags();
for (const Tag &tag : asConst(tags))
{
auto *action = new TriStateAction(tag.toString(), tagsMenu);
auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu);
action->setCloseOnInteraction(false);
const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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()

View file

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

View file

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

View file

@ -6,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": "*",

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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