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

View file

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

View file

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

View file

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

View file

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

View file

@ -78,11 +78,7 @@ repos:
m4/.* | m4/.* |
src/base/3rdparty/.* | src/base/3rdparty/.* |
src/searchengine/nova3/socks.py | src/searchengine/nova3/socks.py |
src/webui/www/private/lang/.* | src/webui/www/private/scripts/lib/.*
src/webui/www/private/scripts/lib/.* |
src/webui/www/public/lang/.* |
src/webui/www/public/scripts/lib/.* |
src/webui/www/transifex/.*
)$ )$
exclude_types: exclude_types:
- ts - ts
@ -106,11 +102,7 @@ repos:
m4/.* | m4/.* |
src/base/3rdparty/.* | src/base/3rdparty/.* |
src/searchengine/nova3/socks.py | src/searchengine/nova3/socks.py |
src/webui/www/private/lang/.* | src/webui/www/private/scripts/lib/.*
src/webui/www/private/scripts/lib/.* |
src/webui/www/public/lang/.* |
src/webui/www/public/scripts/lib/.* |
src/webui/www/transifex/.*
)$ )$
exclude_types: exclude_types:
- svg - svg

View file

@ -17,14 +17,6 @@ type = QT
minimum_perc = 23 minimum_perc = 23
lang_map = pt: pt_PT, zh: zh_CN 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] [o:sledgehammer999:p:qbittorrent:r:qbittorrentdesktop_master]
source_file = dist/unix/org.qbittorrent.qBittorrent.desktop source_file = dist/unix/org.qbittorrent.qBittorrent.desktop
source_lang = en 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: Enable Ctrl+F hotkey for more inputs (thalieht)
- FEATURE: Add seeding limits to RSS and Watched folders options UI (glassez) - FEATURE: Add seeding limits to RSS and Watched folders options UI (glassez)
- FEATURE: Subcategories implicitly follow the parent category options (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 option to name each qbittorrent instance (Chocobo1)
- FEATURE: Add button for sending test email (Thomas Piccirello) - FEATURE: Add button for sending test email (Thomas Piccirello)
- FEATURE: Allow torrents to override default share limit action (glassez) - 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: Display error message when unrecoverable error occurred (glassez)
- BUGFIX: Update size of selected files when selection is changed (glassez) - BUGFIX: Update size of selected files when selection is changed (glassez)
- BUGFIX: Normalize tags by trimming leading/trailing whitespace (glassez) - BUGFIX: Normalize tags by trimming leading/trailing whitespace (glassez)
- BUGFIX: Correctly handle share limits in torrent options dialog (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: Improve WebUI responsiveness (Chocobo1)
- WEBUI: Do not exit the app when WebUI has failed to start (Hanabishi) - WEBUI: Do not exit the app when WebUI has failed to start (Hanabishi)
- WEBUI: Add `Moving` filter to side panel (xavier2k6) - 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: Leave the fields empty when value is invalid (Chocobo1)
- WEBUI: Use natural sorting (Chocobo1) - WEBUI: Use natural sorting (Chocobo1)
- WEBUI: Improve WebUI login behavior (JayRet) - 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: Fix wrong timestamp values (Chocobo1)
- WEBAPI: Send binary data with filename and mime type specified (glassez) - WEBAPI: Send binary data with filename and mime type specified (glassez)
- WEBAPI: Expose API for the torrent creator (glassez, Radu Carpa) - 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: Show RSS feed title in HTML browser (Jay)
- RSS: Allow to set delay between requests to the same host (jNullj) - RSS: Allow to set delay between requests to the same host (jNullj)
- SEARCH: Allow users to specify Python executable path (Chocobo1) - 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: Use Fusion style on Windows 10+. It has better compatibility with dark mode (glassez)
- WINDOWS: Allow to set qBittorrent as default program (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 systemd power management (Chocobo1)
- LINUX: Add support for localized man pages (Victor Chernyakin) - LINUX: Add support for localized man pages (Victor Chernyakin)
- LINUX: Specify a locale if none is set (Chocobo1) - 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> <url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>
<releases> <releases>
<release version="5.0.0~beta1" date="2024-03-19"/> <release version="5.0.0~rc1" date="2024-08-18"/>
</releases> </releases>
</component> </component>

View file

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

View file

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

View file

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

View file

@ -367,6 +367,10 @@ void PeerInfo::determineFlags()
if (useUTPSocket()) if (useUTPSocket())
updateFlags(u'P', C_UTP); 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_flags.chop(1);
m_flagsDescription.chop(1); m_flagsDescription.chop(1);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -77,6 +77,10 @@
#include "base/utils/os.h" #include "base/utils/os.h"
#endif // Q_OS_MACOS || Q_OS_WIN #endif // Q_OS_MACOS || Q_OS_WIN
#ifndef QBT_USES_LIBTORRENT2
#include "customstorage.h"
#endif
using namespace BitTorrent; using namespace BitTorrent;
namespace namespace
@ -456,6 +460,8 @@ Path TorrentImpl::savePath() const
void TorrentImpl::setSavePath(const Path &path) void TorrentImpl::setSavePath(const Path &path)
{ {
Q_ASSERT(!isAutoTMMEnabled()); Q_ASSERT(!isAutoTMMEnabled());
if (isAutoTMMEnabled()) [[unlikely]]
return;
const Path basePath = m_session->useCategoryPathsInManualMode() const Path basePath = m_session->useCategoryPathsInManualMode()
? m_session->categorySavePath(category()) : m_session->savePath(); ? m_session->categorySavePath(category()) : m_session->savePath();
@ -483,6 +489,8 @@ Path TorrentImpl::downloadPath() const
void TorrentImpl::setDownloadPath(const Path &path) void TorrentImpl::setDownloadPath(const Path &path)
{ {
Q_ASSERT(!isAutoTMMEnabled()); Q_ASSERT(!isAutoTMMEnabled());
if (isAutoTMMEnabled()) [[unlikely]]
return;
const Path basePath = m_session->useCategoryPathsInManualMode() const Path basePath = m_session->useCategoryPathsInManualMode()
? m_session->categoryDownloadPath(category()) : m_session->downloadPath(); ? m_session->categoryDownloadPath(category()) : m_session->downloadPath();
@ -982,6 +990,21 @@ PathList TorrentImpl::filePaths() const
return m_filePaths; return m_filePaths;
} }
PathList TorrentImpl::actualFilePaths() const
{
if (!hasMetadata())
return {};
PathList paths;
paths.reserve(filesCount());
const lt::file_storage files = nativeTorrentInfo()->files();
for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes()))
paths.emplaceBack(files.file_path(nativeIndex));
return paths;
}
QVector<DownloadPriority> TorrentImpl::filePriorities() const QVector<DownloadPriority> TorrentImpl::filePriorities() const
{ {
return m_filePriorities; return m_filePriorities;
@ -1447,11 +1470,13 @@ QBitArray TorrentImpl::pieces() const
QBitArray TorrentImpl::downloadingPieces() const QBitArray TorrentImpl::downloadingPieces() const
{ {
QBitArray result(piecesCount()); if (!hasMetadata())
return {};
std::vector<lt::partial_piece_info> queue; std::vector<lt::partial_piece_info> queue;
m_nativeHandle.get_download_queue(queue); m_nativeHandle.get_download_queue(queue);
QBitArray result {piecesCount()};
for (const lt::partial_piece_info &info : queue) for (const lt::partial_piece_info &info : queue)
result.setBit(LT::toUnderlyingType(info.piece_index)); result.setBit(LT::toUnderlyingType(info.piece_index));
@ -1791,12 +1816,13 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
const Path filePath = actualFilePath.removedExtension(QB_EXT); const Path filePath = actualFilePath.removedExtension(QB_EXT);
m_filePaths.append(filePath); m_filePaths.append(filePath);
lt::download_priority_t &nativePriority = p.file_priorities[LT::toUnderlyingType(nativeIndex)]; m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
if ((nativePriority != lt::dont_download) && m_session->isFilenameExcluded(filePath.filename()))
nativePriority = lt::dont_download;
const auto priority = LT::fromNative(nativePriority);
m_filePriorities.append(priority);
} }
m_session->applyFilenameFilter(fileNames, m_filePriorities);
for (int i = 0; i < m_filePriorities.size(); ++i)
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]);
p.save_path = savePath.toString().toStdString(); p.save_path = savePath.toString().toStdString();
p.ti = metadata; p.ti = metadata;
@ -1859,6 +1885,9 @@ void TorrentImpl::reload()
auto *const extensionData = new ExtensionData; auto *const extensionData = new ExtensionData;
p.userdata = LTClientData(extensionData); p.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
m_nativeHandle = m_nativeSession->add_torrent(p); m_nativeHandle = m_nativeSession->add_torrent(p);
m_nativeStatus = extensionData->status; m_nativeStatus = extensionData->status;
@ -1932,9 +1961,18 @@ void TorrentImpl::start(const TorrentOperatingMode mode)
void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext context) void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext context)
{ {
if (!hasMetadata()) if (!hasMetadata())
{
if (context == MoveStorageContext::ChangeSavePath)
{ {
m_savePath = newPath; m_savePath = newPath;
m_session->handleTorrentSavePathChanged(this); m_session->handleTorrentSavePathChanged(this);
}
else if (context == MoveStorageContext::ChangeDownloadPath)
{
m_downloadPath = newPath;
m_session->handleTorrentSavePathChanged(this);
}
return; return;
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -32,7 +32,7 @@
#define QBT_VERSION_MINOR 0 #define QBT_VERSION_MINOR 0
#define QBT_VERSION_BUGFIX 0 #define QBT_VERSION_BUGFIX 0
#define QBT_VERSION_BUILD 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) #x
#define QBT_STRINGIFY(x) QBT__STRINGIFY(x) #define QBT_STRINGIFY(x) QBT__STRINGIFY(x)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -46,9 +47,9 @@ namespace
} }
DownloadedPiecesBar::DownloadedPiecesBar(QWidget *parent) DownloadedPiecesBar::DownloadedPiecesBar(QWidget *parent)
: base {parent} : base(parent)
, m_dlPieceColor {dlPieceColor(pieceColor())}
{ {
updateColorsImpl();
} }
QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin, int reqSize) QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin, int reqSize)
@ -128,25 +129,24 @@ QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin
return result; return result;
} }
bool DownloadedPiecesBar::updateImage(QImage &image) QImage DownloadedPiecesBar::renderImage()
{ {
// qDebug() << "updateImage"; // qDebug() << "updateImage";
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888); QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
if (image2.isNull()) if (image.isNull())
{ {
qDebug() << "QImage image2() allocation failed, width():" << width(); qDebug() << "QImage allocation failed, width():" << width();
return false; return image;
} }
if (m_pieces.isEmpty()) if (m_pieces.isEmpty())
{ {
image2.fill(backgroundColor()); image.fill(backgroundColor());
image = image2; return image;
return true;
} }
QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image2.width()); QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image.width());
QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image2.width()); QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image.width());
// filling image // filling image
for (int x = 0; x < scaledPieces.size(); ++x) 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); QRgb mixedColor = mixTwoColors(pieceColor().rgb(), m_dlPieceColor.rgb(), ratio);
mixedColor = mixTwoColors(backgroundColor().rgb(), mixedColor, fillRatio); mixedColor = mixTwoColors(backgroundColor().rgb(), mixedColor, fillRatio);
image2.setPixel(x, 0, mixedColor); image.setPixel(x, 0, mixedColor);
} }
else 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) 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_pieces = pieces;
m_downloadedPieces = downloadedPieces; m_downloadedPieces = downloadedPieces;
requestImageUpdate(); redraw();
} }
void DownloadedPiecesBar::clear() void DownloadedPiecesBar::clear()
@ -198,3 +198,14 @@ QString DownloadedPiecesBar::simpleToolTipText() const
+ u"</table>"; + 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. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -52,11 +53,13 @@ public:
private: private:
// scale bitfield vector to float vector // scale bitfield vector to float vector
QVector<float> bitfieldToFloatVector(const QBitArray &vecin, int reqSize); QVector<float> bitfieldToFloatVector(const QBitArray &vecin, int reqSize);
bool updateImage(QImage &image) override; QImage renderImage() override;
QString simpleToolTipText() const override; QString simpleToolTipText() const override;
void updateColors() override;
void updateColorsImpl();
// incomplete piece color // incomplete piece color
const QColor m_dlPieceColor; QColor m_dlPieceColor;
// last used bitfields, uses to better resize redraw // 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 // TODO: make a diff pieces to new pieces and update only changed pixels, speedup when update > 20x faster
QBitArray m_pieces; QBitArray m_pieces;

