Compare commits

...

64 Commits

Author SHA1 Message Date
Tom Piccirello
aac8cc0d72 Add missing documentation for WebAPI 2.15.0 2026-01-06 14:49:54 -08:00
Tom Piccirello
dda7f80164 WebUI: Support authenticating via Basic auth
PR #23564.
2026-01-05 10:51:45 +03:00
dependabot[bot]
9902ce2c11 GHA CI: Bump action version
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

PR #23687.
2026-01-04 17:04:08 +08:00
John Veness
9c9888d1b3 Change "Session" column headings to match non-"Session"
"Session Upload" -> "Session Uploaded" (to match existing "Uploaded" column)
"Session Download" -> "Session Downloaded" (to match existing "Downloaded" column)

PR #23648.
2026-01-04 16:45:52 +08:00
Tom Piccirello
f16797f1cd WebUI: Store persistent settings in client data API
This PR moves persistent WebUI settings from browser local storage to the new client data API. This allows these settings to be shared across multiple WebUI instances. This does not include settings that are specific to the local client, like window sizes, selected filters, selected tabs, and sorted columns.

Depends on #23088.
Closes #12100.


PR #23191.
2026-01-03 16:50:54 +08:00
schnurlos
a614bd3e40 Update authors list with new contributors
Now inline with `qbittorrent\scr\gui\translators.html`.

PR #23605.
2026-01-03 16:41:34 +08:00
flower
574ff6f917 NSIS: Improve Turkish translation
PR #23621.
2026-01-03 16:36:38 +08:00
xavier2k6
eb593aa846 GHA CI: Use ubuntu-slim runner image for low priority tasks
As mentioned briefly in https://github.com/qbittorrent/qBittorrent/pull/23631#issuecomment-3677881954 & https://github.com/qbittorrent/qBittorrent/pull/23631#issuecomment-3677940316

Make use of `ubuntu-slim` runner images in our `CI` where applicable/practical.

From https://docs.github.com/en/actions/reference/runners/github-hosted-runners#single-cpu-runners :
>This type of runner is optimized for automation tasks, issue operations and short-running jobs. They are not suitable for typical heavyweight CI/CD builds.

* https://github.com/actions/runner-images?tab=readme-ov-file#available-images
* https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
* https://docs.github.com/en/actions/reference/runners/github-hosted-runners#standard-github-hosted-runners-for-public-repositories


PR #23661.
2026-01-03 16:31:11 +08:00
sledgehammer999
9d5a8270cf Bump to v5.2.0beta1 2025-12-31 21:54:34 +02:00
sledgehammer999
d55b924190 Sync translations from Transifex and run lupdate 2025-12-31 21:50:08 +02:00
Chocobo1
a5b5ba2065 GHA CI: improve manageability for boost version
Adopted suggestion from: https://github.com/qbittorrent/qBittorrent/pull/23631#discussion_r2637707057

PR #23671.
2025-12-30 21:59:40 +08:00
Chocobo1
14d2db185a WebUI: simplify code for filtering nodes
Now it use object property to store visited information instead of an array.
Also prefix temporary properties with underscore.
There are no user visible changes.

PR #23670.
2025-12-30 21:50:09 +08:00
Chocobo1
351b065553 Migrate away from old libtorrent setting
The setting has been renamed from `peer_tos` to `peer_dscp` and the default value has changed
from `0x04` to `0x01`.
Note that in WebAPI and qbt config, the previous name is retained to avoid disruption to user.

Upstream PR:
https://github.com/arvidn/libtorrent/pull/6822
https://github.com/arvidn/libtorrent/pull/8072

PR #23669.
2025-12-30 21:17:37 +08:00
Vladimir Golovnev
52a6e7229b Refresh announce status if trackers were removed or replaced
PR #23668.
2025-12-29 11:50:58 +03:00
xavier2k6
be12be2b79 GHA CI: Bump pandoc version
https://github.com/jgm/pandoc/releases/

PR #23638.
2025-12-27 23:02:46 +08:00
Vladimir Golovnev
0bba3f342e Fix incorrect save path when torrent is added from watched folder
PR #23656.
Closes #23302.
2025-12-27 09:04:06 +03:00
Chocobo1
ae7cc7445b Enable strict type checking for Pyright
Doc: https://github.com/microsoft/pyright/blob/main/docs/configuration.md#type-check-diagnostics-settings

PR #23634.
2025-12-22 12:14:38 +08:00
Chocobo1
e79be5853e Enforce Python to run in UTF-8 mode
When running Python via QProcess on Windows, the output code page will be set to the system default instead of UTF-8.
This commit enforces that UTF-8 will be used unconditionally.

Supersedes #23629.
PR #23633.
2025-12-22 12:00:27 +08:00
Chocobo1
b718cc52fa GHA CI: separate Boost version for different libtorrent branches
PR #23631.
2025-12-22 11:20:33 +08:00
Vladimir Golovnev
57c99b54c8 Move torrents to parent category when category is removed
PR #23620.
2025-12-21 19:49:29 +03:00
Vladimir Golovnev
25224f6050 Remove indentation if no subcategory exists
PR #23619.
2025-12-21 19:49:02 +03:00
Vladimir Golovnev
b5d16dfeee Allow to set torrent share limits per category
PR #23577.
2025-12-21 19:48:31 +03:00
Tom Piccirello
1c231ce014 WebUI: keep preferences window open after unsuccessful save
This gives the user another opportunity to save their changes.

PR #23549.
2025-12-20 17:21:26 +08:00
Vladimir Golovnev
93470f2080 Use subcategories unconditionally
PR #23585.
2025-12-16 21:56:15 +03:00
Andrei Stepanov
260563d340 NSIS: Update Russian translation
PR #23602.
2025-12-15 16:40:17 +08:00
Burak Yavuz
b792ecede5 NSIS: Update Turkish translation
PR #23434.
2025-12-15 16:34:24 +08:00
John Veness
19ebf67c74 Use consistent text for "Do not download" priority
To match priority setting in menu.

PR #23593.
2025-12-15 16:28:19 +08:00
xavier2k6
56cd98e06e GHA CI: Bump Boost dependency in coverity-scan workflow
* Bumped Boost to `1.90.0`
https://www.boost.org/releases/1.90.0/

PR #23591.
2025-12-15 16:21:34 +08:00
Mark Yu
c25bd6aaea GHA CI: Include all image format plugins in Windows installer
Add support for WebP in the RSS viewer

Closes #23169.
Closes #23573.
PR #23590.
2025-12-15 16:14:49 +08:00
tehcneko
26e42abf32 WebUI: Clean up duplicated codes in dynamic table
Call class super methods.

PR #23576.
2025-12-15 16:07:35 +08:00
Chocobo1
5abf458e69 Calculate torrent pieces asynchronously
So the GUI won't hang when the calculation took a long time.
Note that it is not possible to cancel the calculation so it will always run until finish in the background.

Supersedes #23497.
PR #23584.
2025-12-13 14:22:20 +08:00
dependabot[bot]
7451552e6a GHA CI: Bump actions version
PR #23566.

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-13 14:13:34 +08:00
Vladimir Golovnev
96161627d6 Fix invalid index
PR #23586.
2025-12-10 16:26:39 +03:00
xavier2k6
0f80d3a74a GHA CI: Bump Qt version
* Bump `Qt` version to `6.10.1` for (Windows, macOS, coverity-scan)

PR #23563.
2025-12-07 18:45:10 +08:00
sledgehammer999
73c15e1f00 Revert "NSIS: Create a mutex to ensure only one installer is running" (#23540)
This reverts commit 530c7d1bbd.
The installer uses the `UAC` plugin which creates two instances of the
installer when launched. One runs with admin privileges while the other
with user privileges. Creating a mutex in the first instance stops the
other one from continuing.

PR #23540
2025-12-06 11:08:30 +02:00
Vladimir Golovnev
a59238f4ea Correctly detect whether option is actually changed
PR #23574.
2025-12-06 11:02:31 +03:00
Vladimir Golovnev
d918c43aba Fix Trackerless row is removed when it becomes zero
PR #23571.
2025-12-06 11:01:50 +03:00
Vladimir Golovnev
c45dfb6662 Implement separate (advanced) "Tracker status" filter
PR #23452.
2025-12-03 10:09:35 +03:00
Chocobo1
f68bc3fef9 Raise 'torrent share ratio' maximum limit
The default from Qt was `99.99` which could be too small for some.
The new limit is `INT_MAX` (not `DOUBLE_MAX`) as to hide the rounding/approximation errors from the user.

PR #23559.
2025-11-30 21:00:16 +08:00
Chocobo1
ed9a8687ad NSIS: set appropriate error code on error
PR #23557.
2025-11-30 19:51:17 +08:00
Chocobo1
6a4b1b9727 GHA CI: enable ccache by default on forked repository
This change only affects forked repository.
Previously ccache was only enabled on `master` branch regardless of the owner of the repository.
This is not ideal for forked repository where the owner (mostly) wants to run the GHA CI to
create binary for their own use and ccache couldn't be utilized. This change will enable
ccache by default for all non-official repositories.

PR #23558.
2025-11-30 19:37:26 +08:00
Vasiliy Kostin
f2f4676824 Add reboot option when downloads complete
This commit implements a new "Reboot System" option that allows users to automatically reboot the computer when all downloads are complete, similar to the existing shutdown, suspend, and hibernate options.

Closes #10774.
PR #23525.
2025-11-30 19:16:48 +08:00
Mark Yu
8b9064a33c WebUI: Do not hide context menu if the click target has submenu
Add check if the click event occurs in the menu item and if the menu item has a submenu, do not close the context menu.

Closes #23532.
PR #23534.
2025-11-30 19:15:23 +08:00
tehcneko
296c90d688 WebUI: Fix row selection by Shift key with virtual list enabled
Fixes #23336.
PR #23543.
2025-11-30 19:14:30 +08:00
tehcneko
564afc975f WebUI: Fix row collapsing with virtual list enabled
Fixes https://github.com/qbittorrent/qBittorrent/issues/23241#issuecomment-3295352816.
PR #23542.
2025-11-30 19:13:10 +08:00
Vladimir Golovnev
a77b17e6da Allow to configure style and color scheme on all platforms
PR #23522.
2025-11-24 09:04:28 +03:00
Hanabishi
4a3922d152 Make the active torrents filter reflect actual transfers
PR #23431.
Closes #23121.
2025-11-24 09:03:20 +03:00
Chocobo1
1b96a48266 GHA CI: test built binary
This would ensure the built binary is able to start up and rule out compiler or library linking
issues.

PR #23529.
2025-11-24 02:51:46 +08:00
Chocobo1
1f6e7519a0 Raise connection max limits
And remove a few redundant properties as they are the same as the default value (or they have the same effect as using the default value).

Closes #23465.
PR #23528.
2025-11-24 02:38:21 +08:00
sledgehammer999
46cc3a358e Temporarily lower Qt version deprecation
Static builds of Qt fail when deprecating up to 6.6
[QTBUG-141994](https://bugreports.qt.io/browse/QTBUG-141994)

PR #23499
2025-11-16 13:24:48 +02:00
Orgad Shaneh
9c654b8a73 Fix link on mingw cross-compilation
Library names are all lower-case.

PR #23472.
2025-11-14 21:41:32 +08:00
Chocobo1
e036781b36 NSIS: use proper data type when invoking dll functions
Also migrate away from unsupported function `SHGetSpecialFolderPath()` to `SHGetKnownFolderPath()`.

Ref:
https://nsis.sourceforge.io/Docs/System/System.html#callfuncs
https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath

PR #23471.
2025-11-14 21:35:00 +08:00
Orgad Shaneh
ffbd01eb81 Fix MinGW compiler warnings
Cross-compiling with mxe on linux.

PR #23470.
2025-11-14 21:26:35 +08:00
namoen0301
530c7d1bbd NSIS: Create a mutex to ensure only one installer is running
PR #23450.
2025-11-14 21:20:02 +08:00
namoen0301
fec4e01aeb NSIS: add ManifestDPIAwareness function
* This PR added an undocumented function `ManifestDPIAwareness` and enable `PerMonitorV2` support (requires Win10 1703+)
  691211035c/Docs/src/attributes.but (L290-L292)
* Remove `XPStyle` because this function do nothing on NSIS 3.x

PR #23426.
2025-11-14 21:09:12 +08:00
Chocobo1
4541044c42 GHA CI: ensure AppRun hook folder exists
Upstream made some changes and it won't create the AppRun hook folder
anymore, so we create it ourselves.
Upstream PR: https://github.com/linuxdeploy/linuxdeploy-plugin-qt/pull/206

PR #23474.
2025-11-11 15:02:54 +08:00
sledgehammer999
b819b9b7a6 Sync translations from Transifex and run lupdate 2025-11-08 00:29:09 +02:00
dependabot[bot]
6b3519f4eb GHA CI: Bump Github Actions versions
PR #23444.
2025-11-03 13:35:20 +08:00
Chocobo1
33e7cff3b0 Update expected-lite to v0.9.0
* Update expected-lite to v0.9.0
  https://github.com/nonstd-lite/expected-lite/releases/tag/v0.9.0
* Add error messages for various RSS processing error
* Preserve CRLF endings for expected-lite
  So that we can verify the hash matches upstream easily.

PR #23440.
Closes #22950.
2025-11-03 13:03:41 +08:00
Halbast
516f2ef6ec NSIS: Update Kurdish translation
PR #22801.
2025-11-03 12:53:30 +08:00
Vladimir Golovnev
85f1c774f6 Improve search results filtering implementation
PR #23430.
Closes #23396.
2025-11-02 14:47:39 +03:00
Vladimir Golovnev
1be4e646e1 WebAPI: Use native separators for path autofill suggestions
PR #23439.
Closes #23432.
2025-11-02 14:43:57 +03:00
Chocobo1
ee62dd3cda Do not allow orphan processes
If a process is an orphan (without a parent) then the process won't exit when qbt exits and
the process will be still running. This is unwanted behavior.

PR #23422.
2025-10-31 14:19:52 +08:00
Vladimir Golovnev
bc22e8929c Fix incorrect initial state of checkbox
PR #23428.
2025-10-28 17:08:25 +03:00
244 changed files with 145925 additions and 97450 deletions

3
.gitattributes vendored
View File

@@ -8,3 +8,6 @@ core.eol=lf
dist/windows/license.txt text eol=crlf dist/windows/license.txt text eol=crlf
test/testdata/crlf.txt text eol=crlf test/testdata/crlf.txt text eol=crlf
# disable line endings conversions
src/base/3rdparty/expected.hpp -text

View File

@@ -11,12 +11,12 @@ concurrency:
jobs: jobs:
ci: ci:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-slim
permissions: permissions:
security-events: write security-events: write
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
@@ -31,12 +31,13 @@ jobs:
- name: Check doc - name: Check doc
env: env:
pandoc_path: "${{ github.workspace }}/../pandoc" pandoc_path: "${{ github.workspace }}/../pandoc"
pandoc_version: "3.8.3"
run: | run: |
# install pandoc # install pandoc
curl \ curl \
-L \ -L \
-o "${{ runner.temp }}/pandoc.tar.gz" \ -o "${{ runner.temp }}/pandoc.tar.gz" \
"https://github.com/jgm/pandoc/releases/download/3.7.0.2/pandoc-3.7.0.2-linux-amd64.tar.gz" "https://github.com/jgm/pandoc/releases/download/${{ env.pandoc_version }}/pandoc-${{ env.pandoc_version }}-linux-amd64.tar.gz"
tar -xf "${{ runner.temp }}/pandoc.tar.gz" -C "${{ github.workspace }}/.." tar -xf "${{ runner.temp }}/pandoc.tar.gz" -C "${{ github.workspace }}/.."
mv "${{ github.workspace }}/.."/pandoc-* "${{ env.pandoc_path }}" mv "${{ github.workspace }}/.."/pandoc-* "${{ env.pandoc_path }}"
# run pandoc # run pandoc
@@ -65,7 +66,7 @@ jobs:
> "${{ runner.temp }}/zizmor_results.sarif" > "${{ runner.temp }}/zizmor_results.sarif"
- name: Upload zizmor results - name: Upload zizmor results
uses: github/codeql-action/upload-sarif@v3 uses: github/codeql-action/upload-sarif@v4
with: with:
category: zizmor category: zizmor
sarif_file: "${{ runner.temp }}/zizmor_results.sarif" sarif_file: "${{ runner.temp }}/zizmor_results.sarif"

View File

@@ -18,9 +18,17 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
libt_version: ["2.0.11", "1.2.20"] libtorrent:
- version: "2.0.11"
boost_major_version: "1"
boost_minor_version: "90"
boost_patch_version: "0"
- version: "1.2.20"
boost_major_version: "1"
boost_minor_version: "86"
boost_patch_version: "0"
qbt_gui: ["GUI=ON", "GUI=OFF"] qbt_gui: ["GUI=ON", "GUI=OFF"]
qt_version: ["6.9.1"] qt_version: ["6.10.1"]
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"
@@ -28,7 +36,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
@@ -49,19 +57,15 @@ jobs:
- name: Setup ccache - name: Setup ccache
uses: Chocobo1/setup-ccache-action@v1 uses: Chocobo1/setup-ccache-action@v1
with: with:
store_cache: ${{ github.ref == 'refs/heads/master' }} store_cache: ${{ (github.repository != 'qbittorrent/qBittorrent') || (github.ref == 'refs/heads/master') }}
update_packager_index: false update_packager_index: false
ccache_options: | ccache_options: |
max_size=1G max_size=1G
- name: Install boost - name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "86"
BOOST_PATCH_VERSION: "0"
run: | run: |
boost_url="https://archives.boost.io/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" boost_url="https://archives.boost.io/release/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/source/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
set +e set +e
curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url" curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url"
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?" tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
@@ -79,13 +83,14 @@ jobs:
with: with:
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
archives: qtbase qtdeclarative qtsvg qttools archives: qtbase qtdeclarative qtsvg qttools
modules: qtimageformats
# Not sure why Qt made a hard dependency on qtdeclarative, try removing it when Qt > 6.4.0 # Not sure why Qt made a hard dependency on qtdeclarative, try removing it when Qt > 6.4.0
cache: true cache: true
- name: Install libtorrent - name: Install libtorrent
run: | run: |
git clone \ git clone \
--branch v${{ matrix.libt_version }} \ --branch v${{ matrix.libtorrent.version }} \
--depth 1 \ --depth 1 \
--recurse-submodules \ --recurse-submodules \
https://github.com/arvidn/libtorrent.git \ https://github.com/arvidn/libtorrent.git \
@@ -119,6 +124,11 @@ jobs:
cmake --build build --target qbt_update_translations cmake --build build --target qbt_update_translations
cmake --build build cmake --build build
cmake --build build --target check cmake --build build --target check
if [ "${{ matrix.qbt_gui }}" = "GUI=ON" ]; then
build/qbittorrent.app/Contents/MacOS/qbittorrent -v
else
build/qbittorrent-nox.app/Contents/MacOS/qbittorrent-nox -v
fi
- name: Prepare build artifacts - name: Prepare build artifacts
run: | run: |
@@ -156,7 +166,7 @@ jobs:
cp ${{ env.libtorrent_path }}/build/compile_commands.json upload/cmake/libtorrent cp ${{ env.libtorrent_path }}/build/compile_commands.json upload/cmake/libtorrent
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: qBittorrent-CI_macOS_${{ matrix.qbt_gui }}_libtorrent-${{ matrix.libt_version }}_Qt-${{ matrix.qt_version }} name: qBittorrent-CI_macOS_${{ matrix.qbt_gui }}_libtorrent-${{ matrix.libtorrent.version }}_Qt-${{ matrix.qt_version }}
path: upload path: upload

View File

@@ -15,7 +15,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false

View File

@@ -19,7 +19,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
libt_version: ["2.0.11", "1.2.20"] libtorrent:
- version: "2.0.11"
boost_major_version: "1"
boost_minor_version: "77"
boost_patch_version: "0"
- version: "1.2.20"
boost_major_version: "1"
boost_minor_version: "77"
boost_patch_version: "0"
qbt_gui: ["GUI=ON", "GUI=OFF"] qbt_gui: ["GUI=ON", "GUI=OFF"]
qt_version: ["6.6.3"] qt_version: ["6.6.3"]
@@ -30,7 +38,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
@@ -39,24 +47,20 @@ jobs:
sudo apt update sudo apt update
sudo apt install \ sudo apt install \
build-essential cmake ninja-build \ build-essential cmake ninja-build \
libssl-dev libxkbcommon-x11-dev libxcb-cursor-dev zlib1g-dev libssl-dev zlib1g-dev
- name: Setup ccache - name: Setup ccache
uses: Chocobo1/setup-ccache-action@v1 uses: Chocobo1/setup-ccache-action@v1
with: with:
store_cache: ${{ github.ref == 'refs/heads/master' }} store_cache: ${{ (github.repository != 'qbittorrent/qBittorrent') || (github.ref == 'refs/heads/master') }}
update_packager_index: false update_packager_index: false
ccache_options: | ccache_options: |
max_size=1G max_size=1G
- name: Install boost - name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "77"
BOOST_PATCH_VERSION: "0"
run: | run: |
boost_url="https://archives.boost.io/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" boost_url="https://archives.boost.io/release/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/source/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
set +e set +e
curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url" curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url"
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?" tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
@@ -74,12 +78,13 @@ jobs:
with: with:
version: ${{ matrix.qt_version }} version: ${{ matrix.qt_version }}
archives: icu qtbase qtdeclarative qtsvg qttools archives: icu qtbase qtdeclarative qtsvg qttools
modules: qtimageformats
cache: true cache: true
- name: Install libtorrent - name: Install libtorrent
run: | run: |
git clone \ git clone \
--branch v${{ matrix.libt_version }} \ --branch v${{ matrix.libtorrent.version }} \
--depth 1 \ --depth 1 \
--recurse-submodules \ --recurse-submodules \
https://github.com/arvidn/libtorrent.git \ https://github.com/arvidn/libtorrent.git \
@@ -100,8 +105,8 @@ jobs:
# to avoid scanning 3rdparty codebases, initialize it just before building qbt # to avoid scanning 3rdparty codebases, initialize it just before building qbt
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
if: startsWith(matrix.libt_version, 2) && (matrix.qbt_gui == 'GUI=ON') if: startsWith(matrix.libtorrent.version, 2) && (matrix.qbt_gui == 'GUI=ON')
with: with:
config-file: ./.github/workflows/helper/codeql/cpp.yaml config-file: ./.github/workflows/helper/codeql/cpp.yaml
languages: cpp languages: cpp
@@ -123,11 +128,16 @@ jobs:
cmake --build build --target qbt_update_translations cmake --build build --target qbt_update_translations
cmake --build build cmake --build build
cmake --build build --target check cmake --build build --target check
if [ "${{ matrix.qbt_gui }}" = "GUI=ON" ]; then
QT_QPA_PLATFORM=offscreen build/qbittorrent -v
else
build/qbittorrent-nox -v
fi
DESTDIR="qbittorrent" cmake --install build DESTDIR="qbittorrent" cmake --install build
- name: Run CodeQL analysis - name: Run CodeQL analysis
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4
if: startsWith(matrix.libt_version, 2) && (matrix.qbt_gui == 'GUI=ON') if: startsWith(matrix.libtorrent.version, 2) && (matrix.qbt_gui == 'GUI=ON')
with: with:
category: ${{ github.base_ref || github.ref_name }} category: ${{ github.base_ref || github.ref_name }}
@@ -164,14 +174,15 @@ jobs:
run: | run: |
rm -f "${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/sqldrivers/libqsqlmimer.so" rm -f "${{ runner.workspace }}/Qt/${{ matrix.qt_version }}/gcc_64/plugins/sqldrivers/libqsqlmimer.so"
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt ./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
rm qbittorrent/apprun-hooks/* mkdir -p qbittorrent/apprun-hooks
rm -f 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-x86_64.AppImage --appdir qbittorrent --output appimage
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: qBittorrent-CI_Ubuntu-x64_${{ matrix.qbt_gui }}_libtorrent-${{ matrix.libt_version }}_Qt-${{ matrix.qt_version }} name: qBittorrent-CI_Ubuntu-x64_${{ matrix.qbt_gui }}_libtorrent-${{ matrix.libtorrent.version }}_Qt-${{ matrix.qt_version }}
path: upload path: upload

View File

@@ -11,7 +11,7 @@ concurrency:
jobs: jobs:
ci: ci:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-slim
permissions: permissions:
security-events: write security-events: write
@@ -21,12 +21,12 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
- name: Setup nodejs - name: Setup nodejs
uses: actions/setup-node@v5 uses: actions/setup-node@v6
with: with:
node-version: 'lts/*' node-version: 'lts/*'
@@ -52,7 +52,7 @@ jobs:
git diff --exit-code git diff --exit-code
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@v3 uses: github/codeql-action/init@v4
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
with: with:
config-file: .github/workflows/helper/codeql/js.yaml config-file: .github/workflows/helper/codeql/js.yaml
@@ -60,4 +60,4 @@ jobs:
- name: Run CodeQL analysis - name: Run CodeQL analysis
if: ${{ !cancelled() }} if: ${{ !cancelled() }}
uses: github/codeql-action/analyze@v3 uses: github/codeql-action/analyze@v4

View File

@@ -10,7 +10,7 @@ concurrency:
jobs: jobs:
ci: ci:
name: Build (${{ matrix.libt_version }}, ${{ matrix.config.arch }}) name: Build (${{ matrix.libtorrent.version }}, ${{ matrix.config.arch }})
runs-on: ${{ matrix.config.os }} runs-on: ${{ matrix.config.os }}
permissions: permissions:
actions: write actions: write
@@ -18,7 +18,15 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
libt_version: ["2.0.11", "1.2.20"] libtorrent:
- version: "2.0.11"
boost_major_version: "1"
boost_minor_version: "90"
boost_patch_version: "0"
- version: "1.2.20"
boost_major_version: "1"
boost_minor_version: "86"
boost_patch_version: "0"
config: config:
- os: windows-latest - os: windows-latest
arch: x64 arch: x64
@@ -31,7 +39,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
@@ -86,13 +94,9 @@ jobs:
$packages $packages
- name: Install boost - name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "86"
BOOST_PATCH_VERSION: "0"
run: | run: |
$boost_url="https://archives.boost.io/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" $boost_url="https://archives.boost.io/release/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/source/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
$boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" $boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url" curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url"
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.." tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."
if ($LastExitCode -ne 0) if ($LastExitCode -ne 0)
@@ -112,15 +116,16 @@ jobs:
- name: Install Qt - name: Install Qt
uses: jurplel/install-qt-action@v4 uses: jurplel/install-qt-action@v4
with: with:
version: "6.9.1" version: "6.10.1"
arch: ${{ matrix.config.qt_arch }} arch: ${{ matrix.config.qt_arch }}
archives: qtbase qtsvg qttools archives: qtbase qtsvg qttools
modules: qtimageformats
cache: true cache: true
- name: Install libtorrent - name: Install libtorrent
run: | run: |
git clone ` git clone `
--branch v${{ matrix.libt_version }} ` --branch v${{ matrix.libtorrent.version }} `
--depth 1 ` --depth 1 `
--recurse-submodules ` --recurse-submodules `
https://github.com/arvidn/libtorrent.git ` https://github.com/arvidn/libtorrent.git `
@@ -182,8 +187,16 @@ jobs:
mkdir upload/qBittorrent/plugins/iconengines mkdir upload/qBittorrent/plugins/iconengines
copy "${{ env.Qt_ROOT_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
# include all imageformats dlls since CI (dev) build links dynamically
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qgif.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qicns.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qjpeg.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qtga.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qtiff.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qwbmp.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qwebp.dll" upload/qBittorrent/plugins/imageformats
mkdir upload/qBittorrent/plugins/platforms mkdir upload/qBittorrent/plugins/platforms
copy "${{ env.Qt_ROOT_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
@@ -200,13 +213,13 @@ jobs:
copy ${{ env.libtorrent_path }}/build/compile_commands.json upload/cmake/libtorrent copy ${{ env.libtorrent_path }}/build/compile_commands.json upload/cmake/libtorrent
- name: Upload build artifacts - name: Upload build artifacts
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: qBittorrent-CI_Windows-${{ matrix.config.arch }}_libtorrent-${{ matrix.libt_version }} name: qBittorrent-CI_Windows-${{ matrix.config.arch }}_libtorrent-${{ matrix.libtorrent.version }}
path: upload path: upload
- name: Install NSIS - name: Install NSIS
uses: repolevedavaj/install-nsis@265e893c16602d8ccfb0a9ca44173b084078917c # v1.0.3 uses: repolevedavaj/install-nsis@a55ed92772254d1e51d880f85ce9b5719f907801 # v1.1.0
with: with:
nsis-version: '3.11' nsis-version: '3.11'
@@ -216,7 +229,7 @@ jobs:
makensis /DQBT_CPU_ARCH="${{ matrix.config.arch }}" /DQBT_DIST_DIR="../../upload/qBittorrent" /WX dist/windows/qbittorrent.nsi makensis /DQBT_CPU_ARCH="${{ matrix.config.arch }}" /DQBT_DIST_DIR="../../upload/qBittorrent" /WX dist/windows/qbittorrent.nsi
- name: Upload installer - name: Upload installer
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v6
with: with:
name: qBittorrent-CI_Windows-${{ matrix.config.arch }}_libtorrent-${{ matrix.libt_version }}-setup name: qBittorrent-CI_Windows-${{ matrix.config.arch }}_libtorrent-${{ matrix.libtorrent.version }}-setup
path: dist/windows/qbittorrent_*_setup.exe path: dist/windows/qbittorrent_*_setup.exe

View File

@@ -14,9 +14,13 @@ jobs:
strategy: strategy:
matrix: matrix:
libt_version: ["2.0.11"] libtorrent:
- version: "2.0.11"
boost_major_version: "1"
boost_minor_version: "90"
boost_patch_version: "0"
qbt_gui: ["GUI=ON"] qbt_gui: ["GUI=ON"]
qt_version: ["6.9.1"] qt_version: ["6.10.1"]
env: env:
boost_path: "${{ github.workspace }}/../boost" boost_path: "${{ github.workspace }}/../boost"
@@ -25,7 +29,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v5 uses: actions/checkout@v6
with: with:
persist-credentials: false persist-credentials: false
@@ -37,13 +41,9 @@ jobs:
libssl-dev libxkbcommon-x11-dev libxcb-cursor-dev zlib1g-dev libssl-dev libxkbcommon-x11-dev libxcb-cursor-dev zlib1g-dev
- name: Install boost - name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "88"
BOOST_PATCH_VERSION: "0"
run: | run: |
boost_url="https://archives.boost.io/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" boost_url="https://archives.boost.io/release/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/source/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz" boost_url2="https://sourceforge.net/projects/boost/files/boost/${{ matrix.libtorrent.boost_major_version }}.${{ matrix.libtorrent.boost_minor_version }}.${{ matrix.libtorrent.boost_patch_version }}/boost_${{ matrix.libtorrent.boost_major_version }}_${{ matrix.libtorrent.boost_minor_version }}_${{ matrix.libtorrent.boost_patch_version }}.tar.gz"
set +e set +e
curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url" curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url"
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?" tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
@@ -66,7 +66,7 @@ jobs:
- name: Install libtorrent - name: Install libtorrent
run: | run: |
git clone \ git clone \
--branch v${{ matrix.libt_version }} \ --branch v${{ matrix.libtorrent.version }} \
--depth 1 \ --depth 1 \
--recurse-submodules \ --recurse-submodules \
https://github.com/arvidn/libtorrent.git \ https://github.com/arvidn/libtorrent.git \

View File

@@ -8,7 +8,7 @@ permissions: {}
jobs: jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-slim
permissions: permissions:
pull-requests: write pull-requests: write
steps: steps:

View File

@@ -39,6 +39,7 @@ repos:
args: ["--fix=lf"] args: ["--fix=lf"]
exclude: | exclude: |
(?x)^( (?x)^(
src/base/3rdparty/expected.hpp |
src/webui/www/private/css/lib/.* | src/webui/www/private/css/lib/.* |
src/webui/www/private/scripts/lib/.* | src/webui/www/private/scripts/lib/.* |
dist/windows/license.txt | dist/windows/license.txt |

18
AUTHORS
View File

@@ -94,7 +94,7 @@ Translations authors:
* files: src/lang/*.ts * files: src/lang/*.ts
* file: src/icons/qBittorrent.desktop * file: src/icons/qBittorrent.desktop
copyright: copyright:
- Arabic: SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex) - Arabic: SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)
- Armenian: Hrant Ohanyan (hrantohanyan@mail.am) - Armenian: Hrant Ohanyan (hrantohanyan@mail.am)
- Basque: Xabier Aramendi (azpidatziak@gmail.com) - Basque: Xabier Aramendi (azpidatziak@gmail.com)
- Belarusian: Mihas Varantsou (meequz@gmail.com) - Belarusian: Mihas Varantsou (meequz@gmail.com)
@@ -104,31 +104,31 @@ Translations authors:
- Chinese (Traditional): Yi-Shun Wang (dnextstep@gmail.com) and 冥王歐西里斯 s8321414(Transifex) - Chinese (Traditional): Yi-Shun Wang (dnextstep@gmail.com) and 冥王歐西里斯 s8321414(Transifex)
- Croatian: Oliver Mucafir (oliver.untwist@gmail.com) - Croatian: Oliver Mucafir (oliver.untwist@gmail.com)
- Czech: Jirka Vilim (web@tets.cz) and Petr Cernobila abr(Transifex) - Czech: Jirka Vilim (web@tets.cz) and Petr Cernobila abr(Transifex)
- Danish: Mathias Nielsen (comoneo@gmail.com) - Danish: Mathias Nielsen (comoneo@gmail.com), scootergrisen, Nicolaj (fuskern@gmail.com) and Joe Hansen (joedalton2@yahoo.dk)
- Dutch: Pieter Heyvaert (pieter_heyvaert@hotmail.com) - Dutch: Pieter Heyvaert (pieter_heyvaert@hotmail.com)
- English: Christophe Dumez (chris@qbittorrent.org)
- English(Australia): Robert Readman readmanr(Transifex) - English(Australia): Robert Readman readmanr(Transifex)
- English(United Kingdom): Robert Readman readmanr(Transifex) - English(United Kingdom): Robert Readman readmanr(Transifex)
- English: Christophe Dumez (chris@qbittorrent.org)
- Finnish: Niklas Laxström (nikerabbit@users.sourceforge.net), Pekka Niemi (pekka.niemi@iki.fi) and Jiri Grönroos artnay(Transifex) - Finnish: Niklas Laxström (nikerabbit@users.sourceforge.net), Pekka Niemi (pekka.niemi@iki.fi) and Jiri Grönroos artnay(Transifex)
- French: Christophe Dumez (chris@qbittorrent.org) - French: Christophe Dumez (chris@qbittorrent.org)
- Galician: Marcos Lans (marcoslansgarza@gmail.com) and antiparvos(Transifex) - Galician: Marcos Lans (marcoslansgarza@gmail.com) and antiparvos(Transifex)
- Georgian: Beqa Arabuli (arabulibeqa@yahoo.com) - Georgian: Beqa Arabuli (arabulibeqa@yahoo.com)
- German: Niels Hoffmann (zentralmaschine@users.sourceforge.net) - German: Niels Hoffmann (zentralmaschine@users.sourceforge.net), schnurlos (schnurlos@gmail.com)
- Greek: Tsvetan Bankov (emerge_life@users.sourceforge.net), Stephanos Antaris (santaris@csd.auth.gr), sledgehammer999(hammered999@gmail.com) and Γιάννης Ανθυμίδης Evropi(Transifex) - Greek: Tsvetan Bankov (emerge_life@users.sourceforge.net), Stephanos Antaris (santaris@csd.auth.gr), sledgehammer999(hammered999@gmail.com), Γιάννης Ανθυμίδης Evropi(Transifex) and Panagiotis Tabakis(tabakisp@gmail.com)
- Hebrew: David Deutsch (d.deffo@gmail.com) - Hebrew: David Deutsch (d.deffo@gmail.com)
- Hungarian: Majoros Péter - Hungarian: Majoros Péter
- Italian: bovirus (bovirus@live.it) and Matteo Sechi (bu17714@gmail.com) - Italian: bovirus (bovirus@live.it) and Matteo Sechi (bu17714@gmail.com)
- Japanese: Masato Hashimoto (cabezon.hashimoto@gmail.com) - Japanese: Masato Hashimoto (cabezon.hashimoto@gmail.com)
- Korean: Jin Woo Sin (jin828sin@users.sourceforge.net) - Korean: Jin Woo Sin (jin828sin@users.sourceforge.net)
- Lithuanian: Naglis Jonaitis (njonaitis@gmail.com) - Lithuanian: Naglis Jonaitis (njonaitis@gmail.com)
- Norwegian: Tomaso - Norwegian: Tomaso, Imre Kristoffer Eilertsen (imreeil42@gmail.com) and Kingu (Transifex)
- Polish: Mariusz Fik (fisiu@opensuse.org) - Polish: Mariusz Fik (fisiu@opensuse.org)
- Portuguese: Sérgio Marques smarquespt(Transifex)
- Portuguese(Brazil): Nick Marinho (nickmarinho@gmail.com) - Portuguese(Brazil): Nick Marinho (nickmarinho@gmail.com)
- Portuguese: Sérgio Marques smarquespt(Transifex), L.Sousa(Transifex), Hugo Carvalho (hugok Transifex)
- Romanian: Obada Denis (obadadenis@users.sourceforge.net), Adrian Gabor Adriannho(Transifex) and Mihai Coman z0id(Transifex) - Romanian: Obada Denis (obadadenis@users.sourceforge.net), Adrian Gabor Adriannho(Transifex) and Mihai Coman z0id(Transifex)
- Russian: Nick Khazov (m2k3d0n at users.sourceforge.net), Alexey Morsov (samurai@ricom.ru), Nick Tiskov Dayman(daymansmail (at) gmail (dot) com), Dmitry DmitryKX(Transifex) and kraleksandr kraleksandr(Transifex) - Russian: Nick Khazov (m2k3d0n at users.sourceforge.net), Alexey Morsov (samurai@ricom.ru), Nick Tiskov Dayman(daymansmail (at) gmail (dot) com), Dmitry DmitryKX(Transifex) and kraleksandr kraleksandr(Transifex)
- Serbian: Anaximandar Milet (anaximandar at operamail.com) - Serbian: Anaximandar Milet (anaximandar@operamail.com)
- Slovak: helix84 - Slovak: helix84
- Spanish: Francisco Luque Contreras (frannoe@ya.com), Alfredo Monclus alfrix(Transifex) and José Antonio Moray moray33(Transifex) - Spanish: Francisco Luque Contreras (frannoe@ya.com), Alfredo Monclus alfrix(Transifex) and José Antonio Moray moray33(Transifex)
- Swedish: Daniel Nylander (po@danielnylander.se) and Emil Hammarberg Ooglogput(Transifex) - Swedish: Daniel Nylander (po@danielnylander.se) and Emil Hammarberg Ooglogput(Transifex)
- Turkish: Hasan Yilmaz (iletisim@hedefturkce.com) - Turkish: Hasan Yilmaz (iletisim@hedefturkce.com)

View File

@@ -1,5 +1,11 @@
# WebAPI Changelog # WebAPI Changelog
## 2.15.0
* [#23585](https://github.com/qbittorrent/qBittorrent/pull/23585)
* `sync/maindata` endpoint no longer includes the key `use_subcategories` as subcategories are now always enabled
* [#23564](https://github.com/qbittorrent/qBittorrent/pull/23564)
* WebAPI credentials can now be supplied via Basic auth
## 2.14.1 ## 2.14.1
* [#23212](https://github.com/qbittorrent/qBittorrent/pull/23212) * [#23212](https://github.com/qbittorrent/qBittorrent/pull/23212)
* Add `app/rotateAPIKey` endpoint for generating, and rotating, the WebAPI API key * Add `app/rotateAPIKey` endpoint for generating, and rotating, the WebAPI API key

View File

@@ -20,7 +20,7 @@ target_compile_features(qbt_common_cfg INTERFACE
) )
target_compile_definitions(qbt_common_cfg INTERFACE target_compile_definitions(qbt_common_cfg INTERFACE
QT_DISABLE_DEPRECATED_UP_TO=0x060600 QT_DISABLE_DEPRECATED_UP_TO=0x060500
QT_NO_CAST_FROM_ASCII QT_NO_CAST_FROM_ASCII
QT_NO_CAST_TO_ASCII QT_NO_CAST_TO_ASCII
QT_NO_CAST_FROM_BYTEARRAY QT_NO_CAST_FROM_BYTEARRAY

View File

@@ -161,8 +161,8 @@ Name[ta]=qBittorrent
GenericName[te]=క్యు బిట్ టొరెంట్ క్లయింట్ GenericName[te]=క్యు బిట్ టొరెంట్ క్లయింట్
Comment[te]=క్యు బిట్ టొరెంట్ తో ఫైల్స్ దిగుమతి చేసుకోండి , పంచుకోండి Comment[te]=క్యు బిట్ టొరెంట్ తో ఫైల్స్ దిగుమతి చేసుకోండి , పంచుకోండి
Name[te]=qBittorrent Name[te]=qBittorrent
GenericName[th]=ไคลเอนต์บิทอร์เรนต์ GenericName[th]=ไคลเอนต์บิทอร์เรนต์
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่านบิตทอร์เรนต์ Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่าน BitTorrent
Name[th]=qBittorrent Name[th]=qBittorrent
GenericName[tr]=BitTorrent istemcisi GenericName[tr]=BitTorrent istemcisi
Comment[tr]=Dosyaları BitTorrent üzerinden indirin ve paylaşın Comment[tr]=Dosyaları BitTorrent üzerinden indirin ve paylaşın

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.2.0~alpha1" date="2025-02-11"/> <release version="5.2.0~beta1" date="2025-12-31"/>
</releases> </releases>
</component> </component>

View File

@@ -51,6 +51,8 @@
Unicode true Unicode true
ManifestDPIAware true ManifestDPIAware true
; add an undocumented function in NSIS, PMv2 requires Win10 1703+
ManifestDPIAwareness "PerMonitorV2,System"
!ifdef USE_UPX !ifdef USE_UPX
!packhdr "$%TEMP%\exehead.tmp" 'upx.exe -9 --best --ultra-brute "$%TEMP%\exehead.tmp"' !packhdr "$%TEMP%\exehead.tmp" 'upx.exe -9 --best --ultra-brute "$%TEMP%\exehead.tmp"'
@@ -59,7 +61,6 @@ ManifestDPIAware true
;Setting the compression ;Setting the compression
SetCompressor /SOLID LZMA SetCompressor /SOLID LZMA
SetCompressorDictSize 64 SetCompressorDictSize 64
XPStyle on
!include "MUI2.nsh" !include "MUI2.nsh"
!include "UAC.nsh" !include "UAC.nsh"
@@ -73,8 +74,8 @@ XPStyle on
!define SHCNF_IDLIST 0 !define SHCNF_IDLIST 0
;For special folder detection ;For special folder detection
!define CSIDL_APPDATA '0x1A' ;Application Data path !define FOLDERID_RoamingAppData {3EB685DB-65F9-4CF6-A03A-E3EF65729F3D} ; %APPDATA% (%USERPROFILE%\AppData\Roaming)
!define CSIDL_LOCALAPPDATA '0x1C' ;Local Application Data path !define FOLDERID_LocalAppData {F1B32785-6FBA-4FCF-9D55-7B8E7F157091} ; %LOCALAPPDATA% (%USERPROFILE%\AppData\Local)
!define MUI_FINISHPAGE_RUN !define MUI_FINISHPAGE_RUN
!define MUI_FINISHPAGE_RUN_FUNCTION PageFinishRun !define MUI_FINISHPAGE_RUN_FUNCTION PageFinishRun
@@ -156,16 +157,20 @@ ${Case} 0
${IfThen} $3 <> 0 ${|} ${Break} ${|} ;we are admin, let the show go on ${IfThen} $3 <> 0 ${|} ${Break} ${|} ;we are admin, let the show go on
${If} $1 = 3 ;RunAs completed successfully, but with a non-admin user ${If} $1 = 3 ;RunAs completed successfully, but with a non-admin user
MessageBox mb_YesNo|mb_IconExclamation|mb_TopMost|mb_SetForeground "This ${thing} requires admin privileges, try again" /SD IDNO IDYES uac_tryagain IDNO 0 MessageBox mb_YesNo|mb_IconExclamation|mb_TopMost|mb_SetForeground "This ${thing} requires admin privileges, try again" /SD IDNO IDYES uac_tryagain IDNO 0
SetErrorLevel 1314 # WinError.h: `ERROR_PRIVILEGE_NOT_HELD`
${EndIf} ${EndIf}
;fall-through and die ;fall-through and die
${Case} 1223 ${Case} 1223
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "This ${thing} requires admin privileges, aborting!" MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "This ${thing} requires admin privileges, aborting!"
SetErrorLevel 1223 # WinError.h: `ERROR_CANCELLED`
Quit Quit
${Case} 1062 ${Case} 1062
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Logon service not running, aborting!" MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Logon service not running, aborting!"
SetErrorLevel 1062 # WinError.h: `ERROR_SERVICE_NOT_ACTIVE`
Quit Quit
${Default} ${Default}
MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate , error $0" MessageBox mb_IconStop|mb_TopMost|mb_SetForeground "Unable to elevate , error $0"
SetErrorLevel 1603 # WinError.h: `ERROR_INSTALL_FAILURE`
Quit Quit
${EndSwitch} ${EndSwitch}

View File

@@ -1,37 +1,37 @@
;Installer strings ;Installer strings
;LangString inst_qbt_req ${LANG_ENGLISH} "qBittorrent (required)" ;LangString inst_qbt_req ${LANG_ENGLISH} "qBittorrent (required)"
LangString inst_qbt_req ${LANG_KURDISH} "qBittorrent (required)" LangString inst_qbt_req ${LANG_KURDISH} "کیووبیتتۆرێنت (پێویستە)"
;LangString inst_desktop ${LANG_ENGLISH} "Create Desktop Shortcut" ;LangString inst_desktop ${LANG_ENGLISH} "Create Desktop Shortcut"
LangString inst_desktop ${LANG_KURDISH} "Create Desktop Shortcut" LangString inst_desktop ${LANG_KURDISH} "قەدبڕێک دروست بکە لەسەر دێسکتۆپ"
;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut" ;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut"
LangString inst_startmenu ${LANG_KURDISH} "Create Start Menu Shortcut" LangString inst_startmenu ${LANG_KURDISH} "قەدبڕێک دروست بکە لەسەر پێڕستی دەتپێک"
;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up" ;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up"
LangString inst_startup ${LANG_KURDISH} "Start qBittorrent on Windows start up" LangString inst_startup ${LANG_KURDISH} "لەگەڵ هەڵبوونی کۆمپیوتەرەکە کیووبیتتۆرێنت بکەرەوە"
;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent" ;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent"
LangString inst_torrent ${LANG_KURDISH} "Open .torrent files with qBittorrent" LangString inst_torrent ${LANG_KURDISH} "پەڕگەکانی .torrent بە کیووبیتتۆرێنت بکەرەوە"
;LangString inst_magnet ${LANG_ENGLISH} "Open magnet links with qBittorrent" ;LangString inst_magnet ${LANG_ENGLISH} "Open magnet links with qBittorrent"
LangString inst_magnet ${LANG_KURDISH} "Open magnet links with qBittorrent" LangString inst_magnet ${LANG_KURDISH} "بەسەرە موگناتیسییەکان بە کیووبیتتۆرێنت بکەرەوە"
;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule" ;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule"
LangString inst_firewall ${LANG_KURDISH} "Add Windows Firewall rule" LangString inst_firewall ${LANG_KURDISH} "یاسای فایەروۆڵی ویندۆز زیاد بکە"
;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)" ;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)"
LangString inst_pathlimit ${LANG_KURDISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)" LangString inst_pathlimit ${LANG_KURDISH} "سنووری درێژیی ڕێڕەوی ویندۆز ناچالاک بکە (سنووری ٢٦٠ پیتی، پێویستی بە ویندۆزی ١٠ و دواتر هەیە)"
;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule" ;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule"
LangString inst_firewallinfo ${LANG_KURDISH} "Adding Windows Firewall rule" LangString inst_firewallinfo ${LANG_KURDISH} "یاسای فایەروۆڵی ویندۆز زیاد دەبێت"
;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing." ;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing."
LangString inst_warning ${LANG_KURDISH} "qBittorrent is running. Please close the application before installing." LangString inst_warning ${LANG_KURDISH} "کیووبیتتۆرێنت کارایە. تکایە بەرنامەکە دابخەرەوە پێش دامەزراندن."
;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact." ;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact."
LangString inst_uninstall_question ${LANG_KURDISH} "Current version will be uninstalled. User settings and torrents will remain intact." LangString inst_uninstall_question ${LANG_KURDISH} "ئەم وەشانەی ئێستا دەسڕدرێتەوە. ڕێکخستنەکانی بەکارهێنەر و تۆرێنتەکان وەک خۆیان دەمێننەوە."
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version." ;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
LangString inst_unist ${LANG_KURDISH} "Uninstalling previous version." LangString inst_unist ${LANG_KURDISH} "وەشانی پێشوو دەسڕدرێتەوە."
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent." ;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
LangString launch_qbt ${LANG_KURDISH} "Launch qBittorrent." LangString launch_qbt ${LANG_KURDISH} "کیووبیتتۆرێنت کارا بکە."
;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions." ;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions."
LangString inst_requires_64bit ${LANG_KURDISH} "This installer works only in 64-bit Windows versions." LangString inst_requires_64bit ${LANG_KURDISH} "ئەم دامەزرێنەرە تەنیا لەسەر وەشانی ٦٤ بیتی ویندۆز کار دەکات."
;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019." ;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
LangString inst_requires_win10 ${LANG_KURDISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019." LangString inst_requires_win10 ${LANG_KURDISH} "ئەم دامەزرێنەرە لانیکەم پێویستی بە ویندۆزی ١٠ یان ویندۆز سێرڤەری ٢٠١٩ هەیە."
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent" ;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
LangString inst_uninstall_link_description ${LANG_KURDISH} "Uninstall qBittorrent" LangString inst_uninstall_link_description ${LANG_KURDISH} "کیووبیتتۆرێنت بسڕەوە"
;LangString inst_arch_mismatch_x64_on_arm64 ${LANG_ENGLISH} "Error: This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer." ;LangString inst_arch_mismatch_x64_on_arm64 ${LANG_ENGLISH} "Error: This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer."
LangString inst_arch_mismatch_x64_on_arm64 ${LANG_KURDISH} "This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer." LangString inst_arch_mismatch_x64_on_arm64 ${LANG_KURDISH} "This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer."
;LangString inst_arch_mismatch_arm64_on_x64 ${LANG_ENGLISH} "Error: This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer." ;LangString inst_arch_mismatch_arm64_on_x64 ${LANG_ENGLISH} "Error: This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer."
@@ -41,24 +41,24 @@ LangString inst_arch_mismatch_arm64_on_x64 ${LANG_KURDISH} "This ARM64 version o
;Uninstaller strings ;Uninstaller strings
;LangString remove_files ${LANG_ENGLISH} "Remove files" ;LangString remove_files ${LANG_ENGLISH} "Remove files"
LangString remove_files ${LANG_KURDISH} "Remove files" LangString remove_files ${LANG_KURDISH} "پەڕگەکان بسڕەوە"
;LangString remove_shortcuts ${LANG_ENGLISH} "Remove shortcuts" ;LangString remove_shortcuts ${LANG_ENGLISH} "Remove shortcuts"
LangString remove_shortcuts ${LANG_KURDISH} "Remove shortcuts" LangString remove_shortcuts ${LANG_KURDISH} "قەدبڕەکان بسڕەوە"
;LangString remove_associations ${LANG_ENGLISH} "Remove file associations" ;LangString remove_associations ${LANG_ENGLISH} "Remove file associations"
LangString remove_associations ${LANG_KURDISH} "Remove file associations" LangString remove_associations ${LANG_KURDISH} "پەیوەندیی پەڕگەکان بسڕەوە"
;LangString remove_registry ${LANG_ENGLISH} "Remove registry keys" ;LangString remove_registry ${LANG_ENGLISH} "Remove registry keys"
LangString remove_registry ${LANG_KURDISH} "Remove registry keys" LangString remove_registry ${LANG_KURDISH} "کلیلەکانی تۆمار بسڕەوە"
;LangString remove_conf ${LANG_ENGLISH} "Remove configuration files" ;LangString remove_conf ${LANG_ENGLISH} "Remove configuration files"
LangString remove_conf ${LANG_KURDISH} "Remove configuration files" LangString remove_conf ${LANG_KURDISH} "پەڕگەکانی شێوەپێدان بسڕەوە"
;LangString remove_firewall ${LANG_ENGLISH} "Remove Windows Firewall rule" ;LangString remove_firewall ${LANG_ENGLISH} "Remove Windows Firewall rule"
LangString remove_firewall ${LANG_KURDISH} "Remove Windows Firewall rule" LangString remove_firewall ${LANG_KURDISH} "یاسای فایەروۆڵی ویندۆز بسڕەوە"
;LangString remove_firewallinfo ${LANG_ENGLISH} "Removing Windows Firewall rule" ;LangString remove_firewallinfo ${LANG_ENGLISH} "Removing Windows Firewall rule"
LangString remove_firewallinfo ${LANG_KURDISH} "Removing Windows Firewall rule" LangString remove_firewallinfo ${LANG_KURDISH} "یاسای فایەروۆڵی ویندۆز دەسڕێتەوە"
;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data" ;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data"
LangString remove_cache ${LANG_KURDISH} "Remove torrents and cached data" LangString remove_cache ${LANG_KURDISH} "تۆرێنت و داتای کاشکراو بسڕەوە"
;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling." ;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling."
LangString uninst_warning ${LANG_KURDISH} "qBittorrent is running. Please close the application before uninstalling." LangString uninst_warning ${LANG_KURDISH} "کیووبیتتۆرێنت کارایە. تکایە بەرنامەکە دابخە پێش سڕینەوەی."
;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:" ;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:"
LangString uninst_tor_warn ${LANG_KURDISH} "Not removing .torrent association. It is associated with:" LangString uninst_tor_warn ${LANG_KURDISH} "پەیوەندیی .torrent ناسڕێتەوە. پەیوەندیی هەیە بە:"
;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:" ;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:"
LangString uninst_mag_warn ${LANG_KURDISH} "Not removing magnet association. It is associated with:" LangString uninst_mag_warn ${LANG_KURDISH} "پەیوەندیی موگناتیسی ناسڕێتەوە. پەیوەیندیی هەیە بە:"

View File

@@ -33,9 +33,9 @@ LangString inst_requires_win10 ${LANG_RUSSIAN} "Для работы этого
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent" ;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
LangString inst_uninstall_link_description ${LANG_RUSSIAN} "Удалить qBittorrent" LangString inst_uninstall_link_description ${LANG_RUSSIAN} "Удалить qBittorrent"
;LangString inst_arch_mismatch_x64_on_arm64 ${LANG_ENGLISH} "Error: This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer." ;LangString inst_arch_mismatch_x64_on_arm64 ${LANG_ENGLISH} "Error: This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer."
LangString inst_arch_mismatch_x64_on_arm64 ${LANG_RUSSIAN} "This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer." LangString inst_arch_mismatch_x64_on_arm64 ${LANG_RUSSIAN} "Эта версия qBittorrent для x64 не может работать на системах ARM64. Пожалуйста, скачайте установщик для ARM64."
;LangString inst_arch_mismatch_arm64_on_x64 ${LANG_ENGLISH} "Error: This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer." ;LangString inst_arch_mismatch_arm64_on_x64 ${LANG_ENGLISH} "Error: This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer."
LangString inst_arch_mismatch_arm64_on_x64 ${LANG_RUSSIAN} "This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer." LangString inst_arch_mismatch_arm64_on_x64 ${LANG_RUSSIAN} "Эта версия qBittorrent для ARM64 не может работать на системах x64. Пожалуйста, скачайте установщик для x64."
;------------------------------------ ;------------------------------------
;Uninstaller strings ;Uninstaller strings

View File

@@ -3,9 +3,9 @@
;LangString inst_qbt_req ${LANG_ENGLISH} "qBittorrent (required)" ;LangString inst_qbt_req ${LANG_ENGLISH} "qBittorrent (required)"
LangString inst_qbt_req ${LANG_TURKISH} "qBittorrent (zorunlu)" LangString inst_qbt_req ${LANG_TURKISH} "qBittorrent (zorunlu)"
;LangString inst_desktop ${LANG_ENGLISH} "Create Desktop Shortcut" ;LangString inst_desktop ${LANG_ENGLISH} "Create Desktop Shortcut"
LangString inst_desktop ${LANG_TURKISH} "Masaüstü Kısayolu oluştur" LangString inst_desktop ${LANG_TURKISH} "Masaüstü Kısayolu Oluştur"
;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut" ;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut"
LangString inst_startmenu ${LANG_TURKISH} "Başlangıç Menüsü Kısayolu oluştur" LangString inst_startmenu ${LANG_TURKISH} "Başlangıç Menüsü Kısayolu Oluştur"
;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up" ;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up"
LangString inst_startup ${LANG_TURKISH} "Windows başlangıcında qBittorrent'i başlat" LangString inst_startup ${LANG_TURKISH} "Windows başlangıcında qBittorrent'i başlat"
;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent" ;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent"
@@ -15,27 +15,27 @@ LangString inst_magnet ${LANG_TURKISH} "Magnet bağlantılarını qBittorrent il
;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule" ;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule"
LangString inst_firewall ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralı ekle" LangString inst_firewall ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralı ekle"
;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)" ;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)"
LangString inst_pathlimit ${LANG_TURKISH} "Windows yol uzunluğu sınırını etkisizleştir (260 karakterlik MAX_PATH sınırlaması, Windows 10 1607 veya sonrasını gerektirir)" LangString inst_pathlimit ${LANG_TURKISH} "Windows yol uzunluğu sınırını devre dışı bırak (260 karakterlik MAX_PATH sınırlaması, Windows 10 1607 veya üzerini gerektirir)"
;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule" ;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule"
LangString inst_firewallinfo ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralı ekleniyor" LangString inst_firewallinfo ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralı ekleniyor"
;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing." ;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing."
LangString inst_warning ${LANG_TURKISH} "qBittorrent çalışıyor. Lütfen yüklemeden önce uygulamayı kapatın." LangString inst_warning ${LANG_TURKISH} "qBittorrent çalışıyor. Lütfen yüklemeden önce uygulamayı kapatın."
;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact." ;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact."
LangString inst_uninstall_question ${LANG_TURKISH} "Şu anki sürüm kaldırılacaktır. Kullanıcı ayarları ve torrent'ler bozulmadan kalacaktır." LangString inst_uninstall_question ${LANG_TURKISH} "Mevcut sürüm kaldırılacaktır. Kullanıcı ayarları ve torrentler olduğu gibi kalacaktır."
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version." ;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
LangString inst_unist ${LANG_TURKISH} "Önceki sürüm kaldırılıyor." LangString inst_unist ${LANG_TURKISH} "Önceki sürüm kaldırılıyor."
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent." ;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
LangString launch_qbt ${LANG_TURKISH} "qBittorrent'i başlat." LangString launch_qbt ${LANG_TURKISH} "qBittorrent'i başlat."
;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions." ;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions."
LangString inst_requires_64bit ${LANG_TURKISH} "Bu yükleyici sadece 64-bit Windows sürümlerinde çalışır." LangString inst_requires_64bit ${LANG_TURKISH} "Bu yükleyici yalnızca 64-bit Windows sürümlerinde çalışır."
;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019." ;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
LangString inst_requires_win10 ${LANG_TURKISH} "Bu yükleyici en az Windows 10 (1809) / Windows Server 2019 gerektirir." LangString inst_requires_win10 ${LANG_TURKISH} "Bu yükleyici en az Windows 10 (1809) / Windows Server 2019 gerektirir."
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent" ;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
LangString inst_uninstall_link_description ${LANG_TURKISH} "qBittorrent'i kaldır" LangString inst_uninstall_link_description ${LANG_TURKISH} "qBittorrent'i kaldır"
;LangString inst_arch_mismatch_x64_on_arm64 ${LANG_ENGLISH} "Error: This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer." ;LangString inst_arch_mismatch_x64_on_arm64 ${LANG_ENGLISH} "Error: This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer."
LangString inst_arch_mismatch_x64_on_arm64 ${LANG_TURKISH} "This x64 version of qBittorrent cannot run on ARM64 systems. Please download the ARM64 installer." LangString inst_arch_mismatch_x64_on_arm64 ${LANG_TURKISH} "Hata: qBittorrent'in bu x64 sürümü ARM64 sistemlerde çalışamaz. Lütfen ARM64 yükleyicisini indirin."
;LangString inst_arch_mismatch_arm64_on_x64 ${LANG_ENGLISH} "Error: This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer." ;LangString inst_arch_mismatch_arm64_on_x64 ${LANG_ENGLISH} "Error: This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer."
LangString inst_arch_mismatch_arm64_on_x64 ${LANG_TURKISH} "This ARM64 version of qBittorrent cannot run on x64 systems. Please download the x64 installer." LangString inst_arch_mismatch_arm64_on_x64 ${LANG_TURKISH} "Hata: qBittorrent'in bu ARM64 sürümü x64 sistemlerde çalışamaz. Lütfen x64 yükleyiciyi indirin."
;------------------------------------ ;------------------------------------
;Uninstaller strings ;Uninstaller strings
@@ -55,10 +55,10 @@ LangString remove_firewall ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralını
;LangString remove_firewallinfo ${LANG_ENGLISH} "Removing Windows Firewall rule" ;LangString remove_firewallinfo ${LANG_ENGLISH} "Removing Windows Firewall rule"
LangString remove_firewallinfo ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralı kaldırılıyor" LangString remove_firewallinfo ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralı kaldırılıyor"
;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data" ;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data"
LangString remove_cache ${LANG_TURKISH} "Torrent'leri ve önbelleklenen verileri kaldır" LangString remove_cache ${LANG_TURKISH} "Torrentleri ve önbelleğe alınmış verileri kaldır"
;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling." ;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling."
LangString uninst_warning ${LANG_TURKISH} "qBittorrent çalışıyor. Lütfen kaldırmadan önce uygulamayı kapatın." LangString uninst_warning ${LANG_TURKISH} "qBittorrent çalışıyor. Lütfen kaldırmadan önce uygulamayı kapatın."
;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:" ;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:"
LangString uninst_tor_warn ${LANG_TURKISH} ".torrent ilişkilendirmesi kaldırılamıyor. Şununla ilişkilendirildi:" LangString uninst_tor_warn ${LANG_TURKISH} ".torrent ilişkilendirmesi kaldırılmıyor. Şunlarla ilişkilendirilmiş durumda:"
;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:" ;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:"
LangString uninst_mag_warn ${LANG_TURKISH} "Magnet ilişkilendirmesi kaldırılamıyor. Şununla ilişkilendirildi:" LangString uninst_mag_warn ${LANG_TURKISH} "Magnet ilişkilendirmesi kaldırılmıyor. Şunlarla ilişkilendirilmiş durumda:"

View File

@@ -51,7 +51,7 @@ Section $(inst_qbt_req) ;"qBittorrent (required)"
WriteRegStr HKLM "Software\Classes\magnet" "Content Type" "application/x-magnet" WriteRegStr HKLM "Software\Classes\magnet" "Content Type" "application/x-magnet"
WriteRegStr HKLM "Software\Classes\magnet" "URL Protocol" "" WriteRegStr HKLM "Software\Classes\magnet" "URL Protocol" ""
System::Call 'Shell32::SHChangeNotify(i ${SHCNE_ASSOCCHANGED}, i ${SHCNF_IDLIST}, p 0, p 0)' System::Call 'shell32::SHChangeNotify(i ${SHCNE_ASSOCCHANGED}, i ${SHCNF_IDLIST}, p 0, p 0)'
; Write the uninstall keys for Windows ; Write the uninstall keys for Windows
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\qBittorrent" "DisplayName" "qBittorrent" WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\qBittorrent" "DisplayName" "qBittorrent"

View File

@@ -27,7 +27,7 @@ Section "un.$(remove_registry)" ;"un.Remove registry keys"
; Remove ProgIDs ; Remove ProgIDs
DeleteRegKey HKLM "Software\Classes\qBittorrent.File.Torrent" DeleteRegKey HKLM "Software\Classes\qBittorrent.File.Torrent"
DeleteRegKey HKLM "Software\Classes\qBittorrent.Url.Magnet" DeleteRegKey HKLM "Software\Classes\qBittorrent.Url.Magnet"
System::Call 'Shell32::SHChangeNotify(i ${SHCNE_ASSOCCHANGED}, i ${SHCNF_IDLIST}, p 0, p 0)' System::Call 'shell32::SHChangeNotify(i ${SHCNE_ASSOCCHANGED}, i ${SHCNF_IDLIST}, p 0, p 0)'
SectionEnd SectionEnd
Section "un.$(remove_firewall)" ; Section "un.$(remove_firewall)" ;
@@ -45,8 +45,9 @@ SectionEnd
Function un.remove_conf_user Function un.remove_conf_user
System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_APPDATA}, i0)i.r0' System::Call 'shell32::SHGetKnownFolderPath(g "${FOLDERID_RoamingAppData}", i 0, p 0, *w . r1) i . r0'
RMDir /r "$1\qBittorrent" RMDir /r "$1\qBittorrent"
System::Call 'ole32::CoTaskMemFree(p r1)'
FunctionEnd FunctionEnd
@@ -58,8 +59,9 @@ SectionEnd
Function un.remove_cache_user Function un.remove_cache_user
System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_LOCALAPPDATA}, i0)i.r0' System::Call 'shell32::SHGetKnownFolderPath(g "${FOLDERID_LocalAppData}", i 0, p 0, *w . r1) i . r0'
RMDir /r "$1\qBittorrent\" RMDir /r "$1\qBittorrent"
System::Call 'ole32::CoTaskMemFree(p r1)'
FunctionEnd FunctionEnd

View File

@@ -776,8 +776,9 @@ void Application::allTorrentsFinished()
bool isShutdown = pref->shutdownWhenDownloadsComplete(); bool isShutdown = pref->shutdownWhenDownloadsComplete();
bool isSuspend = pref->suspendWhenDownloadsComplete(); bool isSuspend = pref->suspendWhenDownloadsComplete();
bool isHibernate = pref->hibernateWhenDownloadsComplete(); bool isHibernate = pref->hibernateWhenDownloadsComplete();
bool isReboot = pref->rebootWhenDownloadsComplete();
bool haveAction = isExit || isShutdown || isSuspend || isHibernate; const bool haveAction = isExit || isShutdown || isSuspend || isHibernate || isReboot;
if (!haveAction) return; if (!haveAction) return;
ShutdownDialogAction action = ShutdownDialogAction::Exit; ShutdownDialogAction action = ShutdownDialogAction::Exit;
@@ -787,6 +788,8 @@ void Application::allTorrentsFinished()
action = ShutdownDialogAction::Hibernate; action = ShutdownDialogAction::Hibernate;
else if (isShutdown) else if (isShutdown)
action = ShutdownDialogAction::Shutdown; action = ShutdownDialogAction::Shutdown;
else if (isReboot)
action = ShutdownDialogAction::Reboot;
#ifndef DISABLE_GUI #ifndef DISABLE_GUI
// ask confirm // ask confirm
@@ -808,6 +811,7 @@ void Application::allTorrentsFinished()
pref->setShutdownWhenDownloadsComplete(false); pref->setShutdownWhenDownloadsComplete(false);
pref->setSuspendWhenDownloadsComplete(false); pref->setSuspendWhenDownloadsComplete(false);
pref->setHibernateWhenDownloadsComplete(false); pref->setHibernateWhenDownloadsComplete(false);
pref->setRebootWhenDownloadsComplete(false);
// Make sure preferences are synced before exiting // Make sure preferences are synced before exiting
m_shutdownAct = action; m_shutdownAct = action;
} }

View File

@@ -127,10 +127,12 @@ namespace
#endif #endif
} }
#if !defined(Q_OS_WIN) || defined(DISABLE_GUI)
void displayVersion() void displayVersion()
{ {
printf("%s %s\n", qUtf8Printable(qApp->applicationName()), QBT_VERSION); printf("%s %s\n", qUtf8Printable(qApp->applicationName()), QBT_VERSION);
} }
#endif
#ifndef DISABLE_GUI #ifndef DISABLE_GUI
void showSplashScreen() void showSplashScreen()

View File

@@ -32,7 +32,7 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QMetaEnum> #include <QMetaEnum>
#include "base/bittorrent/sharelimitaction.h" #include "base/bittorrent/sharelimits.h"
#include "base/bittorrent/torrentcontentlayout.h" #include "base/bittorrent/torrentcontentlayout.h"
#include "base/global.h" #include "base/global.h"
#include "base/logger.h" #include "base/logger.h"

File diff suppressed because it is too large Load Diff

View File

@@ -34,10 +34,11 @@ add_library(qbt_base STATIC
bittorrent/session.h bittorrent/session.h
bittorrent/sessionimpl.h bittorrent/sessionimpl.h
bittorrent/sessionstatus.h bittorrent/sessionstatus.h
bittorrent/sharelimitaction.h bittorrent/sharelimits.h
bittorrent/speedmonitor.h bittorrent/speedmonitor.h
bittorrent/sslparameters.h bittorrent/sslparameters.h
bittorrent/torrent.h bittorrent/torrent.h
bittorrent/torrentannouncestatus.h
bittorrent/torrentcontenthandler.h bittorrent/torrentcontenthandler.h
bittorrent/torrentcontentlayout.h bittorrent/torrentcontentlayout.h
bittorrent/torrentcontentremoveoption.h bittorrent/torrentcontentremoveoption.h
@@ -247,7 +248,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
${IOKit_LIBRARY} ${IOKit_LIBRARY}
) )
elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows") elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_link_libraries(qbt_base PRIVATE Iphlpapi PowrProf) target_link_libraries(qbt_base PRIVATE iphlpapi powrprof)
endif() endif()
if (NOT GUI) if (NOT GUI)

View File

@@ -125,9 +125,9 @@ BitTorrent::AddTorrentParams BitTorrent::parseAddTorrentParams(const QJsonObject
.useAutoTMM = getOptionalBool(jsonObj, PARAM_AUTOTMM), .useAutoTMM = getOptionalBool(jsonObj, PARAM_AUTOTMM),
.uploadLimit = jsonObj.value(PARAM_UPLOADLIMIT).toInt(-1), .uploadLimit = jsonObj.value(PARAM_UPLOADLIMIT).toInt(-1),
.downloadLimit = jsonObj.value(PARAM_DOWNLOADLIMIT).toInt(-1), .downloadLimit = jsonObj.value(PARAM_DOWNLOADLIMIT).toInt(-1),
.seedingTimeLimit = jsonObj.value(PARAM_SEEDINGTIMELIMIT).toInt(Torrent::USE_GLOBAL_SEEDING_TIME), .seedingTimeLimit = jsonObj.value(PARAM_SEEDINGTIMELIMIT).toInt(DEFAULT_SEEDING_TIME_LIMIT),
.inactiveSeedingTimeLimit = jsonObj.value(PARAM_INACTIVESEEDINGTIMELIMIT).toInt(Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME), .inactiveSeedingTimeLimit = jsonObj.value(PARAM_INACTIVESEEDINGTIMELIMIT).toInt(DEFAULT_SEEDING_TIME_LIMIT),
.ratioLimit = jsonObj.value(PARAM_RATIOLIMIT).toDouble(Torrent::USE_GLOBAL_RATIO), .ratioLimit = jsonObj.value(PARAM_RATIOLIMIT).toDouble(DEFAULT_RATIO_LIMIT),
.shareLimitAction = getEnum<ShareLimitAction>(jsonObj, PARAM_SHARELIMITACTION, ShareLimitAction::Default), .shareLimitAction = getEnum<ShareLimitAction>(jsonObj, PARAM_SHARELIMITACTION, ShareLimitAction::Default),
.sslParameters = .sslParameters =
{ {

View File

@@ -36,7 +36,7 @@
#include "base/path.h" #include "base/path.h"
#include "base/tagset.h" #include "base/tagset.h"
#include "sharelimitaction.h" #include "sharelimits.h"
#include "sslparameters.h" #include "sslparameters.h"
#include "torrent.h" #include "torrent.h"
#include "torrentcontentlayout.h" #include "torrentcontentlayout.h"
@@ -68,9 +68,9 @@ namespace BitTorrent
std::optional<bool> useAutoTMM; std::optional<bool> useAutoTMM;
int uploadLimit = -1; int uploadLimit = -1;
int downloadLimit = -1; int downloadLimit = -1;
int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME; int seedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
int inactiveSeedingTimeLimit = Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME; int inactiveSeedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
qreal ratioLimit = Torrent::USE_GLOBAL_RATIO; qreal ratioLimit = DEFAULT_RATIO_LIMIT;
ShareLimitAction shareLimitAction = ShareLimitAction::Default; ShareLimitAction shareLimitAction = ShareLimitAction::Default;
SSLParameters sslParameters; SSLParameters sslParameters;

View File

@@ -239,8 +239,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
torrentParams.comment = fromLTString(resumeDataRoot.dict_find_string_value("qBt-comment")); torrentParams.comment = fromLTString(resumeDataRoot.dict_find_string_value("qBt-comment"));
torrentParams.hasFinishedStatus = resumeDataRoot.dict_find_int_value("qBt-seedStatus"); torrentParams.hasFinishedStatus = resumeDataRoot.dict_find_int_value("qBt-seedStatus");
torrentParams.firstLastPiecePriority = resumeDataRoot.dict_find_int_value("qBt-firstLastPiecePriority"); torrentParams.firstLastPiecePriority = resumeDataRoot.dict_find_int_value("qBt-firstLastPiecePriority");
torrentParams.seedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME); torrentParams.seedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-seedingTimeLimit", DEFAULT_SEEDING_TIME_LIMIT);
torrentParams.inactiveSeedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-inactiveSeedingTimeLimit", Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME); torrentParams.inactiveSeedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-inactiveSeedingTimeLimit", DEFAULT_SEEDING_TIME_LIMIT);
torrentParams.shareLimitAction = Utils::String::toEnum( torrentParams.shareLimitAction = Utils::String::toEnum(
fromLTString(resumeDataRoot.dict_find_string_value("qBt-shareLimitAction")), ShareLimitAction::Default); fromLTString(resumeDataRoot.dict_find_string_value("qBt-shareLimitAction")), ShareLimitAction::Default);
@@ -283,7 +283,7 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
const lt::string_view ratioLimitString = resumeDataRoot.dict_find_string_value("qBt-ratioLimit"); const lt::string_view ratioLimitString = resumeDataRoot.dict_find_string_value("qBt-ratioLimit");
if (ratioLimitString.empty()) if (ratioLimitString.empty())
torrentParams.ratioLimit = resumeDataRoot.dict_find_int_value("qBt-ratioLimit", Torrent::USE_GLOBAL_RATIO * 1000) / 1000.0; torrentParams.ratioLimit = resumeDataRoot.dict_find_int_value("qBt-ratioLimit", DEFAULT_RATIO_LIMIT * 1000) / 1000.0;
else else
torrentParams.ratioLimit = fromLTString(ratioLimitString).toDouble(); torrentParams.ratioLimit = fromLTString(ratioLimitString).toDouble();

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2021-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2021-2025 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
@@ -32,10 +32,16 @@
#include <QJsonValue> #include <QJsonValue>
#include "base/global.h" #include "base/global.h"
#include "base/utils/string.h"
const QString OPTION_SAVEPATH = u"save_path"_s; const QString OPTION_SAVEPATH = u"save_path"_s;
const QString OPTION_DOWNLOADPATH = u"download_path"_s; const QString OPTION_DOWNLOADPATH = u"download_path"_s;
const QString OPTION_RATIOLIMIT = u"ratio_limit"_s;
const QString OPTION_SEEDINGTIMELIMIT = u"seeding_time_limit"_s;
const QString OPTION_INACTIVESEEDINGTIMELIMIT = u"inactive_seeding_time_limit"_s;
const QString OPTION_SHARELIMITACTION = u"share_limit_action"_s;
BitTorrent::CategoryOptions BitTorrent::CategoryOptions::fromJSON(const QJsonObject &jsonObj) BitTorrent::CategoryOptions BitTorrent::CategoryOptions::fromJSON(const QJsonObject &jsonObj)
{ {
CategoryOptions options; CategoryOptions options;
@@ -47,6 +53,11 @@ BitTorrent::CategoryOptions BitTorrent::CategoryOptions::fromJSON(const QJsonObj
else if (downloadPathValue.isString()) else if (downloadPathValue.isString())
options.downloadPath = {true, Path(downloadPathValue.toString())}; options.downloadPath = {true, Path(downloadPathValue.toString())};
options.ratioLimit = jsonObj.value(OPTION_RATIOLIMIT).toDouble(DEFAULT_RATIO_LIMIT);
options.seedingTimeLimit = jsonObj.value(OPTION_SEEDINGTIMELIMIT).toInt(DEFAULT_SEEDING_TIME_LIMIT);
options.inactiveSeedingTimeLimit = jsonObj.value(OPTION_INACTIVESEEDINGTIMELIMIT).toInt(DEFAULT_SEEDING_TIME_LIMIT);
options.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(jsonObj.value(OPTION_SHARELIMITACTION).toString(), ShareLimitAction::Default);
return options; return options;
} }
@@ -63,12 +74,10 @@ QJsonObject BitTorrent::CategoryOptions::toJSON() const
return { return {
{OPTION_SAVEPATH, savePath.data()}, {OPTION_SAVEPATH, savePath.data()},
{OPTION_DOWNLOADPATH, downloadPathValue} {OPTION_DOWNLOADPATH, downloadPathValue},
{OPTION_RATIOLIMIT, ratioLimit},
{OPTION_SEEDINGTIMELIMIT, seedingTimeLimit},
{OPTION_INACTIVESEEDINGTIMELIMIT, inactiveSeedingTimeLimit},
{OPTION_SHARELIMITACTION, Utils::String::fromEnum(shareLimitAction)}
}; };
} }
bool BitTorrent::operator==(const BitTorrent::CategoryOptions &left, const BitTorrent::CategoryOptions &right)
{
return ((left.savePath == right.savePath)
&& (left.downloadPath == right.downloadPath));
}

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2021-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2021-2025 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
@@ -34,6 +34,7 @@
#include "base/path.h" #include "base/path.h"
#include "downloadpathoption.h" #include "downloadpathoption.h"
#include "sharelimits.h"
class QJsonObject; class QJsonObject;
@@ -44,9 +45,14 @@ namespace BitTorrent
Path savePath; Path savePath;
std::optional<DownloadPathOption> downloadPath; std::optional<DownloadPathOption> downloadPath;
qreal ratioLimit = DEFAULT_RATIO_LIMIT;
int seedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
int inactiveSeedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
ShareLimitAction shareLimitAction = ShareLimitAction::Default;
static CategoryOptions fromJSON(const QJsonObject &jsonObj); static CategoryOptions fromJSON(const QJsonObject &jsonObj);
QJsonObject toJSON() const; QJsonObject toJSON() const;
};
bool operator==(const CategoryOptions &left, const CategoryOptions &right); friend bool operator==(const CategoryOptions &, const CategoryOptions &) = default;
};
} }

View File

@@ -34,7 +34,7 @@
#include "base/path.h" #include "base/path.h"
#include "base/tagset.h" #include "base/tagset.h"
#include "sharelimitaction.h" #include "sharelimits.h"
#include "sslparameters.h" #include "sslparameters.h"
#include "torrent.h" #include "torrent.h"
#include "torrentcontentlayout.h" #include "torrentcontentlayout.h"
@@ -61,9 +61,9 @@ namespace BitTorrent
bool addToQueueTop = false; // only for new torrents bool addToQueueTop = false; // only for new torrents
qreal ratioLimit = Torrent::USE_GLOBAL_RATIO; qreal ratioLimit = DEFAULT_RATIO_LIMIT;
int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME; int seedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
int inactiveSeedingTimeLimit = Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME; int inactiveSeedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
ShareLimitAction shareLimitAction = ShareLimitAction::Default; ShareLimitAction shareLimitAction = ShareLimitAction::Default;
SSLParameters sslParameters; SSLParameters sslParameters;

View File

@@ -37,7 +37,7 @@
#include "addtorrenterror.h" #include "addtorrenterror.h"
#include "addtorrentparams.h" #include "addtorrentparams.h"
#include "categoryoptions.h" #include "categoryoptions.h"
#include "sharelimitaction.h" #include "sharelimits.h"
#include "torrentcontentremoveoption.h" #include "torrentcontentremoveoption.h"
#include "trackerentry.h" #include "trackerentry.h"
#include "trackerentrystatus.h" #include "trackerentrystatus.h"
@@ -158,15 +158,17 @@ namespace BitTorrent
virtual QStringList categories() const = 0; virtual QStringList categories() const = 0;
virtual CategoryOptions categoryOptions(const QString &categoryName) const = 0; virtual CategoryOptions categoryOptions(const QString &categoryName) const = 0;
virtual bool setCategoryOptions(const QString &categoryName, const CategoryOptions &options) = 0;
virtual Path categorySavePath(const QString &categoryName) const = 0; virtual Path categorySavePath(const QString &categoryName) const = 0;
virtual Path categorySavePath(const QString &categoryName, const CategoryOptions &options) const = 0; virtual Path categorySavePath(const QString &categoryName, const CategoryOptions &options) const = 0;
virtual Path categoryDownloadPath(const QString &categoryName) const = 0; virtual Path categoryDownloadPath(const QString &categoryName) const = 0;
virtual Path categoryDownloadPath(const QString &categoryName, const CategoryOptions &options) const = 0; virtual Path categoryDownloadPath(const QString &categoryName, const CategoryOptions &options) const = 0;
virtual qreal categoryRatioLimit(const QString &categoryName) const = 0;
virtual int categorySeedingTimeLimit(const QString &categoryName) const = 0;
virtual int categoryInactiveSeedingTimeLimit(const QString &categoryName) const = 0;
virtual ShareLimitAction categoryShareLimitAction(const QString &categoryName) const = 0;
virtual bool addCategory(const QString &name, const CategoryOptions &options = {}) = 0; virtual bool addCategory(const QString &name, const CategoryOptions &options = {}) = 0;
virtual bool editCategory(const QString &name, const CategoryOptions &options) = 0;
virtual bool removeCategory(const QString &name) = 0; virtual bool removeCategory(const QString &name) = 0;
virtual bool isSubcategoriesEnabled() const = 0;
virtual void setSubcategoriesEnabled(bool value) = 0;
virtual bool useCategoryPathsInManualMode() const = 0; virtual bool useCategoryPathsInManualMode() const = 0;
virtual void setUseCategoryPathsInManualMode(bool value) = 0; virtual void setUseCategoryPathsInManualMode(bool value) = 0;
@@ -385,8 +387,8 @@ namespace BitTorrent
virtual void setOutgoingPortsMax(int max) = 0; virtual void setOutgoingPortsMax(int max) = 0;
virtual int UPnPLeaseDuration() const = 0; virtual int UPnPLeaseDuration() const = 0;
virtual void setUPnPLeaseDuration(int duration) = 0; virtual void setUPnPLeaseDuration(int duration) = 0;
virtual int peerToS() const = 0; virtual int peerDSCP() const = 0;
virtual void setPeerToS(int value) = 0; virtual void setPeerDSCP(int value) = 0;
virtual bool ignoreLimitsOnLAN() const = 0; virtual bool ignoreLimitsOnLAN() const = 0;
virtual void setIgnoreLimitsOnLAN(bool ignore) = 0; virtual void setIgnoreLimitsOnLAN(bool ignore) = 0;
virtual bool includeOverheadInLimits() const = 0; virtual bool includeOverheadInLimits() const = 0;
@@ -518,7 +520,7 @@ namespace BitTorrent
void torrentTagRemoved(Torrent *torrent, const Tag &tag); void torrentTagRemoved(Torrent *torrent, const Tag &tag);
void trackerError(Torrent *torrent, const QString &tracker); void trackerError(Torrent *torrent, const QString &tracker);
void trackersAdded(Torrent *torrent, const QList<TrackerEntry> &trackers); void trackersAdded(Torrent *torrent, const QList<TrackerEntry> &trackers);
void trackersChanged(Torrent *torrent); void trackersReset(Torrent *torrent, const QList<TrackerEntryStatus> &oldEntries, const QList<TrackerEntry> &newEntries);
void trackersRemoved(Torrent *torrent, const QStringList &trackers); void trackersRemoved(Torrent *torrent, const QStringList &trackers);
void trackerSuccess(Torrent *torrent, const QString &tracker); void trackerSuccess(Torrent *torrent, const QString &tracker);
void trackerWarning(Torrent *torrent, const QString &tracker); void trackerWarning(Torrent *torrent, const QString &tracker);

View File

@@ -484,7 +484,7 @@ SessionImpl::SessionImpl(QObject *parent)
, m_outgoingPortsMin(BITTORRENT_SESSION_KEY(u"OutgoingPortsMin"_s), 0) , m_outgoingPortsMin(BITTORRENT_SESSION_KEY(u"OutgoingPortsMin"_s), 0)
, m_outgoingPortsMax(BITTORRENT_SESSION_KEY(u"OutgoingPortsMax"_s), 0) , m_outgoingPortsMax(BITTORRENT_SESSION_KEY(u"OutgoingPortsMax"_s), 0)
, m_UPnPLeaseDuration(BITTORRENT_SESSION_KEY(u"UPnPLeaseDuration"_s), 0) , m_UPnPLeaseDuration(BITTORRENT_SESSION_KEY(u"UPnPLeaseDuration"_s), 0)
, m_peerToS(BITTORRENT_SESSION_KEY(u"PeerToS"_s), 0x04) , m_peerDSCP(BITTORRENT_SESSION_KEY(u"PeerToS"_s), 0x01)
, m_ignoreLimitsOnLAN(BITTORRENT_SESSION_KEY(u"IgnoreLimitsOnLAN"_s), false) , m_ignoreLimitsOnLAN(BITTORRENT_SESSION_KEY(u"IgnoreLimitsOnLAN"_s), false)
, m_includeOverheadInLimits(BITTORRENT_SESSION_KEY(u"IncludeOverheadInLimits"_s), false) , m_includeOverheadInLimits(BITTORRENT_SESSION_KEY(u"IncludeOverheadInLimits"_s), false)
, m_announceIP(BITTORRENT_SESSION_KEY(u"AnnounceIP"_s)) , m_announceIP(BITTORRENT_SESSION_KEY(u"AnnounceIP"_s))
@@ -513,9 +513,9 @@ SessionImpl::SessionImpl(QObject *parent)
, m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s)) , m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s))
, m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; }) , m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; })
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s) , m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s)
, Torrent::NO_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_SEEDING_TIME_LIMIT)) , NO_SEEDING_TIME_LIMIT, lowerLimited(NO_SEEDING_TIME_LIMIT))
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s) , m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s)
, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT)) , NO_SEEDING_TIME_LIMIT, lowerLimited(NO_SEEDING_TIME_LIMIT))
, m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false) , m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false)
, m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false) , m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
, m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None) , m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
@@ -555,7 +555,6 @@ SessionImpl::SessionImpl(QObject *parent)
, m_savePath(BITTORRENT_SESSION_KEY(u"DefaultSavePath"_s), specialFolderLocation(SpecialFolder::Downloads)) , m_savePath(BITTORRENT_SESSION_KEY(u"DefaultSavePath"_s), specialFolderLocation(SpecialFolder::Downloads))
, m_downloadPath(BITTORRENT_SESSION_KEY(u"TempPath"_s), (savePath() / Path(u"temp"_s))) , m_downloadPath(BITTORRENT_SESSION_KEY(u"TempPath"_s), (savePath() / Path(u"temp"_s)))
, m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY(u"TempPathEnabled"_s), false) , m_isDownloadPathEnabled(BITTORRENT_SESSION_KEY(u"TempPathEnabled"_s), false)
, m_isSubcategoriesEnabled(BITTORRENT_SESSION_KEY(u"SubcategoriesEnabled"_s), false)
, m_useCategoryPathsInManualMode(BITTORRENT_SESSION_KEY(u"UseCategoryPathsInManualMode"_s), false) , m_useCategoryPathsInManualMode(BITTORRENT_SESSION_KEY(u"UseCategoryPathsInManualMode"_s), false)
, m_isAutoTMMDisabledByDefault(BITTORRENT_SESSION_KEY(u"DisableAutoTMMByDefault"_s), true) , m_isAutoTMMDisabledByDefault(BITTORRENT_SESSION_KEY(u"DisableAutoTMMByDefault"_s), true)
, m_isDisableAutoTMMWhenCategoryChanged(BITTORRENT_SESSION_KEY(u"DisableAutoTMMTriggers/CategoryChanged"_s), false) , m_isDisableAutoTMMWhenCategoryChanged(BITTORRENT_SESSION_KEY(u"DisableAutoTMMTriggers/CategoryChanged"_s), false)
@@ -626,11 +625,6 @@ SessionImpl::SessionImpl(QObject *parent)
enableBandwidthScheduler(); enableBandwidthScheduler();
loadCategories(); loadCategories();
if (isSubcategoriesEnabled())
{
// if subcategories support changed manually
m_categories = expandCategories(m_categories);
}
const QStringList storedTags = m_storedTags.get(); const QStringList storedTags = m_storedTags.get();
for (const QString &tagStr : storedTags) for (const QString &tagStr : storedTags)
@@ -938,15 +932,8 @@ Path SessionImpl::categorySavePath(const QString &categoryName, const CategoryOp
if (path.isEmpty()) if (path.isEmpty())
{ {
// use implicit save path // use implicit save path
if (isSubcategoriesEnabled()) path = Utils::Fs::toValidPath(subcategoryName(categoryName));
{ basePath = categorySavePath(parentCategoryName(categoryName));
path = Utils::Fs::toValidPath(subcategoryName(categoryName));
basePath = categorySavePath(parentCategoryName(categoryName));
}
else
{
path = Utils::Fs::toValidPath(categoryName);
}
} }
return (path.isAbsolute() ? path : (basePath / path)); return (path.isAbsolute() ? path : (basePath / path));
@@ -966,8 +953,7 @@ Path SessionImpl::categoryDownloadPath(const QString &categoryName, const Catego
if (categoryName.isEmpty()) if (categoryName.isEmpty())
return downloadPath(); return downloadPath();
const bool useSubcategories = isSubcategoriesEnabled(); const QString name = subcategoryName(categoryName);
const QString name = useSubcategories ? subcategoryName(categoryName) : categoryName;
const Path path = !downloadPathOption.path.isEmpty() const Path path = !downloadPathOption.path.isEmpty()
? downloadPathOption.path ? downloadPathOption.path
: Utils::Fs::toValidPath(name); // use implicit download path : Utils::Fs::toValidPath(name); // use implicit download path
@@ -975,7 +961,7 @@ Path SessionImpl::categoryDownloadPath(const QString &categoryName, const Catego
if (path.isAbsolute()) if (path.isAbsolute())
return path; return path;
const QString parentName = useSubcategories ? parentCategoryName(categoryName) : QString(); const QString parentName = parentCategoryName(categoryName);
CategoryOptions parentOptions = categoryOptions(parentName); CategoryOptions parentOptions = categoryOptions(parentName);
// Even if download path of parent category is disabled (directly or by inheritance) // Even if download path of parent category is disabled (directly or by inheritance)
// we need to construct the one as if it would be enabled. // we need to construct the one as if it would be enabled.
@@ -986,6 +972,62 @@ Path SessionImpl::categoryDownloadPath(const QString &categoryName, const Catego
return (basePath / path); return (basePath / path);
} }
qreal SessionImpl::categoryRatioLimit(const QString &categoryName) const
{
if (categoryName.isEmpty())
return globalMaxRatio();
if (const auto ratioLimit = categoryOptions(categoryName).ratioLimit;
ratioLimit != DEFAULT_RATIO_LIMIT)
{
return ratioLimit;
}
return categoryRatioLimit(parentCategoryName(categoryName));
}
int SessionImpl::categorySeedingTimeLimit(const QString &categoryName) const
{
if (categoryName.isEmpty())
return globalMaxSeedingMinutes();
if (const auto seedingTimeLimit = categoryOptions(categoryName).seedingTimeLimit;
seedingTimeLimit != DEFAULT_SEEDING_TIME_LIMIT)
{
return seedingTimeLimit;
}
return categorySeedingTimeLimit(parentCategoryName(categoryName));
}
int SessionImpl::categoryInactiveSeedingTimeLimit(const QString &categoryName) const
{
if (categoryName.isEmpty())
return globalMaxInactiveSeedingMinutes();
if (const auto inactiveSeedingTimeLimit = categoryOptions(categoryName).inactiveSeedingTimeLimit;
inactiveSeedingTimeLimit != DEFAULT_SEEDING_TIME_LIMIT)
{
return inactiveSeedingTimeLimit;
}
return categoryInactiveSeedingTimeLimit(parentCategoryName(categoryName));
}
ShareLimitAction SessionImpl::categoryShareLimitAction(const QString &categoryName) const
{
if (categoryName.isEmpty())
return shareLimitAction();
if (const auto shareLimitAction = categoryOptions(categoryName).shareLimitAction;
shareLimitAction != ShareLimitAction::Default)
{
return shareLimitAction;
}
return categoryShareLimitAction(parentCategoryName(categoryName));
}
DownloadPathOption SessionImpl::resolveCategoryDownloadPathOption(const QString &categoryName, const std::optional<DownloadPathOption> &option) const DownloadPathOption SessionImpl::resolveCategoryDownloadPathOption(const QString &categoryName, const std::optional<DownloadPathOption> &option) const
{ {
if (categoryName.isEmpty()) if (categoryName.isEmpty())
@@ -994,7 +1036,7 @@ DownloadPathOption SessionImpl::resolveCategoryDownloadPathOption(const QString
if (option.has_value()) if (option.has_value())
return *option; return *option;
const QString parentName = isSubcategoriesEnabled() ? parentCategoryName(categoryName) : QString(); const QString parentName = parentCategoryName(categoryName);
return resolveCategoryDownloadPathOption(parentName, categoryOptions(parentName).downloadPath); return resolveCategoryDownloadPathOption(parentName, categoryOptions(parentName).downloadPath);
} }
@@ -1006,15 +1048,12 @@ bool SessionImpl::addCategory(const QString &name, const CategoryOptions &option
if (!isValidCategoryName(name) || m_categories.contains(name)) if (!isValidCategoryName(name) || m_categories.contains(name))
return false; return false;
if (isSubcategoriesEnabled()) for (const QString &parent : asConst(expandCategory(name)))
{ {
for (const QString &parent : asConst(expandCategory(name))) if ((parent != name) && !m_categories.contains(parent))
{ {
if ((parent != name) && !m_categories.contains(parent)) m_categories[parent] = {};
{ emit categoryAdded(parent);
m_categories[parent] = {};
emit categoryAdded(parent);
}
} }
} }
@@ -1025,9 +1064,9 @@ bool SessionImpl::addCategory(const QString &name, const CategoryOptions &option
return true; return true;
} }
bool SessionImpl::editCategory(const QString &name, const CategoryOptions &options) bool SessionImpl::setCategoryOptions(const QString &categoryName, const CategoryOptions &options)
{ {
const auto it = m_categories.find(name); const auto it = m_categories.find(categoryName);
if (it == m_categories.end()) if (it == m_categories.end())
return false; return false;
@@ -1035,14 +1074,15 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
if (options == currentOptions) if (options == currentOptions)
return false; return false;
if (isDisableAutoTMMWhenCategorySavePathChanged()) if (isDisableAutoTMMWhenCategorySavePathChanged()
&& ((options.savePath != currentOptions.savePath) || (options.downloadPath != currentOptions.downloadPath)))
{ {
// This should be done before changing the category options // This should be done before changing the category options
// to prevent the torrent from being moved at the new save path. // to prevent the torrent from being moved at the new save path.
for (TorrentImpl *const torrent : asConst(m_torrents)) for (TorrentImpl *const torrent : asConst(m_torrents))
{ {
if (torrent->category() == name) if (torrent->category() == categoryName)
torrent->setAutoTMMEnabled(false); torrent->setAutoTMMEnabled(false);
} }
} }
@@ -1052,39 +1092,37 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
for (TorrentImpl *const torrent : asConst(m_torrents)) for (TorrentImpl *const torrent : asConst(m_torrents))
{ {
if (torrent->category() == name) if (torrent->category() == categoryName)
torrent->handleCategoryOptionsChanged(); torrent->handleCategoryOptionsChanged();
} }
emit categoryOptionsChanged(name); emit categoryOptionsChanged(categoryName);
return true; return true;
} }
bool SessionImpl::removeCategory(const QString &name) bool SessionImpl::removeCategory(const QString &name)
{ {
const QString parentCategory = parentCategoryName(name);
for (TorrentImpl *const torrent : asConst(m_torrents)) for (TorrentImpl *const torrent : asConst(m_torrents))
{ {
if (torrent->belongsToCategory(name)) if (torrent->belongsToCategory(name))
torrent->setCategory(u""_s); torrent->setCategory(parentCategory);
} }
// remove stored category and its subcategories if exist // remove stored category and its subcategories if exist
bool result = false; bool result = false;
if (isSubcategoriesEnabled()) // remove subcategories
const QString test = name + u'/';
Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const CategoryOptions &)
{ {
// remove subcategories if (category.startsWith(test))
const QString test = name + u'/';
Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const CategoryOptions &)
{ {
if (category.startsWith(test)) result = true;
{ emit categoryRemoved(category);
result = true; return true;
emit categoryRemoved(category); }
return true; return false;
} });
return false;
});
}
result = (m_categories.remove(name) > 0) || result; result = (m_categories.remove(name) > 0) || result;
@@ -1098,32 +1136,6 @@ bool SessionImpl::removeCategory(const QString &name)
return result; return result;
} }
bool SessionImpl::isSubcategoriesEnabled() const
{
return m_isSubcategoriesEnabled;
}
void SessionImpl::setSubcategoriesEnabled(const bool value)
{
if (isSubcategoriesEnabled() == value) return;
if (value)
{
// expand categories to include all parent categories
m_categories = expandCategories(m_categories);
// update stored categories
storeCategories();
}
else
{
// reload categories
loadCategories();
}
m_isSubcategoriesEnabled = value;
emit subcategoriesSupportChanged();
}
bool SessionImpl::useCategoryPathsInManualMode() const bool SessionImpl::useCategoryPathsInManualMode() const
{ {
return m_useCategoryPathsInManualMode; return m_useCategoryPathsInManualMode;
@@ -1281,7 +1293,7 @@ qreal SessionImpl::globalMaxRatio() const
void SessionImpl::setGlobalMaxRatio(qreal ratio) void SessionImpl::setGlobalMaxRatio(qreal ratio)
{ {
if (ratio < 0) if (ratio < 0)
ratio = Torrent::NO_RATIO_LIMIT; ratio = NO_RATIO_LIMIT;
if (ratio != globalMaxRatio()) if (ratio != globalMaxRatio())
{ {
@@ -1297,7 +1309,7 @@ int SessionImpl::globalMaxSeedingMinutes() const
void SessionImpl::setGlobalMaxSeedingMinutes(int minutes) void SessionImpl::setGlobalMaxSeedingMinutes(int minutes)
{ {
minutes = std::max(minutes, Torrent::NO_SEEDING_TIME_LIMIT); minutes = std::max(minutes, NO_SEEDING_TIME_LIMIT);
if (minutes != globalMaxSeedingMinutes()) if (minutes != globalMaxSeedingMinutes())
{ {
@@ -1313,7 +1325,7 @@ int SessionImpl::globalMaxInactiveSeedingMinutes() const
void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes) void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes)
{ {
minutes = std::max(minutes, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT); minutes = std::max(minutes, NO_SEEDING_TIME_LIMIT);
if (minutes != globalMaxInactiveSeedingMinutes()) if (minutes != globalMaxInactiveSeedingMinutes())
{ {
@@ -2057,7 +2069,7 @@ lt::settings_pack SessionImpl::loadLTSettings() const
// UPnP lease duration // UPnP lease duration
settingsPack.set_int(lt::settings_pack::upnp_lease_duration, UPnPLeaseDuration()); settingsPack.set_int(lt::settings_pack::upnp_lease_duration, UPnPLeaseDuration());
// Type of service // Type of service
settingsPack.set_int(lt::settings_pack::peer_tos, peerToS()); settingsPack.set_int(lt::settings_pack::peer_dscp, peerDSCP());
// Include overhead in transfer limits // Include overhead in transfer limits
settingsPack.set_bool(lt::settings_pack::rate_limit_ip_overhead, includeOverheadInLimits()); settingsPack.set_bool(lt::settings_pack::rate_limit_ip_overhead, includeOverheadInLimits());
// IP address to announce to trackers // IP address to announce to trackers
@@ -2356,14 +2368,9 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
if (!torrent->isFinished() || torrent->isForced()) if (!torrent->isFinished() || torrent->isForced())
return; return;
const auto effectiveLimit = []<typename T>(const T limit, const T useGlobalLimit, const T globalLimit) -> T const qreal ratioLimit = torrent->effectiveRatioLimit();
{ const int seedingTimeLimit = torrent->effectiveSeedingTimeLimit();
return (limit == useGlobalLimit) ? globalLimit : limit; const int inactiveSeedingTimeLimit = torrent->effectiveInactiveSeedingTimeLimit();
};
const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio());
const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes());
const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes());
bool reached = false; bool reached = false;
QString description; QString description;
@@ -2390,7 +2397,7 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
if (reached) if (reached)
{ {
const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name()); const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction(); const ShareLimitAction shareLimitAction = torrent->effectiveShareLimitAction();
if (shareLimitAction == ShareLimitAction::Remove) if (shareLimitAction == ShareLimitAction::Remove)
{ {
@@ -4882,17 +4889,17 @@ void SessionImpl::setUPnPLeaseDuration(const int duration)
} }
} }
int SessionImpl::peerToS() const int SessionImpl::peerDSCP() const
{ {
return m_peerToS; return m_peerDSCP;
} }
void SessionImpl::setPeerToS(const int value) void SessionImpl::setPeerDSCP(const int value)
{ {
if (value == m_peerToS) if (value == m_peerDSCP)
return; return;
m_peerToS = value; m_peerDSCP = value;
configureDeferred(); configureDeferred();
} }
@@ -5218,9 +5225,9 @@ bool SessionImpl::isKnownTorrent(const InfoHash &infoHash) const
void SessionImpl::updateSeedingLimitTimer() void SessionImpl::updateSeedingLimitTimer()
{ {
if ((globalMaxRatio() == Torrent::NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit() if ((globalMaxRatio() == NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit()
&& (globalMaxSeedingMinutes() == Torrent::NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit() && (globalMaxSeedingMinutes() == NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit()
&& (globalMaxInactiveSeedingMinutes() == Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit()) && (globalMaxInactiveSeedingMinutes() == NO_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit())
{ {
if (m_seedingLimitTimer->isActive()) if (m_seedingLimitTimer->isActive())
m_seedingLimitTimer->stop(); m_seedingLimitTimer->stop();
@@ -5279,9 +5286,9 @@ void SessionImpl::handleTorrentTrackersRemoved(TorrentImpl *const torrent, const
emit trackersRemoved(torrent, deletedTrackers); emit trackersRemoved(torrent, deletedTrackers);
} }
void SessionImpl::handleTorrentTrackersChanged(TorrentImpl *const torrent) void SessionImpl::handleTorrentTrackersReset(TorrentImpl *const torrent, const QList<TrackerEntryStatus> &oldEntries, const QList<TrackerEntry> &newEntries)
{ {
emit trackersChanged(torrent); emit trackersReset(torrent, oldEntries, newEntries);
} }
void SessionImpl::handleTorrentUrlSeedsAdded(TorrentImpl *const torrent, const QList<QUrl> &newUrlSeeds) void SessionImpl::handleTorrentUrlSeedsAdded(TorrentImpl *const torrent, const QList<QUrl> &newUrlSeeds)
@@ -5584,6 +5591,8 @@ void SessionImpl::loadCategories()
const auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject()); const auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject());
m_categories[categoryName] = categoryOptions; m_categories[categoryName] = categoryOptions;
} }
m_categories = expandCategories(m_categories);
} }
bool SessionImpl::hasPerTorrentRatioLimit() const bool SessionImpl::hasPerTorrentRatioLimit() const

View File

@@ -149,15 +149,17 @@ namespace BitTorrent
QStringList categories() const override; QStringList categories() const override;
CategoryOptions categoryOptions(const QString &categoryName) const override; CategoryOptions categoryOptions(const QString &categoryName) const override;
bool setCategoryOptions(const QString &categoryName, const CategoryOptions &options) override;
Path categorySavePath(const QString &categoryName) const override; Path categorySavePath(const QString &categoryName) const override;
Path categorySavePath(const QString &categoryName, const CategoryOptions &options) const override; Path categorySavePath(const QString &categoryName, const CategoryOptions &options) const override;
Path categoryDownloadPath(const QString &categoryName) const override; Path categoryDownloadPath(const QString &categoryName) const override;
Path categoryDownloadPath(const QString &categoryName, const CategoryOptions &options) const override; Path categoryDownloadPath(const QString &categoryName, const CategoryOptions &options) const override;
qreal categoryRatioLimit(const QString &categoryName) const override;
int categorySeedingTimeLimit(const QString &categoryName) const override;
int categoryInactiveSeedingTimeLimit(const QString &categoryName) const override;
ShareLimitAction categoryShareLimitAction(const QString &categoryName) const override;
bool addCategory(const QString &name, const CategoryOptions &options = {}) override; bool addCategory(const QString &name, const CategoryOptions &options = {}) override;
bool editCategory(const QString &name, const CategoryOptions &options) override;
bool removeCategory(const QString &name) override; bool removeCategory(const QString &name) override;
bool isSubcategoriesEnabled() const override;
void setSubcategoriesEnabled(bool value) override;
bool useCategoryPathsInManualMode() const override; bool useCategoryPathsInManualMode() const override;
void setUseCategoryPathsInManualMode(bool value) override; void setUseCategoryPathsInManualMode(bool value) override;
@@ -359,8 +361,8 @@ namespace BitTorrent
void setOutgoingPortsMax(int max) override; void setOutgoingPortsMax(int max) override;
int UPnPLeaseDuration() const override; int UPnPLeaseDuration() const override;
void setUPnPLeaseDuration(int duration) override; void setUPnPLeaseDuration(int duration) override;
int peerToS() const override; int peerDSCP() const override;
void setPeerToS(int value) override; void setPeerDSCP(int value) override;
bool ignoreLimitsOnLAN() const override; bool ignoreLimitsOnLAN() const override;
void setIgnoreLimitsOnLAN(bool ignore) override; void setIgnoreLimitsOnLAN(bool ignore) override;
bool includeOverheadInLimits() const override; bool includeOverheadInLimits() const override;
@@ -474,7 +476,7 @@ namespace BitTorrent
void handleTorrentFinished(TorrentImpl *torrent); void handleTorrentFinished(TorrentImpl *torrent);
void handleTorrentTrackersAdded(TorrentImpl *torrent, const QList<TrackerEntry> &newTrackers); void handleTorrentTrackersAdded(TorrentImpl *torrent, const QList<TrackerEntry> &newTrackers);
void handleTorrentTrackersRemoved(TorrentImpl *torrent, const QStringList &deletedTrackers); void handleTorrentTrackersRemoved(TorrentImpl *torrent, const QStringList &deletedTrackers);
void handleTorrentTrackersChanged(TorrentImpl *torrent); void handleTorrentTrackersReset(TorrentImpl *torrent, const QList<TrackerEntryStatus> &oldEntries, const QList<TrackerEntry> &newEntries);
void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList<QUrl> &newUrlSeeds); void handleTorrentUrlSeedsAdded(TorrentImpl *torrent, const QList<QUrl> &newUrlSeeds);
void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds); void handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds);
void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data); void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data);
@@ -690,7 +692,7 @@ namespace BitTorrent
CachedSettingValue<int> m_outgoingPortsMin; CachedSettingValue<int> m_outgoingPortsMin;
CachedSettingValue<int> m_outgoingPortsMax; CachedSettingValue<int> m_outgoingPortsMax;
CachedSettingValue<int> m_UPnPLeaseDuration; CachedSettingValue<int> m_UPnPLeaseDuration;
CachedSettingValue<int> m_peerToS; CachedSettingValue<int> m_peerDSCP;
CachedSettingValue<bool> m_ignoreLimitsOnLAN; CachedSettingValue<bool> m_ignoreLimitsOnLAN;
CachedSettingValue<bool> m_includeOverheadInLimits; CachedSettingValue<bool> m_includeOverheadInLimits;
CachedSettingValue<QString> m_announceIP; CachedSettingValue<QString> m_announceIP;
@@ -754,7 +756,6 @@ namespace BitTorrent
CachedSettingValue<Path> m_savePath; CachedSettingValue<Path> m_savePath;
CachedSettingValue<Path> m_downloadPath; CachedSettingValue<Path> m_downloadPath;
CachedSettingValue<bool> m_isDownloadPathEnabled; CachedSettingValue<bool> m_isDownloadPathEnabled;
CachedSettingValue<bool> m_isSubcategoriesEnabled;
CachedSettingValue<bool> m_useCategoryPathsInManualMode; CachedSettingValue<bool> m_useCategoryPathsInManualMode;
CachedSettingValue<bool> m_isAutoTMMDisabledByDefault; CachedSettingValue<bool> m_isAutoTMMDisabledByDefault;
CachedSettingValue<bool> m_isDisableAutoTMMWhenCategoryChanged; CachedSettingValue<bool> m_isDisableAutoTMMWhenCategoryChanged;

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 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
@@ -33,6 +33,12 @@
namespace BitTorrent namespace BitTorrent
{ {
inline const qreal DEFAULT_RATIO_LIMIT = -2;
inline const qreal NO_RATIO_LIMIT = -1;
inline const int DEFAULT_SEEDING_TIME_LIMIT = -2;
inline const int NO_SEEDING_TIME_LIMIT = -1;
// 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

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 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
@@ -44,15 +44,6 @@ namespace BitTorrent
// Torrent // Torrent
const qreal Torrent::USE_GLOBAL_RATIO = -2;
const qreal Torrent::NO_RATIO_LIMIT = -1;
const int Torrent::USE_GLOBAL_SEEDING_TIME = -2;
const int Torrent::NO_SEEDING_TIME_LIMIT = -1;
const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2;
const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1;
const qreal Torrent::MAX_RATIO = std::numeric_limits<qreal>::infinity(); const qreal Torrent::MAX_RATIO = std::numeric_limits<qreal>::infinity();
TorrentID Torrent::id() const TorrentID Torrent::id() const

View File

@@ -37,7 +37,8 @@
#include "base/3rdparty/expected.hpp" #include "base/3rdparty/expected.hpp"
#include "base/pathfwd.h" #include "base/pathfwd.h"
#include "base/tagset.h" #include "base/tagset.h"
#include "sharelimitaction.h" #include "sharelimits.h"
#include "torrentannouncestatus.h"
#include "torrentcontenthandler.h" #include "torrentcontenthandler.h"
class QBitArray; class QBitArray;
@@ -124,15 +125,6 @@ namespace BitTorrent
}; };
Q_ENUM(StopCondition) Q_ENUM(StopCondition)
static const qreal USE_GLOBAL_RATIO;
static const qreal NO_RATIO_LIMIT;
static const int USE_GLOBAL_SEEDING_TIME;
static const int NO_SEEDING_TIME_LIMIT;
static const int USE_GLOBAL_INACTIVE_SEEDING_TIME;
static const int NO_INACTIVE_SEEDING_TIME_LIMIT;
static const qreal MAX_RATIO; static const qreal MAX_RATIO;
using TorrentContentHandler::TorrentContentHandler; using TorrentContentHandler::TorrentContentHandler;
@@ -235,6 +227,10 @@ namespace BitTorrent
virtual void setInactiveSeedingTimeLimit(int limit) = 0; virtual void setInactiveSeedingTimeLimit(int limit) = 0;
virtual ShareLimitAction shareLimitAction() const = 0; virtual ShareLimitAction shareLimitAction() const = 0;
virtual void setShareLimitAction(ShareLimitAction action) = 0; virtual void setShareLimitAction(ShareLimitAction action) = 0;
virtual qreal effectiveRatioLimit() const = 0;
virtual int effectiveSeedingTimeLimit() const = 0;
virtual int effectiveInactiveSeedingTimeLimit() const = 0;
virtual ShareLimitAction effectiveShareLimitAction() const = 0;
virtual PathList filePaths() const = 0; virtual PathList filePaths() const = 0;
virtual PathList actualFilePaths() const = 0; virtual PathList actualFilePaths() const = 0;
@@ -278,9 +274,6 @@ namespace BitTorrent
virtual bool isLSDDisabled() const = 0; virtual bool isLSDDisabled() const = 0;
virtual QBitArray pieces() const = 0; virtual QBitArray pieces() const = 0;
virtual qreal distributedCopies() const = 0; virtual qreal distributedCopies() const = 0;
virtual qreal maxRatio() const = 0;
virtual int maxSeedingTime() const = 0;
virtual int maxInactiveSeedingTime() const = 0;
virtual qreal realRatio() const = 0; virtual qreal realRatio() const = 0;
virtual qreal popularity() const = 0; virtual qreal popularity() const = 0;
virtual int uploadPayloadRate() const = 0; virtual int uploadPayloadRate() const = 0;
@@ -290,6 +283,7 @@ namespace BitTorrent
virtual int connectionsCount() const = 0; virtual int connectionsCount() const = 0;
virtual int connectionsLimit() const = 0; virtual int connectionsLimit() const = 0;
virtual qlonglong nextAnnounce() const = 0; virtual qlonglong nextAnnounce() const = 0;
virtual TorrentAnnounceStatus announceStatus() const = 0;
virtual void setName(const QString &name) = 0; virtual void setName(const QString &name) = 0;
virtual void setSequentialDownload(bool enable) = 0; virtual void setSequentialDownload(bool enable) = 0;

View File

@@ -0,0 +1,47 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 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 <QFlags>
namespace BitTorrent
{
enum class TorrentAnnounceStatusFlag
{
HasNoProblem = 0,
HasWarning = 1,
HasTrackerError = 2,
HasOtherError = 4
};
Q_DECLARE_FLAGS(TorrentAnnounceStatus, TorrentAnnounceStatusFlag);
}
Q_DECLARE_OPERATORS_FOR_FLAGS(BitTorrent::TorrentAnnounceStatus);

View File

@@ -700,6 +700,7 @@ void TorrentImpl::removeTrackers(const QStringList &trackers)
if (!removedTrackers.isEmpty()) if (!removedTrackers.isEmpty())
{ {
m_nativeHandle.replace_trackers(nativeTrackers); m_nativeHandle.replace_trackers(nativeTrackers);
m_announceStatus.reset();
deferredRequestResumeData(); deferredRequestResumeData();
m_session->handleTorrentTrackersRemoved(this, removedTrackers); m_session->handleTorrentTrackersRemoved(this, removedTrackers);
@@ -718,15 +719,16 @@ void TorrentImpl::replaceTrackers(QList<TrackerEntry> trackers)
std::vector<lt::announce_entry> nativeTrackers; std::vector<lt::announce_entry> nativeTrackers;
nativeTrackers.reserve(trackers.size()); nativeTrackers.reserve(trackers.size());
m_trackerEntryStatuses.clear(); const auto oldEntries = std::exchange(m_trackerEntryStatuses, {});
for (const TrackerEntry &tracker : trackers) for (const TrackerEntry &tracker : asConst(trackers))
{ {
nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier)); nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));
m_trackerEntryStatuses.append({tracker.url, tracker.tier}); m_trackerEntryStatuses.append({tracker.url, tracker.tier});
} }
m_nativeHandle.replace_trackers(nativeTrackers); m_nativeHandle.replace_trackers(nativeTrackers);
m_announceStatus.reset();
// Clear the peer list if it's a private torrent since // Clear the peer list if it's a private torrent since
// we do not want to keep connecting with peers from old tracker. // we do not want to keep connecting with peers from old tracker.
@@ -734,7 +736,7 @@ void TorrentImpl::replaceTrackers(QList<TrackerEntry> trackers)
clearPeers(); clearPeers();
deferredRequestResumeData(); deferredRequestResumeData();
m_session->handleTorrentTrackersChanged(this); m_session->handleTorrentTrackersReset(this, oldEntries, trackers);
} }
QList<QUrl> TorrentImpl::urlSeeds() const QList<QUrl> TorrentImpl::urlSeeds() const
@@ -934,7 +936,7 @@ bool TorrentImpl::belongsToCategory(const QString &category) const
if (m_category == category) if (m_category == category)
return true; return true;
return (m_session->isSubcategoriesEnabled() && m_category.startsWith(category + u'/')); return m_category.startsWith(category + u'/');
} }
TagSet TorrentImpl::tags() const TagSet TorrentImpl::tags() const
@@ -1186,25 +1188,7 @@ bool TorrentImpl::isCompleted() const
bool TorrentImpl::isActive() const bool TorrentImpl::isActive() const
{ {
switch (m_state) return ((uploadPayloadRate() > 0) || (downloadPayloadRate() > 0));
{
case TorrentState::StalledDownloading:
return (uploadPayloadRate() > 0);
case TorrentState::DownloadingMetadata:
case TorrentState::ForcedDownloadingMetadata:
case TorrentState::Downloading:
case TorrentState::ForcedDownloading:
case TorrentState::Uploading:
case TorrentState::ForcedUploading:
case TorrentState::Moving:
return true;
default:
break;
};
return false;
} }
bool TorrentImpl::isInactive() const bool TorrentImpl::isInactive() const
@@ -1356,16 +1340,16 @@ qlonglong TorrentImpl::eta() const
if (isFinished()) if (isFinished())
{ {
const qreal maxRatioValue = maxRatio(); const qreal maxRatioValue = effectiveRatioLimit();
const int maxSeedingTimeValue = maxSeedingTime(); const int maxSeedingTimeValue = effectiveSeedingTimeLimit();
const int maxInactiveSeedingTimeValue = maxInactiveSeedingTime(); const int maxInactiveSeedingTimeValue = effectiveInactiveSeedingTimeLimit();
if ((maxRatioValue < 0) && (maxSeedingTimeValue < 0) && (maxInactiveSeedingTimeValue < 0)) return MAX_ETA; if ((maxRatioValue < 0) && (maxSeedingTimeValue < 0) && (maxInactiveSeedingTimeValue < 0))
return MAX_ETA;
qlonglong ratioEta = MAX_ETA; qlonglong ratioEta = MAX_ETA;
if ((speedAverage.upload > 0) && (maxRatioValue >= 0)) if ((speedAverage.upload > 0) && (maxRatioValue >= 0))
{ {
qlonglong realDL = totalDownload(); qlonglong realDL = totalDownload();
if (realDL <= 0) if (realDL <= 0)
realDL = wantedSize(); realDL = wantedSize();
@@ -1497,30 +1481,38 @@ qreal TorrentImpl::distributedCopies() const
return m_nativeStatus.distributed_copies; return m_nativeStatus.distributed_copies;
} }
qreal TorrentImpl::maxRatio() const qreal TorrentImpl::effectiveRatioLimit() const
{ {
if (m_ratioLimit == USE_GLOBAL_RATIO) if (m_ratioLimit == DEFAULT_RATIO_LIMIT)
return m_session->globalMaxRatio(); return m_session->categoryRatioLimit(category());
return m_ratioLimit; return m_ratioLimit;
} }
int TorrentImpl::maxSeedingTime() const int TorrentImpl::effectiveSeedingTimeLimit() const
{ {
if (m_seedingTimeLimit == USE_GLOBAL_SEEDING_TIME) if (m_seedingTimeLimit == DEFAULT_SEEDING_TIME_LIMIT)
return m_session->globalMaxSeedingMinutes(); return m_session->categorySeedingTimeLimit(category());
return m_seedingTimeLimit; return m_seedingTimeLimit;
} }
int TorrentImpl::maxInactiveSeedingTime() const int TorrentImpl::effectiveInactiveSeedingTimeLimit() const
{ {
if (m_inactiveSeedingTimeLimit == USE_GLOBAL_INACTIVE_SEEDING_TIME) if (m_inactiveSeedingTimeLimit == DEFAULT_SEEDING_TIME_LIMIT)
return m_session->globalMaxInactiveSeedingMinutes(); return m_session->categoryInactiveSeedingTimeLimit(category());
return m_inactiveSeedingTimeLimit; return m_inactiveSeedingTimeLimit;
} }
ShareLimitAction TorrentImpl::effectiveShareLimitAction() const
{
if (m_shareLimitAction == ShareLimitAction::Default)
return m_session->categoryShareLimitAction(category());
return m_shareLimitAction;
}
qreal TorrentImpl::realRatio() const qreal TorrentImpl::realRatio() const
{ {
const int64_t upload = m_nativeStatus.all_time_upload; const int64_t upload = m_nativeStatus.all_time_upload;
@@ -1575,6 +1567,46 @@ qlonglong TorrentImpl::nextAnnounce() const
return lt::total_seconds(m_nativeStatus.next_announce); return lt::total_seconds(m_nativeStatus.next_announce);
} }
TorrentAnnounceStatus TorrentImpl::announceStatus() const
{
if (m_announceStatus)
return *m_announceStatus;
TorrentAnnounceStatus announceStatus = TorrentAnnounceStatusFlag::HasNoProblem;
for (const TrackerEntryStatus &trackerEntryStatus : asConst(m_trackerEntryStatuses))
{
switch (trackerEntryStatus.state)
{
case BitTorrent::TrackerEndpointState::Working:
if (!announceStatus.testFlag(TorrentAnnounceStatusFlag::HasWarning))
{
const bool hasWarningMessage = std::ranges::any_of(trackerEntryStatus.endpoints
, [](const TrackerEndpointStatus &endpointEntry)
{
return !endpointEntry.message.isEmpty() && (endpointEntry.state == BitTorrent::TrackerEndpointState::Working);
});
announceStatus.setFlag(TorrentAnnounceStatusFlag::HasWarning, hasWarningMessage);
}
break;
case BitTorrent::TrackerEndpointState::NotWorking:
case BitTorrent::TrackerEndpointState::Unreachable:
announceStatus.setFlag(TorrentAnnounceStatusFlag::HasOtherError);
break;
case BitTorrent::TrackerEndpointState::TrackerError:
announceStatus.setFlag(TorrentAnnounceStatusFlag::HasTrackerError);
break;
case BitTorrent::TrackerEndpointState::NotContacted:
break;
};
}
m_announceStatus = announceStatus;
return *m_announceStatus;
}
qreal TorrentImpl::popularity() const qreal TorrentImpl::popularity() const
{ {
// in order to produce floating-point numbers using `std::chrono::duration_cast`, // in order to produce floating-point numbers using `std::chrono::duration_cast`,
@@ -1761,6 +1793,7 @@ TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entr
#endif #endif
::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo); ::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo);
m_announceStatus.reset();
return *it; return *it;
} }
@@ -1776,6 +1809,8 @@ void TorrentImpl::resetTrackerEntryStatuses()
status.url = tempUrl; status.url = tempUrl;
status.tier = tempTier; status.tier = tempTier;
} }
m_announceStatus = TorrentAnnounceStatusFlag::HasNoProblem;
} }
std::shared_ptr<const libtorrent::torrent_info> TorrentImpl::nativeTorrentInfo() const std::shared_ptr<const libtorrent::torrent_info> TorrentImpl::nativeTorrentInfo() const
@@ -2598,7 +2633,7 @@ void TorrentImpl::updateProgress()
void TorrentImpl::setRatioLimit(qreal limit) void TorrentImpl::setRatioLimit(qreal limit)
{ {
if (limit < USE_GLOBAL_RATIO) if (limit < DEFAULT_RATIO_LIMIT)
limit = NO_RATIO_LIMIT; limit = NO_RATIO_LIMIT;
if (m_ratioLimit != limit) if (m_ratioLimit != limit)
@@ -2611,7 +2646,7 @@ void TorrentImpl::setRatioLimit(qreal limit)
void TorrentImpl::setSeedingTimeLimit(int limit) void TorrentImpl::setSeedingTimeLimit(int limit)
{ {
if (limit < USE_GLOBAL_SEEDING_TIME) if (limit < DEFAULT_SEEDING_TIME_LIMIT)
limit = NO_SEEDING_TIME_LIMIT; limit = NO_SEEDING_TIME_LIMIT;
if (m_seedingTimeLimit != limit) if (m_seedingTimeLimit != limit)
@@ -2624,8 +2659,8 @@ void TorrentImpl::setSeedingTimeLimit(int limit)
void TorrentImpl::setInactiveSeedingTimeLimit(int limit) void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
{ {
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME) if (limit < DEFAULT_SEEDING_TIME_LIMIT)
limit = NO_INACTIVE_SEEDING_TIME_LIMIT; limit = NO_SEEDING_TIME_LIMIT;
if (m_inactiveSeedingTimeLimit != limit) if (m_inactiveSeedingTimeLimit != limit)
{ {

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 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
@@ -156,6 +156,10 @@ namespace BitTorrent
void setInactiveSeedingTimeLimit(int limit) override; void setInactiveSeedingTimeLimit(int limit) override;
ShareLimitAction shareLimitAction() const override; ShareLimitAction shareLimitAction() const override;
void setShareLimitAction(ShareLimitAction action) override; void setShareLimitAction(ShareLimitAction action) override;
qreal effectiveRatioLimit() const override;
int effectiveSeedingTimeLimit() const override;
int effectiveInactiveSeedingTimeLimit() const override;
ShareLimitAction effectiveShareLimitAction() const override;
Path filePath(int index) const override; Path filePath(int index) const override;
Path actualFilePath(int index) const override; Path actualFilePath(int index) const override;
@@ -205,9 +209,6 @@ namespace BitTorrent
bool isLSDDisabled() const override; bool isLSDDisabled() const override;
QBitArray pieces() const override; QBitArray pieces() const override;
qreal distributedCopies() const override; qreal distributedCopies() const override;
qreal maxRatio() const override;
int maxSeedingTime() const override;
int maxInactiveSeedingTime() const override;
qreal realRatio() const override; qreal realRatio() const override;
qreal popularity() const override; qreal popularity() const override;
int uploadPayloadRate() const override; int uploadPayloadRate() const override;
@@ -217,6 +218,7 @@ namespace BitTorrent
int connectionsCount() const override; int connectionsCount() const override;
int connectionsLimit() const override; int connectionsLimit() const override;
qlonglong nextAnnounce() const override; qlonglong nextAnnounce() const override;
TorrentAnnounceStatus announceStatus() const override;
void setName(const QString &name) override; void setName(const QString &name) override;
void setSequentialDownload(bool enable) override; void setSequentialDownload(bool enable) override;
@@ -349,6 +351,7 @@ namespace BitTorrent
MaintenanceJob m_maintenanceJob = MaintenanceJob::None; MaintenanceJob m_maintenanceJob = MaintenanceJob::None;
QList<TrackerEntryStatus> m_trackerEntryStatuses; QList<TrackerEntryStatus> m_trackerEntryStatuses;
mutable std::optional<TorrentAnnounceStatus> m_announceStatus;
QList<QUrl> m_urlSeeds; QList<QUrl> m_urlSeeds;
FileErrorInfo m_lastFileError; FileErrorInfo m_lastFileError;

View File

@@ -65,7 +65,7 @@ namespace
bool hasDriveLetter(const QStringView path) bool hasDriveLetter(const QStringView path)
{ {
const QRegularExpression driveLetterRegex {u"^[A-Za-z]:/"_s}; const QRegularExpression driveLetterRegex {u"^[A-Za-z]:/"_s};
return driveLetterRegex.match(path).hasMatch(); return driveLetterRegex.matchView(path).hasMatch();
} }
#endif #endif
} }
@@ -104,7 +104,7 @@ bool Path::isValid() const
// \\37 is using base-8 number system // \\37 is using base-8 number system
const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_s}; const QRegularExpression regex {u"[\\0-\\37:?\"*<>|]"_s};
return !regex.match(view).hasMatch(); return !regex.matchView(view).hasMatch();
#elif defined(Q_OS_MACOS) #elif defined(Q_OS_MACOS)
const QRegularExpression regex {u"[\\0:]"_s}; const QRegularExpression regex {u"[\\0:]"_s};
#else #else

View File

@@ -494,10 +494,17 @@ void Preferences::setWinStartup(const bool b)
settings.remove(profileID); settings.remove(profileID);
} }
} }
#endif // Q_OS_WIN
QString Preferences::getStyle() const QString Preferences::getStyle() const
{ {
return value<QString>(u"Appearance/Style"_s); #ifdef Q_OS_WIN
const QString defaultStyleName = u"Fusion"_s;
#else
const QString defaultStyleName = u"system"_s;
#endif
const auto styleName = value<QString>(u"Appearance/Style"_s);
return styleName.isEmpty() ? defaultStyleName : styleName;
} }
void Preferences::setStyle(const QString &styleName) void Preferences::setStyle(const QString &styleName)
@@ -507,7 +514,6 @@ void Preferences::setStyle(const QString &styleName)
setValue(u"Appearance/Style"_s, styleName); setValue(u"Appearance/Style"_s, styleName);
} }
#endif // Q_OS_WIN
// Downloads // Downloads
Path Preferences::getScanDirsLastPath() const Path Preferences::getScanDirsLastPath() const
@@ -1291,6 +1297,19 @@ void Preferences::setShutdownWhenDownloadsComplete(const bool shutdown)
setValue(u"Preferences/Downloads/AutoShutDownOnCompletion"_s, shutdown); setValue(u"Preferences/Downloads/AutoShutDownOnCompletion"_s, shutdown);
} }
bool Preferences::rebootWhenDownloadsComplete() const
{
return value(u"Preferences/Downloads/AutoRebootOnCompletion"_s, false);
}
void Preferences::setRebootWhenDownloadsComplete(const bool reboot)
{
if (reboot == rebootWhenDownloadsComplete())
return;
setValue(u"Preferences/Downloads/AutoRebootOnCompletion"_s, reboot);
}
bool Preferences::suspendWhenDownloadsComplete() const bool Preferences::suspendWhenDownloadsComplete() const
{ {
return value(u"Preferences/Downloads/AutoSuspendOnCompletion"_s, false); return value(u"Preferences/Downloads/AutoSuspendOnCompletion"_s, false);
@@ -1885,6 +1904,32 @@ void Preferences::setTrackerFilterState(const bool checked)
setValue(u"TransferListFilters/trackerFilterState"_s, checked); setValue(u"TransferListFilters/trackerFilterState"_s, checked);
} }
bool Preferences::getTrackerStatusFilterState() const
{
return value(u"TransferListFilters/TrackerStatusFilterState"_s, true);
}
void Preferences::setTrackerStatusFilterState(const bool checked)
{
if (checked == getTrackerStatusFilterState())
return;
setValue(u"TransferListFilters/TrackerStatusFilterState"_s, checked);
}
bool Preferences::useSeparateTrackerStatusFilter() const
{
return value(u"TransferListFilters/SeparateTrackerStatusFilter"_s, false);
}
void Preferences::setUseSeparateTrackerStatusFilter(const bool value)
{
if (value == useSeparateTrackerStatusFilter())
return;
setValue(u"TransferListFilters/SeparateTrackerStatusFilter"_s, value);
}
int Preferences::getTransSelFilter() const int Preferences::getTransSelFilter() const
{ {
return value<int>(u"TransferListFilters/selectedFilterIndex"_s, 0); return value<int>(u"TransferListFilters/selectedFilterIndex"_s, 0);

View File

@@ -137,11 +137,11 @@ public:
void setPreventFromSuspendWhenDownloading(bool b); void setPreventFromSuspendWhenDownloading(bool b);
bool preventFromSuspendWhenSeeding() const; bool preventFromSuspendWhenSeeding() const;
void setPreventFromSuspendWhenSeeding(bool b); void setPreventFromSuspendWhenSeeding(bool b);
QString getStyle() const;
void setStyle(const QString &styleName);
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
bool WinStartup() const; bool WinStartup() const;
void setWinStartup(bool b); void setWinStartup(bool b);
QString getStyle() const;
void setStyle(const QString &styleName);
#endif #endif
// Downloads // Downloads
@@ -287,6 +287,8 @@ public:
bool shutdownWhenDownloadsComplete() const; bool shutdownWhenDownloadsComplete() const;
void setShutdownWhenDownloadsComplete(bool shutdown); void setShutdownWhenDownloadsComplete(bool shutdown);
bool rebootWhenDownloadsComplete() const;
void setRebootWhenDownloadsComplete(bool reboot);
bool suspendWhenDownloadsComplete() const; bool suspendWhenDownloadsComplete() const;
void setSuspendWhenDownloadsComplete(bool suspend); void setSuspendWhenDownloadsComplete(bool suspend);
bool hibernateWhenDownloadsComplete() const; bool hibernateWhenDownloadsComplete() const;
@@ -400,6 +402,9 @@ public:
bool getCategoryFilterState() const; bool getCategoryFilterState() const;
bool getTagFilterState() const; bool getTagFilterState() const;
bool getTrackerFilterState() const; bool getTrackerFilterState() const;
bool getTrackerStatusFilterState() const;
bool useSeparateTrackerStatusFilter() const;
void setUseSeparateTrackerStatusFilter(bool value);
int getTransSelFilter() const; int getTransSelFilter() const;
void setTransSelFilter(int index); void setTransSelFilter(int index);
bool getHideZeroStatusFilters() const; bool getHideZeroStatusFilters() const;
@@ -449,6 +454,7 @@ public slots:
void setCategoryFilterState(bool checked); void setCategoryFilterState(bool checked);
void setTagFilterState(bool checked); void setTagFilterState(bool checked);
void setTrackerFilterState(bool checked); void setTrackerFilterState(bool checked);
void setTrackerStatusFilterState(bool checked);
void apply(); void apply();

View File

@@ -405,12 +405,16 @@ void Session::loadLegacy()
const QString feedUrl = Item::relativeName(legacyPath); const QString feedUrl = Item::relativeName(legacyPath);
for (const QString &folderPath : asConst(Item::expandPath(parentFolderPath))) for (const QString &folderPath : asConst(Item::expandPath(parentFolderPath)))
addFolder(folderPath); {
if (const auto result = addFolder(folderPath); !result)
LogMsg(tr("Failed to add RSS folder item. Reason: \"%1\"").arg(result.error()), Log::WARNING);
}
const QString feedPath = feedAliases[i].isEmpty() const QString feedPath = feedAliases[i].isEmpty()
? legacyPath ? legacyPath
: Item::joinPath(parentFolderPath, feedAliases[i]); : Item::joinPath(parentFolderPath, feedAliases[i]);
addFeed(feedUrl, feedPath); if (const auto result = addFeed(feedUrl, feedPath); !result)
LogMsg(tr("Failed to add RSS feed item. Reason: \"%1\"").arg(result.error()), Log::WARNING);
++i; ++i;
} }
} }

View File

@@ -54,6 +54,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QS
const QStringList params const QStringList params
{ {
Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG, Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG,
Utils::ForeignApps::PYTHON_UTF8_MODE_FLAG,
(SearchPluginManager::engineLocation() / Path(u"nova2dl.py"_s)).toString(), (SearchPluginManager::engineLocation() / Path(u"nova2dl.py"_s)).toString(),
pluginName, pluginName,
url url

View File

@@ -102,6 +102,7 @@ SearchHandler::SearchHandler(const QString &pattern, const QString &category, co
const QStringList params const QStringList params
{ {
Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG, Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG,
Utils::ForeignApps::PYTHON_UTF8_MODE_FLAG,
(SearchPluginManager::engineLocation() / Path(u"nova2.py"_s)).toString(), (SearchPluginManager::engineLocation() / Path(u"nova2.py"_s)).toString(),
m_usedPlugins.join(u','), m_usedPlugins.join(u','),
m_category m_category

View File

@@ -554,6 +554,7 @@ void SearchPluginManager::update()
const QStringList params const QStringList params
{ {
Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG, Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG,
Utils::ForeignApps::PYTHON_UTF8_MODE_FLAG,
(engineLocation() / Path(u"/nova2.py"_s)).toString(), (engineLocation() / Path(u"/nova2.py"_s)).toString(),
u"--capabilities"_s u"--capabilities"_s
}; };

View File

@@ -434,9 +434,9 @@ void TorrentFilesWatcher::Worker::processFolder(const Path &path, const Path &wa
} }
else else
{ {
if (!m_failedTorrents.value(path).contains(filePath)) if (!m_failedTorrents.value(watchedFolderPath).contains(filePath))
{ {
m_failedTorrents[path][filePath] = 0; m_failedTorrents[watchedFolderPath][filePath] = 0;
} }
} }
} }
@@ -471,7 +471,7 @@ void TorrentFilesWatcher::Worker::processFailedTorrents()
BitTorrent::AddTorrentParams addTorrentParams = options.addTorrentParams; BitTorrent::AddTorrentParams addTorrentParams = options.addTorrentParams;
if (torrentPath != watchedFolderPath) if (torrentPath != watchedFolderPath)
{ {
const Path subdirPath = watchedFolderPath.relativePathOf(torrentPath); const Path subdirPath = watchedFolderPath.relativePathOf(torrentPath).parentPath();
const bool useAutoTMM = addTorrentParams.useAutoTMM.value_or(!BitTorrent::Session::instance()->isAutoTMMDisabledByDefault()); const bool useAutoTMM = addTorrentParams.useAutoTMM.value_or(!BitTorrent::Session::instance()->isAutoTMMDisabledByDefault());
if (useAutoTMM) if (useAutoTMM)
{ {

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2014-2025 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
@@ -28,94 +28,57 @@
#include "torrentfilter.h" #include "torrentfilter.h"
#include "bittorrent/infohash.h" #include <algorithm>
#include "bittorrent/torrent.h"
#include <QUrl>
#include "base/bittorrent/infohash.h"
#include "base/bittorrent/torrent.h"
#include "base/bittorrent/trackerentrystatus.h"
#include "base/global.h"
using namespace BitTorrent;
const std::optional<QString> TorrentFilter::AnyCategory;
const std::optional<TorrentIDSet> TorrentFilter::AnyID; const std::optional<TorrentIDSet> TorrentFilter::AnyID;
const std::optional<QString> TorrentFilter::AnyCategory;
const std::optional<Tag> TorrentFilter::AnyTag; const std::optional<Tag> TorrentFilter::AnyTag;
const std::optional<QString> TorrentFilter::AnyTrackerHost;
const std::optional<TorrentAnnounceStatus> TorrentFilter::AnyAnnounceStatus;
const TorrentFilter TorrentFilter::DownloadingTorrent(TorrentFilter::Downloading); QString getTrackerHost(const QString &url)
const TorrentFilter TorrentFilter::SeedingTorrent(TorrentFilter::Seeding); {
const TorrentFilter TorrentFilter::CompletedTorrent(TorrentFilter::Completed); // We want the hostname.
const TorrentFilter TorrentFilter::StoppedTorrent(TorrentFilter::Stopped); if (const QString host = QUrl(url).host(); !host.isEmpty())
const TorrentFilter TorrentFilter::RunningTorrent(TorrentFilter::Running); return host;
const TorrentFilter TorrentFilter::ActiveTorrent(TorrentFilter::Active);
const TorrentFilter TorrentFilter::InactiveTorrent(TorrentFilter::Inactive);
const TorrentFilter TorrentFilter::StalledTorrent(TorrentFilter::Stalled);
const TorrentFilter TorrentFilter::StalledUploadingTorrent(TorrentFilter::StalledUploading);
const TorrentFilter TorrentFilter::StalledDownloadingTorrent(TorrentFilter::StalledDownloading);
const TorrentFilter TorrentFilter::CheckingTorrent(TorrentFilter::Checking);
const TorrentFilter TorrentFilter::MovingTorrent(TorrentFilter::Moving);
const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored);
using BitTorrent::Torrent; // If failed to parse the domain, original input should be returned
return url;
}
TorrentFilter::TorrentFilter(const Type type, const std::optional<TorrentIDSet> &idSet TorrentFilter::TorrentFilter(const Status status, const std::optional<TorrentIDSet> &idSet, const std::optional<QString> &category
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate) , const std::optional<Tag> &tag, const std::optional<bool> &isPrivate, const std::optional<QString> &trackerHost
: m_type {type} , const std::optional<TorrentAnnounceStatus> &announceStatus)
: m_status {status}
, m_category {category} , m_category {category}
, m_tag {tag} , m_tag {tag}
, m_idSet {idSet} , m_idSet {idSet}
, m_private {isPrivate} , m_private {isPrivate}
, m_trackerHost {trackerHost}
, m_announceStatus {announceStatus}
{ {
} }
TorrentFilter::TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet bool TorrentFilter::setStatus(const Status status)
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
: m_category {category}
, m_tag {tag}
, m_idSet {idSet}
, m_private {isPrivate}
{ {
setTypeByName(filter); if (m_status != status)
}
bool TorrentFilter::setType(Type type)
{
if (m_type != type)
{ {
m_type = type; m_status = status;
return true; return true;
} }
return false; return false;
} }
bool TorrentFilter::setTypeByName(const QString &filter)
{
Type type = All;
if (filter == u"downloading")
type = Downloading;
else if (filter == u"seeding")
type = Seeding;
else if (filter == u"completed")
type = Completed;
else if (filter == u"stopped")
type = Stopped;
else if (filter == u"running")
type = Running;
else if (filter == u"active")
type = Active;
else if (filter == u"inactive")
type = Inactive;
else if (filter == u"stalled")
type = Stalled;
else if (filter == u"stalled_uploading")
type = StalledUploading;
else if (filter == u"stalled_downloading")
type = StalledDownloading;
else if (filter == u"checking")
type = Checking;
else if (filter == u"moving")
type = Moving;
else if (filter == u"errored")
type = Errored;
return setType(type);
}
bool TorrentFilter::setTorrentIDSet(const std::optional<TorrentIDSet> &idSet) bool TorrentFilter::setTorrentIDSet(const std::optional<TorrentIDSet> &idSet)
{ {
if (m_idSet != idSet) if (m_idSet != idSet)
@@ -160,18 +123,43 @@ bool TorrentFilter::setPrivate(const std::optional<bool> isPrivate)
return false; return false;
} }
bool TorrentFilter::match(const Torrent *const torrent) const bool TorrentFilter::setTrackerHost(const std::optional<QString> &trackerHost)
{ {
if (!torrent) return false; if (m_trackerHost != trackerHost)
{
m_trackerHost = trackerHost;
return true;
}
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent) && matchPrivate(torrent)); return false;
} }
bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const bool TorrentFilter::setAnnounceStatus(const std::optional<TorrentAnnounceStatus> &announceStatus)
{ {
const BitTorrent::TorrentState state = torrent->state(); if (m_announceStatus != announceStatus)
{
m_announceStatus = announceStatus;
return true;
}
switch (m_type) return false;
}
bool TorrentFilter::match(const Torrent *const torrent) const
{
Q_ASSERT(torrent);
if (!torrent) [[unlikely]]
return false;
return (matchStatus(torrent) && matchHash(torrent) && matchCategory(torrent)
&& matchTag(torrent) && matchPrivate(torrent) && matchTracker(torrent));
}
bool TorrentFilter::matchStatus(const Torrent *const torrent) const
{
const TorrentState state = torrent->state();
switch (m_status)
{ {
case All: case All:
return true; return true;
@@ -190,16 +178,16 @@ bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
case Inactive: case Inactive:
return torrent->isInactive(); return torrent->isInactive();
case Stalled: case Stalled:
return (state == BitTorrent::TorrentState::StalledUploading) return (state == TorrentState::StalledUploading)
|| (state == BitTorrent::TorrentState::StalledDownloading); || (state == TorrentState::StalledDownloading);
case StalledUploading: case StalledUploading:
return state == BitTorrent::TorrentState::StalledUploading; return state == TorrentState::StalledUploading;
case StalledDownloading: case StalledDownloading:
return state == BitTorrent::TorrentState::StalledDownloading; return state == TorrentState::StalledDownloading;
case Checking: case Checking:
return (state == BitTorrent::TorrentState::CheckingUploading) return (state == TorrentState::CheckingUploading)
|| (state == BitTorrent::TorrentState::CheckingDownloading) || (state == TorrentState::CheckingDownloading)
|| (state == BitTorrent::TorrentState::CheckingResumeData); || (state == TorrentState::CheckingResumeData);
case Moving: case Moving:
return torrent->isMoving(); return torrent->isMoving();
case Errored: case Errored:
@@ -212,7 +200,7 @@ bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
return false; return false;
} }
bool TorrentFilter::matchHash(const BitTorrent::Torrent *const torrent) const bool TorrentFilter::matchHash(const Torrent *const torrent) const
{ {
if (!m_idSet) if (!m_idSet)
return true; return true;
@@ -220,7 +208,7 @@ bool TorrentFilter::matchHash(const BitTorrent::Torrent *const torrent) const
return m_idSet->contains(torrent->id()); return m_idSet->contains(torrent->id());
} }
bool TorrentFilter::matchCategory(const BitTorrent::Torrent *const torrent) const bool TorrentFilter::matchCategory(const Torrent *const torrent) const
{ {
if (!m_category) if (!m_category)
return true; return true;
@@ -228,7 +216,7 @@ bool TorrentFilter::matchCategory(const BitTorrent::Torrent *const torrent) cons
return (torrent->belongsToCategory(*m_category)); return (torrent->belongsToCategory(*m_category));
} }
bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const bool TorrentFilter::matchTag(const Torrent *const torrent) const
{ {
if (!m_tag) if (!m_tag)
return true; return true;
@@ -240,10 +228,65 @@ 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 bool TorrentFilter::matchPrivate(const Torrent *const torrent) const
{ {
if (!m_private) if (!m_private)
return true; return true;
return m_private == torrent->isPrivate(); return m_private == torrent->isPrivate();
} }
bool TorrentFilter::matchTracker(const Torrent *torrent) const
{
if (!m_trackerHost)
{
if (!m_announceStatus)
return true;
const TorrentAnnounceStatus announceStatus = torrent->announceStatus();
const TorrentAnnounceStatus &testAnnounceStatus = *m_announceStatus;
if (!testAnnounceStatus)
return !announceStatus;
return announceStatus.testAnyFlags(testAnnounceStatus);
}
// Trackerless torrent
if (m_trackerHost->isEmpty())
return torrent->trackers().isEmpty() && !m_announceStatus;
return std::ranges::any_of(asConst(torrent->trackers())
, [trackerHost = m_trackerHost, announceStatus = m_announceStatus](const TrackerEntryStatus &trackerEntryStatus)
{
if (getTrackerHost(trackerEntryStatus.url) != trackerHost)
return false;
if (!announceStatus)
return true;
switch (trackerEntryStatus.state)
{
case TrackerEndpointState::Working:
{
const bool hasWarningMessage = std::ranges::any_of(trackerEntryStatus.endpoints
, [](const TrackerEndpointStatus &endpointEntry)
{
return !endpointEntry.message.isEmpty() && (endpointEntry.state == TrackerEndpointState::Working);
});
return hasWarningMessage ? announceStatus->testFlag(TorrentAnnounceStatusFlag::HasWarning) : !*announceStatus;
}
case TrackerEndpointState::NotWorking:
case TrackerEndpointState::Unreachable:
return announceStatus->testFlag(TorrentAnnounceStatusFlag::HasOtherError);
case TrackerEndpointState::TrackerError:
return announceStatus->testFlag(TorrentAnnounceStatusFlag::HasTrackerError);
case TrackerEndpointState::NotContacted:
return false;
};
return false;
});
}

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2014 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2014-2025 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
@@ -34,6 +34,7 @@
#include <QString> #include <QString>
#include "base/bittorrent/infohash.h" #include "base/bittorrent/infohash.h"
#include "base/bittorrent/torrentannouncestatus.h"
#include "base/tag.h" #include "base/tag.h"
namespace BitTorrent namespace BitTorrent
@@ -46,7 +47,7 @@ using TorrentIDSet = QSet<BitTorrent::TorrentID>;
class TorrentFilter class TorrentFilter
{ {
public: public:
enum Type enum Status
{ {
All, All,
Downloading, Downloading,
@@ -67,57 +68,47 @@ public:
}; };
// These mean any permutation, including no category / tag. // These mean any permutation, including no category / tag.
static const std::optional<QString> AnyCategory;
static const std::optional<TorrentIDSet> AnyID; static const std::optional<TorrentIDSet> AnyID;
static const std::optional<QString> AnyCategory;
static const std::optional<Tag> AnyTag; static const std::optional<Tag> AnyTag;
static const std::optional<QString> AnyTrackerHost;
static const TorrentFilter DownloadingTorrent; static const std::optional<BitTorrent::TorrentAnnounceStatus> AnyAnnounceStatus;
static const TorrentFilter SeedingTorrent;
static const TorrentFilter CompletedTorrent;
static const TorrentFilter StoppedTorrent;
static const TorrentFilter RunningTorrent;
static const TorrentFilter ActiveTorrent;
static const TorrentFilter InactiveTorrent;
static const TorrentFilter StalledTorrent;
static const TorrentFilter StalledUploadingTorrent;
static const TorrentFilter StalledDownloadingTorrent;
static const TorrentFilter CheckingTorrent;
static const TorrentFilter MovingTorrent;
static const TorrentFilter ErroredTorrent;
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 TorrentFilter(Status status
, const std::optional<TorrentIDSet> &idSet = AnyID , const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory , const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tag = AnyTag , const std::optional<Tag> &tag = AnyTag
, std::optional<bool> isPrivate = {}); , const std::optional<bool> &isPrivate = {}
TorrentFilter(const QString &filter , const std::optional<QString> &trackerHost = AnyTrackerHost
, const std::optional<TorrentIDSet> &idSet = AnyID , const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus = AnyAnnounceStatus);
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tags = AnyTag
, std::optional<bool> isPrivate = {});
bool setStatus(Status status);
bool setType(Type type);
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 setPrivate(std::optional<bool> isPrivate);
bool setTrackerHost(const std::optional<QString> &trackerHost);
bool setAnnounceStatus(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus);
bool match(const BitTorrent::Torrent *torrent) const; bool match(const BitTorrent::Torrent *torrent) const;
private: private:
bool matchState(const BitTorrent::Torrent *torrent) const; bool matchStatus(const BitTorrent::Torrent *torrent) const;
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; bool matchPrivate(const BitTorrent::Torrent *torrent) const;
bool matchTracker(const BitTorrent::Torrent *torrent) const;
Type m_type {All}; Status m_status {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; std::optional<bool> m_private;
std::optional<QString> m_trackerHost;
std::optional<BitTorrent::TorrentAnnounceStatus> m_announceStatus;
}; };
QString getTrackerHost(const QString &url);

View File

@@ -37,7 +37,8 @@ enum class ShutdownDialogAction
Exit, Exit,
Shutdown, Shutdown,
Suspend, Suspend,
Hibernate Hibernate,
Reboot
}; };
using QStringMap = QMap<QString, QString>; using QStringMap = QMap<QString, QString>;

View File

@@ -38,6 +38,7 @@
namespace Utils::ForeignApps namespace Utils::ForeignApps
{ {
inline const QString PYTHON_ISOLATE_MODE_FLAG = u"-I"_s; inline const QString PYTHON_ISOLATE_MODE_FLAG = u"-I"_s;
inline const QString PYTHON_UTF8_MODE_FLAG = u"-Xutf8=1"_s;
struct PythonInfo struct PythonInfo
{ {

View File

@@ -87,22 +87,30 @@ void Utils::OS::shutdownComputer([[maybe_unused]] const ShutdownDialogAction &ac
{ {
::SetSuspendState(TRUE, FALSE, FALSE); ::SetSuspendState(TRUE, FALSE, FALSE);
} }
else else if (action == ShutdownDialogAction::Shutdown)
{ {
std::wstring msg = QCoreApplication::translate("misc" std::wstring msg = QCoreApplication::translate("misc"
, "qBittorrent will shutdown the computer now because all downloads are complete.").toStdWString(); , "qBittorrent will shutdown the computer now because all downloads are complete.").toStdWString();
::InitiateSystemShutdownW(nullptr, msg.data(), 10, TRUE, FALSE); ::InitiateSystemShutdownW(nullptr, msg.data(), 10, TRUE, FALSE);
} }
else if (action == ShutdownDialogAction::Reboot)
{
std::wstring msg = QCoreApplication::translate("misc"
, "qBittorrent will reboot the computer now because all downloads are complete.").toStdWString();
::InitiateSystemShutdownW(nullptr, msg.data(), 10, TRUE, TRUE);
}
// Disable shutdown privilege. // Disable shutdown privilege.
tkp.Privileges[0].Attributes = 0; tkp.Privileges[0].Attributes = 0;
::AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, NULL, 0); ::AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, NULL, 0);
#elif defined(Q_OS_MACOS) #elif defined(Q_OS_MACOS)
AEEventID EventToSend; AEEventID EventToSend {};
if (action != ShutdownDialogAction::Shutdown) if (action == ShutdownDialogAction::Suspend)
EventToSend = kAESleep; EventToSend = kAESleep;
else else if (action == ShutdownDialogAction::Reboot)
EventToSend = kAERestart;
else if (action == ShutdownDialogAction::Shutdown)
EventToSend = kAEShutDown; EventToSend = kAEShutDown;
AEAddressDesc targetDesc; AEAddressDesc targetDesc;
const ProcessSerialNumber kPSNOfSystemProcess = {0, kSystemProcess}; const ProcessSerialNumber kPSNOfSystemProcess = {0, kSystemProcess};
@@ -133,7 +141,7 @@ void Utils::OS::shutdownComputer([[maybe_unused]] const ShutdownDialogAction &ac
#elif defined(QBT_USES_DBUS) #elif defined(QBT_USES_DBUS)
// Use dbus to power off / suspend the system // Use dbus to power off / suspend the system
if (action != ShutdownDialogAction::Shutdown) if ((action == ShutdownDialogAction::Suspend) || (action == ShutdownDialogAction::Hibernate))
{ {
// Some recent systems use systemd's logind // Some recent systems use systemd's logind
QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s, QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s,
@@ -166,7 +174,7 @@ void Utils::OS::shutdownComputer([[maybe_unused]] const ShutdownDialogAction &ac
else else
halIface.call(u"Hibernate"_s); halIface.call(u"Hibernate"_s);
} }
else else if (action == ShutdownDialogAction::Shutdown)
{ {
// Some recent systems use systemd's logind // Some recent systems use systemd's logind
QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s, QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s,
@@ -190,6 +198,30 @@ void Utils::OS::shutdownComputer([[maybe_unused]] const ShutdownDialogAction &ac
QDBusConnection::systemBus()); QDBusConnection::systemBus());
halIface.call(u"Shutdown"_s); halIface.call(u"Shutdown"_s);
} }
else if (action == ShutdownDialogAction::Reboot)
{
// Some recent systems use systemd's logind
QDBusInterface login1Iface(u"org.freedesktop.login1"_s, u"/org/freedesktop/login1"_s,
u"org.freedesktop.login1.Manager"_s, QDBusConnection::systemBus());
if (login1Iface.isValid())
{
login1Iface.call(u"Reboot"_s, false);
return;
}
// Else, other recent systems use ConsoleKit
QDBusInterface consolekitIface(u"org.freedesktop.ConsoleKit"_s, u"/org/freedesktop/ConsoleKit/Manager"_s,
u"org.freedesktop.ConsoleKit.Manager"_s, QDBusConnection::systemBus());
if (consolekitIface.isValid())
{
consolekitIface.call(u"Restart"_s);
return;
}
// HAL (older systems)
QDBusInterface halIface(u"org.freedesktop.Hal"_s, u"/org/freedesktop/Hal/devices/computer"_s,
u"org.freedesktop.Hal.Device.SystemPowerManagement"_s,
QDBusConnection::systemBus());
halIface.call(u"Reboot"_s);
}
#endif #endif
} }
@@ -275,11 +307,11 @@ bool Utils::OS::applyMarkOfTheWeb(const Path &file, const QString &url)
const QByteArray zoneID = QByteArrayLiteral("[ZoneTransfer]\r\nZoneId=3\r\n") const QByteArray zoneID = QByteArrayLiteral("[ZoneTransfer]\r\nZoneId=3\r\n")
+ u"HostUrl=%1\r\n"_s.arg(hostURL).toUtf8(); + u"HostUrl=%1\r\n"_s.arg(hostURL).toUtf8();
if (LARGE_INTEGER streamSize = {0}; if (LARGE_INTEGER streamSize {};
::GetFileSizeEx(handle, &streamSize) && (streamSize.QuadPart > 0)) ::GetFileSizeEx(handle, &streamSize) && (streamSize.QuadPart > 0))
{ {
const DWORD expectedReadSize = std::min<LONGLONG>(streamSize.QuadPart, 1024); const DWORD expectedReadSize = std::min<LONGLONG>(streamSize.QuadPart, 1024);
QByteArray buf {expectedReadSize, '\0'}; QByteArray buf {static_cast<qsizetype>(expectedReadSize), '\0'};
if (DWORD actualReadSize = 0; if (DWORD actualReadSize = 0;
::ReadFile(handle, buf.data(), expectedReadSize, &actualReadSize, nullptr) && (actualReadSize == expectedReadSize)) ::ReadFile(handle, buf.data(), expectedReadSize, &actualReadSize, nullptr) && (actualReadSize == expectedReadSize))
@@ -289,14 +321,14 @@ bool Utils::OS::applyMarkOfTheWeb(const Path &file, const QString &url)
} }
} }
if (!::SetFilePointerEx(handle, {0}, nullptr, FILE_BEGIN)) if (!::SetFilePointerEx(handle, {}, nullptr, FILE_BEGIN))
return false; return false;
if (!::SetEndOfFile(handle)) if (!::SetEndOfFile(handle))
return false; return false;
DWORD written = 0; DWORD written = 0;
const BOOL writeResult = ::WriteFile(handle, zoneID.constData(), zoneID.size(), &written, nullptr); const BOOL writeResult = ::WriteFile(handle, zoneID.constData(), zoneID.size(), &written, nullptr);
return writeResult && (written == zoneID.size()); return writeResult && (static_cast<qsizetype>(written) == zoneID.size());
#endif #endif
} }
#endif // Q_OS_MACOS || Q_OS_WIN #endif // Q_OS_MACOS || Q_OS_WIN

View File

@@ -32,7 +32,7 @@
#define QBT_VERSION_MINOR 2 #define QBT_VERSION_MINOR 2
#define QBT_VERSION_BUGFIX 0 #define QBT_VERSION_BUGFIX 0
#define QBT_VERSION_BUILD 0 #define QBT_VERSION_BUILD 0
#define QBT_VERSION_STATUS "alpha1" // Should be empty for stable releases! #define QBT_VERSION_STATUS "beta1" // 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

@@ -129,7 +129,9 @@ add_library(qbt_gui STATIC
transferlistfilters/tagfilterproxymodel.h transferlistfilters/tagfilterproxymodel.h
transferlistfilters/tagfilterwidget.h transferlistfilters/tagfilterwidget.h
transferlistfilters/trackersfilterwidget.h transferlistfilters/trackersfilterwidget.h
transferlistfilters/trackerstatusfilterwidget.h
transferlistfilterswidget.h transferlistfilterswidget.h
transferlistfilterswidgetitem.h
transferlistmodel.h transferlistmodel.h
transferlistsortmodel.h transferlistsortmodel.h
transferlistwidget.h transferlistwidget.h
@@ -230,7 +232,9 @@ add_library(qbt_gui STATIC
transferlistfilters/tagfilterproxymodel.cpp transferlistfilters/tagfilterproxymodel.cpp
transferlistfilters/tagfilterwidget.cpp transferlistfilters/tagfilterwidget.cpp
transferlistfilters/trackersfilterwidget.cpp transferlistfilters/trackersfilterwidget.cpp
transferlistfilters/trackerstatusfilterwidget.cpp
transferlistfilterswidget.cpp transferlistfilterswidget.cpp
transferlistfilterswidgetitem.cpp
transferlistmodel.cpp transferlistmodel.cpp
transferlistsortmodel.cpp transferlistsortmodel.cpp
transferlistwidget.cpp transferlistwidget.cpp

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-2025 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
@@ -190,6 +190,7 @@ void AddTorrentParamsWidget::populate()
} }
populateDefaultPaths(); populateDefaultPaths();
resetShareLimitsWidgetDefaults();
}); });
m_ui->savePathEdit->disconnect(this); m_ui->savePathEdit->disconnect(this);
@@ -241,7 +242,7 @@ void AddTorrentParamsWidget::populate()
m_ui->startTorrentComboBox->disconnect(this); m_ui->startTorrentComboBox->disconnect(this);
m_ui->startTorrentComboBox->setCurrentIndex(m_addTorrentParams.addStopped m_ui->startTorrentComboBox->setCurrentIndex(m_addTorrentParams.addStopped
? m_ui->startTorrentComboBox->findData(!*m_addTorrentParams.addStopped) : 0); ? m_ui->startTorrentComboBox->findData(!*m_addTorrentParams.addStopped) : 0);
connect(m_ui->startTorrentComboBox, &QComboBox::currentIndexChanged, this, [this] connect(m_ui->startTorrentComboBox, &QComboBox::currentIndexChanged, this, [this]
{ {
const QVariant data = m_ui->startTorrentComboBox->currentData(); const QVariant data = m_ui->startTorrentComboBox->currentData();
@@ -270,6 +271,7 @@ void AddTorrentParamsWidget::populate()
m_addTorrentParams.addToQueueTop = data.toBool(); m_addTorrentParams.addToQueueTop = data.toBool();
}); });
resetShareLimitsWidgetDefaults();
m_ui->torrentShareLimitsWidget->setRatioLimit(m_addTorrentParams.ratioLimit); m_ui->torrentShareLimitsWidget->setRatioLimit(m_addTorrentParams.ratioLimit);
m_ui->torrentShareLimitsWidget->setSeedingTimeLimit(m_addTorrentParams.seedingTimeLimit); m_ui->torrentShareLimitsWidget->setSeedingTimeLimit(m_addTorrentParams.seedingTimeLimit);
m_ui->torrentShareLimitsWidget->setInactiveSeedingTimeLimit(m_addTorrentParams.inactiveSeedingTimeLimit); m_ui->torrentShareLimitsWidget->setInactiveSeedingTimeLimit(m_addTorrentParams.inactiveSeedingTimeLimit);
@@ -418,3 +420,11 @@ void AddTorrentParamsWidget::populateSavePathOptions()
populateDefaultPaths(); populateDefaultPaths();
} }
void AddTorrentParamsWidget::resetShareLimitsWidgetDefaults()
{
const auto *btSession = BitTorrent::Session::instance();
m_ui->torrentShareLimitsWidget->setDefaults((m_addTorrentParams.category.isEmpty() ? TorrentShareLimitsWidget::UsedDefaults::Global : TorrentShareLimitsWidget::UsedDefaults::Category)
, btSession->categoryRatioLimit(m_addTorrentParams.category), btSession->categorySeedingTimeLimit(m_addTorrentParams.category)
, btSession->categoryInactiveSeedingTimeLimit(m_addTorrentParams.category), btSession->categoryShareLimitAction(m_addTorrentParams.category));
}

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-2025 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
@@ -57,7 +57,7 @@ private:
void populateDefaultPaths(); void populateDefaultPaths();
void populateDefaultDownloadPath(); void populateDefaultDownloadPath();
void populateSavePathOptions(); void populateSavePathOptions();
void resetShareLimitsWidgetDefaults();
Ui::AddTorrentParamsWidget *m_ui; Ui::AddTorrentParamsWidget *m_ui;
BitTorrent::AddTorrentParams m_addTorrentParams; BitTorrent::AddTorrentParams m_addTorrentParams;

View File

@@ -149,7 +149,7 @@ namespace
OUTGOING_PORT_MIN, OUTGOING_PORT_MIN,
OUTGOING_PORT_MAX, OUTGOING_PORT_MAX,
UPNP_LEASE_DURATION, UPNP_LEASE_DURATION,
PEER_TOS, PEER_DSCP,
UTP_MIX_MODE, UTP_MIX_MODE,
HOSTNAME_CACHE_TTL, HOSTNAME_CACHE_TTL,
IDN_SUPPORT, IDN_SUPPORT,
@@ -276,7 +276,7 @@ void AdvancedSettings::saveAdvancedSettings() const
// UPnP lease duration // UPnP lease duration
session->setUPnPLeaseDuration(m_spinBoxUPnPLeaseDuration.value()); session->setUPnPLeaseDuration(m_spinBoxUPnPLeaseDuration.value());
// Type of service // Type of service
session->setPeerToS(m_spinBoxPeerToS.value()); session->setPeerDSCP(m_spinBoxPeerDSCP.value());
// uTP-TCP mixed mode // uTP-TCP mixed mode
session->setUtpMixedMode(m_comboBoxUtpMixedMode.currentData().value<BitTorrent::MixedModeAlgorithm>()); session->setUtpMixedMode(m_comboBoxUtpMixedMode.currentData().value<BitTorrent::MixedModeAlgorithm>());
// Hostname resolver cache TTL // Hostname resolver cache TTL
@@ -723,11 +723,11 @@ void AdvancedSettings::loadAdvancedSettings()
addRow(UPNP_LEASE_DURATION, (tr("UPnP lease duration [0: permanent lease]") + u' ' + makeLink(u"https://www.libtorrent.org/reference-Settings.html#upnp_lease_duration", u"(?)")) addRow(UPNP_LEASE_DURATION, (tr("UPnP lease duration [0: permanent lease]") + u' ' + makeLink(u"https://www.libtorrent.org/reference-Settings.html#upnp_lease_duration", u"(?)"))
, &m_spinBoxUPnPLeaseDuration); , &m_spinBoxUPnPLeaseDuration);
// Type of service // Type of service
m_spinBoxPeerToS.setMinimum(0); m_spinBoxPeerDSCP.setMinimum(0);
m_spinBoxPeerToS.setMaximum(255); m_spinBoxPeerDSCP.setMaximum(255);
m_spinBoxPeerToS.setValue(session->peerToS()); m_spinBoxPeerDSCP.setValue(session->peerDSCP());
addRow(PEER_TOS, (tr("Type of service (ToS) for connections to peers") + u' ' + makeLink(u"https://www.libtorrent.org/reference-Settings.html#peer_tos", u"(?)")) addRow(PEER_DSCP, (tr("Differentiated Services Code Point (DSCP) for connections to peers") + u' ' + makeLink(u"https://www.libtorrent.org/reference-Settings.html#peer_dscp", u"(?)"))
, &m_spinBoxPeerToS); , &m_spinBoxPeerDSCP);
// uTP-TCP mixed mode // uTP-TCP mixed mode
m_comboBoxUtpMixedMode.addItem(tr("Prefer TCP"), QVariant::fromValue(BitTorrent::MixedModeAlgorithm::TCP)); m_comboBoxUtpMixedMode.addItem(tr("Prefer TCP"), QVariant::fromValue(BitTorrent::MixedModeAlgorithm::TCP));
m_comboBoxUtpMixedMode.addItem(tr("Peer proportional (throttles TCP)"), QVariant::fromValue(BitTorrent::MixedModeAlgorithm::Proportional)); m_comboBoxUtpMixedMode.addItem(tr("Peer proportional (throttles TCP)"), QVariant::fromValue(BitTorrent::MixedModeAlgorithm::Proportional));

View File

@@ -70,7 +70,7 @@ private:
QSpinBox m_spinBoxSaveResumeDataInterval, m_spinBoxSaveStatisticsInterval, m_spinBoxTorrentFileSizeLimit, m_spinBoxBdecodeDepthLimit, m_spinBoxBdecodeTokenLimit, QSpinBox m_spinBoxSaveResumeDataInterval, m_spinBoxSaveStatisticsInterval, m_spinBoxTorrentFileSizeLimit, m_spinBoxBdecodeDepthLimit, m_spinBoxBdecodeTokenLimit,
m_spinBoxAsyncIOThreads, m_spinBoxFilePoolSize, m_spinBoxCheckingMemUsage, m_spinBoxDiskQueueSize, m_spinBoxAsyncIOThreads, m_spinBoxFilePoolSize, m_spinBoxCheckingMemUsage, m_spinBoxDiskQueueSize,
m_spinBoxOutgoingPortsMin, m_spinBoxOutgoingPortsMax, m_spinBoxUPnPLeaseDuration, m_spinBoxPeerToS, m_spinBoxHostnameCacheTTL, m_spinBoxOutgoingPortsMin, m_spinBoxOutgoingPortsMax, m_spinBoxUPnPLeaseDuration, m_spinBoxPeerDSCP, m_spinBoxHostnameCacheTTL,
m_spinBoxListRefresh, m_spinBoxTrackerPort, m_spinBoxSendBufferWatermark, m_spinBoxSendBufferLowWatermark, m_spinBoxListRefresh, m_spinBoxTrackerPort, m_spinBoxSendBufferWatermark, m_spinBoxSendBufferLowWatermark,
m_spinBoxSendBufferWatermarkFactor, m_spinBoxConnectionSpeed, m_spinBoxSocketSendBufferSize, m_spinBoxSocketReceiveBufferSize, m_spinBoxSocketBacklogSize, m_spinBoxSendBufferWatermarkFactor, m_spinBoxConnectionSpeed, m_spinBoxSocketSendBufferSize, m_spinBoxSocketReceiveBufferSize, m_spinBoxSocketBacklogSize,
m_spinBoxAnnouncePort, m_spinBoxMaxConcurrentHTTPAnnounces, m_spinBoxStopTrackerTimeout, m_spinBoxSessionShutdownTimeout, m_spinBoxAnnouncePort, m_spinBoxMaxConcurrentHTTPAnnounces, m_spinBoxStopTrackerTimeout, m_spinBoxSessionShutdownTimeout,

View File

@@ -388,10 +388,16 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
autoShutdownGroup->addAction(m_ui->actionAutoShutdown); autoShutdownGroup->addAction(m_ui->actionAutoShutdown);
autoShutdownGroup->addAction(m_ui->actionAutoSuspend); autoShutdownGroup->addAction(m_ui->actionAutoSuspend);
autoShutdownGroup->addAction(m_ui->actionAutoHibernate); autoShutdownGroup->addAction(m_ui->actionAutoHibernate);
autoShutdownGroup->addAction(m_ui->actionAutoReboot);
#if (!defined(Q_OS_UNIX) || defined(Q_OS_MACOS)) || defined(QBT_USES_DBUS) #if (!defined(Q_OS_UNIX) || defined(Q_OS_MACOS)) || defined(QBT_USES_DBUS)
m_ui->actionAutoShutdown->setChecked(pref->shutdownWhenDownloadsComplete()); m_ui->actionAutoShutdown->setChecked(pref->shutdownWhenDownloadsComplete());
m_ui->actionAutoReboot->setChecked(pref->rebootWhenDownloadsComplete());
m_ui->actionAutoSuspend->setChecked(pref->suspendWhenDownloadsComplete()); m_ui->actionAutoSuspend->setChecked(pref->suspendWhenDownloadsComplete());
m_ui->actionAutoHibernate->setChecked(pref->hibernateWhenDownloadsComplete()); m_ui->actionAutoHibernate->setChecked(pref->hibernateWhenDownloadsComplete());
#ifdef Q_OS_MACOS
// macOS doesn't support Hibernate via Apple Events API
m_ui->actionAutoHibernate->setDisabled(true);
#endif
#else #else
m_ui->actionAutoShutdown->setDisabled(true); m_ui->actionAutoShutdown->setDisabled(true);
m_ui->actionAutoSuspend->setDisabled(true); m_ui->actionAutoSuspend->setDisabled(true);
@@ -486,7 +492,7 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
m_transferListWidget->applyStatusFilter(pref->getTransSelFilter()); m_transferListWidget->applyStatusFilter(pref->getTransSelFilter());
m_transferListWidget->applyCategoryFilter(QString()); m_transferListWidget->applyCategoryFilter(QString());
m_transferListWidget->applyTagFilter(std::nullopt); m_transferListWidget->applyTagFilter(std::nullopt);
m_transferListWidget->applyTrackerFilterAll(); m_transferListWidget->applyTrackerFilter({});
} }
// Start watching the executable for updates // Start watching the executable for updates
@@ -1355,11 +1361,6 @@ void MainWindow::showFiltersSidebar(const bool show)
if (show && !m_transferListFiltersWidget) if (show && !m_transferListFiltersWidget)
{ {
m_transferListFiltersWidget = new TransferListFiltersWidget(m_splitter, m_transferListWidget, isDownloadTrackerFavicon()); m_transferListFiltersWidget = new TransferListFiltersWidget(m_splitter, m_transferListWidget, isDownloadTrackerFavicon());
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersAdded, m_transferListFiltersWidget, &TransferListFiltersWidget::addTrackers);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersRemoved, m_transferListFiltersWidget, &TransferListFiltersWidget::removeTrackers);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersChanged, m_transferListFiltersWidget, &TransferListFiltersWidget::refreshTrackers);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackerEntryStatusesUpdated, m_transferListFiltersWidget, &TransferListFiltersWidget::trackerEntryStatusesUpdated);
m_splitter->insertWidget(0, m_transferListFiltersWidget); m_splitter->insertWidget(0, m_transferListFiltersWidget);
m_splitter->setCollapsible(0, true); m_splitter->setCollapsible(0, true);
// From https://doc.qt.io/qt-5/qsplitter.html#setSizes: // From https://doc.qt.io/qt-5/qsplitter.html#setSizes:
@@ -1790,30 +1791,31 @@ void MainWindow::on_actionCriticalMessages_triggered(const bool checked)
setExecutionLogMsgTypes(flags); setExecutionLogMsgTypes(flags);
} }
void MainWindow::on_actionAutoExit_toggled(bool enabled) void MainWindow::on_actionAutoExit_toggled(const bool enabled)
{ {
qDebug() << Q_FUNC_INFO << enabled;
Preferences::instance()->setShutdownqBTWhenDownloadsComplete(enabled); Preferences::instance()->setShutdownqBTWhenDownloadsComplete(enabled);
} }
void MainWindow::on_actionAutoSuspend_toggled(bool enabled) void MainWindow::on_actionAutoSuspend_toggled(const bool enabled)
{ {
qDebug() << Q_FUNC_INFO << enabled;
Preferences::instance()->setSuspendWhenDownloadsComplete(enabled); Preferences::instance()->setSuspendWhenDownloadsComplete(enabled);
} }
void MainWindow::on_actionAutoHibernate_toggled(bool enabled) void MainWindow::on_actionAutoHibernate_toggled(const bool enabled)
{ {
qDebug() << Q_FUNC_INFO << enabled;
Preferences::instance()->setHibernateWhenDownloadsComplete(enabled); Preferences::instance()->setHibernateWhenDownloadsComplete(enabled);
} }
void MainWindow::on_actionAutoShutdown_toggled(bool enabled) void MainWindow::on_actionAutoShutdown_toggled(const bool enabled)
{ {
qDebug() << Q_FUNC_INFO << enabled;
Preferences::instance()->setShutdownWhenDownloadsComplete(enabled); Preferences::instance()->setShutdownWhenDownloadsComplete(enabled);
} }
void MainWindow::on_actionAutoReboot_toggled(const bool enabled)
{
Preferences::instance()->setRebootWhenDownloadsComplete(enabled);
}
void MainWindow::updatePowerManagementState() const void MainWindow::updatePowerManagementState() const
{ {
const auto *pref = Preferences::instance(); const auto *pref = Preferences::instance();

View File

@@ -158,6 +158,7 @@ private slots:
void on_actionAutoSuspend_toggled(bool); void on_actionAutoSuspend_toggled(bool);
void on_actionAutoHibernate_toggled(bool); void on_actionAutoHibernate_toggled(bool);
void on_actionAutoShutdown_toggled(bool); void on_actionAutoShutdown_toggled(bool);
void on_actionAutoReboot_toggled(bool);
void on_actionAbout_triggered(); void on_actionAbout_triggered();
void on_actionStatistics_triggered(); void on_actionStatistics_triggered();
void on_actionCreateTorrent_triggered(); void on_actionCreateTorrent_triggered();

View File

@@ -76,6 +76,7 @@
<addaction name="actionAutoExit"/> <addaction name="actionAutoExit"/>
<addaction name="actionAutoSuspend"/> <addaction name="actionAutoSuspend"/>
<addaction name="actionAutoHibernate"/> <addaction name="actionAutoHibernate"/>
<addaction name="actionAutoReboot"/>
<addaction name="actionAutoShutdown"/> <addaction name="actionAutoShutdown"/>
</widget> </widget>
<addaction name="actionCreateTorrent"/> <addaction name="actionCreateTorrent"/>
@@ -397,6 +398,14 @@
<string>Sh&amp;utdown System</string> <string>Sh&amp;utdown System</string>
</property> </property>
</action> </action>
<action name="actionAutoReboot">
<property name="checkable">
<bool>true</bool>
</property>
<property name="text">
<string>&amp;Reboot System</string>
</property>
</action>
<action name="actionAutoShutdownDisabled"> <action name="actionAutoShutdownDisabled">
<property name="checkable"> <property name="checkable">
<bool>true</bool> <bool>true</bool>

View File

@@ -43,15 +43,12 @@
#include <QEvent> #include <QEvent>
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QStyleFactory>
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
#include <QTranslator> #include <QTranslator>
#ifdef Q_OS_WIN
#include <QStyleFactory>
#endif
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/sharelimitaction.h" #include "base/bittorrent/sharelimits.h"
#include "base/exceptions.h" #include "base/exceptions.h"
#include "base/global.h" #include "base/global.h"
#include "base/net/downloadmanager.h" #include "base/net/downloadmanager.h"
@@ -125,12 +122,17 @@ namespace
} }
}; };
bool isValidWebUIUsername(const QString &username) bool isValidWebUIUsernameLength(const QString &username)
{ {
return (username.length() >= WEBUI_MIN_USERNAME_LENGTH); return (username.length() >= WEBUI_MIN_USERNAME_LENGTH);
} }
bool isValidWebUIPassword(const QString &password) bool isValidWebUIUsernameCharacterSet(const QString &username)
{
return !username.contains(u":");
}
bool isValidWebUIPasswordLength(const QString &password)
{ {
return (password.length() >= WEBUI_MIN_PASSWORD_LENGTH); return (password.length() >= WEBUI_MIN_PASSWORD_LENGTH);
} }
@@ -274,7 +276,7 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->checkAltRowColors->setChecked(pref->useAlternatingRowColors()); m_ui->checkAltRowColors->setChecked(pref->useAlternatingRowColors());
m_ui->checkUseTorrentStatesColors->setChecked(pref->useTorrentStatesColors()); m_ui->checkUseTorrentStatesColors->setChecked(pref->useTorrentStatesColors());
m_ui->checkProgressBarFollowsTextColor->setChecked(pref->getProgressBarFollowsTextColor()); m_ui->checkProgressBarFollowsTextColor->setChecked(pref->getProgressBarFollowsTextColor());
m_ui->checkProgressBarFollowsTextColor->setEnabled(m_ui->checkProgressBarFollowsTextColor->isChecked()); m_ui->checkProgressBarFollowsTextColor->setEnabled(m_ui->checkUseTorrentStatesColors->isChecked());
m_ui->checkHideZero->setChecked(pref->getHideZeroValues()); m_ui->checkHideZero->setChecked(pref->getHideZeroValues());
m_ui->comboHideZero->setCurrentIndex(pref->getHideZeroComboValues()); m_ui->comboHideZero->setCurrentIndex(pref->getHideZeroComboValues());
m_ui->comboHideZero->setEnabled(m_ui->checkHideZero->isChecked()); m_ui->comboHideZero->setEnabled(m_ui->checkHideZero->isChecked());
@@ -300,6 +302,7 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->actionTorrentFnOnDblClBox->setCurrentIndex(m_ui->actionTorrentFnOnDblClBox->findData(actionSeeding)); m_ui->actionTorrentFnOnDblClBox->setCurrentIndex(m_ui->actionTorrentFnOnDblClBox->findData(actionSeeding));
m_ui->checkBoxHideZeroStatusFilters->setChecked(pref->getHideZeroStatusFilters()); m_ui->checkBoxHideZeroStatusFilters->setChecked(pref->getHideZeroStatusFilters());
m_ui->checkBoxUseSeparateTrackerStatusFilter->setChecked(pref->useSeparateTrackerStatusFilter());
m_ui->checkTorrentContentDrag->setChecked(pref->isTorrentContentDragEnabled()); m_ui->checkTorrentContentDrag->setChecked(pref->isTorrentContentDragEnabled());
@@ -376,11 +379,7 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->checkBoxPerformanceWarning->setChecked(session->isPerformanceWarningEnabled()); m_ui->checkBoxPerformanceWarning->setChecked(session->isPerformanceWarningEnabled());
connect(m_ui->comboLanguage, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->comboLanguage, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
#ifdef Q_OS_WIN
connect(m_ui->comboStyle, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->comboStyle, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
#endif
#ifdef QBT_HAS_COLORSCHEME_OPTION #ifdef QBT_HAS_COLORSCHEME_OPTION
connect(m_ui->comboColorScheme, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->comboColorScheme, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
#endif #endif
@@ -414,6 +413,7 @@ void OptionsDialog::loadBehaviorTabOptions()
connect(m_ui->actionTorrentDlOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->actionTorrentDlOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->actionTorrentFnOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->actionTorrentFnOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkBoxHideZeroStatusFilters, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkBoxHideZeroStatusFilters, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkBoxUseSeparateTrackerStatusFilter, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkTorrentContentDrag, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkTorrentContentDrag, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
@@ -488,9 +488,7 @@ void OptionsDialog::saveBehaviorTabOptions() const
} }
pref->setLocale(locale); pref->setLocale(locale);
#ifdef Q_OS_WIN
pref->setStyle(m_ui->comboStyle->currentData().toString()); pref->setStyle(m_ui->comboStyle->currentData().toString());
#endif
#ifdef QBT_HAS_COLORSCHEME_OPTION #ifdef QBT_HAS_COLORSCHEME_OPTION
UIThemeManager::instance()->setColorScheme(m_ui->comboColorScheme->currentData().value<ColorScheme>()); UIThemeManager::instance()->setColorScheme(m_ui->comboColorScheme->currentData().value<ColorScheme>());
@@ -513,6 +511,7 @@ void OptionsDialog::saveBehaviorTabOptions() const
pref->setActionOnDblClOnTorrentFn(m_ui->actionTorrentFnOnDblClBox->currentData().toInt()); pref->setActionOnDblClOnTorrentFn(m_ui->actionTorrentFnOnDblClBox->currentData().toInt());
pref->setHideZeroStatusFilters(m_ui->checkBoxHideZeroStatusFilters->isChecked()); pref->setHideZeroStatusFilters(m_ui->checkBoxHideZeroStatusFilters->isChecked());
pref->setUseSeparateTrackerStatusFilter(m_ui->checkBoxUseSeparateTrackerStatusFilter->isChecked());
pref->setTorrentContentDragEnabled(m_ui->checkTorrentContentDrag->isChecked()); pref->setTorrentContentDragEnabled(m_ui->checkTorrentContentDrag->isChecked());
@@ -631,7 +630,6 @@ void OptionsDialog::loadDownloadsTabOptions()
m_ui->comboCategoryChanged->setCurrentIndex(session->isDisableAutoTMMWhenCategorySavePathChanged()); m_ui->comboCategoryChanged->setCurrentIndex(session->isDisableAutoTMMWhenCategorySavePathChanged());
m_ui->comboCategoryDefaultPathChanged->setCurrentIndex(session->isDisableAutoTMMWhenDefaultSavePathChanged()); m_ui->comboCategoryDefaultPathChanged->setCurrentIndex(session->isDisableAutoTMMWhenDefaultSavePathChanged());
m_ui->checkUseSubcategories->setChecked(session->isSubcategoriesEnabled());
m_ui->checkUseCategoryPaths->setChecked(session->useCategoryPathsInManualMode()); m_ui->checkUseCategoryPaths->setChecked(session->useCategoryPathsInManualMode());
m_ui->textSavePath->setDialogCaption(tr("Choose a save directory")); m_ui->textSavePath->setDialogCaption(tr("Choose a save directory"));
@@ -736,7 +734,6 @@ void OptionsDialog::loadDownloadsTabOptions()
connect(m_ui->comboCategoryChanged, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->comboCategoryChanged, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->comboCategoryDefaultPathChanged, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton); connect(m_ui->comboCategoryDefaultPathChanged, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkUseSubcategories, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkUseCategoryPaths, &QAbstractButton::toggled, this, &ThisType::enableApplyButton); connect(m_ui->checkUseCategoryPaths, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->textSavePath, &FileSystemPathEdit::selectedPathChanged, this, &ThisType::enableApplyButton); connect(m_ui->textSavePath, &FileSystemPathEdit::selectedPathChanged, this, &ThisType::enableApplyButton);
@@ -808,7 +805,6 @@ void OptionsDialog::saveDownloadsTabOptions() const
session->setDisableAutoTMMWhenCategorySavePathChanged(m_ui->comboCategoryChanged->currentIndex() == 1); session->setDisableAutoTMMWhenCategorySavePathChanged(m_ui->comboCategoryChanged->currentIndex() == 1);
session->setDisableAutoTMMWhenDefaultSavePathChanged(m_ui->comboCategoryDefaultPathChanged->currentIndex() == 1); session->setDisableAutoTMMWhenDefaultSavePathChanged(m_ui->comboCategoryDefaultPathChanged->currentIndex() == 1);
session->setSubcategoriesEnabled(m_ui->checkUseSubcategories->isChecked());
session->setUseCategoryPathsInManualMode(m_ui->checkUseCategoryPaths->isChecked()); session->setUseCategoryPathsInManualMode(m_ui->checkUseCategoryPaths->isChecked());
session->setSavePath(Path(m_ui->textSavePath->selectedPath())); session->setSavePath(Path(m_ui->textSavePath->selectedPath()));
@@ -1126,6 +1122,8 @@ void OptionsDialog::loadBittorrentTabOptions()
m_ui->spinUploadRateForSlowTorrents->setValue(session->uploadRateForSlowTorrents()); m_ui->spinUploadRateForSlowTorrents->setValue(session->uploadRateForSlowTorrents());
m_ui->spinSlowTorrentsInactivityTimer->setValue(session->slowTorrentsInactivityTimer()); m_ui->spinSlowTorrentsInactivityTimer->setValue(session->slowTorrentsInactivityTimer());
m_ui->spinMaxRatio->setMaximum(std::numeric_limits<int>::max());
if (session->globalMaxRatio() >= 0.) if (session->globalMaxRatio() >= 0.)
{ {
// Enable // Enable
@@ -1170,7 +1168,7 @@ void OptionsDialog::loadBittorrentTabOptions()
const QHash<BitTorrent::ShareLimitAction, int> actIndex = const QHash<BitTorrent::ShareLimitAction, int> actIndex =
{ {
{BitTorrent::ShareLimitAction::Stop, 0}, {BitTorrent::ShareLimitAction::Stop, 0},
{BitTorrent::ShareLimitAction::Remove, 1}, {BitTorrent::ShareLimitAction::Remove, 1},
{BitTorrent::ShareLimitAction::RemoveWithContent, 2}, {BitTorrent::ShareLimitAction::RemoveWithContent, 2},
{BitTorrent::ShareLimitAction::EnableSuperSeeding, 3} {BitTorrent::ShareLimitAction::EnableSuperSeeding, 3}
@@ -1451,9 +1449,9 @@ void OptionsDialog::saveWebUITabOptions() const
pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()}); pref->setWebUIBanDuration(std::chrono::seconds {m_ui->spinBanDuration->value()});
pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value()); pref->setWebUISessionTimeout(m_ui->spinSessionTimeout->value());
// Authentication // Authentication
if (const QString username = webUIUsername(); isValidWebUIUsername(username)) if (const QString username = webUIUsername(); isValidWebUIUsernameLength(username) && isValidWebUIUsernameCharacterSet(username))
pref->setWebUIUsername(username); pref->setWebUIUsername(username);
if (const QString password = webUIPassword(); isValidWebUIPassword(password)) if (const QString password = webUIPassword(); isValidWebUIPasswordLength(password))
pref->setWebUIPassword(Utils::Password::PBKDF2::generate(password)); pref->setWebUIPassword(Utils::Password::PBKDF2::generate(password));
pref->setWebUILocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked()); pref->setWebUILocalAuthEnabled(!m_ui->checkBypassLocalAuth->isChecked());
pref->setWebUIAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked()); pref->setWebUIAuthSubnetWhitelistEnabled(m_ui->checkBypassAuthSubnetWhitelist->isChecked());
@@ -1846,6 +1844,11 @@ void OptionsDialog::initializeStyleCombo()
#ifdef Q_OS_WIN #ifdef Q_OS_WIN
m_ui->labelStyleHint->setText(tr("%1 is recommended for best compatibility with Windows dark mode" m_ui->labelStyleHint->setText(tr("%1 is recommended for best compatibility with Windows dark mode"
, "Fusion is recommended for best compatibility with Windows dark mode").arg(u"Fusion"_s)); , "Fusion is recommended for best compatibility with Windows dark mode").arg(u"Fusion"_s));
#else
m_ui->labelStyleHint->hide();
m_ui->layoutStyle->removeWidget(m_ui->labelStyleHint);
#endif
m_ui->comboStyle->addItem(tr("System", "System default Qt style"), u"system"_s); m_ui->comboStyle->addItem(tr("System", "System default Qt style"), u"system"_s);
m_ui->comboStyle->setItemData(0, tr("Let Qt decide the style for this system"), Qt::ToolTipRole); m_ui->comboStyle->setItemData(0, tr("Let Qt decide the style for this system"), Qt::ToolTipRole);
m_ui->comboStyle->insertSeparator(1); m_ui->comboStyle->insertSeparator(1);
@@ -1859,14 +1862,6 @@ void OptionsDialog::initializeStyleCombo()
const QString selectedStyleName = prefStyleName.isEmpty() ? QApplication::style()->name() : prefStyleName; const QString selectedStyleName = prefStyleName.isEmpty() ? QApplication::style()->name() : prefStyleName;
const int styleIndex = m_ui->comboStyle->findData(selectedStyleName, Qt::UserRole, Qt::MatchFixedString); const int styleIndex = m_ui->comboStyle->findData(selectedStyleName, Qt::UserRole, Qt::MatchFixedString);
m_ui->comboStyle->setCurrentIndex(std::max(0, styleIndex)); m_ui->comboStyle->setCurrentIndex(std::max(0, styleIndex));
#else
m_ui->labelStyle->hide();
m_ui->comboStyle->hide();
m_ui->labelStyleHint->hide();
m_ui->layoutStyle->removeWidget(m_ui->labelStyle);
m_ui->layoutStyle->removeWidget(m_ui->comboStyle);
m_ui->layoutStyle->removeWidget(m_ui->labelStyleHint);
#endif
} }
void OptionsDialog::initializeColorSchemeOptions() void OptionsDialog::initializeColorSchemeOptions()
@@ -2116,14 +2111,20 @@ QString OptionsDialog::webUIPassword() const
bool OptionsDialog::webUIAuthenticationOk() bool OptionsDialog::webUIAuthenticationOk()
{ {
if (!isValidWebUIUsername(webUIUsername())) const QString username = webUIUsername();
if (!isValidWebUIUsernameLength(username))
{ {
QMessageBox::warning(this, tr("Length Error"), tr("The WebUI username must be at least 3 characters long.")); QMessageBox::warning(this, tr("Length Error"), tr("The WebUI username must be at least 3 characters long."));
return false; return false;
} }
if (!isValidWebUIUsernameCharacterSet(username))
{
QMessageBox::warning(this, tr("Character Error"), tr("The WebUI username must not contain a colon."));
return false;
}
const bool dontChangePassword = webUIPassword().isEmpty() && !Preferences::instance()->getWebUIPassword().isEmpty(); const bool dontChangePassword = webUIPassword().isEmpty() && !Preferences::instance()->getWebUIPassword().isEmpty();
if (!isValidWebUIPassword(webUIPassword()) && !dontChangePassword) if (!isValidWebUIPasswordLength(webUIPassword()) && !dontChangePassword)
{ {
QMessageBox::warning(this, tr("Length Error"), tr("The WebUI password must be at least 6 characters long.")); QMessageBox::warning(this, tr("Length Error"), tr("The WebUI password must be at least 6 characters long."));
return false; return false;

View File

@@ -456,6 +456,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QCheckBox" name="checkBoxUseSeparateTrackerStatusFilter">
<property name="toolTip">
<string>Use separate &quot;Tracker status&quot; filter. Otherwise it gets merged with &quot;Trackers&quot; filter.</string>
</property>
<property name="text">
<string>Use separate &quot;Tracker status&quot; filter</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
</item> </item>
@@ -1371,13 +1381,6 @@ Manual: Various torrent properties (e.g. save path) must be assigned manually</s
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QCheckBox" name="checkUseSubcategories">
<property name="text">
<string>Use Subcategories</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QCheckBox" name="checkUseCategoryPaths"> <widget class="QCheckBox" name="checkUseCategoryPaths">
<property name="toolTip"> <property name="toolTip">
@@ -1908,7 +1911,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<number>2</number> <number>2</number>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>2000</number> <number>2147483647</number>
</property> </property>
<property name="value"> <property name="value">
<number>500</number> <number>500</number>
@@ -1944,7 +1947,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<number>2</number> <number>2</number>
</property> </property>
<property name="maximum"> <property name="maximum">
<number>2000</number> <number>2147483647</number>
</property> </property>
<property name="value"> <property name="value">
<number>100</number> <number>100</number>
@@ -1961,7 +1964,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<item row="2" column="1"> <item row="2" column="1">
<widget class="QSpinBox" name="spinMaxUploads"> <widget class="QSpinBox" name="spinMaxUploads">
<property name="maximum"> <property name="maximum">
<number>2000</number> <number>2147483647</number>
</property> </property>
<property name="value"> <property name="value">
<number>8</number> <number>8</number>
@@ -1978,7 +1981,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<item row="3" column="1"> <item row="3" column="1">
<widget class="QSpinBox" name="spinMaxUploadsPerTorrent"> <widget class="QSpinBox" name="spinMaxUploadsPerTorrent">
<property name="maximum"> <property name="maximum">
<number>500</number> <number>2147483647</number>
</property> </property>
<property name="value"> <property name="value">
<number>4</number> <number>4</number>
@@ -3789,9 +3792,6 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
<layout class="QHBoxLayout" name="horizontalLayoutAPIKey"> <layout class="QHBoxLayout" name="horizontalLayoutAPIKey">
<item> <item>
<widget class="QLineEdit" name="textWebUIAPIKey"> <widget class="QLineEdit" name="textWebUIAPIKey">
<property name="enabled">
<bool>false</bool>
</property>
<property name="readOnly"> <property name="readOnly">
<bool>true</bool> <bool>true</bool>
</property> </property>
@@ -3805,89 +3805,35 @@ Specify an IPv4 or IPv6 address. You can specify &quot;0.0.0.0&quot; for any IPv
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Copy API key</string> <string>Copy API key</string>
</property> </property>
<property name="text">
<string/>
</property>
<property name="icon"> <property name="icon">
<iconset resource="../icons.qrc"> <iconset>
<normaloff>:/icons/edit-copy.svg</normaloff>:/icons/edit-copy.svg</iconset> <normaloff>:/icons/edit-copy.svg</normaloff>:/icons/edit-copy.svg</iconset>
</property> </property>
<property name="flat">
<bool>false</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="btnWebUIAPIKeyRotate"> <widget class="QPushButton" name="btnWebUIAPIKeyRotate">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Generate API key</string> <string>Generate API key</string>
</property> </property>
<property name="text">
<string/>
</property>
<property name="icon"> <property name="icon">
<iconset resource="../icons.qrc"> <iconset>
<normaloff>:/icons/view-refresh.svg</normaloff>:/icons/view-refresh.svg</iconset> <normaloff>:/icons/view-refresh.svg</normaloff>:/icons/view-refresh.svg</iconset>
</property> </property>
<property name="flat">
<bool>false</bool>
</property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="btnWebUIAPIKeyDelete"> <widget class="QPushButton" name="btnWebUIAPIKeyDelete">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Delete API key</string> <string>Delete API key</string>
</property> </property>
<property name="text">
<string/>
</property>
<property name="icon"> <property name="icon">
<iconset resource="../icons.qrc"> <iconset>
<normaloff>:/icons/list-remove.svg</normaloff>:/icons/list-remove.svg</iconset> <normaloff>:/icons/list-remove.svg</normaloff>:/icons/list-remove.svg</iconset>
</property> </property>
<property name="flat">
<bool>false</bool>
</property>
</widget> </widget>
</item> </item>
</layout> </layout>

View File

@@ -85,6 +85,8 @@ namespace
} }
} }
using Utils::Misc::SizeUnit;
SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidget *parent) SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidget *parent)
: GUIApplicationComponent(app, parent) : GUIApplicationComponent(app, parent)
, m_nameFilteringMode {u"Search/FilteringMode"_s} , m_nameFilteringMode {u"Search/FilteringMode"_s}
@@ -99,6 +101,8 @@ SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidge
header()->setStretchLastSection(false); header()->setStretchLastSection(false);
header()->setTextElideMode(Qt::ElideRight); header()->setTextElideMode(Qt::ElideRight);
fillFilterComboBoxes();
// Set Search results list model // Set Search results list model
m_searchListModel = new QStandardItemModel(0, SearchSortModel::NB_SEARCH_COLUMNS, this); m_searchListModel = new QStandardItemModel(0, SearchSortModel::NB_SEARCH_COLUMNS, this);
m_searchListModel->setHeaderData(SearchSortModel::NAME, Qt::Horizontal, tr("Name", "i.e: file name")); m_searchListModel->setHeaderData(SearchSortModel::NAME, Qt::Horizontal, tr("Name", "i.e: file name"));
@@ -116,6 +120,13 @@ SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidge
m_proxyModel = new SearchSortModel(this); m_proxyModel = new SearchSortModel(this);
m_proxyModel->setDynamicSortFilter(true); m_proxyModel->setDynamicSortFilter(true);
m_proxyModel->setSourceModel(m_searchListModel); m_proxyModel->setSourceModel(m_searchListModel);
m_proxyModel->enableNameFilter(m_nameFilteringMode.get(NameFilteringMode::OnlyNames) == NameFilteringMode::OnlyNames);
m_proxyModel->setSeedsFilter(m_ui->minSeeds->value(), m_ui->maxSeeds->value());
m_proxyModel->setSizeFilter(sizeInBytes(m_ui->minSize->value(), static_cast<SizeUnit>(m_ui->minSizeUnit->currentIndex()))
, sizeInBytes(m_ui->maxSize->value(), static_cast<SizeUnit>(m_ui->maxSizeUnit->currentIndex())));
updateResultsCount();
m_ui->resultsBrowser->setModel(m_proxyModel); m_ui->resultsBrowser->setModel(m_proxyModel);
m_ui->resultsBrowser->hideColumn(SearchSortModel::DL_LINK); // Hide url column m_ui->resultsBrowser->hideColumn(SearchSortModel::DL_LINK); // Hide url column
@@ -154,8 +165,6 @@ SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidge
connect(header(), &QHeaderView::sectionMoved, this, &SearchJobWidget::saveSettings); connect(header(), &QHeaderView::sectionMoved, this, &SearchJobWidget::saveSettings);
connect(header(), &QHeaderView::sortIndicatorChanged, this, &SearchJobWidget::saveSettings); connect(header(), &QHeaderView::sortIndicatorChanged, this, &SearchJobWidget::saveSettings);
fillFilterComboBoxes();
m_lineEditSearchResultsFilter = new LineEdit(this); m_lineEditSearchResultsFilter = new LineEdit(this);
m_lineEditSearchResultsFilter->setPlaceholderText(tr("Filter search results...")); m_lineEditSearchResultsFilter->setPlaceholderText(tr("Filter search results..."));
m_lineEditSearchResultsFilter->setContextMenuPolicy(Qt::CustomContextMenu); m_lineEditSearchResultsFilter->setContextMenuPolicy(Qt::CustomContextMenu);
@@ -163,24 +172,17 @@ SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidge
connect(m_lineEditSearchResultsFilter, &LineEdit::textChanged, this, &SearchJobWidget::filterSearchResults); connect(m_lineEditSearchResultsFilter, &LineEdit::textChanged, this, &SearchJobWidget::filterSearchResults);
m_ui->horizontalLayout->insertWidget(0, m_lineEditSearchResultsFilter); m_ui->horizontalLayout->insertWidget(0, m_lineEditSearchResultsFilter);
connect(m_ui->filterMode, qOverload<int>(&QComboBox::currentIndexChanged) connect(m_ui->filterMode, qOverload<int>(&QComboBox::currentIndexChanged), this, &SearchJobWidget::updateNameFilter);
, this, &SearchJobWidget::updateFilter); connect(m_ui->minSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSeedsFilter);
connect(m_ui->minSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter); connect(m_ui->minSeeds, qOverload<int>(&QSpinBox::valueChanged), this, &SearchJobWidget::updateSeedsFilter);
connect(m_ui->minSeeds, qOverload<int>(&QSpinBox::valueChanged) connect(m_ui->maxSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSeedsFilter);
, this, &SearchJobWidget::updateFilter); connect(m_ui->maxSeeds, qOverload<int>(&QSpinBox::valueChanged), this, &SearchJobWidget::updateSeedsFilter);
connect(m_ui->maxSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter); connect(m_ui->minSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->maxSeeds, qOverload<int>(&QSpinBox::valueChanged) connect(m_ui->minSize, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &SearchJobWidget::updateSizeFilter);
, this, &SearchJobWidget::updateFilter); connect(m_ui->maxSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->minSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter); connect(m_ui->maxSize, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->minSize, qOverload<double>(&QDoubleSpinBox::valueChanged) connect(m_ui->minSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged), this, &SearchJobWidget::updateSizeFilter);
, this, &SearchJobWidget::updateFilter); connect(m_ui->maxSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged), this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->maxSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
connect(m_ui->maxSize, qOverload<double>(&QDoubleSpinBox::valueChanged)
, this, &SearchJobWidget::updateFilter);
connect(m_ui->minSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged)
, this, &SearchJobWidget::updateFilter);
connect(m_ui->maxSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged)
, this, &SearchJobWidget::updateFilter);
connect(m_ui->resultsBrowser, &QAbstractItemView::doubleClicked, this, &SearchJobWidget::onItemDoubleClicked); connect(m_ui->resultsBrowser, &QAbstractItemView::doubleClicked, this, &SearchJobWidget::onItemDoubleClicked);
@@ -193,7 +195,6 @@ SearchJobWidget::SearchJobWidget(const QString &id, const QString &searchPattern
{ {
m_searchPattern = searchPattern; m_searchPattern = searchPattern;
m_proxyModel->setNameFilter(m_searchPattern); m_proxyModel->setNameFilter(m_searchPattern);
updateFilter();
appendSearchResults(searchResults); appendSearchResults(searchResults);
} }
@@ -301,8 +302,8 @@ void SearchJobWidget::assignSearchHandler(SearchHandler *searchHandler)
m_searchPattern = m_searchHandler->pattern(); m_searchPattern = m_searchHandler->pattern();
m_proxyModel->setNameFilter(m_searchPattern); m_proxyModel->setNameFilter(m_searchPattern);
updateFilter();
updateResultsCount();
setStatus(Status::Ongoing); setStatus(Status::Ongoing);
} }
@@ -454,26 +455,34 @@ void SearchJobWidget::updateResultsCount()
emit resultsCountUpdated(); emit resultsCountUpdated();
} }
void SearchJobWidget::updateFilter() void SearchJobWidget::updateNameFilter()
{ {
using Utils::Misc::SizeUnit; const auto filteringMode = static_cast<NameFilteringMode>(m_ui->filterMode->itemData(m_ui->filterMode->currentIndex()).toInt());
m_proxyModel->enableNameFilter(filteringMode == NameFilteringMode::OnlyNames);
m_nameFilteringMode = filteringMode;
m_proxyModel->enableNameFilter(filteringMode() == NameFilteringMode::OnlyNames); updateResultsCount();
}
void SearchJobWidget::updateSeedsFilter()
{
// we update size and seeds filter parameters in the model even if they are disabled // we update size and seeds filter parameters in the model even if they are disabled
m_proxyModel->setSeedsFilter(m_ui->minSeeds->value(), m_ui->maxSeeds->value()); m_proxyModel->setSeedsFilter(m_ui->minSeeds->value(), m_ui->maxSeeds->value());
m_proxyModel->setSizeFilter(
sizeInBytes(m_ui->minSize->value(), static_cast<SizeUnit>(m_ui->minSizeUnit->currentIndex())),
sizeInBytes(m_ui->maxSize->value(), static_cast<SizeUnit>(m_ui->maxSizeUnit->currentIndex())));
m_nameFilteringMode = filteringMode(); updateResultsCount();
}
void SearchJobWidget::updateSizeFilter()
{
// we update size and seeds filter parameters in the model even if they are disabled
m_proxyModel->setSizeFilter(sizeInBytes(m_ui->minSize->value(), static_cast<SizeUnit>(m_ui->minSizeUnit->currentIndex()))
, sizeInBytes(m_ui->maxSize->value(), static_cast<SizeUnit>(m_ui->maxSizeUnit->currentIndex())));
m_proxyModel->invalidate();
updateResultsCount(); updateResultsCount();
} }
void SearchJobWidget::fillFilterComboBoxes() void SearchJobWidget::fillFilterComboBoxes()
{ {
using Utils::Misc::SizeUnit;
using Utils::Misc::unitString; using Utils::Misc::unitString;
QStringList unitStrings; QStringList unitStrings;
@@ -500,16 +509,15 @@ void SearchJobWidget::fillFilterComboBoxes()
m_ui->filterMode->addItem(tr("Torrent names only"), static_cast<int>(NameFilteringMode::OnlyNames)); m_ui->filterMode->addItem(tr("Torrent names only"), static_cast<int>(NameFilteringMode::OnlyNames));
m_ui->filterMode->addItem(tr("Everywhere"), static_cast<int>(NameFilteringMode::Everywhere)); m_ui->filterMode->addItem(tr("Everywhere"), static_cast<int>(NameFilteringMode::Everywhere));
const auto selectedFilteringMode = static_cast<int>(m_nameFilteringMode.get(NameFilteringMode::OnlyNames));
const QVariant selectedMode = static_cast<int>(m_nameFilteringMode.get(NameFilteringMode::OnlyNames)); const int index = m_ui->filterMode->findData(selectedFilteringMode);
const int index = m_ui->filterMode->findData(selectedMode);
m_ui->filterMode->setCurrentIndex((index == -1) ? 0 : index); m_ui->filterMode->setCurrentIndex((index == -1) ? 0 : index);
} }
void SearchJobWidget::filterSearchResults(const QString &name) void SearchJobWidget::filterSearchResults(const QString &name)
{ {
const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForSearchJob() const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForSearchJob()
? name : Utils::String::wildcardToRegexPattern(name)); ? name : Utils::String::wildcardToRegexPattern(name));
m_proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption)); m_proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
updateResultsCount(); updateResultsCount();
} }
@@ -557,11 +565,6 @@ void SearchJobWidget::contextMenuEvent(QContextMenuEvent *event)
menu->popup(event->globalPos()); menu->popup(event->globalPos());
} }
SearchJobWidget::NameFilteringMode SearchJobWidget::filteringMode() const
{
return static_cast<NameFilteringMode>(m_ui->filterMode->itemData(m_ui->filterMode->currentIndex()).toInt());
}
void SearchJobWidget::loadSettings() void SearchJobWidget::loadSettings()
{ {
header()->restoreState(Preferences::instance()->getSearchTabHeaderState()); header()->restoreState(Preferences::instance()->getSearchTabHeaderState());

View File

@@ -106,7 +106,9 @@ private:
void loadSettings(); void loadSettings();
void saveSettings() const; void saveSettings() const;
void updateFilter(); void updateNameFilter();
void updateSeedsFilter();
void updateSizeFilter();
void filterSearchResults(const QString &name); void filterSearchResults(const QString &name);
void showFilterContextMenu(); void showFilterContextMenu();
void contextMenuEvent(QContextMenuEvent *event) override; void contextMenuEvent(QContextMenuEvent *event) override;
@@ -119,7 +121,6 @@ private:
void downloadTorrent(const QModelIndex &rowIndex, AddTorrentOption option = AddTorrentOption::Default); void downloadTorrent(const QModelIndex &rowIndex, AddTorrentOption option = AddTorrentOption::Default);
void addTorrentToSession(const QString &source, AddTorrentOption option = AddTorrentOption::Default); void addTorrentToSession(const QString &source, AddTorrentOption option = AddTorrentOption::Default);
void fillFilterComboBoxes(); void fillFilterComboBoxes();
NameFilteringMode filteringMode() const;
QHeaderView *header() const; QHeaderView *header() const;
int visibleColumnsCount() const; int visibleColumnsCount() const;
void setRowColor(int row, const QColor &color); void setRowColor(int row, const QColor &color);

View File

@@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2013 sledgehammer999 <hammered999@gmail.com> * Copyright (C) 2013 sledgehammer999 <hammered999@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
@@ -39,34 +40,103 @@ SearchSortModel::SearchSortModel(QObject *parent)
void SearchSortModel::enableNameFilter(const bool enabled) void SearchSortModel::enableNameFilter(const bool enabled)
{ {
if (m_isNameFilterEnabled == enabled)
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
m_isNameFilterEnabled = enabled; m_isNameFilterEnabled = enabled;
endFilterChange(Direction::Rows);
#else
m_isNameFilterEnabled = enabled;
invalidateRowsFilter();
#endif
} }
void SearchSortModel::setNameFilter(const QString &searchTerm) void SearchSortModel::setNameFilter(const QString &searchTerm)
{ {
if (m_searchTerm == searchTerm)
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
m_searchTerm = searchTerm; m_searchTerm = searchTerm;
if ((searchTerm.length() > 2) && searchTerm.startsWith(u'"') && searchTerm.endsWith(u'"')) if ((searchTerm.length() > 2) && searchTerm.startsWith(u'"') && searchTerm.endsWith(u'"'))
m_searchTermWords = QStringList(m_searchTerm.sliced(1, (m_searchTerm.length() - 2))); m_searchTermWords = QStringList(m_searchTerm.sliced(1, (m_searchTerm.length() - 2)));
else else
m_searchTermWords = searchTerm.split(u' ', Qt::SkipEmptyParts); m_searchTermWords = searchTerm.split(u' ', Qt::SkipEmptyParts);
endFilterChange(Direction::Rows);
#else
m_searchTerm = searchTerm;
if ((searchTerm.length() > 2) && searchTerm.startsWith(u'"') && searchTerm.endsWith(u'"'))
m_searchTermWords = QStringList(m_searchTerm.sliced(1, (m_searchTerm.length() - 2)));
else
m_searchTermWords = searchTerm.split(u' ', Qt::SkipEmptyParts);
invalidateRowsFilter();
#endif
} }
void SearchSortModel::setSizeFilter(const qint64 minSize, const qint64 maxSize) void SearchSortModel::setSizeFilter(qint64 minSize, qint64 maxSize)
{ {
m_minSize = std::max(static_cast<qint64>(0), minSize); minSize = std::max<qint64>(0, minSize);
m_maxSize = std::max(static_cast<qint64>(-1), maxSize); maxSize = std::max<qint64>(-1, maxSize);
if ((m_minSize == minSize) && (m_maxSize == maxSize))
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
m_minSize = minSize;
m_maxSize = maxSize;
endFilterChange(Direction::Rows);
#else
m_minSize = minSize;
m_maxSize = maxSize;
invalidateRowsFilter();
#endif
} }
void SearchSortModel::setSeedsFilter(const int minSeeds, const int maxSeeds) void SearchSortModel::setSeedsFilter(int minSeeds, int maxSeeds)
{ {
m_minSeeds = std::max(0, minSeeds); minSeeds = std::max(0, minSeeds);
m_maxSeeds = std::max(-1, maxSeeds); maxSeeds = std::max(-1, maxSeeds);
if ((m_minSeeds == minSeeds) && (m_maxSeeds == maxSeeds))
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
m_minSeeds = minSeeds;
m_maxSeeds = maxSeeds;
endFilterChange(Direction::Rows);
#else
m_minSeeds = minSeeds;
m_maxSeeds = maxSeeds;
invalidateRowsFilter();
#endif
} }
void SearchSortModel::setLeechesFilter(const int minLeeches, const int maxLeeches) void SearchSortModel::setLeechesFilter(int minLeeches, int maxLeeches)
{ {
m_minLeeches = std::max(0, minLeeches); minLeeches = std::max(0, minLeeches);
m_maxLeeches = std::max(-1, maxLeeches); maxLeeches = std::max(-1, maxLeeches);
if ((m_minLeeches == minLeeches) && (m_maxLeeches == maxLeeches))
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
m_minLeeches = minLeeches;
m_maxLeeches = maxLeeches;
endFilterChange(Direction::Rows);
#else
m_minLeeches = minLeeches;
m_maxLeeches = maxLeeches;
invalidateRowsFilter();
#endif
} }
bool SearchSortModel::isNameFilterEnabled() const bool SearchSortModel::isNameFilterEnabled() const

View File

@@ -1,5 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2013 sledgehammer999 <hammered999@gmail.com> * Copyright (C) 2013 sledgehammer999 <hammered999@gmail.com>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or

View File

@@ -134,6 +134,11 @@ void ShutdownConfirmDialog::initText()
okButton->setText(tr("&Hibernate Now")); okButton->setText(tr("&Hibernate Now"));
setWindowTitle(tr("Hibernate confirmation")); setWindowTitle(tr("Hibernate confirmation"));
break; break;
case ShutdownDialogAction::Reboot:
m_msg = tr("The computer is going to reboot.");
okButton->setText(tr("&Reboot Now"));
setWindowTitle(tr("Reboot confirmation"));
break;
} }
m_msg += u'\n'; m_msg += u'\n';

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017, 2021 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2017-2025 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
@@ -32,11 +32,12 @@
#include <QPushButton> #include <QPushButton>
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/utils/fs.h" #include "base/bittorrent/torrent.h"
#include "torrentsharelimitswidget.h"
#include "ui_torrentcategorydialog.h" #include "ui_torrentcategorydialog.h"
TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent) TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent)
: QDialog {parent} : QDialog(parent)
, m_ui {new Ui::TorrentCategoryDialog} , m_ui {new Ui::TorrentCategoryDialog}
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
@@ -56,6 +57,15 @@ TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent)
connect(m_ui->textCategoryName, &QLineEdit::textChanged, this, &TorrentCategoryDialog::categoryNameChanged); connect(m_ui->textCategoryName, &QLineEdit::textChanged, this, &TorrentCategoryDialog::categoryNameChanged);
connect(m_ui->comboUseDownloadPath, &QComboBox::currentIndexChanged, this, &TorrentCategoryDialog::useDownloadPathChanged); connect(m_ui->comboUseDownloadPath, &QComboBox::currentIndexChanged, this, &TorrentCategoryDialog::useDownloadPathChanged);
resetShareLimitsWidgetDefaults();
}
TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent, const QString &categoryName, const BitTorrent::CategoryOptions &categoryOptions)
: TorrentCategoryDialog(parent)
{
setCategoryName(categoryName);
setCategoryOptions(categoryOptions);
} }
TorrentCategoryDialog::~TorrentCategoryDialog() TorrentCategoryDialog::~TorrentCategoryDialog()
@@ -73,8 +83,7 @@ QString TorrentCategoryDialog::createCategory(QWidget *parent, const QString &pa
newCategoryName += u'/'; newCategoryName += u'/';
newCategoryName += tr("New Category"); newCategoryName += tr("New Category");
TorrentCategoryDialog dialog {parent}; TorrentCategoryDialog dialog {parent, newCategoryName, {}};
dialog.setCategoryName(newCategoryName);
while (dialog.exec() == TorrentCategoryDialog::Accepted) while (dialog.exec() == TorrentCategoryDialog::Accepted)
{ {
newCategoryName = dialog.categoryName(); newCategoryName = dialog.categoryName();
@@ -110,14 +119,12 @@ void TorrentCategoryDialog::editCategory(QWidget *parent, const QString &categor
Q_ASSERT(Session::instance()->categories().contains(categoryName)); Q_ASSERT(Session::instance()->categories().contains(categoryName));
auto *dialog = new TorrentCategoryDialog(parent); auto *dialog = new TorrentCategoryDialog(parent, categoryName, Session::instance()->categoryOptions(categoryName));
dialog->setAttribute(Qt::WA_DeleteOnClose); dialog->setAttribute(Qt::WA_DeleteOnClose);
dialog->setCategoryNameEditable(false); dialog->setCategoryNameEditable(false);
dialog->setCategoryName(categoryName);
dialog->setCategoryOptions(Session::instance()->categoryOptions(categoryName));
connect(dialog, &TorrentCategoryDialog::accepted, parent, [dialog, categoryName]() connect(dialog, &TorrentCategoryDialog::accepted, parent, [dialog, categoryName]()
{ {
Session::instance()->editCategory(categoryName, dialog->categoryOptions()); Session::instance()->setCategoryOptions(categoryName, dialog->categoryOptions());
}); });
dialog->open(); dialog->open();
} }
@@ -149,6 +156,11 @@ BitTorrent::CategoryOptions TorrentCategoryDialog::categoryOptions() const
else if (m_ui->comboUseDownloadPath->currentIndex() == 2) else if (m_ui->comboUseDownloadPath->currentIndex() == 2)
categoryOptions.downloadPath = {false, {}}; categoryOptions.downloadPath = {false, {}};
categoryOptions.ratioLimit = m_ui->torrentShareLimitsWidget->ratioLimit().value_or(BitTorrent::DEFAULT_RATIO_LIMIT);
categoryOptions.seedingTimeLimit = m_ui->torrentShareLimitsWidget->seedingTimeLimit().value_or(BitTorrent::DEFAULT_SEEDING_TIME_LIMIT);
categoryOptions.inactiveSeedingTimeLimit = m_ui->torrentShareLimitsWidget->inactiveSeedingTimeLimit().value_or(BitTorrent::DEFAULT_SEEDING_TIME_LIMIT);
categoryOptions.shareLimitAction = m_ui->torrentShareLimitsWidget->shareLimitAction().value_or(BitTorrent::ShareLimitAction::Default);
return categoryOptions; return categoryOptions;
} }
@@ -165,6 +177,11 @@ void TorrentCategoryDialog::setCategoryOptions(const BitTorrent::CategoryOptions
m_ui->comboUseDownloadPath->setCurrentIndex(0); m_ui->comboUseDownloadPath->setCurrentIndex(0);
m_ui->comboDownloadPath->setSelectedPath({}); m_ui->comboDownloadPath->setSelectedPath({});
} }
m_ui->torrentShareLimitsWidget->setRatioLimit(categoryOptions.ratioLimit);
m_ui->torrentShareLimitsWidget->setSeedingTimeLimit(categoryOptions.seedingTimeLimit);
m_ui->torrentShareLimitsWidget->setInactiveSeedingTimeLimit(categoryOptions.inactiveSeedingTimeLimit);
m_ui->torrentShareLimitsWidget->setShareLimitAction(categoryOptions.shareLimitAction);
} }
void TorrentCategoryDialog::categoryNameChanged(const QString &categoryName) void TorrentCategoryDialog::categoryNameChanged(const QString &categoryName)
@@ -177,6 +194,13 @@ void TorrentCategoryDialog::categoryNameChanged(const QString &categoryName)
if (useDownloadPath) if (useDownloadPath)
m_ui->comboDownloadPath->setPlaceholder(btSession->categoryDownloadPath(categoryName, categoryOptions())); m_ui->comboDownloadPath->setPlaceholder(btSession->categoryDownloadPath(categoryName, categoryOptions()));
const QString parentCategoryName = BitTorrent::Session::parentCategoryName(categoryName);
if (m_parentCategoryName != parentCategoryName)
{
m_parentCategoryName = parentCategoryName;
resetShareLimitsWidgetDefaults();
}
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!categoryName.isEmpty()); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!categoryName.isEmpty());
} }
@@ -195,3 +219,11 @@ void TorrentCategoryDialog::useDownloadPathChanged(const int index)
const Path categoryPath = btSession->categoryDownloadPath(categoryName, categoryOptions()); const Path categoryPath = btSession->categoryDownloadPath(categoryName, categoryOptions());
m_ui->comboDownloadPath->setPlaceholder(useDownloadPath ? categoryPath : Path()); m_ui->comboDownloadPath->setPlaceholder(useDownloadPath ? categoryPath : Path());
} }
void TorrentCategoryDialog::resetShareLimitsWidgetDefaults()
{
const auto *btSession = BitTorrent::Session::instance();
m_ui->torrentShareLimitsWidget->setDefaults((m_parentCategoryName.isEmpty() ? TorrentShareLimitsWidget::UsedDefaults::Global : TorrentShareLimitsWidget::UsedDefaults::Category)
, btSession->categoryRatioLimit(m_parentCategoryName), btSession->categorySeedingTimeLimit(m_parentCategoryName)
, btSession->categoryInactiveSeedingTimeLimit(m_parentCategoryName), btSession->categoryShareLimitAction(m_parentCategoryName));
}

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017, 2021 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2017-2025 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
@@ -52,6 +52,7 @@ public:
static void editCategory(QWidget *parent, const QString &categoryName); static void editCategory(QWidget *parent, const QString &categoryName);
explicit TorrentCategoryDialog(QWidget *parent = nullptr); explicit TorrentCategoryDialog(QWidget *parent = nullptr);
TorrentCategoryDialog(QWidget *parent, const QString &categoryName, const BitTorrent::CategoryOptions &categoryOptions);
~TorrentCategoryDialog() override; ~TorrentCategoryDialog() override;
void setCategoryNameEditable(bool editable); void setCategoryNameEditable(bool editable);
@@ -65,6 +66,9 @@ private slots:
void useDownloadPathChanged(int index); void useDownloadPathChanged(int index);
private: private:
void resetShareLimitsWidgetDefaults();
Ui::TorrentCategoryDialog *m_ui = nullptr; Ui::TorrentCategoryDialog *m_ui = nullptr;
Path m_lastEnteredDownloadPath; Path m_lastEnteredDownloadPath;
QString m_parentCategoryName;
}; };

View File

@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>493</width> <width>493</width>
<height>208</height> <height>268</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -140,6 +140,18 @@
</layout> </layout>
</widget> </widget>
</item> </item>
<item>
<widget class="QGroupBox" name="torrentShareLimitsBox">
<property name="title">
<string>Torrent share limits</string>
</property>
<layout class="QVBoxLayout" name="torrentShareLimitsBoxLayout">
<item>
<widget class="TorrentShareLimitsWidget" name="torrentShareLimitsWidget" native="true"/>
</item>
</layout>
</widget>
</item>
<item> <item>
<spacer name="verticalSpacer_2"> <spacer name="verticalSpacer_2">
<property name="orientation"> <property name="orientation">
@@ -172,6 +184,12 @@
<header>gui/fspathedit.h</header> <header>gui/fspathedit.h</header>
<container>1</container> <container>1</container>
</customwidget> </customwidget>
<customwidget>
<class>TorrentShareLimitsWidget</class>
<extends>QWidget</extends>
<header>gui/torrentsharelimitswidget.h</header>
<container>1</container>
</customwidget>
</customwidgets> </customwidgets>
<resources/> <resources/>
<connections/> <connections/>

View File

@@ -112,7 +112,7 @@ QString TorrentContentModelItem::displayData(const int column) const
case BitTorrent::DownloadPriority::Mixed: case BitTorrent::DownloadPriority::Mixed:
return tr("Mixed", "Mixed (priorities"); return tr("Mixed", "Mixed (priorities");
case BitTorrent::DownloadPriority::Ignored: case BitTorrent::DownloadPriority::Ignored:
return tr("Not downloaded"); return tr("Do not download", "Do not download (priority)");
case BitTorrent::DownloadPriority::High: case BitTorrent::DownloadPriority::High:
return tr("High", "High (priority)"); return tr("High", "High (priority)");
case BitTorrent::DownloadPriority::Maximum: case BitTorrent::DownloadPriority::Maximum:

View File

@@ -492,14 +492,14 @@ void TorrentContentWidget::openItem(const QModelIndex &index) const
Utils::Gui::openPath(getFullPath(index)); Utils::Gui::openPath(getFullPath(index));
} }
void TorrentContentWidget::openParentFolder(const QModelIndex &index) const void TorrentContentWidget::openParentFolder(const QModelIndex &index)
{ {
const Path path = getFullPath(index); const Path path = getFullPath(index);
m_model->contentHandler()->flushCache(); // Flush data m_model->contentHandler()->flushCache(); // Flush data
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
MacUtils::openFiles({path}); MacUtils::openFiles({path});
#else #else
Utils::Gui::openFolderSelect(path); Utils::Gui::openFolderSelect(path, this);
#endif #endif
} }

View File

@@ -113,7 +113,7 @@ private:
void displayColumnHeaderMenu(); void displayColumnHeaderMenu();
void displayContextMenu(); void displayContextMenu();
void openItem(const QModelIndex &index) const; void openItem(const QModelIndex &index) const;
void openParentFolder(const QModelIndex &index) const; void openParentFolder(const QModelIndex &index);
void openSelectedFile(); void openSelectedFile();
void renameSelectedFile(); void renameSelectedFile();
void applyPriorities(BitTorrent::DownloadPriority priority); void applyPriorities(BitTorrent::DownloadPriority priority);

View File

@@ -31,13 +31,17 @@
#include "torrentcreatordialog.h" #include "torrentcreatordialog.h"
#include <functional>
#include <QCloseEvent> #include <QCloseEvent>
#include <QFileDialog> #include <QFileDialog>
#include <QMessageBox> #include <QMessageBox>
#include <QMimeData> #include <QMimeData>
#include <QThread>
#include <QUrl> #include <QUrl>
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrentcreator.h"
#include "base/bittorrent/torrentdescriptor.h" #include "base/bittorrent/torrentdescriptor.h"
#include "base/global.h" #include "base/global.h"
#include "base/utils/fs.h" #include "base/utils/fs.h"
@@ -57,6 +61,37 @@ namespace
#else #else
const QFileDialog::Options FILE_DIALOG_OPTIONS {}; const QFileDialog::Options FILE_DIALOG_OPTIONS {};
#endif #endif
class PieceCalculationThread final : public QThread
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(PieceCalculationThread)
public:
using CalcFunc = std::function<int ()>;
explicit PieceCalculationThread(CalcFunc calc, QObject *parent = nullptr)
: QThread(parent)
, m_calc {std::move(calc)}
{
}
~PieceCalculationThread() override
{
wait();
}
signals:
void resultReady(int pieces);
private:
void run() override
{
emit resultReady(m_calc());
}
CalcFunc m_calc;
};
} }
TorrentCreatorDialog::TorrentCreatorDialog(QWidget *parent, const Path &defaultPath) TorrentCreatorDialog::TorrentCreatorDialog(QWidget *parent, const Path &defaultPath)
@@ -98,7 +133,7 @@ TorrentCreatorDialog::TorrentCreatorDialog(QWidget *parent, const Path &defaultP
connect(m_ui->addFolderButton, &QPushButton::clicked, this, &TorrentCreatorDialog::onAddFolderButtonClicked); connect(m_ui->addFolderButton, &QPushButton::clicked, this, &TorrentCreatorDialog::onAddFolderButtonClicked);
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &TorrentCreatorDialog::onCreateButtonClicked); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &TorrentCreatorDialog::onCreateButtonClicked);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_ui->buttonCalcTotalPieces, &QPushButton::clicked, this, &TorrentCreatorDialog::updatePiecesCount); connect(m_ui->buttonCalcTotalPieces, &QPushButton::clicked, this, &TorrentCreatorDialog::onCalculatePiecesButtonClicked);
connect(m_ui->checkStartSeeding, &QCheckBox::clicked, m_ui->checkIgnoreShareLimits, &QWidget::setEnabled); connect(m_ui->checkStartSeeding, &QCheckBox::clicked, m_ui->checkIgnoreShareLimits, &QWidget::setEnabled);
loadSettings(); loadSettings();
@@ -192,6 +227,41 @@ void TorrentCreatorDialog::dragEnterEvent(QDragEnterEvent *event)
event->acceptProposedAction(); event->acceptProposedAction();
} }
void TorrentCreatorDialog::onCalculatePiecesButtonClicked()
{
m_ui->buttonCalcTotalPieces->setEnabled(false);
m_ui->labelTotalPieces->setText(tr("Calculating..."));
#ifdef QBT_USES_LIBTORRENT2
PieceCalculationThread::CalcFunc calc = [path = m_ui->textInputPath->selectedPath()
, pieceSize = getPieceSize()
, torrentFormat = getTorrentFormat()]() -> int
{
return BitTorrent::TorrentCreator::calculateTotalPieces(path, pieceSize, torrentFormat);
};
#else
PieceCalculationThread::CalcFunc calc = [path = m_ui->textInputPath->selectedPath()
, pieceSize = getPieceSize()
, isAlignmentOptimized = m_ui->checkOptimizeAlignment->isChecked()
, paddedFileSizeLimit = getPaddedFileSizeLimit()]() -> int
{
return BitTorrent::TorrentCreator::calculateTotalPieces(path, pieceSize, isAlignmentOptimized, paddedFileSizeLimit);
};
#endif
// since the calculation (in libtorrent) cannot be interrupted, always let it run to completion and
// not managed by `parent`
auto *thread = new PieceCalculationThread(std::move(calc), nullptr);
thread->setObjectName("PieceCalculationThread thread");
connect(thread, &PieceCalculationThread::finished, thread, &QObject::deleteLater);
connect(thread, &PieceCalculationThread::resultReady, this, [this](const int pieces)
{
m_ui->labelTotalPieces->setText(QString::number(pieces));
m_ui->buttonCalcTotalPieces->setEnabled(true);
});
thread->start();
}
// Main function that create a .torrent file // Main function that create a .torrent file
void TorrentCreatorDialog::onCreateButtonClicked() void TorrentCreatorDialog::onCreateButtonClicked()
{ {
@@ -277,9 +347,9 @@ void TorrentCreatorDialog::handleCreationSuccess(const BitTorrent::TorrentCreato
params.skipChecking = true; params.skipChecking = true;
if (m_ui->checkIgnoreShareLimits->isChecked()) if (m_ui->checkIgnoreShareLimits->isChecked())
{ {
params.ratioLimit = BitTorrent::Torrent::NO_RATIO_LIMIT; params.ratioLimit = BitTorrent::NO_RATIO_LIMIT;
params.seedingTimeLimit = BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT; params.seedingTimeLimit = BitTorrent::NO_SEEDING_TIME_LIMIT;
params.inactiveSeedingTimeLimit = BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT; params.inactiveSeedingTimeLimit = BitTorrent::NO_SEEDING_TIME_LIMIT;
} }
params.useAutoTMM = false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path params.useAutoTMM = false; // otherwise if it is on by default, it will overwrite `savePath` to the default save path
@@ -298,20 +368,6 @@ void TorrentCreatorDialog::updateProgressBar(int progress)
m_ui->progressBar->setValue(progress); m_ui->progressBar->setValue(progress);
} }
void TorrentCreatorDialog::updatePiecesCount()
{
const Path path = m_ui->textInputPath->selectedPath();
#ifdef QBT_USES_LIBTORRENT2
const int count = BitTorrent::TorrentCreator::calculateTotalPieces(
path, getPieceSize(), getTorrentFormat());
#else
const bool isAlignmentOptimized = m_ui->checkOptimizeAlignment->isChecked();
const int count = BitTorrent::TorrentCreator::calculateTotalPieces(path
, getPieceSize(), isAlignmentOptimized, getPaddedFileSizeLimit());
#endif
m_ui->labelTotalPieces->setText(QString::number(count));
}
void TorrentCreatorDialog::setInteractionEnabled(const bool enabled) const void TorrentCreatorDialog::setInteractionEnabled(const bool enabled) const
{ {
m_ui->textInputPath->setEnabled(enabled); m_ui->textInputPath->setEnabled(enabled);
@@ -382,3 +438,5 @@ void TorrentCreatorDialog::loadSettings()
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid()) if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize); resize(dialogSize);
} }
#include "torrentcreatordialog.moc"

View File

@@ -33,10 +33,15 @@
#include <QDialog> #include <QDialog>
#include <QThreadPool> #include <QThreadPool>
#include "base/bittorrent/torrentcreator.h"
#include "base/path.h" #include "base/path.h"
#include "base/settingvalue.h" #include "base/settingvalue.h"
namespace BitTorrent
{
enum class TorrentFormat;
struct TorrentCreatorResult;
}
namespace Ui namespace Ui
{ {
class TorrentCreatorDialog; class TorrentCreatorDialog;
@@ -54,7 +59,7 @@ public:
private slots: private slots:
void updateProgressBar(int progress); void updateProgressBar(int progress);
void updatePiecesCount(); void onCalculatePiecesButtonClicked();
void onCreateButtonClicked(); void onCreateButtonClicked();
void onAddFileButtonClicked(); void onAddFileButtonClicked();
void onAddFolderButtonClicked(); void onAddFolderButtonClicked();

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2024-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2020 thalieht * Copyright (C) 2020 thalieht
* Copyright (C) 2011 Christian Kandeler * Copyright (C) 2011 Christian Kandeler
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
@@ -289,7 +289,12 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QList<BitTorre
, this, &TorrentOptionsDialog::handleDownSpeedLimitChanged); , this, &TorrentOptionsDialog::handleDownSpeedLimitChanged);
} }
m_ui->torrentShareLimitsWidget->setDefaultLimits(session->globalMaxRatio(), session->globalMaxSeedingMinutes(), session->globalMaxInactiveSeedingMinutes()); if (m_allSameCategory)
{
m_ui->torrentShareLimitsWidget->setDefaults((firstTorrentCategory.isEmpty() ? TorrentShareLimitsWidget::UsedDefaults::Global : TorrentShareLimitsWidget::UsedDefaults::Category)
, session->categoryRatioLimit(firstTorrentCategory), session->categorySeedingTimeLimit(firstTorrentCategory)
, session->categoryInactiveSeedingTimeLimit(firstTorrentCategory), session->categoryShareLimitAction(firstTorrentCategory));
}
if (allSameRatio) if (allSameRatio)
m_ui->torrentShareLimitsWidget->setRatioLimit(firstTorrentRatio); m_ui->torrentShareLimitsWidget->setRatioLimit(firstTorrentRatio);
if (allSameSeedingTime) if (allSameSeedingTime)
@@ -477,30 +482,37 @@ void TorrentOptionsDialog::accept()
void TorrentOptionsDialog::handleCategoryChanged([[maybe_unused]] const int index) void TorrentOptionsDialog::handleCategoryChanged([[maybe_unused]] const int index)
{ {
if (m_ui->checkAutoTMM->checkState() == Qt::Checked) const auto *btSession = BitTorrent::Session::instance();
{
if (!m_allSameCategory && (m_ui->comboCategory->currentIndex() == 0))
{
m_ui->savePath->setSelectedPath({});
}
else
{
const Path savePath = BitTorrent::Session::instance()->categorySavePath(m_ui->comboCategory->currentText());
m_ui->savePath->setSelectedPath(savePath);
const Path downloadPath = BitTorrent::Session::instance()->categoryDownloadPath(m_ui->comboCategory->currentText());
m_ui->downloadPath->setSelectedPath(downloadPath);
m_ui->checkUseDownloadPath->setChecked(!downloadPath.isEmpty());
}
}
if (!m_allSameCategory && (m_ui->comboCategory->currentIndex() == 0)) if (!m_allSameCategory && (m_ui->comboCategory->currentIndex() == 0))
{ {
if (m_ui->checkAutoTMM->checkState() == Qt::Checked)
m_ui->savePath->setSelectedPath({});
m_ui->comboCategory->clearEditText(); m_ui->comboCategory->clearEditText();
m_ui->comboCategory->lineEdit()->setPlaceholderText(m_currentCategoriesString); m_ui->comboCategory->lineEdit()->setPlaceholderText(m_currentCategoriesString);
m_ui->torrentShareLimitsWidget->setDefaults(TorrentShareLimitsWidget::UsedDefaults::Global
, BitTorrent::DEFAULT_RATIO_LIMIT, BitTorrent::DEFAULT_SEEDING_TIME_LIMIT
, BitTorrent::DEFAULT_SEEDING_TIME_LIMIT, BitTorrent::ShareLimitAction::Default);
} }
else else
{ {
const QString categoryName = m_ui->comboCategory->currentText();
if (m_ui->checkAutoTMM->checkState() == Qt::Checked)
{
const Path savePath = btSession->categorySavePath(categoryName);
m_ui->savePath->setSelectedPath(savePath);
const Path downloadPath = btSession->categoryDownloadPath(categoryName);
m_ui->downloadPath->setSelectedPath(downloadPath);
m_ui->checkUseDownloadPath->setChecked(!downloadPath.isEmpty());
}
m_ui->comboCategory->lineEdit()->setPlaceholderText(QString()); m_ui->comboCategory->lineEdit()->setPlaceholderText(QString());
m_ui->torrentShareLimitsWidget->setDefaults((categoryName.isEmpty() ? TorrentShareLimitsWidget::UsedDefaults::Global : TorrentShareLimitsWidget::UsedDefaults::Category)
, btSession->categoryRatioLimit(categoryName), btSession->categorySeedingTimeLimit(categoryName)
, btSession->categoryInactiveSeedingTimeLimit(categoryName), btSession->categoryShareLimitAction(categoryName));
} }
} }

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2024-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2020 thalieht * Copyright (C) 2020 thalieht
* Copyright (C) 2011 Christian Kandeler * Copyright (C) 2011 Christian Kandeler
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org> * Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
@@ -35,7 +35,7 @@
#include <QDialog> #include <QDialog>
#include "base/bittorrent/sharelimitaction.h" #include "base/bittorrent/sharelimits.h"
#include "base/path.h" #include "base/path.h"
#include "base/settingvalue.h" #include "base/settingvalue.h"

View File

@@ -28,6 +28,8 @@
#include "torrentsharelimitswidget.h" #include "torrentsharelimitswidget.h"
#include <limits>
#include "base/bittorrent/torrent.h" #include "base/bittorrent/torrent.h"
#include "ui_torrentsharelimitswidget.h" #include "ui_torrentsharelimitswidget.h"
@@ -50,6 +52,29 @@ namespace
RemoveWithContentActionIndex, RemoveWithContentActionIndex,
SuperSeedingActionIndex SuperSeedingActionIndex
}; };
QString shareLimitActionName(const BitTorrent::ShareLimitAction shareLimitAction)
{
switch (shareLimitAction)
{
case BitTorrent::ShareLimitAction::Stop:
return TorrentShareLimitsWidget::tr("Stop torrent");
case BitTorrent::ShareLimitAction::Remove:
return TorrentShareLimitsWidget::tr("Remove torrent");
case BitTorrent::ShareLimitAction::RemoveWithContent:
return TorrentShareLimitsWidget::tr("Remove torrent and its content");
case BitTorrent::ShareLimitAction::EnableSuperSeeding:
return TorrentShareLimitsWidget::tr("Enable super seeding for torrent");
case BitTorrent::ShareLimitAction::Default:
return TorrentShareLimitsWidget::tr("Default");
}
return {};
}
} }
TorrentShareLimitsWidget::TorrentShareLimitsWidget(QWidget *parent) TorrentShareLimitsWidget::TorrentShareLimitsWidget(QWidget *parent)
@@ -58,7 +83,32 @@ TorrentShareLimitsWidget::TorrentShareLimitsWidget(QWidget *parent)
{ {
m_ui->setupUi(this); m_ui->setupUi(this);
m_ui->comboBoxRatioMode->addItem({});
m_ui->comboBoxRatioMode->addItem(tr("Unlimited"));
m_ui->comboBoxRatioMode->addItem(tr("Set to"));
m_ui->comboBoxRatioMode->setCurrentIndex(UninitializedModeIndex);
m_ui->comboBoxSeedingTimeMode->addItem({});
m_ui->comboBoxSeedingTimeMode->addItem(tr("Unlimited"));
m_ui->comboBoxSeedingTimeMode->addItem(tr("Set to"));
m_ui->comboBoxSeedingTimeMode->setCurrentIndex(UninitializedModeIndex);
m_ui->comboBoxInactiveSeedingTimeMode->addItem({});
m_ui->comboBoxInactiveSeedingTimeMode->addItem(tr("Unlimited"));
m_ui->comboBoxInactiveSeedingTimeMode->addItem(tr("Set to"));
m_ui->comboBoxInactiveSeedingTimeMode->setCurrentIndex(UninitializedModeIndex);
m_ui->comboBoxAction->addItem({});
m_ui->comboBoxAction->addItem(shareLimitActionName(BitTorrent::ShareLimitAction::Stop));
m_ui->comboBoxAction->addItem(shareLimitActionName(BitTorrent::ShareLimitAction::Remove));
m_ui->comboBoxAction->addItem(shareLimitActionName(BitTorrent::ShareLimitAction::RemoveWithContent));
m_ui->comboBoxAction->addItem(shareLimitActionName(BitTorrent::ShareLimitAction::EnableSuperSeeding));
m_ui->comboBoxAction->setCurrentIndex(UninitializedActionIndex);
resetDefaultItemsText();
m_ui->spinBoxRatioValue->setEnabled(false); m_ui->spinBoxRatioValue->setEnabled(false);
m_ui->spinBoxRatioValue->setMaximum(std::numeric_limits<int>::max());
m_ui->spinBoxRatioValue->setSuffix({}); m_ui->spinBoxRatioValue->setSuffix({});
m_ui->spinBoxRatioValue->clear(); m_ui->spinBoxRatioValue->clear();
m_ui->spinBoxSeedingTimeValue->setEnabled(false); m_ui->spinBoxSeedingTimeValue->setEnabled(false);
@@ -68,9 +118,25 @@ TorrentShareLimitsWidget::TorrentShareLimitsWidget(QWidget *parent)
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix({}); m_ui->spinBoxInactiveSeedingTimeValue->setSuffix({});
m_ui->spinBoxInactiveSeedingTimeValue->clear(); m_ui->spinBoxInactiveSeedingTimeValue->clear();
connect(m_ui->comboBoxRatioMode, &QComboBox::currentIndexChanged, this, &TorrentShareLimitsWidget::refreshRatioLimitControls); int prevIndex = UninitializedModeIndex;
connect(m_ui->comboBoxSeedingTimeMode, &QComboBox::currentIndexChanged, this, &TorrentShareLimitsWidget::refreshSeedingTimeLimitControls); connect(m_ui->comboBoxRatioMode, &QComboBox::currentIndexChanged, this
connect(m_ui->comboBoxInactiveSeedingTimeMode, &QComboBox::currentIndexChanged, this, &TorrentShareLimitsWidget::refreshInactiveSeedingTimeLimitControls); , [this, prevIndex](const int currentIndex) mutable
{
onRatioLimitModeChanged(currentIndex, prevIndex);
prevIndex = currentIndex;
});
connect(m_ui->comboBoxSeedingTimeMode, &QComboBox::currentIndexChanged, this
, [this, prevIndex](const int currentIndex) mutable
{
onSeedingTimeLimitModeChanged(currentIndex, prevIndex);
prevIndex = currentIndex;
});
connect(m_ui->comboBoxInactiveSeedingTimeMode, &QComboBox::currentIndexChanged, this
, [this, prevIndex](const int currentIndex) mutable
{
onInactiveSeedingTimeLimitModeChanged(currentIndex, prevIndex);
prevIndex = currentIndex;
});
} }
TorrentShareLimitsWidget::~TorrentShareLimitsWidget() TorrentShareLimitsWidget::~TorrentShareLimitsWidget()
@@ -80,11 +146,11 @@ TorrentShareLimitsWidget::~TorrentShareLimitsWidget()
void TorrentShareLimitsWidget::setRatioLimit(const qreal ratioLimit) void TorrentShareLimitsWidget::setRatioLimit(const qreal ratioLimit)
{ {
if (ratioLimit == BitTorrent::Torrent::USE_GLOBAL_RATIO) if (ratioLimit == BitTorrent::DEFAULT_RATIO_LIMIT)
{ {
m_ui->comboBoxRatioMode->setCurrentIndex(DefaultModeIndex); m_ui->comboBoxRatioMode->setCurrentIndex(DefaultModeIndex);
} }
else if (ratioLimit == BitTorrent::Torrent::NO_RATIO_LIMIT) else if (ratioLimit == BitTorrent::NO_RATIO_LIMIT)
{ {
m_ui->comboBoxRatioMode->setCurrentIndex(UnlimitedModeIndex); m_ui->comboBoxRatioMode->setCurrentIndex(UnlimitedModeIndex);
} }
@@ -97,11 +163,11 @@ void TorrentShareLimitsWidget::setRatioLimit(const qreal ratioLimit)
void TorrentShareLimitsWidget::setSeedingTimeLimit(const int seedingTimeLimit) void TorrentShareLimitsWidget::setSeedingTimeLimit(const int seedingTimeLimit)
{ {
if (seedingTimeLimit == BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME) if (seedingTimeLimit == BitTorrent::DEFAULT_SEEDING_TIME_LIMIT)
{ {
m_ui->comboBoxSeedingTimeMode->setCurrentIndex(DefaultModeIndex); m_ui->comboBoxSeedingTimeMode->setCurrentIndex(DefaultModeIndex);
} }
else if (seedingTimeLimit == BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT) else if (seedingTimeLimit == BitTorrent::NO_SEEDING_TIME_LIMIT)
{ {
m_ui->comboBoxSeedingTimeMode->setCurrentIndex(UnlimitedModeIndex); m_ui->comboBoxSeedingTimeMode->setCurrentIndex(UnlimitedModeIndex);
} }
@@ -114,11 +180,11 @@ void TorrentShareLimitsWidget::setSeedingTimeLimit(const int seedingTimeLimit)
void TorrentShareLimitsWidget::setInactiveSeedingTimeLimit(const int inactiveSeedingTimeLimit) void TorrentShareLimitsWidget::setInactiveSeedingTimeLimit(const int inactiveSeedingTimeLimit)
{ {
if (inactiveSeedingTimeLimit == BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME) if (inactiveSeedingTimeLimit == BitTorrent::DEFAULT_SEEDING_TIME_LIMIT)
{ {
m_ui->comboBoxInactiveSeedingTimeMode->setCurrentIndex(DefaultModeIndex); m_ui->comboBoxInactiveSeedingTimeMode->setCurrentIndex(DefaultModeIndex);
} }
else if (inactiveSeedingTimeLimit == BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT) else if (inactiveSeedingTimeLimit == BitTorrent::NO_SEEDING_TIME_LIMIT)
{ {
m_ui->comboBoxInactiveSeedingTimeMode->setCurrentIndex(UnlimitedModeIndex); m_ui->comboBoxInactiveSeedingTimeMode->setCurrentIndex(UnlimitedModeIndex);
} }
@@ -152,25 +218,56 @@ void TorrentShareLimitsWidget::setShareLimitAction(const BitTorrent::ShareLimitA
} }
} }
void TorrentShareLimitsWidget::setDefaultLimits(const qreal ratioLimit, const int seedingTimeLimit, const int inactiveSeedingTimeLimit) void TorrentShareLimitsWidget::setDefaults(UsedDefaults usedDefaults
, const qreal ratioLimit, const int seedingTimeLimit
, const int inactiveSeedingTimeLimit, const BitTorrent::ShareLimitAction action)
{ {
if (m_defaultRatioLimit != ratioLimit) m_defaultRatioLimit = ratioLimit;
if (m_ui->comboBoxRatioMode->currentIndex() == DefaultModeIndex)
{ {
m_defaultRatioLimit = ratioLimit; if (m_defaultRatioLimit >= 0)
refreshRatioLimitControls(); {
m_ui->spinBoxRatioValue->setValue(m_defaultRatioLimit);
}
else
{
m_ui->spinBoxRatioValue->clear();
}
} }
if (m_defaultSeedingTimeLimit != seedingTimeLimit) m_defaultSeedingTimeLimit = seedingTimeLimit;
if (m_ui->comboBoxSeedingTimeMode->currentIndex() == DefaultModeIndex)
{ {
m_defaultSeedingTimeLimit = seedingTimeLimit; if (m_defaultSeedingTimeLimit >= 0)
refreshSeedingTimeLimitControls(); {
m_ui->spinBoxSeedingTimeValue->setValue(m_defaultSeedingTimeLimit);
m_ui->spinBoxSeedingTimeValue->setSuffix(tr(" min"));
}
else
{
m_ui->spinBoxSeedingTimeValue->setSuffix({});
m_ui->spinBoxSeedingTimeValue->clear();
}
} }
if (m_defaultInactiveSeedingTimeLimit != inactiveSeedingTimeLimit) m_defaultInactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
if (m_ui->comboBoxInactiveSeedingTimeMode->currentIndex() == DefaultModeIndex)
{ {
m_defaultInactiveSeedingTimeLimit = inactiveSeedingTimeLimit; if (m_defaultInactiveSeedingTimeLimit >= 0)
refreshInactiveSeedingTimeLimitControls(); {
m_ui->spinBoxInactiveSeedingTimeValue->setValue(m_defaultInactiveSeedingTimeLimit);
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix(tr(" min"));
}
else
{
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix({});
m_ui->spinBoxInactiveSeedingTimeValue->clear();
}
} }
m_defaultShareLimitAction = action;
m_usedDefaults = usedDefaults;
resetDefaultItemsText();
} }
std::optional<qreal> TorrentShareLimitsWidget::ratioLimit() const std::optional<qreal> TorrentShareLimitsWidget::ratioLimit() const
@@ -178,9 +275,9 @@ std::optional<qreal> TorrentShareLimitsWidget::ratioLimit() const
switch (m_ui->comboBoxRatioMode->currentIndex()) switch (m_ui->comboBoxRatioMode->currentIndex())
{ {
case DefaultModeIndex: case DefaultModeIndex:
return BitTorrent::Torrent::USE_GLOBAL_RATIO; return BitTorrent::DEFAULT_RATIO_LIMIT;
case UnlimitedModeIndex: case UnlimitedModeIndex:
return BitTorrent::Torrent::NO_RATIO_LIMIT; return BitTorrent::NO_RATIO_LIMIT;
case AssignedModeIndex: case AssignedModeIndex:
return m_ui->spinBoxRatioValue->value(); return m_ui->spinBoxRatioValue->value();
default: default:
@@ -193,9 +290,9 @@ std::optional<int> TorrentShareLimitsWidget::seedingTimeLimit() const
switch (m_ui->comboBoxSeedingTimeMode->currentIndex()) switch (m_ui->comboBoxSeedingTimeMode->currentIndex())
{ {
case DefaultModeIndex: case DefaultModeIndex:
return BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME; return BitTorrent::DEFAULT_SEEDING_TIME_LIMIT;
case UnlimitedModeIndex: case UnlimitedModeIndex:
return BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT; return BitTorrent::NO_SEEDING_TIME_LIMIT;
case AssignedModeIndex: case AssignedModeIndex:
return m_ui->spinBoxSeedingTimeValue->value(); return m_ui->spinBoxSeedingTimeValue->value();
default: default:
@@ -208,9 +305,9 @@ std::optional<int> TorrentShareLimitsWidget::inactiveSeedingTimeLimit() const
switch (m_ui->comboBoxInactiveSeedingTimeMode->currentIndex()) switch (m_ui->comboBoxInactiveSeedingTimeMode->currentIndex())
{ {
case DefaultModeIndex: case DefaultModeIndex:
return BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME; return BitTorrent::DEFAULT_SEEDING_TIME_LIMIT;
case UnlimitedModeIndex: case UnlimitedModeIndex:
return BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT; return BitTorrent::NO_SEEDING_TIME_LIMIT;
case AssignedModeIndex: case AssignedModeIndex:
return m_ui->spinBoxInactiveSeedingTimeValue->value(); return m_ui->spinBoxInactiveSeedingTimeValue->value();
default: default:
@@ -237,68 +334,97 @@ std::optional<BitTorrent::ShareLimitAction> TorrentShareLimitsWidget::shareLimit
} }
} }
void TorrentShareLimitsWidget::refreshRatioLimitControls() void TorrentShareLimitsWidget::onRatioLimitModeChanged(const int currentIndex, const int previousIndex)
{ {
const auto index = m_ui->comboBoxRatioMode->currentIndex(); m_ui->spinBoxRatioValue->setEnabled(currentIndex == AssignedModeIndex);
m_ui->spinBoxRatioValue->setEnabled(index == AssignedModeIndex); if (previousIndex == AssignedModeIndex)
if (index == AssignedModeIndex) m_ratioLimit = m_ui->spinBoxRatioValue->value();
if (currentIndex == AssignedModeIndex)
{ {
m_ui->spinBoxRatioValue->setValue(m_ratioLimit); m_ui->spinBoxRatioValue->setValue(m_ratioLimit);
} }
else if ((index == DefaultModeIndex) && (m_defaultRatioLimit >= 0)) else if ((currentIndex == DefaultModeIndex) && (m_defaultRatioLimit >= 0))
{ {
m_ui->spinBoxRatioValue->setValue(m_defaultRatioLimit); m_ui->spinBoxRatioValue->setValue(m_defaultRatioLimit);
} }
else else
{ {
m_ratioLimit = m_ui->spinBoxRatioValue->value();
m_ui->spinBoxRatioValue->clear(); m_ui->spinBoxRatioValue->clear();
} }
} }
void TorrentShareLimitsWidget::refreshSeedingTimeLimitControls() void TorrentShareLimitsWidget::onSeedingTimeLimitModeChanged(const int currentIndex, const int previousIndex)
{ {
const auto index = m_ui->comboBoxSeedingTimeMode->currentIndex(); m_ui->spinBoxSeedingTimeValue->setEnabled(currentIndex == AssignedModeIndex);
m_ui->spinBoxSeedingTimeValue->setEnabled(index == AssignedModeIndex); if (previousIndex == AssignedModeIndex)
if (index == AssignedModeIndex) m_seedingTimeLimit = m_ui->spinBoxSeedingTimeValue->value();
if (currentIndex == AssignedModeIndex)
{ {
m_ui->spinBoxSeedingTimeValue->setValue(m_seedingTimeLimit); m_ui->spinBoxSeedingTimeValue->setValue(m_seedingTimeLimit);
m_ui->spinBoxSeedingTimeValue->setSuffix(tr(" min")); m_ui->spinBoxSeedingTimeValue->setSuffix(tr(" min"));
} }
else if ((index == DefaultModeIndex) && (m_defaultSeedingTimeLimit >= 0)) else if ((currentIndex == DefaultModeIndex) && (m_defaultSeedingTimeLimit >= 0))
{ {
m_ui->spinBoxSeedingTimeValue->setValue(m_defaultSeedingTimeLimit); m_ui->spinBoxSeedingTimeValue->setValue(m_defaultSeedingTimeLimit);
m_ui->spinBoxSeedingTimeValue->setSuffix(tr(" min")); m_ui->spinBoxSeedingTimeValue->setSuffix(tr(" min"));
} }
else else
{ {
m_seedingTimeLimit = m_ui->spinBoxSeedingTimeValue->value();
m_ui->spinBoxSeedingTimeValue->setSuffix({}); m_ui->spinBoxSeedingTimeValue->setSuffix({});
m_ui->spinBoxSeedingTimeValue->clear(); m_ui->spinBoxSeedingTimeValue->clear();
} }
} }
void TorrentShareLimitsWidget::refreshInactiveSeedingTimeLimitControls() void TorrentShareLimitsWidget::onInactiveSeedingTimeLimitModeChanged(const int currentIndex, const int previousIndex)
{ {
const auto index = m_ui->comboBoxInactiveSeedingTimeMode->currentIndex(); m_ui->spinBoxInactiveSeedingTimeValue->setEnabled(currentIndex == AssignedModeIndex);
m_ui->spinBoxInactiveSeedingTimeValue->setEnabled(index == AssignedModeIndex); if (previousIndex == AssignedModeIndex)
if (index == AssignedModeIndex) m_inactiveSeedingTimeLimit = m_ui->spinBoxInactiveSeedingTimeValue->value();
if (currentIndex == AssignedModeIndex)
{ {
m_ui->spinBoxInactiveSeedingTimeValue->setValue(m_inactiveSeedingTimeLimit); m_ui->spinBoxInactiveSeedingTimeValue->setValue(m_inactiveSeedingTimeLimit);
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix(tr(" min")); m_ui->spinBoxInactiveSeedingTimeValue->setSuffix(tr(" min"));
} }
else if ((index == DefaultModeIndex) && (m_defaultInactiveSeedingTimeLimit >= 0)) else if ((currentIndex == DefaultModeIndex) && (m_defaultInactiveSeedingTimeLimit >= 0))
{ {
m_ui->spinBoxInactiveSeedingTimeValue->setValue(m_defaultInactiveSeedingTimeLimit); m_ui->spinBoxInactiveSeedingTimeValue->setValue(m_defaultInactiveSeedingTimeLimit);
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix(tr(" min")); m_ui->spinBoxInactiveSeedingTimeValue->setSuffix(tr(" min"));
} }
else else
{ {
m_inactiveSeedingTimeLimit = m_ui->spinBoxInactiveSeedingTimeValue->value();
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix({}); m_ui->spinBoxInactiveSeedingTimeValue->setSuffix({});
m_ui->spinBoxInactiveSeedingTimeValue->clear(); m_ui->spinBoxInactiveSeedingTimeValue->clear();
} }
} }
void TorrentShareLimitsWidget::resetDefaultItemsText()
{
if (m_usedDefaults == UsedDefaults::Global)
{
m_ui->comboBoxRatioMode->setItemText(DefaultModeIndex, tr("Default"));
m_ui->comboBoxSeedingTimeMode->setItemText(DefaultModeIndex, tr("Default"));
m_ui->comboBoxInactiveSeedingTimeMode->setItemText(DefaultModeIndex, tr("Default"));
m_ui->comboBoxAction->setItemText(DefaultActionIndex
, (m_defaultShareLimitAction == BitTorrent::ShareLimitAction::Default)
? tr("Default")
: tr("Default (%1)", "Default (share limit action)").arg(shareLimitActionName(m_defaultShareLimitAction)));
}
else // TorrentOptions
{
m_ui->comboBoxRatioMode->setItemText(DefaultModeIndex, tr("From category"));
m_ui->comboBoxSeedingTimeMode->setItemText(DefaultModeIndex, tr("From category"));
m_ui->comboBoxInactiveSeedingTimeMode->setItemText(DefaultModeIndex, tr("From category"));
m_ui->comboBoxAction->setItemText(DefaultActionIndex
, (m_defaultShareLimitAction == BitTorrent::ShareLimitAction::Default)
? tr("From category")
: tr("From category (%1)", "From category (share limit action)").arg(shareLimitActionName(m_defaultShareLimitAction)));
}
}

View File

@@ -32,7 +32,7 @@
#include <QWidget> #include <QWidget>
#include "base/bittorrent/sharelimitaction.h" #include "base/bittorrent/sharelimits.h"
namespace Ui namespace Ui
{ {
@@ -45,6 +45,12 @@ class TorrentShareLimitsWidget final : public QWidget
Q_DISABLE_COPY_MOVE(TorrentShareLimitsWidget) Q_DISABLE_COPY_MOVE(TorrentShareLimitsWidget)
public: public:
enum class UsedDefaults
{
Global,
Category
};
explicit TorrentShareLimitsWidget(QWidget *parent = nullptr); explicit TorrentShareLimitsWidget(QWidget *parent = nullptr);
~TorrentShareLimitsWidget() override; ~TorrentShareLimitsWidget() override;
@@ -53,7 +59,8 @@ public:
void setInactiveSeedingTimeLimit(int inactiveSeedingTimeLimit); void setInactiveSeedingTimeLimit(int inactiveSeedingTimeLimit);
void setShareLimitAction(BitTorrent::ShareLimitAction action); void setShareLimitAction(BitTorrent::ShareLimitAction action);
void setDefaultLimits(qreal ratioLimit, int seedingTimeLimit, int inactiveSeedingTimeLimit); void setDefaults(UsedDefaults usedDefaults, qreal ratioLimit, int seedingTimeLimit
, int inactiveSeedingTimeLimit, BitTorrent::ShareLimitAction action);
std::optional<qreal> ratioLimit() const; std::optional<qreal> ratioLimit() const;
std::optional<int> seedingTimeLimit() const; std::optional<int> seedingTimeLimit() const;
@@ -61,9 +68,10 @@ public:
std::optional<BitTorrent::ShareLimitAction> shareLimitAction() const; std::optional<BitTorrent::ShareLimitAction> shareLimitAction() const;
private: private:
void refreshRatioLimitControls(); void onRatioLimitModeChanged(int currentIndex, int previousIndex);
void refreshSeedingTimeLimitControls(); void onSeedingTimeLimitModeChanged(int currentIndex, int previousIndex);
void refreshInactiveSeedingTimeLimitControls(); void onInactiveSeedingTimeLimitModeChanged(int currentIndex, int previousIndex);
void resetDefaultItemsText();
Ui::TorrentShareLimitsWidget *m_ui = nullptr; Ui::TorrentShareLimitsWidget *m_ui = nullptr;
@@ -74,4 +82,7 @@ private:
int m_defaultSeedingTimeLimit = -1; int m_defaultSeedingTimeLimit = -1;
int m_defaultInactiveSeedingTimeLimit = -1; int m_defaultInactiveSeedingTimeLimit = -1;
qreal m_defaultRatioLimit = -1; qreal m_defaultRatioLimit = -1;
BitTorrent::ShareLimitAction m_defaultShareLimitAction = BitTorrent::ShareLimitAction::Default;
UsedDefaults m_usedDefaults = UsedDefaults::Global;
}; };

View File

@@ -21,26 +21,7 @@
</widget> </widget>
</item> </item>
<item row="0" column="1"> <item row="0" column="1">
<widget class="QComboBox" name="comboBoxRatioMode"> <widget class="QComboBox" name="comboBoxRatioMode"/>
<property name="currentIndex">
<number>-1</number>
</property>
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Unlimited</string>
</property>
</item>
<item>
<property name="text">
<string>Set to</string>
</property>
</item>
</widget>
</item> </item>
<item row="0" column="2"> <item row="0" column="2">
<widget class="QDoubleSpinBox" name="spinBoxRatioValue"> <widget class="QDoubleSpinBox" name="spinBoxRatioValue">
@@ -63,26 +44,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<widget class="QComboBox" name="comboBoxSeedingTimeMode"> <widget class="QComboBox" name="comboBoxSeedingTimeMode"/>
<property name="currentIndex">
<number>-1</number>
</property>
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Unlimited</string>
</property>
</item>
<item>
<property name="text">
<string>Set to</string>
</property>
</item>
</widget>
</item> </item>
<item row="1" column="2"> <item row="1" column="2">
<widget class="QSpinBox" name="spinBoxSeedingTimeValue"> <widget class="QSpinBox" name="spinBoxSeedingTimeValue">
@@ -108,26 +70,7 @@
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="2" column="1">
<widget class="QComboBox" name="comboBoxInactiveSeedingTimeMode"> <widget class="QComboBox" name="comboBoxInactiveSeedingTimeMode"/>
<property name="currentIndex">
<number>-1</number>
</property>
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Unlimited</string>
</property>
</item>
<item>
<property name="text">
<string>Set to</string>
</property>
</item>
</widget>
</item> </item>
<item row="2" column="2"> <item row="2" column="2">
<widget class="QSpinBox" name="spinBoxInactiveSeedingTimeValue"> <widget class="QSpinBox" name="spinBoxInactiveSeedingTimeValue">
@@ -157,36 +100,7 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QComboBox" name="comboBoxAction"> <widget class="QComboBox" name="comboBoxAction"/>
<property name="currentIndex">
<number>-1</number>
</property>
<item>
<property name="text">
<string>Default</string>
</property>
</item>
<item>
<property name="text">
<string>Stop torrent</string>
</property>
</item>
<item>
<property name="text">
<string>Remove torrent</string>
</property>
</item>
<item>
<property name="text">
<string>Remove torrent and its content</string>
</property>
</item>
<item>
<property name="text">
<string>Enable super seeding for torrent</string>
</property>
</item>
</widget>
</item> </item>
<item> <item>
<spacer name="actionLayoutSpacer"> <spacer name="actionLayoutSpacer">

View File

@@ -267,7 +267,7 @@ TrackerListModel::TrackerListModel(BitTorrent::Session *btSession, QObject *pare
if (torrent == m_torrent) if (torrent == m_torrent)
onTrackersRemoved(deletedTrackers); onTrackersRemoved(deletedTrackers);
}); });
connect(m_btSession, &BitTorrent::Session::trackersChanged, this connect(m_btSession, &BitTorrent::Session::trackersReset, this
, [this](BitTorrent::Torrent *torrent) , [this](BitTorrent::Torrent *torrent)
{ {
if (torrent == m_torrent) if (torrent == m_torrent)

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2016-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2016-2025 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
@@ -324,17 +324,17 @@ void CategoryFilterModel::categoryAdded(const QString &categoryName)
{ {
CategoryModelItem *parent = m_rootItem; CategoryModelItem *parent = m_rootItem;
if (m_isSubcategoriesEnabled) const QStringList expanded = BitTorrent::Session::expandCategory(categoryName);
{ if (expanded.count() > 1)
QStringList expanded = BitTorrent::Session::expandCategory(categoryName); parent = findItem(expanded[expanded.count() - 2]);
if (expanded.count() > 1)
parent = findItem(expanded[expanded.count() - 2]); Q_ASSERT(parent);
} if (!parent) [[unlikely]]
return;
const int row = parent->childCount(); const int row = parent->childCount();
beginInsertRows(index(parent), row, row); beginInsertRows(index(parent), row, row);
new CategoryModelItem( new CategoryModelItem(parent, shortName(categoryName));
parent, m_isSubcategoriesEnabled ? shortName(categoryName) : categoryName);
endInsertRows(); endInsertRows();
} }
@@ -426,7 +426,6 @@ void CategoryFilterModel::populate()
const auto *session = BitTorrent::Session::instance(); const auto *session = BitTorrent::Session::instance();
const auto torrents = session->torrents(); const auto torrents = session->torrents();
m_isSubcategoriesEnabled = session->isSubcategoriesEnabled();
// All torrents // All torrents
m_rootItem->addChild(CategoryModelItem::UID_ALL m_rootItem->addChild(CategoryModelItem::UID_ALL
@@ -440,31 +439,19 @@ void CategoryFilterModel::populate()
, new CategoryModelItem(nullptr, tr("Uncategorized"), torrentsCount)); , new CategoryModelItem(nullptr, tr("Uncategorized"), torrentsCount));
using BitTorrent::Torrent; using BitTorrent::Torrent;
if (m_isSubcategoriesEnabled) for (const QString &categoryName : asConst(session->categories()))
{ {
for (const QString &categoryName : asConst(session->categories())) CategoryModelItem *parent = m_rootItem;
for (const QString &subcat : asConst(BitTorrent::Session::expandCategory(categoryName)))
{ {
CategoryModelItem *parent = m_rootItem; const QString subcatName = shortName(subcat);
for (const QString &subcat : asConst(BitTorrent::Session::expandCategory(categoryName))) if (!parent->hasChild(subcatName))
{ {
const QString subcatName = shortName(subcat); const int torrentsCount = std::ranges::count_if(torrents
if (!parent->hasChild(subcatName)) , [&subcat](const Torrent *torrent) { return torrent->category() == subcat; });
{ new CategoryModelItem(parent, subcatName, torrentsCount);
const int torrentsCount = std::ranges::count_if(torrents
, [&subcat](const Torrent *torrent) { return torrent->category() == subcat; });
new CategoryModelItem(parent, subcatName, torrentsCount);
}
parent = parent->child(subcatName);
} }
} parent = parent->child(subcatName);
}
else
{
for (const QString &categoryName : asConst(session->categories()))
{
const int torrentsCount = std::ranges::count_if(torrents
, [&categoryName](const Torrent *torrent) { return torrent->belongsToCategory(categoryName); });
new CategoryModelItem(m_rootItem, categoryName, torrentsCount);
} }
} }
} }
@@ -474,9 +461,6 @@ CategoryModelItem *CategoryFilterModel::findItem(const QString &fullName) const
if (fullName.isEmpty()) if (fullName.isEmpty())
return m_rootItem->childAt(1); // "Uncategorized" item return m_rootItem->childAt(1); // "Uncategorized" item
if (!m_isSubcategoriesEnabled)
return m_rootItem->child(fullName);
CategoryModelItem *item = m_rootItem; CategoryModelItem *item = m_rootItem;
for (const QString &subcat : asConst(BitTorrent::Session::expandCategory(fullName))) for (const QString &subcat : asConst(BitTorrent::Session::expandCategory(fullName)))
{ {

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2016-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2016-2025 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
@@ -72,6 +72,5 @@ private:
QModelIndex index(CategoryModelItem *item) const; QModelIndex index(CategoryModelItem *item) const;
CategoryModelItem *findItem(const QString &fullName) const; CategoryModelItem *findItem(const QString &fullName) const;
bool m_isSubcategoriesEnabled = false;
CategoryModelItem *m_rootItem = nullptr; CategoryModelItem *m_rootItem = nullptr;
}; };

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2016 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2016-2025 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
@@ -72,9 +72,6 @@ CategoryFilterWidget::CategoryFilterWidget(QWidget *parent)
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
setAttribute(Qt::WA_MacShowFocusRect, false); setAttribute(Qt::WA_MacShowFocusRect, false);
#endif #endif
m_defaultIndentation = indentation();
if (!BitTorrent::Session::instance()->isSubcategoriesEnabled())
setIndentation(0);
setContextMenuPolicy(Qt::CustomContextMenu); setContextMenuPolicy(Qt::CustomContextMenu);
sortByColumn(0, Qt::AscendingOrder); sortByColumn(0, Qt::AscendingOrder);
setCurrentIndex(model()->index(0, 0)); setCurrentIndex(model()->index(0, 0));
@@ -84,7 +81,18 @@ CategoryFilterWidget::CategoryFilterWidget(QWidget *parent)
connect(this, &QWidget::customContextMenuRequested, this, &CategoryFilterWidget::showMenu); connect(this, &QWidget::customContextMenuRequested, this, &CategoryFilterWidget::showMenu);
connect(selectionModel(), &QItemSelectionModel::currentRowChanged connect(selectionModel(), &QItemSelectionModel::currentRowChanged
, this, &CategoryFilterWidget::onCurrentRowChanged); , this, &CategoryFilterWidget::onCurrentRowChanged);
connect(model(), &QAbstractItemModel::modelReset, this, &CategoryFilterWidget::callUpdateGeometry); connect(model(), &QAbstractItemModel::rowsRemoved, this, [this]
{
adjustIndentation();
updateGeometry();
});
connect(model(), &QAbstractItemModel::modelReset, this, [this]
{
adjustIndentation();
updateGeometry();
});
adjustIndentation();
} }
QString CategoryFilterWidget::currentCategory() const QString CategoryFilterWidget::currentCategory() const
@@ -113,12 +121,8 @@ void CategoryFilterWidget::showMenu()
const auto selectedRows = selectionModel()->selectedRows(); const auto selectedRows = selectionModel()->selectedRows();
if (!selectedRows.empty() && !CategoryFilterModel::isSpecialItem(selectedRows.first())) if (!selectedRows.empty() && !CategoryFilterModel::isSpecialItem(selectedRows.first()))
{ {
if (BitTorrent::Session::instance()->isSubcategoriesEnabled()) menu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("Add subcategory...")
{ , this, &CategoryFilterWidget::addSubcategory);
menu->addAction(UIThemeManager::instance()->getIcon(u"list-add"_s), tr("Add subcategory...")
, this, &CategoryFilterWidget::addSubcategory);
}
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s, u"document-edit"_s), tr("Edit category...") menu->addAction(UIThemeManager::instance()->getIcon(u"edit-rename"_s, u"document-edit"_s), tr("Edit category...")
, this, &CategoryFilterWidget::editCategory); , this, &CategoryFilterWidget::editCategory);
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove category") menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove category")
@@ -140,11 +144,6 @@ void CategoryFilterWidget::showMenu()
void CategoryFilterWidget::callUpdateGeometry() void CategoryFilterWidget::callUpdateGeometry()
{ {
if (!BitTorrent::Session::instance()->isSubcategoriesEnabled())
setIndentation(0);
else
setIndentation(m_defaultIndentation);
updateGeometry(); updateGeometry();
} }
@@ -168,10 +167,13 @@ QSize CategoryFilterWidget::minimumSizeHint() const
return size; return size;
} }
void CategoryFilterWidget::rowsInserted(const QModelIndex &parent, int start, int end) void CategoryFilterWidget::rowsInserted(const QModelIndex &parent, const int start, const int end)
{ {
QTreeView::rowsInserted(parent, start, end); QTreeView::rowsInserted(parent, start, end);
if (parent.isValid())
adjustIndentation();
// Expand all parents if the parent(s) of the node are not expanded. // Expand all parents if the parent(s) of the node are not expanded.
QModelIndex p = parent; QModelIndex p = parent;
while (p.isValid()) while (p.isValid())
@@ -184,6 +186,26 @@ void CategoryFilterWidget::rowsInserted(const QModelIndex &parent, int start, in
updateGeometry(); updateGeometry();
} }
bool CategoryFilterWidget::hasAnySubcategory() const
{
const int rowCount = model()->rowCount();
for (int row = 0; row < rowCount; ++row)
{
if (model()->hasChildren(model()->index(row, 0)))
return true;
}
return false;
}
void CategoryFilterWidget::adjustIndentation()
{
if (hasAnySubcategory())
resetIndentation();
else
setIndentation(0);
}
void CategoryFilterWidget::addCategory() void CategoryFilterWidget::addCategory()
{ {
TorrentCategoryDialog::createCategory(this); TorrentCategoryDialog::createCategory(this);

View File

@@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2016 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2016-2025 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
@@ -60,6 +60,6 @@ private:
QSize sizeHint() const override; QSize sizeHint() const override;
QSize minimumSizeHint() const override; QSize minimumSizeHint() const override;
void rowsInserted(const QModelIndex &parent, int start, int end) override; void rowsInserted(const QModelIndex &parent, int start, int end) override;
bool hasAnySubcategory() const;
int m_defaultIndentation; void adjustIndentation();
}; };

View File

@@ -99,8 +99,6 @@ StatusFilterWidget::StatusFilterWidget(QWidget *parent, TransferListWidget *tran
setCurrentRow(TorrentFilter::All, QItemSelectionModel::SelectCurrent); setCurrentRow(TorrentFilter::All, QItemSelectionModel::SelectCurrent);
else else
setCurrentRow(storedRow, QItemSelectionModel::SelectCurrent); setCurrentRow(storedRow, QItemSelectionModel::SelectCurrent);
toggleFilter(pref->getStatusFilterState());
} }
StatusFilterWidget::~StatusFilterWidget() StatusFilterWidget::~StatusFilterWidget()
@@ -128,7 +126,7 @@ void StatusFilterWidget::updateTorrentStatus(const BitTorrent::Torrent *torrent)
{ {
TorrentFilterBitset &torrentStatus = m_torrentsStatus[torrent]; TorrentFilterBitset &torrentStatus = m_torrentsStatus[torrent];
const auto update = [torrent, &torrentStatus](const TorrentFilter::Type status, int &counter) const auto update = [torrent, &torrentStatus](const TorrentFilter::Status status, int &counter)
{ {
const bool hasStatus = torrentStatus[status]; const bool hasStatus = torrentStatus[status];
const bool needStatus = TorrentFilter(status).match(torrent); const bool needStatus = TorrentFilter(status).match(torrent);

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-2025 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
@@ -36,13 +36,13 @@
#include <QMessageBox> #include <QMessageBox>
#include <QUrl> #include <QUrl>
#include "base/algorithm.h"
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/trackerentry.h" #include "base/bittorrent/trackerentry.h"
#include "base/bittorrent/trackerentrystatus.h" #include "base/bittorrent/trackerentrystatus.h"
#include "base/global.h" #include "base/global.h"
#include "base/net/downloadmanager.h" #include "base/net/downloadmanager.h"
#include "base/preferences.h" #include "base/preferences.h"
#include "base/torrentfilter.h"
#include "base/utils/compare.h" #include "base/utils/compare.h"
#include "base/utils/fs.h" #include "base/utils/fs.h"
#include "gui/transferlistwidget.h" #include "gui/transferlistwidget.h"
@@ -69,16 +69,35 @@ namespace
return !scheme.isEmpty() ? scheme : u"http"_s; return !scheme.isEmpty() ? scheme : u"http"_s;
} }
QString getHost(const QString &url) template <typename T>
concept HasUrlMember = requires (T t) { { t.url } -> std::convertible_to<QString>; };
template <HasUrlMember T>
QString getTrackerHost(const T &t)
{ {
// We want the hostname. return getTrackerHost(t.url);
// If failed to parse the domain, original input should be returned }
const QString host = QUrl(url).host(); template <typename T>
if (host.isEmpty()) QSet<QString> extractTrackerHosts(const T &trackerEntries)
return url; {
QSet<QString> trackerHosts;
trackerHosts.reserve(trackerEntries.size());
for (const auto &trackerEntry : trackerEntries)
trackerHosts.insert(getTrackerHost(trackerEntry));
return host; return trackerHosts;
}
template <typename T>
QSet<QString> extractTrackerURLs(const T &trackerEntries)
{
QSet<QString> trackerURLs;
trackerURLs.reserve(trackerEntries.size());
for (const auto &trackerEntry : trackerEntries)
trackerURLs.insert(trackerEntry.url);
return trackerURLs;
} }
QString getFaviconHost(const QString &trackerHost) QString getFaviconHost(const QString &trackerHost)
@@ -123,7 +142,7 @@ namespace
TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon) TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
: BaseFilterWidget(parent, transferList) : BaseFilterWidget(parent, transferList)
, m_downloadTrackerFavicon(downloadFavicon) , m_downloadTrackerFavicon {downloadFavicon}
{ {
auto *allTrackersItem = new QListWidgetItem(this); auto *allTrackersItem = new QListWidgetItem(this);
allTrackersItem->setData(Qt::DisplayRole, formatItemText(ALL_ROW, 0)); allTrackersItem->setData(Qt::DisplayRole, formatItemText(ALL_ROW, 0));
@@ -131,22 +150,34 @@ TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget *
auto *trackerlessItem = new QListWidgetItem(this); auto *trackerlessItem = new QListWidgetItem(this);
trackerlessItem->setData(Qt::DisplayRole, formatItemText(TRACKERLESS_ROW, 0)); trackerlessItem->setData(Qt::DisplayRole, formatItemText(TRACKERLESS_ROW, 0));
trackerlessItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackerless"_s, u"network-server"_s)); trackerlessItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackerless"_s, u"network-server"_s));
auto *trackerErrorItem = new QListWidgetItem(this);
trackerErrorItem->setData(Qt::DisplayRole, formatItemText(TRACKERERROR_ROW, 0));
trackerErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s));
auto *otherErrorItem = new QListWidgetItem(this);
otherErrorItem->setData(Qt::DisplayRole, formatItemText(OTHERERROR_ROW, 0));
otherErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s));
auto *warningItem = new QListWidgetItem(this);
warningItem->setData(Qt::DisplayRole, formatItemText(WARNING_ROW, 0));
warningItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_s, u"dialog-warning"_s));
m_trackers[NULL_HOST] = {{}, trackerlessItem}; m_trackers[NULL_HOST] = {0, trackerlessItem};
handleTorrentsLoaded(BitTorrent::Session::instance()->torrents()); const auto *pref = Preferences::instance();
const bool useSeparateTrackerStatusFilter = pref->useSeparateTrackerStatusFilter();
if (useSeparateTrackerStatusFilter == m_handleTrackerStatuses)
enableTrackerStatusItems(!useSeparateTrackerStatusFilter);
connect(pref, &Preferences::changed, this, [this, pref]
{
const bool useSeparateTrackerStatusFilter = pref->useSeparateTrackerStatusFilter();
if (useSeparateTrackerStatusFilter == m_handleTrackerStatuses)
{
enableTrackerStatusItems(!useSeparateTrackerStatusFilter);
updateGeometry();
if (m_handleTrackerStatuses)
applyFilter(currentRow());
}
});
const auto *btSession = BitTorrent::Session::instance();
handleTorrentsLoaded(btSession->torrents());
connect(btSession, &BitTorrent::Session::trackersAdded, this, &TrackersFilterWidget::handleTorrentTrackersAdded);
connect(btSession, &BitTorrent::Session::trackersRemoved, this, &TrackersFilterWidget::handleTorrentTrackersRemoved);
connect(btSession, &BitTorrent::Session::trackersReset, this, &TrackersFilterWidget::handleTorrentTrackersReset);
connect(btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this, &TrackersFilterWidget::handleTorrentTrackerStatusesUpdated);
setCurrentRow(0, QItemSelectionModel::SelectCurrent); setCurrentRow(0, QItemSelectionModel::SelectCurrent);
toggleFilter(Preferences::instance()->getTrackerFilterState());
} }
TrackersFilterWidget::~TrackersFilterWidget() TrackersFilterWidget::~TrackersFilterWidget()
@@ -155,83 +186,64 @@ TrackersFilterWidget::~TrackersFilterWidget()
Utils::Fs::removeFile(iconPath); Utils::Fs::removeFile(iconPath);
} }
void TrackersFilterWidget::addTrackers(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers) void TrackersFilterWidget::handleTorrentTrackersAdded(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers)
{ {
const BitTorrent::TorrentID torrentID = torrent->id(); const QSet<QString> prevTrackerURLs = extractTrackerURLs(torrent->trackers()).subtract(extractTrackerURLs(trackers));
const QSet<QString> addedTrackerHosts = extractTrackerHosts(trackers).subtract(extractTrackerHosts(prevTrackerURLs));
for (const BitTorrent::TrackerEntry &tracker : trackers) for (const QString &trackerHost : addedTrackerHosts)
addItems(tracker.url, {torrentID}); increaseTorrentsCount(trackerHost, 1);
removeItem(NULL_HOST, torrentID); if (prevTrackerURLs.isEmpty())
decreaseTorrentsCount(NULL_HOST); // torrent was trackerless previously
} }
void TrackersFilterWidget::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers) void TrackersFilterWidget::handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent, const QStringList &trackers)
{ {
const BitTorrent::TorrentID torrentID = torrent->id(); const QList<BitTorrent::TrackerEntryStatus> currentTrackerEntries = torrent->trackers();
const QSet<QString> removedTrackerHosts = extractTrackerHosts(trackers).subtract(extractTrackerHosts(currentTrackerEntries));
for (const QString &trackerHost : removedTrackerHosts)
decreaseTorrentsCount(trackerHost);
for (const QString &tracker : trackers) if (currentTrackerEntries.isEmpty())
removeItem(tracker, torrentID); increaseTorrentsCount(NULL_HOST, 1);
if (torrent->trackers().isEmpty()) if (m_handleTrackerStatuses)
addItems(NULL_HOST, {torrentID}); refreshStatusItems(torrent);
} }
void TrackersFilterWidget::refreshTrackers(const BitTorrent::Torrent *torrent) void TrackersFilterWidget::handleTorrentTrackersReset(const BitTorrent::Torrent *torrent
, const QList<BitTorrent::TrackerEntryStatus> &oldEntries, const QList<BitTorrent::TrackerEntry> &newEntries)
{ {
const BitTorrent::TorrentID torrentID = torrent->id(); if (oldEntries.isEmpty())
m_errors.remove(torrentID);
m_trackerErrors.remove(torrentID);
m_warnings.remove(torrentID);
Algorithm::removeIf(m_trackers, [this, &torrentID](const QString &host, TrackerData &trackerData)
{ {
QSet<BitTorrent::TorrentID> &torrentIDs = trackerData.torrents; decreaseTorrentsCount(NULL_HOST);
if (!torrentIDs.remove(torrentID))
return false;
QListWidgetItem *trackerItem = trackerData.item;
if (!host.isEmpty() && torrentIDs.isEmpty())
{
if (currentItem() == trackerItem)
setCurrentRow(0, QItemSelectionModel::SelectCurrent);
delete trackerItem;
return true;
}
trackerItem->setText(formatItemText(host, torrentIDs.size()));
return false;
});
const QList<BitTorrent::TrackerEntryStatus> trackers = torrent->trackers();
if (trackers.isEmpty())
{
addItems(NULL_HOST, {torrentID});
} }
else else
{ {
for (const BitTorrent::TrackerEntryStatus &status : trackers) for (const QString &trackerHost : asConst(extractTrackerHosts(oldEntries)))
addItems(status.url, {torrentID}); decreaseTorrentsCount(trackerHost);
} }
item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size())); if (newEntries.isEmpty())
item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size()));
if (const int row = currentRow(); (row == OTHERERROR_ROW)
|| (row == TRACKERERROR_ROW) || (row == WARNING_ROW))
{ {
applyFilter(row); increaseTorrentsCount(NULL_HOST, 1);
} }
else
{
for (const QString &trackerHost : asConst(extractTrackerHosts(newEntries)))
increaseTorrentsCount(trackerHost, 1);
}
if (m_handleTrackerStatuses)
refreshStatusItems(torrent);
updateGeometry(); updateGeometry();
} }
void TrackersFilterWidget::addItems(const QString &trackerURL, const QList<BitTorrent::TorrentID> &torrents) void TrackersFilterWidget::increaseTorrentsCount(const QString &trackerHost, const qsizetype torrentsCount)
{ {
const QString host = getHost(trackerURL); auto trackersIt = m_trackers.find(trackerHost);
auto trackersIt = m_trackers.find(host);
const bool exists = (trackersIt != m_trackers.end()); const bool exists = (trackersIt != m_trackers.end());
QListWidgetItem *trackerItem = nullptr; QListWidgetItem *trackerItem = nullptr;
@@ -244,33 +256,27 @@ void TrackersFilterWidget::addItems(const QString &trackerURL, const QList<BitTo
trackerItem = new QListWidgetItem(); trackerItem = new QListWidgetItem();
trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_s, u"network-server"_s)); trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_s, u"network-server"_s));
const TrackerData trackerData {{}, trackerItem}; const TrackerData trackerData {0, trackerItem};
trackersIt = m_trackers.insert(host, trackerData); trackersIt = m_trackers.insert(trackerHost, trackerData);
const QString scheme = getScheme(trackerURL); const QString scheme = getScheme(trackerHost);
downloadFavicon(host, u"%1://%2/favicon.ico"_s.arg((scheme.startsWith(u"http") ? scheme : u"http"_s), getFaviconHost(host))); downloadFavicon(trackerHost, u"%1://%2/favicon.ico"_s.arg((scheme.startsWith(u"http") ? scheme : u"http"_s), getFaviconHost(trackerHost)));
} }
Q_ASSERT(trackerItem); Q_ASSERT(trackerItem);
QSet<BitTorrent::TorrentID> &torrentIDs = trackersIt->torrents; trackersIt->torrentsCount += torrentsCount;
for (const BitTorrent::TorrentID &torrentID : torrents)
torrentIDs.insert(torrentID);
trackerItem->setText(formatItemText(host, torrentIDs.size())); trackerItem->setText(formatItemText(trackerHost, trackersIt->torrentsCount));
if (exists) if (exists)
{
if (item(currentRow()) == trackerItem)
applyFilter(currentRow());
return; return;
}
Q_ASSERT(count() >= NUM_SPECIAL_ROWS); Q_ASSERT(count() >= numSpecialRows());
const Utils::Compare::NaturalLessThan<Qt::CaseSensitive> naturalLessThan {}; const Utils::Compare::NaturalLessThan<Qt::CaseSensitive> naturalLessThan {};
int insPos = count(); int insPos = count();
for (int i = NUM_SPECIAL_ROWS; i < count(); ++i) for (int i = numSpecialRows(); i < count(); ++i)
{ {
if (naturalLessThan(host, item(i)->text())) if (naturalLessThan(trackerHost, item(i)->text()))
{ {
insPos = i; insPos = i;
break; break;
@@ -280,88 +286,59 @@ void TrackersFilterWidget::addItems(const QString &trackerURL, const QList<BitTo
updateGeometry(); updateGeometry();
} }
void TrackersFilterWidget::removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id) void TrackersFilterWidget::decreaseTorrentsCount(const QString &trackerHost)
{ {
const QString host = getHost(trackerURL); const auto iter = m_trackers.find(trackerHost);
Q_ASSERT(iter != m_trackers.end());
if (iter == m_trackers.end()) [[unlikely]]
return;
QSet<BitTorrent::TorrentID> torrentIDs = m_trackers.value(host).torrents; TrackerData &trackerData = iter.value();
torrentIDs.remove(id); Q_ASSERT(trackerData.torrentsCount > 0);
if (trackerData.torrentsCount <= 0) [[unlikely]]
return;
QListWidgetItem *trackerItem = nullptr; --trackerData.torrentsCount;
if (!host.isEmpty()) if ((trackerData.torrentsCount == 0) && (trackerHost != NULL_HOST))
{ {
// Remove from 'Error', 'Tracker error' and 'Warning' view if (currentItem() == trackerData.item)
if (const auto errorHashesIt = m_errors.find(id) setCurrentRow(0, QItemSelectionModel::SelectCurrent);
; errorHashesIt != m_errors.end()) delete trackerData.item;
{ m_trackers.erase(iter);
QSet<QString> &errored = *errorHashesIt; updateGeometry();
errored.remove(trackerURL);
if (errored.isEmpty())
{
m_errors.erase(errorHashesIt);
item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
if (currentRow() == OTHERERROR_ROW)
applyFilter(OTHERERROR_ROW);
}
}
if (const auto trackerErrorHashesIt = m_trackerErrors.find(id)
; trackerErrorHashesIt != m_trackerErrors.end())
{
QSet<QString> &errored = *trackerErrorHashesIt;
errored.remove(trackerURL);
if (errored.isEmpty())
{
m_trackerErrors.erase(trackerErrorHashesIt);
item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
if (currentRow() == TRACKERERROR_ROW)
applyFilter(TRACKERERROR_ROW);
}
}
if (const auto warningHashesIt = m_warnings.find(id)
; warningHashesIt != m_warnings.end())
{
QSet<QString> &warned = *warningHashesIt;
warned.remove(trackerURL);
if (warned.isEmpty())
{
m_warnings.erase(warningHashesIt);
item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size()));
if (currentRow() == WARNING_ROW)
applyFilter(WARNING_ROW);
}
}
trackerItem = m_trackers.value(host).item;
if (torrentIDs.isEmpty())
{
if (currentItem() == trackerItem)
setCurrentRow(0, QItemSelectionModel::SelectCurrent);
delete trackerItem;
m_trackers.remove(host);
updateGeometry();
return;
}
if (trackerItem)
trackerItem->setText(u"%1 (%2)"_s.arg(host, QString::number(torrentIDs.size())));
} }
else else
{ {
trackerItem = item(TRACKERLESS_ROW); trackerData.item->setText(formatItemText(trackerHost, trackerData.torrentsCount));
trackerItem->setText(formatItemText(TRACKERLESS_ROW, torrentIDs.size()));
} }
m_trackers.insert(host, {torrentIDs, trackerItem});
if (currentItem() == trackerItem)
applyFilter(currentRow());
} }
void TrackersFilterWidget::setDownloadTrackerFavicon(bool value) void TrackersFilterWidget::refreshStatusItems(const BitTorrent::Torrent *torrent)
{
const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus();
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning))
m_warnings.insert(torrent);
else
m_warnings.remove(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError))
m_trackerErrors.insert(torrent);
else
m_trackerErrors.remove(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError))
m_errors.insert(torrent);
else
m_errors.remove(torrent);
item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size()));
}
void TrackersFilterWidget::setDownloadTrackerFavicon(const bool value)
{ {
if (value == m_downloadTrackerFavicon) return; if (value == m_downloadTrackerFavicon) return;
m_downloadTrackerFavicon = value; m_downloadTrackerFavicon = value;
@@ -381,107 +358,11 @@ void TrackersFilterWidget::setDownloadTrackerFavicon(bool value)
} }
} }
void TrackersFilterWidget::handleTrackerStatusesUpdated(const BitTorrent::Torrent *torrent void TrackersFilterWidget::handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers) , [[maybe_unused]] const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
{ {
const BitTorrent::TorrentID id = torrent->id(); if (m_handleTrackerStatuses)
refreshStatusItems(torrent);
auto errorHashesIt = m_errors.find(id);
auto trackerErrorHashesIt = m_trackerErrors.find(id);
auto warningHashesIt = m_warnings.find(id);
for (const BitTorrent::TrackerEntryStatus &trackerEntryStatus : updatedTrackers)
{
switch (trackerEntryStatus.state)
{
case BitTorrent::TrackerEndpointState::Working:
{
// remove tracker from "error" and "tracker error" categories
if (errorHashesIt != m_errors.end())
errorHashesIt->remove(trackerEntryStatus.url);
if (trackerErrorHashesIt != m_trackerErrors.end())
trackerErrorHashesIt->remove(trackerEntryStatus.url);
const bool hasNoWarningMessages = std::ranges::all_of(trackerEntryStatus.endpoints
, [](const BitTorrent::TrackerEndpointStatus &endpointEntry)
{
return endpointEntry.message.isEmpty() || (endpointEntry.state != BitTorrent::TrackerEndpointState::Working);
});
if (hasNoWarningMessages)
{
if (warningHashesIt != m_warnings.end())
{
warningHashesIt->remove(trackerEntryStatus.url);
}
}
else
{
if (warningHashesIt == m_warnings.end())
warningHashesIt = m_warnings.insert(id, {});
warningHashesIt->insert(trackerEntryStatus.url);
}
}
break;
case BitTorrent::TrackerEndpointState::NotWorking:
case BitTorrent::TrackerEndpointState::Unreachable:
{
// remove tracker from "tracker error" and "warning" categories
if (warningHashesIt != m_warnings.end())
warningHashesIt->remove(trackerEntryStatus.url);
if (trackerErrorHashesIt != m_trackerErrors.end())
trackerErrorHashesIt->remove(trackerEntryStatus.url);
if (errorHashesIt == m_errors.end())
errorHashesIt = m_errors.insert(id, {});
errorHashesIt->insert(trackerEntryStatus.url);
}
break;
case BitTorrent::TrackerEndpointState::TrackerError:
{
// remove tracker from "error" and "warning" categories
if (warningHashesIt != m_warnings.end())
warningHashesIt->remove(trackerEntryStatus.url);
if (errorHashesIt != m_errors.end())
errorHashesIt->remove(trackerEntryStatus.url);
if (trackerErrorHashesIt == m_trackerErrors.end())
trackerErrorHashesIt = m_trackerErrors.insert(id, {});
trackerErrorHashesIt->insert(trackerEntryStatus.url);
}
break;
case BitTorrent::TrackerEndpointState::NotContacted:
{
// remove tracker from "error", "tracker error" and "warning" categories
if (warningHashesIt != m_warnings.end())
warningHashesIt->remove(trackerEntryStatus.url);
if (errorHashesIt != m_errors.end())
errorHashesIt->remove(trackerEntryStatus.url);
if (trackerErrorHashesIt != m_trackerErrors.end())
trackerErrorHashesIt->remove(trackerEntryStatus.url);
}
break;
};
}
if ((errorHashesIt != m_errors.end()) && errorHashesIt->isEmpty())
m_errors.erase(errorHashesIt);
if ((trackerErrorHashesIt != m_trackerErrors.end()) && trackerErrorHashesIt->isEmpty())
m_trackerErrors.erase(trackerErrorHashesIt);
if ((warningHashesIt != m_warnings.end()) && warningHashesIt->isEmpty())
m_warnings.erase(warningHashesIt);
item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size()));
if (const int row = currentRow(); (row == OTHERERROR_ROW)
|| (row == TRACKERERROR_ROW) || (row == WARNING_ROW))
{
applyFilter(row);
}
} }
void TrackersFilterWidget::downloadFavicon(const QString &trackerHost, const QString &faviconURL) void TrackersFilterWidget::downloadFavicon(const QString &trackerHost, const QString &faviconURL)
@@ -500,19 +381,14 @@ void TrackersFilterWidget::downloadFavicon(const QString &trackerHost, const QSt
downloadingFaviconNode.insert(trackerHost); downloadingFaviconNode.insert(trackerHost);
} }
void TrackersFilterWidget::removeTracker(const QString &tracker) void TrackersFilterWidget::removeTracker(const QString &trackerHost)
{ {
for (const BitTorrent::TorrentID &torrentID : asConst(m_trackers.value(tracker).torrents)) for (BitTorrent::Torrent *torrent : asConst(BitTorrent::Session::instance()->torrents()))
{ {
auto *torrent = BitTorrent::Session::instance()->getTorrent(torrentID);
Q_ASSERT(torrent);
if (!torrent) [[unlikely]]
continue;
QStringList trackersToRemove; QStringList trackersToRemove;
for (const BitTorrent::TrackerEntryStatus &trackerEntryStatus : asConst(torrent->trackers())) for (const BitTorrent::TrackerEntryStatus &trackerEntryStatus : asConst(torrent->trackers()))
{ {
if ((trackerEntryStatus.url == tracker) || (QUrl(trackerEntryStatus.url).host() == tracker)) if (getTrackerHost(trackerEntryStatus) == trackerHost)
trackersToRemove.append(trackerEntryStatus.url); trackersToRemove.append(trackerEntryStatus.url);
} }
@@ -522,6 +398,72 @@ void TrackersFilterWidget::removeTracker(const QString &tracker)
updateGeometry(); updateGeometry();
} }
void TrackersFilterWidget::enableTrackerStatusItems(const bool value)
{
m_handleTrackerStatuses = value;
if (m_handleTrackerStatuses)
{
auto *trackerErrorItem = new QListWidgetItem;
trackerErrorItem->setData(Qt::DisplayRole, formatItemText(TRACKERERROR_ROW, 0));
trackerErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s));
insertItem(TRACKERERROR_ROW, trackerErrorItem);
auto *otherErrorItem = new QListWidgetItem;
otherErrorItem->setData(Qt::DisplayRole, formatItemText(OTHERERROR_ROW, 0));
otherErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s));
insertItem(OTHERERROR_ROW, otherErrorItem);
auto *warningItem = new QListWidgetItem;
warningItem->setData(Qt::DisplayRole, formatItemText(WARNING_ROW, 0));
warningItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_s, u"dialog-warning"_s));
insertItem(WARNING_ROW, warningItem);
const QList<BitTorrent::Torrent *> torrents = BitTorrent::Session::instance()->torrents();
for (const BitTorrent::Torrent *torrent : torrents)
{
const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus();
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning))
m_warnings.insert(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError))
m_trackerErrors.insert(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError))
m_errors.insert(torrent);
}
warningItem->setText(formatItemText(WARNING_ROW, m_warnings.size()));
trackerErrorItem->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
otherErrorItem->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
}
else
{
if (const int row = currentRow();
(row == WARNING_ROW) || (row == TRACKERERROR_ROW) || (row == OTHERERROR_ROW))
{
setCurrentRow(0, QItemSelectionModel::ClearAndSelect);
}
// Need to be removed in reversed order
takeItem(WARNING_ROW);
takeItem(OTHERERROR_ROW);
takeItem(TRACKERERROR_ROW);
m_warnings.clear();
m_trackerErrors.clear();
m_errors.clear();
}
}
qsizetype TrackersFilterWidget::numSpecialRows() const
{
if (m_handleTrackerStatuses)
return NUM_SPECIAL_ROWS;
return NUM_SPECIAL_ROWS - 3;
}
void TrackersFilterWidget::handleFavicoDownloadFinished(const Net::DownloadResult &result) void TrackersFilterWidget::handleFavicoDownloadFinished(const Net::DownloadResult &result)
{ {
const QSet<QString> trackerHosts = m_downloadingFavicons.take(result.url); const QSet<QString> trackerHosts = m_downloadingFavicons.take(result.url);
@@ -590,7 +532,7 @@ void TrackersFilterWidget::showMenu()
QMenu *menu = new QMenu(this); QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose); menu->setAttribute(Qt::WA_DeleteOnClose);
if (currentRow() >= NUM_SPECIAL_ROWS) if (currentRow() >= numSpecialRows())
{ {
menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove tracker") menu->addAction(UIThemeManager::instance()->getIcon(u"edit-clear"_s, u"list-remove"_s), tr("Remove tracker")
, this, &TrackersFilterWidget::onRemoveTrackerTriggered); , this, &TrackersFilterWidget::onRemoveTrackerTriggered);
@@ -609,30 +551,80 @@ void TrackersFilterWidget::showMenu()
void TrackersFilterWidget::applyFilter(const int row) void TrackersFilterWidget::applyFilter(const int row)
{ {
if (row == ALL_ROW) if (m_handleTrackerStatuses)
transferList()->applyTrackerFilterAll(); {
else if (isVisible()) switch (row)
transferList()->applyTrackerFilter(getTorrentIDs(row)); {
case ALL_ROW:
transferList()->applyTrackerFilter(std::nullopt);
transferList()->applyAnnounceStatusFilter(std::nullopt);
break;
case TRACKERLESS_ROW:
transferList()->applyTrackerFilter(NULL_HOST);
transferList()->applyAnnounceStatusFilter(std::nullopt);
break;
case OTHERERROR_ROW:
transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError);
transferList()->applyTrackerFilter(std::nullopt);
break;
case TRACKERERROR_ROW:
transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError);
transferList()->applyTrackerFilter(std::nullopt);
break;
case WARNING_ROW:
transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasWarning);
transferList()->applyTrackerFilter(std::nullopt);
break;
default:
transferList()->applyTrackerFilter(trackerFromRow(row));
transferList()->applyAnnounceStatusFilter(std::nullopt);
break;
}
}
else
{
switch (row)
{
case ALL_ROW:
transferList()->applyTrackerFilter(std::nullopt);
break;
case TRACKERLESS_ROW:
transferList()->applyTrackerFilter(NULL_HOST);
break;
default:
transferList()->applyTrackerFilter(trackerFromRow(row));
break;
}
}
} }
void TrackersFilterWidget::handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents) void TrackersFilterWidget::handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents)
{ {
QHash<QString, QList<BitTorrent::TorrentID>> torrentsPerTracker; QHash<QString, qsizetype> torrentsPerTrackerHost;
for (const BitTorrent::Torrent *torrent : torrents) for (const BitTorrent::Torrent *torrent : torrents)
{ {
const BitTorrent::TorrentID torrentID = torrent->id();
const QList<BitTorrent::TrackerEntryStatus> trackers = torrent->trackers();
for (const BitTorrent::TrackerEntryStatus &tracker : trackers)
torrentsPerTracker[tracker.url].append(torrentID);
// Check for trackerless torrent // Check for trackerless torrent
if (trackers.isEmpty()) if (const QList<BitTorrent::TrackerEntryStatus> trackers = torrent->trackers(); trackers.isEmpty())
torrentsPerTracker[NULL_HOST].append(torrentID); {
++torrentsPerTrackerHost[NULL_HOST];
}
else
{
for (const QString &trackerHost : asConst(extractTrackerHosts(trackers)))
++torrentsPerTrackerHost[trackerHost];
}
} }
for (const auto &[trackerURL, torrents] : asConst(torrentsPerTracker).asKeyValueRange()) for (const auto &[trackerHost, torrentsCount] : asConst(torrentsPerTrackerHost).asKeyValueRange())
{ {
addItems(trackerURL, torrents); increaseTorrentsCount(trackerHost, torrentsCount);
} }
m_totalTorrents += torrents.count(); m_totalTorrents += torrents.count();
@@ -641,22 +633,35 @@ void TrackersFilterWidget::handleTorrentsLoaded(const QList<BitTorrent::Torrent
void TrackersFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent) void TrackersFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
{ {
const BitTorrent::TorrentID torrentID = torrent->id();
const QList<BitTorrent::TrackerEntryStatus> trackers = torrent->trackers();
for (const BitTorrent::TrackerEntryStatus &tracker : trackers)
removeItem(tracker.url, torrentID);
// Check for trackerless torrent // Check for trackerless torrent
if (trackers.isEmpty()) if (const QList<BitTorrent::TrackerEntryStatus> trackers = torrent->trackers(); trackers.isEmpty())
removeItem(NULL_HOST, torrentID); {
decreaseTorrentsCount(NULL_HOST);
}
else
{
for (const QString &trackerHost : asConst(extractTrackerHosts(trackers)))
decreaseTorrentsCount(trackerHost);
}
item(ALL_ROW)->setText(formatItemText(ALL_ROW, --m_totalTorrents)); item(ALL_ROW)->setText(formatItemText(ALL_ROW, --m_totalTorrents));
if (m_handleTrackerStatuses)
{
m_warnings.remove(torrent);
m_trackerErrors.remove(torrent);
m_errors.remove(torrent);
item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size()));
item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
}
} }
void TrackersFilterWidget::onRemoveTrackerTriggered() void TrackersFilterWidget::onRemoveTrackerTriggered()
{ {
const int row = currentRow(); const int row = currentRow();
if (row < NUM_SPECIAL_ROWS) if (row < numSpecialRows())
return; return;
const QString &tracker = trackerFromRow(row); const QString &tracker = trackerFromRow(row);
@@ -694,27 +699,10 @@ QString TrackersFilterWidget::trackerFromRow(int row) const
int TrackersFilterWidget::rowFromTracker(const QString &tracker) const int TrackersFilterWidget::rowFromTracker(const QString &tracker) const
{ {
Q_ASSERT(!tracker.isEmpty()); Q_ASSERT(!tracker.isEmpty());
for (int i = NUM_SPECIAL_ROWS; i < count(); ++i) for (int i = numSpecialRows(); i < count(); ++i)
{ {
if (tracker == trackerFromRow(i)) if (tracker == trackerFromRow(i))
return i; return i;
} }
return -1; return -1;
} }
QSet<BitTorrent::TorrentID> TrackersFilterWidget::getTorrentIDs(const int row) const
{
switch (row)
{
case TRACKERLESS_ROW:
return m_trackers.value(NULL_HOST).torrents;
case OTHERERROR_ROW:
return {m_errors.keyBegin(), m_errors.keyEnd()};
case TRACKERERROR_ROW:
return {m_trackerErrors.keyBegin(), m_trackerErrors.keyEnd()};
case WARNING_ROW:
return {m_warnings.keyBegin(), m_warnings.keyEnd()};
default:
return m_trackers.value(trackerFromRow(row)).torrents;
}
}

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-2025 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
@@ -57,11 +57,6 @@ public:
TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon); TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon);
~TrackersFilterWidget() override; ~TrackersFilterWidget() override;
void addTrackers(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers);
void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void refreshTrackers(const BitTorrent::Torrent *torrent);
void handleTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers);
void setDownloadTrackerFavicon(bool value); void setDownloadTrackerFavicon(bool value);
private slots: private slots:
@@ -75,28 +70,40 @@ private:
void handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents) override; void handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *torrent) override; void torrentAboutToBeDeleted(BitTorrent::Torrent *torrent) override;
void handleTorrentTrackersAdded(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers);
void handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void handleTorrentTrackersReset(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntryStatus> &oldEntries
, const QList<BitTorrent::TrackerEntry> &newEntries);
void handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers);
void onRemoveTrackerTriggered(); void onRemoveTrackerTriggered();
void addItems(const QString &trackerURL, const QList<BitTorrent::TorrentID> &torrents); void increaseTorrentsCount(const QString &trackerHost, qsizetype torrentsCount);
void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id); void decreaseTorrentsCount(const QString &trackerHost);
void refreshStatusItems(const BitTorrent::Torrent *torrent);
QString trackerFromRow(int row) const; QString trackerFromRow(int row) const;
int rowFromTracker(const QString &tracker) const; int rowFromTracker(const QString &tracker) const;
QSet<BitTorrent::TorrentID> getTorrentIDs(int row) const;
void downloadFavicon(const QString &trackerHost, const QString &faviconURL); void downloadFavicon(const QString &trackerHost, const QString &faviconURL);
void removeTracker(const QString &tracker); void removeTracker(const QString &trackerHost);
void enableTrackerStatusItems(bool value);
qsizetype numSpecialRows() const;
struct TrackerData struct TrackerData
{ {
QSet<BitTorrent::TorrentID> torrents; qsizetype torrentsCount = 0;
QListWidgetItem *item = nullptr; QListWidgetItem *item = nullptr;
}; };
QHash<QString, TrackerData> m_trackers; // <tracker host, tracker data> QHash<QString, TrackerData> m_trackers; // <tracker host, tracker data>
QHash<BitTorrent::TorrentID, QSet<QString>> m_errors; // <torrent ID, tracker hosts> QSet<const BitTorrent::Torrent *> m_errors;
QHash<BitTorrent::TorrentID, QSet<QString>> m_trackerErrors; // <torrent ID, tracker hosts> QSet<const BitTorrent::Torrent *> m_trackerErrors;
QHash<BitTorrent::TorrentID, QSet<QString>> m_warnings; // <torrent ID, tracker hosts> QSet<const BitTorrent::Torrent *> m_warnings;
PathList m_iconPaths; PathList m_iconPaths;
int m_totalTorrents = 0; int m_totalTorrents = 0;
bool m_downloadTrackerFavicon = false; bool m_downloadTrackerFavicon = false;
bool m_handleTrackerStatuses = false;
QHash<QString, QSet<QString>> m_downloadingFavicons; // <favicon URL, tracker hosts> QHash<QString, QSet<QString>> m_downloadingFavicons; // <favicon URL, tracker hosts>
}; };

View File

@@ -0,0 +1,218 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* 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 "trackerstatusfilterwidget.h"
#include <QCheckBox>
#include <QIcon>
#include <QListWidgetItem>
#include <QMenu>
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "base/preferences.h"
#include "gui/transferlistwidget.h"
#include "gui/uithememanager.h"
namespace
{
enum TRACKERSTATUS_FILTER_ROW
{
ANY_ROW,
WARNING_ROW,
TRACKERERROR_ROW,
OTHERERROR_ROW,
NUM_SPECIAL_ROWS
};
QString getFormatStringForRow(const int row)
{
switch (row)
{
case ANY_ROW:
return TrackerStatusFilterWidget::tr("All (%1)", "this is for the tracker filter");
case WARNING_ROW:
return TrackerStatusFilterWidget::tr("Warning (%1)");
case TRACKERERROR_ROW:
return TrackerStatusFilterWidget::tr("Tracker error (%1)");
case OTHERERROR_ROW:
return TrackerStatusFilterWidget::tr("Other error (%1)");
default:
return {};
}
}
QString formatItemText(const int row, const int torrentsCount)
{
return getFormatStringForRow(row).arg(torrentsCount);
}
}
TrackerStatusFilterWidget::TrackerStatusFilterWidget(QWidget *parent, TransferListWidget *transferList)
: BaseFilterWidget(parent, transferList)
{
auto *anyStatusItem = new QListWidgetItem(this);
anyStatusItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_s, u"network-server"_s));
auto *warningItem = new QListWidgetItem(this);
warningItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-warning"_s, u"dialog-warning"_s));
auto *trackerErrorItem = new QListWidgetItem(this);
trackerErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s));
auto *otherErrorItem = new QListWidgetItem(this);
otherErrorItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"tracker-error"_s, u"dialog-error"_s));
const auto *btSession = BitTorrent::Session::instance();
const QList<BitTorrent::Torrent *> torrents = btSession->torrents();
m_totalTorrents += torrents.count();
for (const BitTorrent::Torrent *torrent : torrents)
{
const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus();
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning))
m_warnings.insert(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError))
m_trackerErrors.insert(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError))
m_errors.insert(torrent);
}
connect(btSession, &BitTorrent::Session::trackersRemoved, this, &TrackerStatusFilterWidget::handleTorrentTrackersRemoved);
connect(btSession, &BitTorrent::Session::trackersReset, this, &TrackerStatusFilterWidget::handleTorrentTrackersReset);
connect(btSession, &BitTorrent::Session::trackerEntryStatusesUpdated, this, &TrackerStatusFilterWidget::handleTorrentTrackerStatusesUpdated);
anyStatusItem->setText(formatItemText(ANY_ROW, m_totalTorrents));
warningItem->setText(formatItemText(WARNING_ROW, m_warnings.size()));
trackerErrorItem->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
otherErrorItem->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
setCurrentRow(0, QItemSelectionModel::SelectCurrent);
setVisible(Preferences::instance()->getTrackerStatusFilterState());
}
void TrackerStatusFilterWidget::handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent)
{
refreshItems(torrent);
}
void TrackerStatusFilterWidget::handleTorrentTrackersReset(const BitTorrent::Torrent *torrent
, [[maybe_unused]] const QList<BitTorrent::TrackerEntryStatus> &oldEntries, [[maybe_unused]] const QList<BitTorrent::TrackerEntry> &newEntries)
{
refreshItems(torrent);
}
void TrackerStatusFilterWidget::handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent)
{
refreshItems(torrent);
}
void TrackerStatusFilterWidget::showMenu()
{
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s), tr("Start torrents")
, transferList(), &TransferListWidget::startVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s), tr("Stop torrents")
, transferList(), &TransferListWidget::stopVisibleTorrents);
menu->addAction(UIThemeManager::instance()->getIcon(u"list-remove"_s), tr("Remove torrents")
, transferList(), &TransferListWidget::deleteVisibleTorrents);
menu->popup(QCursor::pos());
}
void TrackerStatusFilterWidget::applyFilter(const int row)
{
switch (row)
{
case ANY_ROW:
transferList()->applyAnnounceStatusFilter(std::nullopt);
break;
case WARNING_ROW:
transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasWarning);
break;
case TRACKERERROR_ROW:
transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError);
break;
case OTHERERROR_ROW:
transferList()->applyAnnounceStatusFilter(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError);
break;
}
}
void TrackerStatusFilterWidget::handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents)
{
m_totalTorrents += torrents.count();
item(ANY_ROW)->setText(formatItemText(ANY_ROW, m_totalTorrents));
}
void TrackerStatusFilterWidget::refreshItems(const BitTorrent::Torrent *torrent)
{
const BitTorrent::TorrentAnnounceStatus announceStatus = torrent->announceStatus();
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasWarning))
m_warnings.insert(torrent);
else
m_warnings.remove(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasTrackerError))
m_trackerErrors.insert(torrent);
else
m_trackerErrors.remove(torrent);
if (announceStatus.testFlag(BitTorrent::TorrentAnnounceStatusFlag::HasOtherError))
m_errors.insert(torrent);
else
m_errors.remove(torrent);
item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size()));
}
void TrackerStatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
{
m_warnings.remove(torrent);
m_trackerErrors.remove(torrent);
m_errors.remove(torrent);
item(ANY_ROW)->setText(formatItemText(ANY_ROW, --m_totalTorrents));
item(WARNING_ROW)->setText(formatItemText(WARNING_ROW, m_warnings.size()));
item(TRACKERERROR_ROW)->setText(formatItemText(TRACKERERROR_ROW, m_trackerErrors.size()));
item(OTHERERROR_ROW)->setText(formatItemText(OTHERERROR_ROW, m_errors.size()));
}

View File

@@ -0,0 +1,66 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* 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 <QtContainerFwd>
#include <QSet>
#include "basefilterwidget.h"
class TransferListWidget;
class TrackerStatusFilterWidget final : public BaseFilterWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TrackerStatusFilterWidget)
public:
TrackerStatusFilterWidget(QWidget *parent, TransferListWidget *transferList);
private:
// These 4 methods are virtual slots in the base class.
// No need to redeclare them here as slots.
void showMenu() override;
void applyFilter(int row) override;
void handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents) override;
void torrentAboutToBeDeleted(BitTorrent::Torrent *torrent) override;
void handleTorrentTrackersRemoved(const BitTorrent::Torrent *torrent);
void handleTorrentTrackersReset(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntryStatus> &oldEntries
, const QList<BitTorrent::TrackerEntry> &newEntries);
void handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent);
void refreshItems(const BitTorrent::Torrent *torrent);
QSet<const BitTorrent::Torrent *> m_errors;
QSet<const BitTorrent::Torrent *> m_trackerErrors;
QSet<const BitTorrent::Torrent *> m_warnings;
int m_totalTorrents = 0;
};

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-2025 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
@@ -29,61 +29,26 @@
#include "transferlistfilterswidget.h" #include "transferlistfilterswidget.h"
#include <QCheckBox>
#include <QIcon> #include <QIcon>
#include <QListWidgetItem> #include <QListWidgetItem>
#include <QMenu> #include <QMenu>
#include <QPainter>
#include <QScrollArea> #include <QScrollArea>
#include <QStyleOptionButton>
#include <QUrl> #include <QUrl>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "base/bittorrent/session.h" #include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h"
#include "base/bittorrent/trackerentrystatus.h"
#include "base/global.h" #include "base/global.h"
#include "base/logger.h"
#include "base/net/downloadmanager.h"
#include "base/preferences.h" #include "base/preferences.h"
#include "base/torrentfilter.h"
#include "base/utils/compare.h" #include "base/utils/compare.h"
#include "base/utils/fs.h"
#include "transferlistfilters/categoryfilterwidget.h" #include "transferlistfilters/categoryfilterwidget.h"
#include "transferlistfilters/statusfilterwidget.h" #include "transferlistfilters/statusfilterwidget.h"
#include "transferlistfilters/tagfilterwidget.h" #include "transferlistfilters/tagfilterwidget.h"
#include "transferlistfilters/trackersfilterwidget.h" #include "transferlistfilters/trackersfilterwidget.h"
#include "transferlistfilters/trackerstatusfilterwidget.h"
#include "transferlistfilterswidgetitem.h"
#include "transferlistwidget.h" #include "transferlistwidget.h"
#include "uithememanager.h"
#include "utils.h" #include "utils.h"
namespace
{
class ArrowCheckBox final : public QCheckBox
{
public:
using QCheckBox::QCheckBox;
private:
void paintEvent(QPaintEvent *) override
{
QPainter painter(this);
QStyleOptionViewItem indicatorOption;
indicatorOption.initFrom(this);
indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this);
indicatorOption.state |= (QStyle::State_Children
| (isChecked() ? QStyle::State_Open : QStyle::State_None));
style()->drawPrimitive(QStyle::PE_IndicatorBranch, &indicatorOption, &painter, this);
QStyleOptionButton labelOption;
initStyleOption(&labelOption);
labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this);
style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &painter, this);
}
};
}
TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon) TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
: QWidget(parent) : QWidget(parent)
, m_transferList {transferList} , m_transferList {transferList}
@@ -99,66 +64,65 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi
mainWidgetLayout->setSpacing(2); mainWidgetLayout->setSpacing(2);
mainWidgetLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop); mainWidgetLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
QFont font; {
font.setBold(true); auto *item = new TransferListFiltersWidgetItem(tr("Status"), new StatusFilterWidget(this, transferList), this);
font.setCapitalization(QFont::AllUppercase); item->setChecked(pref->getStatusFilterState());
connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setStatusFilterState);
mainWidgetLayout->addWidget(item);
}
QCheckBox *statusLabel = new ArrowCheckBox(tr("Status"), this); {
statusLabel->setChecked(pref->getStatusFilterState()); auto *categoryFilterWidget = new CategoryFilterWidget(this);
statusLabel->setFont(font); connect(categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered
connect(statusLabel, &QCheckBox::toggled, pref, &Preferences::setStatusFilterState); , transferList, &TransferListWidget::deleteVisibleTorrents);
mainWidgetLayout->addWidget(statusLabel); connect(categoryFilterWidget, &CategoryFilterWidget::actionStopTorrentsTriggered
, transferList, &TransferListWidget::stopVisibleTorrents);
connect(categoryFilterWidget, &CategoryFilterWidget::actionStartTorrentsTriggered
, transferList, &TransferListWidget::startVisibleTorrents);
connect(categoryFilterWidget, &CategoryFilterWidget::categoryChanged
, transferList, &TransferListWidget::applyCategoryFilter);
auto *statusFilters = new StatusFilterWidget(this, transferList); auto *item = new TransferListFiltersWidgetItem(tr("Categories"), categoryFilterWidget, this);
connect(statusLabel, &QCheckBox::toggled, statusFilters, &StatusFilterWidget::toggleFilter); item->setChecked(pref->getCategoryFilterState());
mainWidgetLayout->addWidget(statusFilters); connect(item, &TransferListFiltersWidgetItem::toggled, this, [this, categoryFilterWidget](const bool enabled)
{
m_transferList->applyCategoryFilter(enabled ? categoryFilterWidget->currentCategory() : QString());
});
connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setCategoryFilterState);
mainWidgetLayout->addWidget(item);
}
QCheckBox *categoryLabel = new ArrowCheckBox(tr("Categories"), this); {
categoryLabel->setChecked(pref->getCategoryFilterState()); auto *tagFilterWidget = new TagFilterWidget(this);
categoryLabel->setFont(font); connect(tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered
connect(categoryLabel, &QCheckBox::toggled, this , transferList, &TransferListWidget::deleteVisibleTorrents);
, &TransferListFiltersWidget::onCategoryFilterStateChanged); connect(tagFilterWidget, &TagFilterWidget::actionStopTorrentsTriggered
mainWidgetLayout->addWidget(categoryLabel); , transferList, &TransferListWidget::stopVisibleTorrents);
connect(tagFilterWidget, &TagFilterWidget::actionStartTorrentsTriggered
, transferList, &TransferListWidget::startVisibleTorrents);
connect(tagFilterWidget, &TagFilterWidget::tagChanged
, transferList, &TransferListWidget::applyTagFilter);
m_categoryFilterWidget = new CategoryFilterWidget(this); auto *item = new TransferListFiltersWidgetItem(tr("Tags"), tagFilterWidget, this);
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered item->setChecked(pref->getTagFilterState());
, transferList, &TransferListWidget::deleteVisibleTorrents); connect(item, &TransferListFiltersWidgetItem::toggled, this, [this, tagFilterWidget](const bool enabled)
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionStopTorrentsTriggered {
, transferList, &TransferListWidget::stopVisibleTorrents); m_transferList->applyTagFilter(enabled ? tagFilterWidget->currentTag() : std::nullopt);
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionStartTorrentsTriggered });
, transferList, &TransferListWidget::startVisibleTorrents); connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTagFilterState);
connect(m_categoryFilterWidget, &CategoryFilterWidget::categoryChanged mainWidgetLayout->addWidget(item);
, transferList, &TransferListWidget::applyCategoryFilter); }
toggleCategoryFilter(pref->getCategoryFilterState());
mainWidgetLayout->addWidget(m_categoryFilterWidget);
QCheckBox *tagsLabel = new ArrowCheckBox(tr("Tags"), this); const int trackerStatusItemPos = mainWidgetLayout->count();
tagsLabel->setChecked(pref->getTagFilterState());
tagsLabel->setFont(font);
connect(tagsLabel, &QCheckBox::toggled, this, &TransferListFiltersWidget::onTagFilterStateChanged);
mainWidgetLayout->addWidget(tagsLabel);
m_tagFilterWidget = new TagFilterWidget(this); {
connect(m_tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered m_trackersFilterWidget = new TrackersFilterWidget(this, transferList, downloadFavicon);
, transferList, &TransferListWidget::deleteVisibleTorrents);
connect(m_tagFilterWidget, &TagFilterWidget::actionStopTorrentsTriggered
, transferList, &TransferListWidget::stopVisibleTorrents);
connect(m_tagFilterWidget, &TagFilterWidget::actionStartTorrentsTriggered
, transferList, &TransferListWidget::startVisibleTorrents);
connect(m_tagFilterWidget, &TagFilterWidget::tagChanged
, transferList, &TransferListWidget::applyTagFilter);
toggleTagFilter(pref->getTagFilterState());
mainWidgetLayout->addWidget(m_tagFilterWidget);
QCheckBox *trackerLabel = new ArrowCheckBox(tr("Trackers"), this); auto *item = new TransferListFiltersWidgetItem(tr("Trackers"), m_trackersFilterWidget, this);
trackerLabel->setChecked(pref->getTrackerFilterState()); item->setChecked(pref->getTrackerFilterState());
trackerLabel->setFont(font); connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTrackerFilterState);
connect(trackerLabel, &QCheckBox::toggled, pref, &Preferences::setTrackerFilterState); mainWidgetLayout->addWidget(item);
mainWidgetLayout->addWidget(trackerLabel); }
m_trackersFilterWidget = new TrackersFilterWidget(this, transferList, downloadFavicon);
connect(trackerLabel, &QCheckBox::toggled, m_trackersFilterWidget, &TrackersFilterWidget::toggleFilter);
mainWidgetLayout->addWidget(m_trackersFilterWidget);
auto *scroll = new QScrollArea(this); auto *scroll = new QScrollArea(this);
scroll->setWidgetResizable(true); scroll->setWidgetResizable(true);
@@ -169,54 +133,40 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi
auto *vLayout = new QVBoxLayout(this); auto *vLayout = new QVBoxLayout(this);
vLayout->setContentsMargins(0, 0, 0, 0); vLayout->setContentsMargins(0, 0, 0, 0);
vLayout->addWidget(scroll); vLayout->addWidget(scroll);
const auto createTrackerStatusItem = [this, mainWidgetLayout, trackerStatusItemPos, pref]
{
auto *item = new TransferListFiltersWidgetItem(tr("Tracker status"), new TrackerStatusFilterWidget(this, m_transferList), this);
item->setChecked(pref->getTrackerStatusFilterState());
connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTrackerStatusFilterState);
mainWidgetLayout->insertWidget(trackerStatusItemPos, item);
};
const auto removeTrackerStatusItem = [mainWidgetLayout, trackerStatusItemPos]
{
QLayoutItem *layoutItem = mainWidgetLayout->takeAt(trackerStatusItemPos);
delete layoutItem->widget();
delete layoutItem;
};
m_useSeparateTrackerStatusFilter = pref->useSeparateTrackerStatusFilter();
if (m_useSeparateTrackerStatusFilter)
createTrackerStatusItem();
connect(pref, &Preferences::changed, this, [this, pref, createTrackerStatusItem, removeTrackerStatusItem]
{
if (m_useSeparateTrackerStatusFilter == pref->useSeparateTrackerStatusFilter())
return;
m_useSeparateTrackerStatusFilter = !m_useSeparateTrackerStatusFilter;
if (m_useSeparateTrackerStatusFilter)
createTrackerStatusItem();
else
removeTrackerStatusItem();
});
} }
void TransferListFiltersWidget::setDownloadTrackerFavicon(bool value) void TransferListFiltersWidget::setDownloadTrackerFavicon(bool value)
{ {
m_trackersFilterWidget->setDownloadTrackerFavicon(value); m_trackersFilterWidget->setDownloadTrackerFavicon(value);
} }
void TransferListFiltersWidget::addTrackers(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers)
{
m_trackersFilterWidget->addTrackers(torrent, trackers);
}
void TransferListFiltersWidget::removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers)
{
m_trackersFilterWidget->removeTrackers(torrent, trackers);
}
void TransferListFiltersWidget::refreshTrackers(const BitTorrent::Torrent *torrent)
{
m_trackersFilterWidget->refreshTrackers(torrent);
}
void TransferListFiltersWidget::trackerEntryStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
{
m_trackersFilterWidget->handleTrackerStatusesUpdated(torrent, updatedTrackers);
}
void TransferListFiltersWidget::onCategoryFilterStateChanged(bool enabled)
{
toggleCategoryFilter(enabled);
Preferences::instance()->setCategoryFilterState(enabled);
}
void TransferListFiltersWidget::toggleCategoryFilter(bool enabled)
{
m_categoryFilterWidget->setVisible(enabled);
m_transferList->applyCategoryFilter(enabled ? m_categoryFilterWidget->currentCategory() : QString());
}
void TransferListFiltersWidget::onTagFilterStateChanged(bool enabled)
{
toggleTagFilter(enabled);
Preferences::instance()->setTagFilterState(enabled);
}
void TransferListFiltersWidget::toggleTagFilter(bool enabled)
{
m_tagFilterWidget->setVisible(enabled);
m_transferList->applyTagFilter(enabled ? m_tagFilterWidget->currentTag() : std::nullopt);
}

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-2025 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,11 +32,6 @@
#include <QtContainerFwd> #include <QtContainerFwd>
#include <QWidget> #include <QWidget>
#include "base/bittorrent/trackerentry.h"
class CategoryFilterWidget;
class StatusFilterWidget;
class TagFilterWidget;
class TrackersFilterWidget; class TrackersFilterWidget;
class TransferListWidget; class TransferListWidget;
@@ -55,23 +50,8 @@ public:
TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon); TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon);
void setDownloadTrackerFavicon(bool value); void setDownloadTrackerFavicon(bool value);
public slots:
void addTrackers(const BitTorrent::Torrent *torrent, const QList<BitTorrent::TrackerEntry> &trackers);
void removeTrackers(const BitTorrent::Torrent *torrent, const QStringList &trackers);
void refreshTrackers(const BitTorrent::Torrent *torrent);
void trackerEntryStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers);
private slots:
void onCategoryFilterStateChanged(bool enabled);
void onTagFilterStateChanged(bool enabled);
private: private:
void toggleCategoryFilter(bool enabled);
void toggleTagFilter(bool enabled);
TransferListWidget *m_transferList = nullptr; TransferListWidget *m_transferList = nullptr;
TrackersFilterWidget *m_trackersFilterWidget = nullptr; TrackersFilterWidget *m_trackersFilterWidget = nullptr;
CategoryFilterWidget *m_categoryFilterWidget = nullptr; bool m_useSeparateTrackerStatusFilter = false;
TagFilterWidget *m_tagFilterWidget = nullptr;
}; };

View File

@@ -0,0 +1,96 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 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 "transferlistfilterswidgetitem.h"
#include <QCheckBox>
#include <QFont>
#include <QPainter>
#include <QString>
#include <QStyleOptionViewItem>
#include <QVBoxLayout>
namespace
{
class ArrowCheckBox final : public QCheckBox
{
public:
using QCheckBox::QCheckBox;
private:
void paintEvent(QPaintEvent *) override
{
QPainter painter {this};
QStyleOptionViewItem indicatorOption;
indicatorOption.initFrom(this);
indicatorOption.rect = style()->subElementRect(QStyle::SE_CheckBoxIndicator, &indicatorOption, this);
indicatorOption.state |= (QStyle::State_Children
| (isChecked() ? QStyle::State_Open : QStyle::State_None));
style()->drawPrimitive(QStyle::PE_IndicatorBranch, &indicatorOption, &painter, this);
QStyleOptionButton labelOption;
initStyleOption(&labelOption);
labelOption.rect = style()->subElementRect(QStyle::SE_CheckBoxContents, &labelOption, this);
style()->drawControl(QStyle::CE_CheckBoxLabel, &labelOption, &painter, this);
}
};
}
TransferListFiltersWidgetItem::TransferListFiltersWidgetItem(const QString &caption, QWidget *filterWidget, QWidget *parent)
: QWidget(parent)
, m_caption {new ArrowCheckBox(caption, this)}
, m_filterWidget {filterWidget}
{
QFont font;
font.setBold(true);
font.setCapitalization(QFont::AllUppercase);
m_caption->setFont(font);
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 2, 0, 0);
layout->setSpacing(2);
layout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
layout->addWidget(m_caption);
layout->addWidget(m_filterWidget);
m_filterWidget->setVisible(m_caption->isChecked());
connect(m_caption, &QCheckBox::toggled, m_filterWidget, &QWidget::setVisible);
connect(m_caption, &QCheckBox::toggled, this, &TransferListFiltersWidgetItem::toggled);
}
bool TransferListFiltersWidgetItem::isChecked() const
{
return m_caption->isChecked();
}
void TransferListFiltersWidgetItem::setChecked(const bool value)
{
m_caption->setChecked(value);
}

View File

@@ -0,0 +1,53 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 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 <QWidget>
class QCheckBox;
class QString;
class TransferListFiltersWidgetItem final : public QWidget
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TransferListFiltersWidgetItem)
public:
TransferListFiltersWidgetItem(const QString &caption, QWidget *filterWidget, QWidget *parent = nullptr);
bool isChecked() const;
void setChecked(bool value);
signals:
void toggled(bool checked);
private:
QCheckBox *m_caption = nullptr;
QWidget *m_filterWidget = nullptr;
};

View File

@@ -91,27 +91,25 @@ namespace
TransferListModel::TransferListModel(QObject *parent) TransferListModel::TransferListModel(QObject *parent)
: QAbstractListModel {parent} : QAbstractListModel {parent}
, m_statusStrings , m_statusStrings {
{ {BitTorrent::TorrentState::Downloading, tr("Downloading")},
{BitTorrent::TorrentState::Downloading, tr("Downloading")}, {BitTorrent::TorrentState::StalledDownloading, tr("Stalled", "Torrent is waiting for download to begin")},
{BitTorrent::TorrentState::StalledDownloading, tr("Stalled", "Torrent is waiting for download to begin")}, {BitTorrent::TorrentState::DownloadingMetadata, tr("Downloading metadata", "Used when loading a magnet link")},
{BitTorrent::TorrentState::DownloadingMetadata, tr("Downloading metadata", "Used when loading a magnet link")}, {BitTorrent::TorrentState::ForcedDownloadingMetadata, tr("[F] Downloading metadata", "Used when forced to load a magnet link. You probably shouldn't translate the F.")},
{BitTorrent::TorrentState::ForcedDownloadingMetadata, tr("[F] Downloading metadata", "Used when forced to load a magnet link. You probably shouldn't translate the F.")}, {BitTorrent::TorrentState::ForcedDownloading, tr("[F] Downloading", "Used when the torrent is forced started. You probably shouldn't translate the F.")},
{BitTorrent::TorrentState::ForcedDownloading, tr("[F] Downloading", "Used when the torrent is forced started. You probably shouldn't translate the F.")}, {BitTorrent::TorrentState::Uploading, tr("Seeding", "Torrent is complete and in upload-only mode")},
{BitTorrent::TorrentState::Uploading, tr("Seeding", "Torrent is complete and in upload-only mode")}, {BitTorrent::TorrentState::StalledUploading, tr("Seeding", "Torrent is complete and in upload-only mode")},
{BitTorrent::TorrentState::StalledUploading, tr("Seeding", "Torrent is complete and in upload-only mode")}, {BitTorrent::TorrentState::ForcedUploading, tr("[F] Seeding", "Used when the torrent is forced started. You probably shouldn't translate the F.")},
{BitTorrent::TorrentState::ForcedUploading, tr("[F] Seeding", "Used when the torrent is forced started. You probably shouldn't translate the F.")}, {BitTorrent::TorrentState::QueuedDownloading, tr("Queued", "Torrent is queued")},
{BitTorrent::TorrentState::QueuedDownloading, tr("Queued", "Torrent is queued")}, {BitTorrent::TorrentState::QueuedUploading, tr("Queued", "Torrent is queued")},
{BitTorrent::TorrentState::QueuedUploading, tr("Queued", "Torrent is queued")}, {BitTorrent::TorrentState::CheckingDownloading, tr("Checking", "Torrent local data is being checked")},
{BitTorrent::TorrentState::CheckingDownloading, tr("Checking", "Torrent local data is being checked")}, {BitTorrent::TorrentState::CheckingUploading, tr("Checking", "Torrent local data is being checked")},
{BitTorrent::TorrentState::CheckingUploading, tr("Checking", "Torrent local data is being checked")}, {BitTorrent::TorrentState::CheckingResumeData, tr("Checking resume data", "Used when loading the torrents from disk after qbt is launched. It checks the correctness of the .fastresume file. Normally it is completed in a fraction of a second, unless loading many many torrents.")},
{BitTorrent::TorrentState::CheckingResumeData, tr("Checking resume data", "Used when loading the torrents from disk after qbt is launched. It checks the correctness of the .fastresume file. Normally it is completed in a fraction of a second, unless loading many many torrents.")}, {BitTorrent::TorrentState::StoppedDownloading, tr("Stopped")},
{BitTorrent::TorrentState::StoppedDownloading, tr("Stopped")}, {BitTorrent::TorrentState::StoppedUploading, tr("Completed")},
{BitTorrent::TorrentState::StoppedUploading, tr("Completed")}, {BitTorrent::TorrentState::Moving, tr("Moving", "Torrent local data are being moved/relocated")},
{BitTorrent::TorrentState::Moving, tr("Moving", "Torrent local data are being moved/relocated")}, {BitTorrent::TorrentState::MissingFiles, tr("Missing Files")},
{BitTorrent::TorrentState::MissingFiles, tr("Missing Files")}, {BitTorrent::TorrentState::Error, tr("Errored", "Torrent status, the torrent has an error")}}
{BitTorrent::TorrentState::Error, tr("Errored", "Torrent status, the torrent has an error")}
}
{ {
configure(); configure();
connect(Preferences::instance(), &Preferences::changed, this, &TransferListModel::configure); connect(Preferences::instance(), &Preferences::changed, this, &TransferListModel::configure);
@@ -137,6 +135,8 @@ TransferListModel::TransferListModel(QObject *parent)
connect(Session::instance(), &Session::torrentStarted, this, &TransferListModel::handleTorrentStatusUpdated); connect(Session::instance(), &Session::torrentStarted, this, &TransferListModel::handleTorrentStatusUpdated);
connect(Session::instance(), &Session::torrentStopped, this, &TransferListModel::handleTorrentStatusUpdated); connect(Session::instance(), &Session::torrentStopped, this, &TransferListModel::handleTorrentStatusUpdated);
connect(Session::instance(), &Session::torrentFinishedChecking, this, &TransferListModel::handleTorrentStatusUpdated); connect(Session::instance(), &Session::torrentFinishedChecking, this, &TransferListModel::handleTorrentStatusUpdated);
connect(Session::instance(), &Session::trackerEntryStatusesUpdated, this, &TransferListModel::handleTorrentStatusUpdated);
} }
int TransferListModel::rowCount(const QModelIndex &) const int TransferListModel::rowCount(const QModelIndex &) const
@@ -178,8 +178,8 @@ QVariant TransferListModel::headerData(const int section, const Qt::Orientation
case TR_UPLIMIT: return tr("Up Limit", "i.e: Upload limit"); case TR_UPLIMIT: return tr("Up Limit", "i.e: Upload limit");
case TR_AMOUNT_DOWNLOADED: return tr("Downloaded", "Amount of data downloaded (e.g. in MB)"); case TR_AMOUNT_DOWNLOADED: return tr("Downloaded", "Amount of data downloaded (e.g. in MB)");
case TR_AMOUNT_UPLOADED: return tr("Uploaded", "Amount of data uploaded (e.g. in MB)"); case TR_AMOUNT_UPLOADED: return tr("Uploaded", "Amount of data uploaded (e.g. in MB)");
case TR_AMOUNT_DOWNLOADED_SESSION: return tr("Session Download", "Amount of data downloaded since program open (e.g. in MB)"); case TR_AMOUNT_DOWNLOADED_SESSION: return tr("Session Downloaded", "Amount of data downloaded since program open (e.g. in MB)");
case TR_AMOUNT_UPLOADED_SESSION: return tr("Session Upload", "Amount of data uploaded since program open (e.g. in MB)"); case TR_AMOUNT_UPLOADED_SESSION: return tr("Session Uploaded", "Amount of data uploaded since program open (e.g. in MB)");
case TR_AMOUNT_LEFT: return tr("Remaining", "Amount of data left to download (e.g. in MB)"); case TR_AMOUNT_LEFT: return tr("Remaining", "Amount of data left to download (e.g. in MB)");
case TR_TIME_ELAPSED: return tr("Time Active", "Time (duration) the torrent is active (not stopped)"); case TR_TIME_ELAPSED: return tr("Time Active", "Time (duration) the torrent is active (not stopped)");
case TR_SAVE_PATH: return tr("Save Path", "Torrent save path"); case TR_SAVE_PATH: return tr("Save Path", "Torrent save path");
@@ -392,7 +392,7 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
case TR_RATIO: case TR_RATIO:
return ratioString(torrent->realRatio()); return ratioString(torrent->realRatio());
case TR_RATIO_LIMIT: case TR_RATIO_LIMIT:
return ratioString(torrent->maxRatio()); return ratioString(torrent->effectiveRatioLimit());
case TR_POPULARITY: case TR_POPULARITY:
return ratioString(torrent->popularity()); return ratioString(torrent->popularity());
case TR_CATEGORY: case TR_CATEGORY:
@@ -509,7 +509,7 @@ QVariant TransferListModel::internalValue(const BitTorrent::Torrent *torrent, co
case TR_COMPLETED: case TR_COMPLETED:
return torrent->completedSize(); return torrent->completedSize();
case TR_RATIO_LIMIT: case TR_RATIO_LIMIT:
return torrent->maxRatio(); return torrent->effectiveRatioLimit();
case TR_SEEN_COMPLETE_DATE: case TR_SEEN_COMPLETE_DATE:
return torrent->lastSeenComplete(); return torrent->lastSeenComplete();
case TR_LAST_ACTIVITY: case TR_LAST_ACTIVITY:

View File

@@ -124,14 +124,14 @@ void TransferListSortModel::sort(const int column, const Qt::SortOrder order)
QSortFilterProxyModel::sort(column, order); QSortFilterProxyModel::sort(column, order);
} }
void TransferListSortModel::setStatusFilter(const TorrentFilter::Type filter) void TransferListSortModel::setStatusFilter(const TorrentFilter::Status status)
{ {
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange(); beginFilterChange();
m_filter.setType(filter); m_filter.setStatus(status);
endFilterChange(Direction::Rows); endFilterChange(Direction::Rows);
#else #else
if (m_filter.setType(filter)) if (m_filter.setStatus(status))
invalidateRowsFilter(); invalidateRowsFilter();
#endif #endif
} }
@@ -184,26 +184,26 @@ void TransferListSortModel::disableTagFilter()
#endif #endif
} }
void TransferListSortModel::setTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs) void TransferListSortModel::setTrackerFilter(const std::optional<QString> &trackerHost)
{ {
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange(); beginFilterChange();
m_filter.setTorrentIDSet(torrentIDs); m_filter.setTrackerHost(trackerHost);
endFilterChange(Direction::Rows); endFilterChange(Direction::Rows);
#else #else
if (m_filter.setTorrentIDSet(torrentIDs)) if (m_filter.setTrackerHost(trackerHost))
invalidateRowsFilter(); invalidateRowsFilter();
#endif #endif
} }
void TransferListSortModel::disableTrackerFilter() void TransferListSortModel::setAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus)
{ {
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0) #if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange(); beginFilterChange();
m_filter.setTorrentIDSet(TorrentFilter::AnyID); m_filter.setAnnounceStatus(announceStatus);
endFilterChange(Direction::Rows); endFilterChange(Direction::Rows);
#else #else
if (m_filter.setTorrentIDSet(TorrentFilter::AnyID)) if (m_filter.setAnnounceStatus(announceStatus))
invalidateRowsFilter(); invalidateRowsFilter();
#endif #endif
} }

View File

@@ -49,13 +49,13 @@ public:
void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override; void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) override;
void setStatusFilter(TorrentFilter::Type filter); void setStatusFilter(TorrentFilter::Status status);
void setCategoryFilter(const QString &category); void setCategoryFilter(const QString &category);
void disableCategoryFilter(); void disableCategoryFilter();
void setTagFilter(const Tag &tag); void setTagFilter(const Tag &tag);
void disableTagFilter(); void disableTagFilter();
void setTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs); void setTrackerFilter(const std::optional<QString> &trackerHost);
void disableTrackerFilter(); void setAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus);
private: private:
int compare(const QModelIndex &left, const QModelIndex &right) const; int compare(const QModelIndex &left, const QModelIndex &right) const;

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