View file

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

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -126,39 +127,38 @@ QVector<float> PieceAvailabilityBar::intToFloatVector(const QVector<int> &vecin,
return result; return result;
} }
bool PieceAvailabilityBar::updateImage(QImage &image) QImage PieceAvailabilityBar::renderImage()
{ {
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888); QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
if (image2.isNull()) if (image.isNull())
{ {
qDebug() << "QImage image2() allocation failed, width():" << width(); qDebug() << "QImage allocation failed, width():" << width();
return false; return image;
} }
if (m_pieces.empty()) if (m_pieces.empty())
{ {
image2.fill(backgroundColor()); image.fill(backgroundColor());
image = image2; return image;
return true;
} }
QVector<float> scaledPieces = intToFloatVector(m_pieces, image2.width()); QVector<float> scaledPieces = intToFloatVector(m_pieces, image.width());
// filling image // filling image
for (int x = 0; x < scaledPieces.size(); ++x) for (int x = 0; x < scaledPieces.size(); ++x)
{ {
float piecesToValue = scaledPieces.at(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) void PieceAvailabilityBar::setAvailability(const QVector<int> &avail)
{ {
m_pieces = avail; m_pieces = avail;
requestImageUpdate(); redraw();
} }
void PieceAvailabilityBar::clear() void PieceAvailabilityBar::clear()

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@ -46,7 +47,7 @@ public:
void clear() override; void clear() override;
private: private:
bool updateImage(QImage &image) override; QImage renderImage() override;
QString simpleToolTipText() const override; QString simpleToolTipText() const override;
// last used int vector, uses to better resize redraw // last used int vector, uses to better resize redraw

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2016 Eugene Shalygin * Copyright (C) 2016 Eugene Shalygin
* Copyright (C) 2006 Christophe Dumez * Copyright (C) 2006 Christophe Dumez
* *
@ -41,6 +42,7 @@
#include "base/indexrange.h" #include "base/indexrange.h"
#include "base/path.h" #include "base/path.h"
#include "base/utils/misc.h" #include "base/utils/misc.h"
#include "gui/uithememanager.h"
namespace namespace
{ {
@ -114,10 +116,16 @@ namespace
} }
PiecesBar::PiecesBar(QWidget *parent) PiecesBar::PiecesBar(QWidget *parent)
: QWidget {parent} : QWidget(parent)
{ {
updatePieceColors();
setMouseTracking(true); setMouseTracking(true);
updateColorsImpl();
connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, [this]
{
updateColors();
redraw();
});
} }
void PiecesBar::setTorrent(const BitTorrent::Torrent *torrent) void PiecesBar::setTorrent(const BitTorrent::Torrent *torrent)
@ -154,7 +162,7 @@ void PiecesBar::leaveEvent(QEvent *e)
{ {
m_hovered = false; m_hovered = false;
m_highlightedRegion = {}; m_highlightedRegion = {};
requestImageUpdate(); redraw();
base::leaveEvent(e); base::leaveEvent(e);
} }
@ -178,7 +186,10 @@ void PiecesBar::paintEvent(QPaintEvent *)
else else
{ {
if (m_image.width() != imageRect.width()) if (m_image.width() != imageRect.width())
updateImage(m_image); {
if (const QImage image = renderImage(); !image.isNull())
m_image = image;
}
painter.drawImage(imageRect, m_image); painter.drawImage(imageRect, m_image);
} }
@ -196,30 +207,33 @@ void PiecesBar::paintEvent(QPaintEvent *)
painter.drawPath(border); painter.drawPath(border);
} }
void PiecesBar::requestImageUpdate() void PiecesBar::redraw()
{ {
if (updateImage(m_image)) if (const QImage image = renderImage(); !image.isNull())
{
m_image = image;
update(); update();
}
} }
QColor PiecesBar::backgroundColor() const QColor PiecesBar::backgroundColor() const
{ {
return palette().color(QPalette::Base); return palette().color(QPalette::Active, QPalette::Base);
} }
QColor PiecesBar::borderColor() const QColor PiecesBar::borderColor() const
{ {
return palette().color(QPalette::Dark); return palette().color(QPalette::Active, QPalette::Dark);
} }
QColor PiecesBar::pieceColor() const QColor PiecesBar::pieceColor() const
{ {
return palette().color(QPalette::Highlight); return palette().color(QPalette::Active, QPalette::Highlight);
} }
QColor PiecesBar::colorBoxBorderColor() const QColor PiecesBar::colorBoxBorderColor() const
{ {
return palette().color(QPalette::ToolTipText); return palette().color(QPalette::Active, QPalette::ToolTipText);
} }
const QVector<QRgb> &PiecesBar::pieceColors() const 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); m_pieceColors = QVector<QRgb>(256);
for (int i = 0; i < 256; ++i) 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); m_pieceColors[i] = mixTwoColors(backgroundColor().rgb(), pieceColor().rgb(), ratio);
} }
} }

View file

@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2016 Eugene Shalygin * Copyright (C) 2016 Eugene Shalygin
* Copyright (C) 2006 Christophe Dumez * Copyright (C) 2006 Christophe Dumez
* *
@ -54,17 +55,15 @@ public:
virtual void clear(); virtual void clear();
// QObject interface
bool event(QEvent *e) override;
protected: protected:
// QWidget interface bool event(QEvent *e) override;
void enterEvent(QEnterEvent *e) override; void enterEvent(QEnterEvent *e) override;
void leaveEvent(QEvent *e) override; void leaveEvent(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override; void mouseMoveEvent(QMouseEvent *e) override;
void paintEvent(QPaintEvent *e) override; void paintEvent(QPaintEvent *e) override;
void requestImageUpdate();
virtual void updateColors();
void redraw();
QColor backgroundColor() const; QColor backgroundColor() const;
QColor borderColor() const; QColor borderColor() const;
@ -82,11 +81,9 @@ private:
void highlightFile(int imagePos); void highlightFile(int imagePos);
virtual QString simpleToolTipText() const = 0; virtual QString simpleToolTipText() const = 0;
virtual QImage renderImage() = 0;
// draw new image to replace the actual image void updateColorsImpl();
// returns true if image was successfully updated
virtual bool updateImage(QImage &image) = 0;
void updatePieceColors();
const BitTorrent::Torrent *m_torrent = nullptr; const BitTorrent::Torrent *m_torrent = nullptr;
QImage m_image; QImage m_image;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -235,10 +235,7 @@ void StatusFilterWidget::applyFilter(int row)
void StatusFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents) void StatusFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
{ {
for (const BitTorrent::Torrent *torrent : torrents) update(torrents);
updateTorrentStatus(torrent);
updateTexts();
} }
void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
@ -273,6 +270,12 @@ void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torr
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading; m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
updateTexts(); updateTexts();
if (Preferences::instance()->getHideZeroStatusFilters())
{
hideZeroItems();
updateGeometry();
}
} }
void StatusFilterWidget::configure() void StatusFilterWidget::configure()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -533,8 +533,6 @@ void WebApplication::sendFile(const Path &path)
const QDateTime lastModified = Utils::Fs::lastModified(path); const QDateTime lastModified = Utils::Fs::lastModified(path);
// find translated file in cache // find translated file in cache
if (!m_isAltUIUsed)
{
if (const auto it = m_translatedFiles.constFind(path); if (const auto it = m_translatedFiles.constFind(path);
(it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified)) (it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified))
{ {
@ -542,7 +540,6 @@ void WebApplication::sendFile(const Path &path)
setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(it->mimeType)}); setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(it->mimeType)});
return; return;
} }
}
const auto readResult = Utils::IO::readFile(path, MAX_ALLOWED_FILESIZE); const auto readResult = Utils::IO::readFile(path, MAX_ALLOWED_FILESIZE);
if (!readResult) if (!readResult)
@ -572,7 +569,7 @@ void WebApplication::sendFile(const Path &path)
QByteArray data = readResult.value(); QByteArray data = readResult.value();
const QMimeType mimeType = QMimeDatabase().mimeTypeForFileNameAndData(path.data(), data); 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) if (isTranslatable)
{ {
@ -740,16 +737,15 @@ void WebApplication::sessionStart()
connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace); connect(m_freeDiskSpaceChecker, &FreeDiskSpaceChecker::checked, syncController, &SyncController::updateFreeDiskSpace);
m_currentSession->registerAPIController(u"sync"_s, syncController); m_currentSession->registerAPIController(u"sync"_s, syncController);
QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toUtf8()}; QNetworkCookie cookie {m_sessionCookieName.toLatin1(), m_currentSession->id().toLatin1()};
cookie.setHttpOnly(true); cookie.setHttpOnly(true);
cookie.setSecure(m_isSecureCookieEnabled && m_isHttpsEnabled); cookie.setSecure(m_isSecureCookieEnabled && m_isHttpsEnabled);
cookie.setPath(u"/"_s); cookie.setPath(u"/"_s);
QByteArray cookieRawForm = cookie.toRawForm();
if (m_isCSRFProtectionEnabled) if (m_isCSRFProtectionEnabled)
cookieRawForm.append("; SameSite=Strict"); cookie.setSameSitePolicy(QNetworkCookie::SameSite::Strict);
else if (cookie.isSecure()) else if (cookie.isSecure())
cookieRawForm.append("; SameSite=None"); cookie.setSameSitePolicy(QNetworkCookie::SameSite::None);
setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookieRawForm)}); setHeader({Http::HEADER_SET_COOKIE, QString::fromLatin1(cookie.toRawForm())});
} }
void WebApplication::sessionEnd() void WebApplication::sessionEnd()

View file

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

View file

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

View file

@ -6,9 +6,8 @@
"url": "https://github.com/qbittorrent/qBittorrent.git" "url": "https://github.com/qbittorrent/qBittorrent.git"
}, },
"scripts": { "scripts": {
"extract_translation": "i18next -c public/i18next-parser.config.mjs public/index.html public/scripts/login.js", "format": "js-beautify -r *.mjs private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && prettier --write **.css",
"format": "js-beautify -r 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"
"lint": "eslint private/*.html private/scripts/*.js private/views/*.html public/*.html public/scripts/*.js && stylelint **/*.css && html-validate private public"
}, },
"devDependencies": { "devDependencies": {
"@stylistic/eslint-plugin": "*", "@stylistic/eslint-plugin": "*",
@ -16,7 +15,6 @@
"eslint-plugin-html": "*", "eslint-plugin-html": "*",
"eslint-plugin-regexp": "*", "eslint-plugin-regexp": "*",
"html-validate": "*", "html-validate": "*",
"i18next-parser": "*",
"js-beautify": "*", "js-beautify": "*",
"prettier": "*", "prettier": "*",
"stylelint": "*", "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-Core-1.6.0-compat-compressed.js"></script>
<script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script> <script src="scripts/lib/MooTools-More-1.6.0-compat-compressed.js"></script>
<script> <script>
'use strict'; "use strict";
new Keyboard({ new Keyboard({
defaultEventType: 'keydown', defaultEventType: "keydown",
events: { events: {
'Escape': function(event) { "Escape": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
}, },
'Esc': function(event) { "Esc": function(event) {
window.parent.qBittorrent.Client.closeWindows(); window.parent.qBittorrent.Client.closeWindows();
event.preventDefault(); event.preventDefault();
} }
} }
}).activate(); }).activate();
window.addEvent('domready', function() { window.addEvent("domready", () => {
const hash = new URI().getData('hash'); const hash = new URI().getData("hash");
if (!hash) if (!hash)
return false; return false;
$('peers').focus(); $("peers").focus();
$('addPeersOk').addEvent('click', function(e) { $("addPeersOk").addEvent("click", (e) => {
new Event(e).stop(); new Event(e).stop();
const peers = $('peers').get('value').trim().split(/[\r\n]+/); const peers = $("peers").get("value").trim().split(/[\r\n]+/);
if (peers.length === 0) if (peers.length === 0)
return; return;
new Request({ new Request({
url: 'api/v2/torrents/addPeers', url: "api/v2/torrents/addPeers",
method: 'post', method: "post",
data: { data: {
hashes: hash, hashes: hash,
peers: peers.join('|') peers: peers.join("|")
}, },
onFailure: function() { onFailure: function() {
alert("QBT_TR(Unable to add peers. Please ensure you are adhering to the IP:port format.)QBT_TR[CONTEXT=HttpServer]"); alert("QBT_TR(Unable to add peers. Please ensure you are adhering to the IP:port format.)QBT_TR[CONTEXT=HttpServer]");

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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