Compare commits

...

57 Commits

Author SHA1 Message Date
dependabot[bot]
576774bcae GHA CI: Bump actions/upload-artifact in the github-actions group
Bumps the github-actions group with 1 update: [actions/upload-artifact](https://github.com/actions/upload-artifact).


Updates `actions/upload-artifact` from 5 to 6
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-01 10:03:09 +00: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
227 changed files with 145471 additions and 97228 deletions

3
.gitattributes vendored
View File

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

View File

@@ -16,7 +16,7 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
@@ -31,12 +31,13 @@ jobs:
- name: Check doc
env:
pandoc_path: "${{ github.workspace }}/../pandoc"
pandoc_version: "3.8.3"
run: |
# install pandoc
curl \
-L \
-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 }}/.."
mv "${{ github.workspace }}/.."/pandoc-* "${{ env.pandoc_path }}"
# run pandoc
@@ -65,7 +66,7 @@ jobs:
> "${{ runner.temp }}/zizmor_results.sarif"
- name: Upload zizmor results
uses: github/codeql-action/upload-sarif@v3
uses: github/codeql-action/upload-sarif@v4
with:
category: zizmor
sarif_file: "${{ runner.temp }}/zizmor_results.sarif"

View File

@@ -18,9 +18,17 @@ jobs:
strategy:
fail-fast: false
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"]
qt_version: ["6.9.1"]
qt_version: ["6.10.1"]
env:
boost_path: "${{ github.workspace }}/../boost"
@@ -28,7 +36,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
@@ -49,19 +57,15 @@ jobs:
- name: Setup ccache
uses: Chocobo1/setup-ccache-action@v1
with:
store_cache: ${{ github.ref == 'refs/heads/master' }}
store_cache: ${{ (github.repository != 'qbittorrent/qBittorrent') || (github.ref == 'refs/heads/master') }}
update_packager_index: false
ccache_options: |
max_size=1G
- name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "86"
BOOST_PATCH_VERSION: "0"
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_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_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/${{ 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
curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url"
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
@@ -79,13 +83,14 @@ jobs:
with:
version: ${{ matrix.qt_version }}
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
cache: true
- name: Install libtorrent
run: |
git clone \
--branch v${{ matrix.libt_version }} \
--branch v${{ matrix.libtorrent.version }} \
--depth 1 \
--recurse-submodules \
https://github.com/arvidn/libtorrent.git \
@@ -119,6 +124,11 @@ jobs:
cmake --build build --target qbt_update_translations
cmake --build build
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
run: |
@@ -156,7 +166,7 @@ jobs:
cp ${{ env.libtorrent_path }}/build/compile_commands.json upload/cmake/libtorrent
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ concurrency:
jobs:
ci:
name: Build (${{ matrix.libt_version }}, ${{ matrix.config.arch }})
name: Build (${{ matrix.libtorrent.version }}, ${{ matrix.config.arch }})
runs-on: ${{ matrix.config.os }}
permissions:
actions: write
@@ -18,7 +18,15 @@ jobs:
strategy:
fail-fast: false
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:
- os: windows-latest
arch: x64
@@ -31,7 +39,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
@@ -86,13 +94,9 @@ jobs:
$packages
- name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "86"
BOOST_PATCH_VERSION: "0"
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_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_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/${{ 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"
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."
if ($LastExitCode -ne 0)
@@ -112,15 +116,16 @@ jobs:
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: "6.9.1"
version: "6.10.1"
arch: ${{ matrix.config.qt_arch }}
archives: qtbase qtsvg qttools
modules: qtimageformats
cache: true
- name: Install libtorrent
run: |
git clone `
--branch v${{ matrix.libt_version }} `
--branch v${{ matrix.libtorrent.version }} `
--depth 1 `
--recurse-submodules `
https://github.com/arvidn/libtorrent.git `
@@ -182,8 +187,16 @@ jobs:
mkdir upload/qBittorrent/plugins/iconengines
copy "${{ env.Qt_ROOT_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
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/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/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
copy "${{ env.Qt_ROOT_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
mkdir upload/qBittorrent/plugins/sqldrivers
@@ -200,13 +213,13 @@ jobs:
copy ${{ env.libtorrent_path }}/build/compile_commands.json upload/cmake/libtorrent
- name: Upload build artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
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
- name: Install NSIS
uses: repolevedavaj/install-nsis@265e893c16602d8ccfb0a9ca44173b084078917c # v1.0.3
uses: repolevedavaj/install-nsis@a55ed92772254d1e51d880f85ce9b5719f907801 # v1.1.0
with:
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
- name: Upload installer
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
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

View File

@@ -14,9 +14,13 @@ jobs:
strategy:
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"]
qt_version: ["6.9.1"]
qt_version: ["6.10.1"]
env:
boost_path: "${{ github.workspace }}/../boost"
@@ -25,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
persist-credentials: false
@@ -37,13 +41,9 @@ jobs:
libssl-dev libxkbcommon-x11-dev libxcb-cursor-dev zlib1g-dev
- name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "88"
BOOST_PATCH_VERSION: "0"
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_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_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/${{ 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
curl -L -o "${{ runner.temp }}/boost.tar.gz" "$boost_url"
tar -xf "${{ runner.temp }}/boost.tar.gz" -C "${{ github.workspace }}/.."; _exitCode="$?"
@@ -66,7 +66,7 @@ jobs:
- name: Install libtorrent
run: |
git clone \
--branch v${{ matrix.libt_version }} \
--branch v${{ matrix.libtorrent.version }} \
--depth 1 \
--recurse-submodules \
https://github.com/arvidn/libtorrent.git \

View File

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

View File

@@ -20,7 +20,7 @@ target_compile_features(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_TO_ASCII
QT_NO_CAST_FROM_BYTEARRAY

View File

@@ -161,8 +161,8 @@ Name[ta]=qBittorrent
GenericName[te]=క్యు బిట్ టొరెంట్ క్లయింట్
Comment[te]=క్యు బిట్ టొరెంట్ తో ఫైల్స్ దిగుమతి చేసుకోండి , పంచుకోండి
Name[te]=qBittorrent
GenericName[th]=ไคลเอนต์บิทอร์เรนต์
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่านบิตทอร์เรนต์
GenericName[th]=ไคลเอนต์บิทอร์เรนต์
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่าน BitTorrent
Name[th]=qBittorrent
GenericName[tr]=BitTorrent istemcisi
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>
<content_rating type="oars-1.1"/>
<releases>
<release version="5.2.0~alpha1" date="2025-02-11"/>
<release version="5.2.0~beta1" date="2025-12-31"/>
</releases>
</component>

View File

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

View File

@@ -1,37 +1,37 @@
;Installer strings
;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_KURDISH} "Create Desktop Shortcut"
LangString inst_desktop ${LANG_KURDISH} "قەدبڕێک دروست بکە لەسەر دێسکتۆپ"
;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_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_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_KURDISH} "Open magnet links with qBittorrent"
LangString inst_magnet ${LANG_KURDISH} "بەسەرە موگناتیسییەکان بە کیووبیتتۆرێنت بکەرەوە"
;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_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_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_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_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_KURDISH} "Uninstalling previous version."
LangString inst_unist ${LANG_KURDISH} "وەشانی پێشوو دەسڕدرێتەوە."
;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_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_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_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_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."
@@ -41,24 +41,24 @@ LangString inst_arch_mismatch_arm64_on_x64 ${LANG_KURDISH} "This ARM64 version o
;Uninstaller strings
;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_KURDISH} "Remove shortcuts"
LangString remove_shortcuts ${LANG_KURDISH} "قەدبڕەکان بسڕەوە"
;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_KURDISH} "Remove registry keys"
LangString remove_registry ${LANG_KURDISH} "کلیلەکانی تۆمار بسڕەوە"
;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_KURDISH} "Remove Windows Firewall rule"
LangString remove_firewall ${LANG_KURDISH} "یاسای فایەروۆڵی ویندۆز بسڕەوە"
;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_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_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_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_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_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_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_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

View File

@@ -21,7 +21,7 @@ LangString inst_firewallinfo ${LANG_TURKISH} "Windows Güvenlik Duvarı kuralı
;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_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} "Şu anki sürüm kaldırılacak. Kullanıcı ayarları ve torrent'ler bozulmadan kalacaktır."
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
LangString inst_unist ${LANG_TURKISH} "Önceki sürüm kaldırılıyor."
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
@@ -33,9 +33,9 @@ LangString inst_requires_win10 ${LANG_TURKISH} "Bu yükleyici en az Windows 10
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
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_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} "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_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} "qBittorrent'in bu ARM64 sürümü x64 sistemlerde çalışamaz. Lütfen x64 yükleyicisini indirin."
;------------------------------------
;Uninstaller strings

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" "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
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
DeleteRegKey HKLM "Software\Classes\qBittorrent.File.Torrent"
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
Section "un.$(remove_firewall)" ;
@@ -45,8 +45,9 @@ SectionEnd
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"
System::Call 'ole32::CoTaskMemFree(p r1)'
FunctionEnd
@@ -58,8 +59,9 @@ SectionEnd
Function un.remove_cache_user
System::Call 'shell32::SHGetSpecialFolderPath(i $HWNDPARENT, t .r1, i ${CSIDL_LOCALAPPDATA}, i0)i.r0'
RMDir /r "$1\qBittorrent\"
System::Call 'shell32::SHGetKnownFolderPath(g "${FOLDERID_LocalAppData}", i 0, p 0, *w . r1) i . r0'
RMDir /r "$1\qBittorrent"
System::Call 'ole32::CoTaskMemFree(p r1)'
FunctionEnd

View File

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

View File

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

View File

@@ -32,7 +32,7 @@
#include <QCoreApplication>
#include <QMetaEnum>
#include "base/bittorrent/sharelimitaction.h"
#include "base/bittorrent/sharelimits.h"
#include "base/bittorrent/torrentcontentlayout.h"
#include "base/global.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/sessionimpl.h
bittorrent/sessionstatus.h
bittorrent/sharelimitaction.h
bittorrent/sharelimits.h
bittorrent/speedmonitor.h
bittorrent/sslparameters.h
bittorrent/torrent.h
bittorrent/torrentannouncestatus.h
bittorrent/torrentcontenthandler.h
bittorrent/torrentcontentlayout.h
bittorrent/torrentcontentremoveoption.h
@@ -247,7 +248,7 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
${IOKit_LIBRARY}
)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_link_libraries(qbt_base PRIVATE Iphlpapi PowrProf)
target_link_libraries(qbt_base PRIVATE iphlpapi powrprof)
endif()
if (NOT GUI)

View File

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

View File

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

View File

@@ -239,8 +239,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
torrentParams.comment = fromLTString(resumeDataRoot.dict_find_string_value("qBt-comment"));
torrentParams.hasFinishedStatus = resumeDataRoot.dict_find_int_value("qBt-seedStatus");
torrentParams.firstLastPiecePriority = resumeDataRoot.dict_find_int_value("qBt-firstLastPiecePriority");
torrentParams.seedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-seedingTimeLimit", Torrent::USE_GLOBAL_SEEDING_TIME);
torrentParams.inactiveSeedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-inactiveSeedingTimeLimit", Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME);
torrentParams.seedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-seedingTimeLimit", DEFAULT_SEEDING_TIME_LIMIT);
torrentParams.inactiveSeedingTimeLimit = resumeDataRoot.dict_find_int_value("qBt-inactiveSeedingTimeLimit", DEFAULT_SEEDING_TIME_LIMIT);
torrentParams.shareLimitAction = Utils::String::toEnum(
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");
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
torrentParams.ratioLimit = fromLTString(ratioLimitString).toDouble();

View File

@@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@@ -32,10 +32,16 @@
#include <QJsonValue>
#include "base/global.h"
#include "base/utils/string.h"
const QString OPTION_SAVEPATH = u"save_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)
{
CategoryOptions options;
@@ -47,6 +53,11 @@ BitTorrent::CategoryOptions BitTorrent::CategoryOptions::fromJSON(const QJsonObj
else if (downloadPathValue.isString())
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;
}
@@ -63,12 +74,10 @@ QJsonObject BitTorrent::CategoryOptions::toJSON() const
return {
{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.
* 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
* modify it under the terms of the GNU General Public License
@@ -34,6 +34,7 @@
#include "base/path.h"
#include "downloadpathoption.h"
#include "sharelimits.h"
class QJsonObject;
@@ -44,9 +45,14 @@ namespace BitTorrent
Path savePath;
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);
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/tagset.h"
#include "sharelimitaction.h"
#include "sharelimits.h"
#include "sslparameters.h"
#include "torrent.h"
#include "torrentcontentlayout.h"
@@ -61,9 +61,9 @@ namespace BitTorrent
bool addToQueueTop = false; // only for new torrents
qreal ratioLimit = Torrent::USE_GLOBAL_RATIO;
int seedingTimeLimit = Torrent::USE_GLOBAL_SEEDING_TIME;
int inactiveSeedingTimeLimit = Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME;
qreal ratioLimit = DEFAULT_RATIO_LIMIT;
int seedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
int inactiveSeedingTimeLimit = DEFAULT_SEEDING_TIME_LIMIT;
ShareLimitAction shareLimitAction = ShareLimitAction::Default;
SSLParameters sslParameters;

View File

@@ -37,7 +37,7 @@
#include "addtorrenterror.h"
#include "addtorrentparams.h"
#include "categoryoptions.h"
#include "sharelimitaction.h"
#include "sharelimits.h"
#include "torrentcontentremoveoption.h"
#include "trackerentry.h"
#include "trackerentrystatus.h"
@@ -158,15 +158,17 @@ namespace BitTorrent
virtual QStringList categories() 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 CategoryOptions &options) const = 0;
virtual Path categoryDownloadPath(const QString &categoryName) 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 editCategory(const QString &name, const CategoryOptions &options) = 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 void setUseCategoryPathsInManualMode(bool value) = 0;
@@ -385,8 +387,8 @@ namespace BitTorrent
virtual void setOutgoingPortsMax(int max) = 0;
virtual int UPnPLeaseDuration() const = 0;
virtual void setUPnPLeaseDuration(int duration) = 0;
virtual int peerToS() const = 0;
virtual void setPeerToS(int value) = 0;
virtual int peerDSCP() const = 0;
virtual void setPeerDSCP(int value) = 0;
virtual bool ignoreLimitsOnLAN() const = 0;
virtual void setIgnoreLimitsOnLAN(bool ignore) = 0;
virtual bool includeOverheadInLimits() const = 0;
@@ -518,7 +520,7 @@ namespace BitTorrent
void torrentTagRemoved(Torrent *torrent, const Tag &tag);
void trackerError(Torrent *torrent, const QString &tracker);
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 trackerSuccess(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_outgoingPortsMax(BITTORRENT_SESSION_KEY(u"OutgoingPortsMax"_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_includeOverheadInLimits(BITTORRENT_SESSION_KEY(u"IncludeOverheadInLimits"_s), false)
, 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_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; })
, 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)
, 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_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
, 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_downloadPath(BITTORRENT_SESSION_KEY(u"TempPath"_s), (savePath() / Path(u"temp"_s)))
, 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_isAutoTMMDisabledByDefault(BITTORRENT_SESSION_KEY(u"DisableAutoTMMByDefault"_s), true)
, m_isDisableAutoTMMWhenCategoryChanged(BITTORRENT_SESSION_KEY(u"DisableAutoTMMTriggers/CategoryChanged"_s), false)
@@ -626,11 +625,6 @@ SessionImpl::SessionImpl(QObject *parent)
enableBandwidthScheduler();
loadCategories();
if (isSubcategoriesEnabled())
{
// if subcategories support changed manually
m_categories = expandCategories(m_categories);
}
const QStringList storedTags = m_storedTags.get();
for (const QString &tagStr : storedTags)
@@ -938,15 +932,8 @@ Path SessionImpl::categorySavePath(const QString &categoryName, const CategoryOp
if (path.isEmpty())
{
// use implicit save path
if (isSubcategoriesEnabled())
{
path = Utils::Fs::toValidPath(subcategoryName(categoryName));
basePath = categorySavePath(parentCategoryName(categoryName));
}
else
{
path = Utils::Fs::toValidPath(categoryName);
}
path = Utils::Fs::toValidPath(subcategoryName(categoryName));
basePath = categorySavePath(parentCategoryName(categoryName));
}
return (path.isAbsolute() ? path : (basePath / path));
@@ -966,8 +953,7 @@ Path SessionImpl::categoryDownloadPath(const QString &categoryName, const Catego
if (categoryName.isEmpty())
return downloadPath();
const bool useSubcategories = isSubcategoriesEnabled();
const QString name = useSubcategories ? subcategoryName(categoryName) : categoryName;
const QString name = subcategoryName(categoryName);
const Path path = !downloadPathOption.path.isEmpty()
? downloadPathOption.path
: Utils::Fs::toValidPath(name); // use implicit download path
@@ -975,7 +961,7 @@ Path SessionImpl::categoryDownloadPath(const QString &categoryName, const Catego
if (path.isAbsolute())
return path;
const QString parentName = useSubcategories ? parentCategoryName(categoryName) : QString();
const QString parentName = parentCategoryName(categoryName);
CategoryOptions parentOptions = categoryOptions(parentName);
// 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.
@@ -986,6 +972,62 @@ Path SessionImpl::categoryDownloadPath(const QString &categoryName, const Catego
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
{
if (categoryName.isEmpty())
@@ -994,7 +1036,7 @@ DownloadPathOption SessionImpl::resolveCategoryDownloadPathOption(const QString
if (option.has_value())
return *option;
const QString parentName = isSubcategoriesEnabled() ? parentCategoryName(categoryName) : QString();
const QString parentName = parentCategoryName(categoryName);
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))
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;
}
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())
return false;
@@ -1035,14 +1074,15 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
if (options == currentOptions)
return false;
if (isDisableAutoTMMWhenCategorySavePathChanged())
if (isDisableAutoTMMWhenCategorySavePathChanged()
&& ((options.savePath != currentOptions.savePath) || (options.downloadPath != currentOptions.downloadPath)))
{
// This should be done before changing the category options
// to prevent the torrent from being moved at the new save path.
for (TorrentImpl *const torrent : asConst(m_torrents))
{
if (torrent->category() == name)
if (torrent->category() == categoryName)
torrent->setAutoTMMEnabled(false);
}
}
@@ -1052,39 +1092,37 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
for (TorrentImpl *const torrent : asConst(m_torrents))
{
if (torrent->category() == name)
if (torrent->category() == categoryName)
torrent->handleCategoryOptionsChanged();
}
emit categoryOptionsChanged(name);
emit categoryOptionsChanged(categoryName);
return true;
}
bool SessionImpl::removeCategory(const QString &name)
{
const QString parentCategory = parentCategoryName(name);
for (TorrentImpl *const torrent : asConst(m_torrents))
{
if (torrent->belongsToCategory(name))
torrent->setCategory(u""_s);
torrent->setCategory(parentCategory);
}
// remove stored category and its subcategories if exist
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
const QString test = name + u'/';
Algorithm::removeIf(m_categories, [this, &test, &result](const QString &category, const CategoryOptions &)
if (category.startsWith(test))
{
if (category.startsWith(test))
{
result = true;
emit categoryRemoved(category);
return true;
}
return false;
});
}
result = true;
emit categoryRemoved(category);
return true;
}
return false;
});
result = (m_categories.remove(name) > 0) || result;
@@ -1098,32 +1136,6 @@ bool SessionImpl::removeCategory(const QString &name)
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
{
return m_useCategoryPathsInManualMode;
@@ -1281,7 +1293,7 @@ qreal SessionImpl::globalMaxRatio() const
void SessionImpl::setGlobalMaxRatio(qreal ratio)
{
if (ratio < 0)
ratio = Torrent::NO_RATIO_LIMIT;
ratio = NO_RATIO_LIMIT;
if (ratio != globalMaxRatio())
{
@@ -1297,7 +1309,7 @@ int SessionImpl::globalMaxSeedingMinutes() const
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())
{
@@ -1313,7 +1325,7 @@ int SessionImpl::globalMaxInactiveSeedingMinutes() const
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())
{
@@ -2057,7 +2069,7 @@ lt::settings_pack SessionImpl::loadLTSettings() const
// UPnP lease duration
settingsPack.set_int(lt::settings_pack::upnp_lease_duration, UPnPLeaseDuration());
// 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
settingsPack.set_bool(lt::settings_pack::rate_limit_ip_overhead, includeOverheadInLimits());
// IP address to announce to trackers
@@ -2356,14 +2368,9 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
if (!torrent->isFinished() || torrent->isForced())
return;
const auto effectiveLimit = []<typename T>(const T limit, const T useGlobalLimit, const T globalLimit) -> T
{
return (limit == useGlobalLimit) ? globalLimit : limit;
};
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());
const qreal ratioLimit = torrent->effectiveRatioLimit();
const int seedingTimeLimit = torrent->effectiveSeedingTimeLimit();
const int inactiveSeedingTimeLimit = torrent->effectiveInactiveSeedingTimeLimit();
bool reached = false;
QString description;
@@ -2390,7 +2397,7 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
if (reached)
{
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)
{
@@ -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;
m_peerToS = value;
m_peerDSCP = value;
configureDeferred();
}
@@ -5218,9 +5225,9 @@ bool SessionImpl::isKnownTorrent(const InfoHash &infoHash) const
void SessionImpl::updateSeedingLimitTimer()
{
if ((globalMaxRatio() == Torrent::NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit()
&& (globalMaxSeedingMinutes() == Torrent::NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit()
&& (globalMaxInactiveSeedingMinutes() == Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit())
if ((globalMaxRatio() == NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit()
&& (globalMaxSeedingMinutes() == NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit()
&& (globalMaxInactiveSeedingMinutes() == NO_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit())
{
if (m_seedingLimitTimer->isActive())
m_seedingLimitTimer->stop();
@@ -5279,9 +5286,9 @@ void SessionImpl::handleTorrentTrackersRemoved(TorrentImpl *const torrent, const
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)
@@ -5584,6 +5591,8 @@ void SessionImpl::loadCategories()
const auto categoryOptions = CategoryOptions::fromJSON(it.value().toObject());
m_categories[categoryName] = categoryOptions;
}
m_categories = expandCategories(m_categories);
}
bool SessionImpl::hasPerTorrentRatioLimit() const

View File

@@ -149,15 +149,17 @@ namespace BitTorrent
QStringList categories() 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 CategoryOptions &options) const override;
Path categoryDownloadPath(const QString &categoryName) 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 editCategory(const QString &name, const CategoryOptions &options) override;
bool removeCategory(const QString &name) override;
bool isSubcategoriesEnabled() const override;
void setSubcategoriesEnabled(bool value) override;
bool useCategoryPathsInManualMode() const override;
void setUseCategoryPathsInManualMode(bool value) override;
@@ -359,8 +361,8 @@ namespace BitTorrent
void setOutgoingPortsMax(int max) override;
int UPnPLeaseDuration() const override;
void setUPnPLeaseDuration(int duration) override;
int peerToS() const override;
void setPeerToS(int value) override;
int peerDSCP() const override;
void setPeerDSCP(int value) override;
bool ignoreLimitsOnLAN() const override;
void setIgnoreLimitsOnLAN(bool ignore) override;
bool includeOverheadInLimits() const override;
@@ -474,7 +476,7 @@ namespace BitTorrent
void handleTorrentFinished(TorrentImpl *torrent);
void handleTorrentTrackersAdded(TorrentImpl *torrent, const QList<TrackerEntry> &newTrackers);
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 handleTorrentUrlSeedsRemoved(TorrentImpl *torrent, const QList<QUrl> &urlSeeds);
void handleTorrentResumeDataReady(TorrentImpl *torrent, LoadTorrentParams data);
@@ -690,7 +692,7 @@ namespace BitTorrent
CachedSettingValue<int> m_outgoingPortsMin;
CachedSettingValue<int> m_outgoingPortsMax;
CachedSettingValue<int> m_UPnPLeaseDuration;
CachedSettingValue<int> m_peerToS;
CachedSettingValue<int> m_peerDSCP;
CachedSettingValue<bool> m_ignoreLimitsOnLAN;
CachedSettingValue<bool> m_includeOverheadInLimits;
CachedSettingValue<QString> m_announceIP;
@@ -754,7 +756,6 @@ namespace BitTorrent
CachedSettingValue<Path> m_savePath;
CachedSettingValue<Path> m_downloadPath;
CachedSettingValue<bool> m_isDownloadPathEnabled;
CachedSettingValue<bool> m_isSubcategoriesEnabled;
CachedSettingValue<bool> m_useCategoryPathsInManualMode;
CachedSettingValue<bool> m_isAutoTMMDisabledByDefault;
CachedSettingValue<bool> m_isDisableAutoTMMWhenCategoryChanged;

View File

@@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@@ -33,6 +33,12 @@
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
// 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

View File

@@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@@ -44,15 +44,6 @@ namespace BitTorrent
// 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();
TorrentID Torrent::id() const

View File

@@ -37,7 +37,8 @@
#include "base/3rdparty/expected.hpp"
#include "base/pathfwd.h"
#include "base/tagset.h"
#include "sharelimitaction.h"
#include "sharelimits.h"
#include "torrentannouncestatus.h"
#include "torrentcontenthandler.h"
class QBitArray;
@@ -124,15 +125,6 @@ namespace BitTorrent
};
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;
using TorrentContentHandler::TorrentContentHandler;
@@ -235,6 +227,10 @@ namespace BitTorrent
virtual void setInactiveSeedingTimeLimit(int limit) = 0;
virtual ShareLimitAction shareLimitAction() const = 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 actualFilePaths() const = 0;
@@ -278,9 +274,6 @@ namespace BitTorrent
virtual bool isLSDDisabled() const = 0;
virtual QBitArray pieces() 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 popularity() const = 0;
virtual int uploadPayloadRate() const = 0;
@@ -290,6 +283,7 @@ namespace BitTorrent
virtual int connectionsCount() const = 0;
virtual int connectionsLimit() const = 0;
virtual qlonglong nextAnnounce() const = 0;
virtual TorrentAnnounceStatus announceStatus() const = 0;
virtual void setName(const QString &name) = 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())
{
m_nativeHandle.replace_trackers(nativeTrackers);
m_announceStatus.reset();
deferredRequestResumeData();
m_session->handleTorrentTrackersRemoved(this, removedTrackers);
@@ -718,15 +719,16 @@ void TorrentImpl::replaceTrackers(QList<TrackerEntry> trackers)
std::vector<lt::announce_entry> nativeTrackers;
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));
m_trackerEntryStatuses.append({tracker.url, tracker.tier});
}
m_nativeHandle.replace_trackers(nativeTrackers);
m_announceStatus.reset();
// Clear the peer list if it's a private torrent since
// we do not want to keep connecting with peers from old tracker.
@@ -734,7 +736,7 @@ void TorrentImpl::replaceTrackers(QList<TrackerEntry> trackers)
clearPeers();
deferredRequestResumeData();
m_session->handleTorrentTrackersChanged(this);
m_session->handleTorrentTrackersReset(this, oldEntries, trackers);
}
QList<QUrl> TorrentImpl::urlSeeds() const
@@ -934,7 +936,7 @@ bool TorrentImpl::belongsToCategory(const QString &category) const
if (m_category == category)
return true;
return (m_session->isSubcategoriesEnabled() && m_category.startsWith(category + u'/'));
return m_category.startsWith(category + u'/');
}
TagSet TorrentImpl::tags() const
@@ -1186,25 +1188,7 @@ bool TorrentImpl::isCompleted() const
bool TorrentImpl::isActive() const
{
switch (m_state)
{
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;
return ((uploadPayloadRate() > 0) || (downloadPayloadRate() > 0));
}
bool TorrentImpl::isInactive() const
@@ -1356,16 +1340,16 @@ qlonglong TorrentImpl::eta() const
if (isFinished())
{
const qreal maxRatioValue = maxRatio();
const int maxSeedingTimeValue = maxSeedingTime();
const int maxInactiveSeedingTimeValue = maxInactiveSeedingTime();
if ((maxRatioValue < 0) && (maxSeedingTimeValue < 0) && (maxInactiveSeedingTimeValue < 0)) return MAX_ETA;
const qreal maxRatioValue = effectiveRatioLimit();
const int maxSeedingTimeValue = effectiveSeedingTimeLimit();
const int maxInactiveSeedingTimeValue = effectiveInactiveSeedingTimeLimit();
if ((maxRatioValue < 0) && (maxSeedingTimeValue < 0) && (maxInactiveSeedingTimeValue < 0))
return MAX_ETA;
qlonglong ratioEta = MAX_ETA;
if ((speedAverage.upload > 0) && (maxRatioValue >= 0))
{
qlonglong realDL = totalDownload();
if (realDL <= 0)
realDL = wantedSize();
@@ -1497,30 +1481,38 @@ qreal TorrentImpl::distributedCopies() const
return m_nativeStatus.distributed_copies;
}
qreal TorrentImpl::maxRatio() const
qreal TorrentImpl::effectiveRatioLimit() const
{
if (m_ratioLimit == USE_GLOBAL_RATIO)
return m_session->globalMaxRatio();
if (m_ratioLimit == DEFAULT_RATIO_LIMIT)
return m_session->categoryRatioLimit(category());
return m_ratioLimit;
}
int TorrentImpl::maxSeedingTime() const
int TorrentImpl::effectiveSeedingTimeLimit() const
{
if (m_seedingTimeLimit == USE_GLOBAL_SEEDING_TIME)
return m_session->globalMaxSeedingMinutes();
if (m_seedingTimeLimit == DEFAULT_SEEDING_TIME_LIMIT)
return m_session->categorySeedingTimeLimit(category());
return m_seedingTimeLimit;
}
int TorrentImpl::maxInactiveSeedingTime() const
int TorrentImpl::effectiveInactiveSeedingTimeLimit() const
{
if (m_inactiveSeedingTimeLimit == USE_GLOBAL_INACTIVE_SEEDING_TIME)
return m_session->globalMaxInactiveSeedingMinutes();
if (m_inactiveSeedingTimeLimit == DEFAULT_SEEDING_TIME_LIMIT)
return m_session->categoryInactiveSeedingTimeLimit(category());
return m_inactiveSeedingTimeLimit;
}
ShareLimitAction TorrentImpl::effectiveShareLimitAction() const
{
if (m_shareLimitAction == ShareLimitAction::Default)
return m_session->categoryShareLimitAction(category());
return m_shareLimitAction;
}
qreal TorrentImpl::realRatio() const
{
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);
}
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
{
// in order to produce floating-point numbers using `std::chrono::duration_cast`,
@@ -1761,6 +1793,7 @@ TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entr
#endif
::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo);
m_announceStatus.reset();
return *it;
}
@@ -1776,6 +1809,8 @@ void TorrentImpl::resetTrackerEntryStatuses()
status.url = tempUrl;
status.tier = tempTier;
}
m_announceStatus = TorrentAnnounceStatusFlag::HasNoProblem;
}
std::shared_ptr<const libtorrent::torrent_info> TorrentImpl::nativeTorrentInfo() const
@@ -2598,7 +2633,7 @@ void TorrentImpl::updateProgress()
void TorrentImpl::setRatioLimit(qreal limit)
{
if (limit < USE_GLOBAL_RATIO)
if (limit < DEFAULT_RATIO_LIMIT)
limit = NO_RATIO_LIMIT;
if (m_ratioLimit != limit)
@@ -2611,7 +2646,7 @@ void TorrentImpl::setRatioLimit(qreal limit)
void TorrentImpl::setSeedingTimeLimit(int limit)
{
if (limit < USE_GLOBAL_SEEDING_TIME)
if (limit < DEFAULT_SEEDING_TIME_LIMIT)
limit = NO_SEEDING_TIME_LIMIT;
if (m_seedingTimeLimit != limit)
@@ -2624,8 +2659,8 @@ void TorrentImpl::setSeedingTimeLimit(int limit)
void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
{
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
if (limit < DEFAULT_SEEDING_TIME_LIMIT)
limit = NO_SEEDING_TIME_LIMIT;
if (m_inactiveSeedingTimeLimit != limit)
{

View File

@@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@@ -156,6 +156,10 @@ namespace BitTorrent
void setInactiveSeedingTimeLimit(int limit) override;
ShareLimitAction shareLimitAction() const 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 actualFilePath(int index) const override;
@@ -205,9 +209,6 @@ namespace BitTorrent
bool isLSDDisabled() const override;
QBitArray pieces() const override;
qreal distributedCopies() const override;
qreal maxRatio() const override;
int maxSeedingTime() const override;
int maxInactiveSeedingTime() const override;
qreal realRatio() const override;
qreal popularity() const override;
int uploadPayloadRate() const override;
@@ -217,6 +218,7 @@ namespace BitTorrent
int connectionsCount() const override;
int connectionsLimit() const override;
qlonglong nextAnnounce() const override;
TorrentAnnounceStatus announceStatus() const override;
void setName(const QString &name) override;
void setSequentialDownload(bool enable) override;
@@ -349,6 +351,7 @@ namespace BitTorrent
MaintenanceJob m_maintenanceJob = MaintenanceJob::None;
QList<TrackerEntryStatus> m_trackerEntryStatuses;
mutable std::optional<TorrentAnnounceStatus> m_announceStatus;
QList<QUrl> m_urlSeeds;
FileErrorInfo m_lastFileError;

View File

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

View File

@@ -494,10 +494,17 @@ void Preferences::setWinStartup(const bool b)
settings.remove(profileID);
}
}
#endif // Q_OS_WIN
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)
@@ -507,7 +514,6 @@ void Preferences::setStyle(const QString &styleName)
setValue(u"Appearance/Style"_s, styleName);
}
#endif // Q_OS_WIN
// Downloads
Path Preferences::getScanDirsLastPath() const
@@ -1291,6 +1297,19 @@ void Preferences::setShutdownWhenDownloadsComplete(const bool 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
{
return value(u"Preferences/Downloads/AutoSuspendOnCompletion"_s, false);
@@ -1885,6 +1904,32 @@ void Preferences::setTrackerFilterState(const bool 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
{
return value<int>(u"TransferListFilters/selectedFilterIndex"_s, 0);

View File

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

View File

@@ -405,12 +405,16 @@ void Session::loadLegacy()
const QString feedUrl = Item::relativeName(legacyPath);
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()
? legacyPath
: 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;
}
}

View File

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

View File

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

View File

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

View File

@@ -434,9 +434,9 @@ void TorrentFilesWatcher::Worker::processFolder(const Path &path, const Path &wa
}
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;
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());
if (useAutoTMM)
{

View File

@@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@@ -28,94 +28,57 @@
#include "torrentfilter.h"
#include "bittorrent/infohash.h"
#include "bittorrent/torrent.h"
#include <algorithm>
#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<QString> TorrentFilter::AnyCategory;
const std::optional<Tag> TorrentFilter::AnyTag;
const std::optional<QString> TorrentFilter::AnyTrackerHost;
const std::optional<TorrentAnnounceStatus> TorrentFilter::AnyAnnounceStatus;
const TorrentFilter TorrentFilter::DownloadingTorrent(TorrentFilter::Downloading);
const TorrentFilter TorrentFilter::SeedingTorrent(TorrentFilter::Seeding);
const TorrentFilter TorrentFilter::CompletedTorrent(TorrentFilter::Completed);
const TorrentFilter TorrentFilter::StoppedTorrent(TorrentFilter::Stopped);
const TorrentFilter TorrentFilter::RunningTorrent(TorrentFilter::Running);
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);
QString getTrackerHost(const QString &url)
{
// We want the hostname.
if (const QString host = QUrl(url).host(); !host.isEmpty())
return host;
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
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
: m_type {type}
TorrentFilter::TorrentFilter(const Status status, const std::optional<TorrentIDSet> &idSet, const std::optional<QString> &category
, const std::optional<Tag> &tag, const std::optional<bool> &isPrivate, const std::optional<QString> &trackerHost
, const std::optional<TorrentAnnounceStatus> &announceStatus)
: m_status {status}
, m_category {category}
, m_tag {tag}
, m_idSet {idSet}
, m_private {isPrivate}
, m_trackerHost {trackerHost}
, m_announceStatus {announceStatus}
{
}
TorrentFilter::TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet
, 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}
bool TorrentFilter::setStatus(const Status status)
{
setTypeByName(filter);
}
bool TorrentFilter::setType(Type type)
{
if (m_type != type)
if (m_status != status)
{
m_type = type;
m_status = status;
return true;
}
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)
{
if (m_idSet != idSet)
@@ -160,18 +123,43 @@ bool TorrentFilter::setPrivate(const std::optional<bool> isPrivate)
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:
return true;
@@ -190,16 +178,16 @@ bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
case Inactive:
return torrent->isInactive();
case Stalled:
return (state == BitTorrent::TorrentState::StalledUploading)
|| (state == BitTorrent::TorrentState::StalledDownloading);
return (state == TorrentState::StalledUploading)
|| (state == TorrentState::StalledDownloading);
case StalledUploading:
return state == BitTorrent::TorrentState::StalledUploading;
return state == TorrentState::StalledUploading;
case StalledDownloading:
return state == BitTorrent::TorrentState::StalledDownloading;
return state == TorrentState::StalledDownloading;
case Checking:
return (state == BitTorrent::TorrentState::CheckingUploading)
|| (state == BitTorrent::TorrentState::CheckingDownloading)
|| (state == BitTorrent::TorrentState::CheckingResumeData);
return (state == TorrentState::CheckingUploading)
|| (state == TorrentState::CheckingDownloading)
|| (state == TorrentState::CheckingResumeData);
case Moving:
return torrent->isMoving();
case Errored:
@@ -212,7 +200,7 @@ bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
return false;
}
bool TorrentFilter::matchHash(const BitTorrent::Torrent *const torrent) const
bool TorrentFilter::matchHash(const Torrent *const torrent) const
{
if (!m_idSet)
return true;
@@ -220,7 +208,7 @@ bool TorrentFilter::matchHash(const BitTorrent::Torrent *const torrent) const
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)
return true;
@@ -228,7 +216,7 @@ bool TorrentFilter::matchCategory(const BitTorrent::Torrent *const torrent) cons
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)
return true;
@@ -240,10 +228,65 @@ bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const
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)
return true;
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.
* 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
* modify it under the terms of the GNU General Public License
@@ -34,6 +34,7 @@
#include <QString>
#include "base/bittorrent/infohash.h"
#include "base/bittorrent/torrentannouncestatus.h"
#include "base/tag.h"
namespace BitTorrent
@@ -46,7 +47,7 @@ using TorrentIDSet = QSet<BitTorrent::TorrentID>;
class TorrentFilter
{
public:
enum Type
enum Status
{
All,
Downloading,
@@ -67,57 +68,47 @@ public:
};
// These mean any permutation, including no category / tag.
static const std::optional<QString> AnyCategory;
static const std::optional<TorrentIDSet> AnyID;
static const std::optional<QString> AnyCategory;
static const std::optional<Tag> AnyTag;
static const TorrentFilter DownloadingTorrent;
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;
static const std::optional<QString> AnyTrackerHost;
static const std::optional<BitTorrent::TorrentAnnounceStatus> AnyAnnounceStatus;
TorrentFilter() = default;
// category & tags: pass empty string for uncategorized / untagged torrents.
TorrentFilter(Type type
TorrentFilter(Status status
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tag = AnyTag
, std::optional<bool> isPrivate = {});
TorrentFilter(const QString &filter
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tags = AnyTag
, std::optional<bool> isPrivate = {});
, const std::optional<bool> &isPrivate = {}
, const std::optional<QString> &trackerHost = AnyTrackerHost
, const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus = AnyAnnounceStatus);
bool setType(Type type);
bool setTypeByName(const QString &filter);
bool setStatus(Status status);
bool setTorrentIDSet(const std::optional<TorrentIDSet> &idSet);
bool setCategory(const std::optional<QString> &category);
bool setTag(const std::optional<Tag> &tag);
bool setPrivate(std::optional<bool> isPrivate);
bool setTrackerHost(const std::optional<QString> &trackerHost);
bool setAnnounceStatus(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus);
bool match(const BitTorrent::Torrent *torrent) const;
private:
bool matchState(const BitTorrent::Torrent *torrent) const;
bool matchStatus(const BitTorrent::Torrent *torrent) const;
bool matchHash(const BitTorrent::Torrent *torrent) const;
bool matchCategory(const BitTorrent::Torrent *torrent) const;
bool matchTag(const BitTorrent::Torrent *torrent) const;
bool matchPrivate(const BitTorrent::Torrent *torrent) const;
bool matchTracker(const BitTorrent::Torrent *torrent) const;
Type m_type {All};
Status m_status {All};
std::optional<QString> m_category;
std::optional<Tag> m_tag;
std::optional<TorrentIDSet> m_idSet;
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,
Shutdown,
Suspend,
Hibernate
Hibernate,
Reboot
};
using QStringMap = QMap<QString, QString>;

View File

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

View File

@@ -87,22 +87,30 @@ void Utils::OS::shutdownComputer([[maybe_unused]] const ShutdownDialogAction &ac
{
::SetSuspendState(TRUE, FALSE, FALSE);
}
else
else if (action == ShutdownDialogAction::Shutdown)
{
std::wstring msg = QCoreApplication::translate("misc"
, "qBittorrent will shutdown the computer now because all downloads are complete.").toStdWString();
::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.
tkp.Privileges[0].Attributes = 0;
::AdjustTokenPrivileges(hToken, FALSE, &tkp, 0, NULL, 0);
#elif defined(Q_OS_MACOS)
AEEventID EventToSend;
if (action != ShutdownDialogAction::Shutdown)
AEEventID EventToSend {};
if (action == ShutdownDialogAction::Suspend)
EventToSend = kAESleep;
else
else if (action == ShutdownDialogAction::Reboot)
EventToSend = kAERestart;
else if (action == ShutdownDialogAction::Shutdown)
EventToSend = kAEShutDown;
AEAddressDesc targetDesc;
const ProcessSerialNumber kPSNOfSystemProcess = {0, kSystemProcess};
@@ -133,7 +141,7 @@ void Utils::OS::shutdownComputer([[maybe_unused]] const ShutdownDialogAction &ac
#elif defined(QBT_USES_DBUS)
// 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
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
halIface.call(u"Hibernate"_s);
}
else
else if (action == ShutdownDialogAction::Shutdown)
{
// Some recent systems use systemd's logind
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());
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
}
@@ -275,11 +307,11 @@ bool Utils::OS::applyMarkOfTheWeb(const Path &file, const QString &url)
const QByteArray zoneID = QByteArrayLiteral("[ZoneTransfer]\r\nZoneId=3\r\n")
+ u"HostUrl=%1\r\n"_s.arg(hostURL).toUtf8();
if (LARGE_INTEGER streamSize = {0};
if (LARGE_INTEGER streamSize {};
::GetFileSizeEx(handle, &streamSize) && (streamSize.QuadPart > 0))
{
const DWORD expectedReadSize = std::min<LONGLONG>(streamSize.QuadPart, 1024);
QByteArray buf {expectedReadSize, '\0'};
QByteArray buf {static_cast<qsizetype>(expectedReadSize), '\0'};
if (DWORD actualReadSize = 0;
::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;
if (!::SetEndOfFile(handle))
return false;
DWORD written = 0;
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 // Q_OS_MACOS || Q_OS_WIN

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@@ -190,6 +190,7 @@ void AddTorrentParamsWidget::populate()
}
populateDefaultPaths();
resetShareLimitsWidgetDefaults();
});
m_ui->savePathEdit->disconnect(this);
@@ -241,7 +242,7 @@ void AddTorrentParamsWidget::populate()
m_ui->startTorrentComboBox->disconnect(this);
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]
{
const QVariant data = m_ui->startTorrentComboBox->currentData();
@@ -270,6 +271,7 @@ void AddTorrentParamsWidget::populate()
m_addTorrentParams.addToQueueTop = data.toBool();
});
resetShareLimitsWidgetDefaults();
m_ui->torrentShareLimitsWidget->setRatioLimit(m_addTorrentParams.ratioLimit);
m_ui->torrentShareLimitsWidget->setSeedingTimeLimit(m_addTorrentParams.seedingTimeLimit);
m_ui->torrentShareLimitsWidget->setInactiveSeedingTimeLimit(m_addTorrentParams.inactiveSeedingTimeLimit);
@@ -418,3 +420,11 @@ void AddTorrentParamsWidget::populateSavePathOptions()
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.
* 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
* modify it under the terms of the GNU General Public License
@@ -57,7 +57,7 @@ private:
void populateDefaultPaths();
void populateDefaultDownloadPath();
void populateSavePathOptions();
void resetShareLimitsWidgetDefaults();
Ui::AddTorrentParamsWidget *m_ui;
BitTorrent::AddTorrentParams m_addTorrentParams;

View File

@@ -149,7 +149,7 @@ namespace
OUTGOING_PORT_MIN,
OUTGOING_PORT_MAX,
UPNP_LEASE_DURATION,
PEER_TOS,
PEER_DSCP,
UTP_MIX_MODE,
HOSTNAME_CACHE_TTL,
IDN_SUPPORT,
@@ -276,7 +276,7 @@ void AdvancedSettings::saveAdvancedSettings() const
// UPnP lease duration
session->setUPnPLeaseDuration(m_spinBoxUPnPLeaseDuration.value());
// Type of service
session->setPeerToS(m_spinBoxPeerToS.value());
session->setPeerDSCP(m_spinBoxPeerDSCP.value());
// uTP-TCP mixed mode
session->setUtpMixedMode(m_comboBoxUtpMixedMode.currentData().value<BitTorrent::MixedModeAlgorithm>());
// 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"(?)"))
, &m_spinBoxUPnPLeaseDuration);
// Type of service
m_spinBoxPeerToS.setMinimum(0);
m_spinBoxPeerToS.setMaximum(255);
m_spinBoxPeerToS.setValue(session->peerToS());
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"(?)"))
, &m_spinBoxPeerToS);
m_spinBoxPeerDSCP.setMinimum(0);
m_spinBoxPeerDSCP.setMaximum(255);
m_spinBoxPeerDSCP.setValue(session->peerDSCP());
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_spinBoxPeerDSCP);
// uTP-TCP mixed mode
m_comboBoxUtpMixedMode.addItem(tr("Prefer TCP"), QVariant::fromValue(BitTorrent::MixedModeAlgorithm::TCP));
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,
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_spinBoxSendBufferWatermarkFactor, m_spinBoxConnectionSpeed, m_spinBoxSocketSendBufferSize, m_spinBoxSocketReceiveBufferSize, m_spinBoxSocketBacklogSize,
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->actionAutoSuspend);
autoShutdownGroup->addAction(m_ui->actionAutoHibernate);
autoShutdownGroup->addAction(m_ui->actionAutoReboot);
#if (!defined(Q_OS_UNIX) || defined(Q_OS_MACOS)) || defined(QBT_USES_DBUS)
m_ui->actionAutoShutdown->setChecked(pref->shutdownWhenDownloadsComplete());
m_ui->actionAutoReboot->setChecked(pref->rebootWhenDownloadsComplete());
m_ui->actionAutoSuspend->setChecked(pref->suspendWhenDownloadsComplete());
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
m_ui->actionAutoShutdown->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->applyCategoryFilter(QString());
m_transferListWidget->applyTagFilter(std::nullopt);
m_transferListWidget->applyTrackerFilterAll();
m_transferListWidget->applyTrackerFilter({});
}
// Start watching the executable for updates
@@ -1355,11 +1361,6 @@ void MainWindow::showFiltersSidebar(const bool show)
if (show && !m_transferListFiltersWidget)
{
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->setCollapsible(0, true);
// From https://doc.qt.io/qt-5/qsplitter.html#setSizes:
@@ -1790,30 +1791,31 @@ void MainWindow::on_actionCriticalMessages_triggered(const bool checked)
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);
}
void MainWindow::on_actionAutoSuspend_toggled(bool enabled)
void MainWindow::on_actionAutoSuspend_toggled(const bool enabled)
{
qDebug() << Q_FUNC_INFO << 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);
}
void MainWindow::on_actionAutoShutdown_toggled(bool enabled)
void MainWindow::on_actionAutoShutdown_toggled(const bool enabled)
{
qDebug() << Q_FUNC_INFO << enabled;
Preferences::instance()->setShutdownWhenDownloadsComplete(enabled);
}
void MainWindow::on_actionAutoReboot_toggled(const bool enabled)
{
Preferences::instance()->setRebootWhenDownloadsComplete(enabled);
}
void MainWindow::updatePowerManagementState() const
{
const auto *pref = Preferences::instance();

View File

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

View File

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

View File

@@ -43,15 +43,12 @@
#include <QEvent>
#include <QFileDialog>
#include <QMessageBox>
#include <QStyleFactory>
#include <QSystemTrayIcon>
#include <QTranslator>
#ifdef Q_OS_WIN
#include <QStyleFactory>
#endif
#include "base/bittorrent/session.h"
#include "base/bittorrent/sharelimitaction.h"
#include "base/bittorrent/sharelimits.h"
#include "base/exceptions.h"
#include "base/global.h"
#include "base/net/downloadmanager.h"
@@ -274,7 +271,7 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->checkAltRowColors->setChecked(pref->useAlternatingRowColors());
m_ui->checkUseTorrentStatesColors->setChecked(pref->useTorrentStatesColors());
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->comboHideZero->setCurrentIndex(pref->getHideZeroComboValues());
m_ui->comboHideZero->setEnabled(m_ui->checkHideZero->isChecked());
@@ -300,6 +297,7 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->actionTorrentFnOnDblClBox->setCurrentIndex(m_ui->actionTorrentFnOnDblClBox->findData(actionSeeding));
m_ui->checkBoxHideZeroStatusFilters->setChecked(pref->getHideZeroStatusFilters());
m_ui->checkBoxUseSeparateTrackerStatusFilter->setChecked(pref->useSeparateTrackerStatusFilter());
m_ui->checkTorrentContentDrag->setChecked(pref->isTorrentContentDragEnabled());
@@ -376,11 +374,7 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->checkBoxPerformanceWarning->setChecked(session->isPerformanceWarningEnabled());
connect(m_ui->comboLanguage, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
#ifdef Q_OS_WIN
connect(m_ui->comboStyle, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
#endif
#ifdef QBT_HAS_COLORSCHEME_OPTION
connect(m_ui->comboColorScheme, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
#endif
@@ -414,6 +408,7 @@ void OptionsDialog::loadBehaviorTabOptions()
connect(m_ui->actionTorrentDlOnDblClBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->actionTorrentFnOnDblClBox, qComboBoxCurrentIndexChanged, 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);
@@ -488,9 +483,7 @@ void OptionsDialog::saveBehaviorTabOptions() const
}
pref->setLocale(locale);
#ifdef Q_OS_WIN
pref->setStyle(m_ui->comboStyle->currentData().toString());
#endif
#ifdef QBT_HAS_COLORSCHEME_OPTION
UIThemeManager::instance()->setColorScheme(m_ui->comboColorScheme->currentData().value<ColorScheme>());
@@ -513,6 +506,7 @@ void OptionsDialog::saveBehaviorTabOptions() const
pref->setActionOnDblClOnTorrentFn(m_ui->actionTorrentFnOnDblClBox->currentData().toInt());
pref->setHideZeroStatusFilters(m_ui->checkBoxHideZeroStatusFilters->isChecked());
pref->setUseSeparateTrackerStatusFilter(m_ui->checkBoxUseSeparateTrackerStatusFilter->isChecked());
pref->setTorrentContentDragEnabled(m_ui->checkTorrentContentDrag->isChecked());
@@ -631,7 +625,6 @@ void OptionsDialog::loadDownloadsTabOptions()
m_ui->comboCategoryChanged->setCurrentIndex(session->isDisableAutoTMMWhenCategorySavePathChanged());
m_ui->comboCategoryDefaultPathChanged->setCurrentIndex(session->isDisableAutoTMMWhenDefaultSavePathChanged());
m_ui->checkUseSubcategories->setChecked(session->isSubcategoriesEnabled());
m_ui->checkUseCategoryPaths->setChecked(session->useCategoryPathsInManualMode());
m_ui->textSavePath->setDialogCaption(tr("Choose a save directory"));
@@ -736,7 +729,6 @@ void OptionsDialog::loadDownloadsTabOptions()
connect(m_ui->comboCategoryChanged, 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->textSavePath, &FileSystemPathEdit::selectedPathChanged, this, &ThisType::enableApplyButton);
@@ -808,7 +800,6 @@ void OptionsDialog::saveDownloadsTabOptions() const
session->setDisableAutoTMMWhenCategorySavePathChanged(m_ui->comboCategoryChanged->currentIndex() == 1);
session->setDisableAutoTMMWhenDefaultSavePathChanged(m_ui->comboCategoryDefaultPathChanged->currentIndex() == 1);
session->setSubcategoriesEnabled(m_ui->checkUseSubcategories->isChecked());
session->setUseCategoryPathsInManualMode(m_ui->checkUseCategoryPaths->isChecked());
session->setSavePath(Path(m_ui->textSavePath->selectedPath()));
@@ -1126,6 +1117,8 @@ void OptionsDialog::loadBittorrentTabOptions()
m_ui->spinUploadRateForSlowTorrents->setValue(session->uploadRateForSlowTorrents());
m_ui->spinSlowTorrentsInactivityTimer->setValue(session->slowTorrentsInactivityTimer());
m_ui->spinMaxRatio->setMaximum(std::numeric_limits<int>::max());
if (session->globalMaxRatio() >= 0.)
{
// Enable
@@ -1170,7 +1163,7 @@ void OptionsDialog::loadBittorrentTabOptions()
const QHash<BitTorrent::ShareLimitAction, int> actIndex =
{
{BitTorrent::ShareLimitAction::Stop, 0},
{BitTorrent::ShareLimitAction::Stop, 0},
{BitTorrent::ShareLimitAction::Remove, 1},
{BitTorrent::ShareLimitAction::RemoveWithContent, 2},
{BitTorrent::ShareLimitAction::EnableSuperSeeding, 3}
@@ -1846,6 +1839,11 @@ void OptionsDialog::initializeStyleCombo()
#ifdef Q_OS_WIN
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));
#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->setItemData(0, tr("Let Qt decide the style for this system"), Qt::ToolTipRole);
m_ui->comboStyle->insertSeparator(1);
@@ -1859,14 +1857,6 @@ void OptionsDialog::initializeStyleCombo()
const QString selectedStyleName = prefStyleName.isEmpty() ? QApplication::style()->name() : prefStyleName;
const int styleIndex = m_ui->comboStyle->findData(selectedStyleName, Qt::UserRole, Qt::MatchFixedString);
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()

View File

@@ -456,6 +456,16 @@
</property>
</widget>
</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>
</widget>
</item>
@@ -1371,13 +1381,6 @@ Manual: Various torrent properties (e.g. save path) must be assigned manually</s
</item>
</layout>
</item>
<item>
<widget class="QCheckBox" name="checkUseSubcategories">
<property name="text">
<string>Use Subcategories</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkUseCategoryPaths">
<property name="toolTip">
@@ -1908,7 +1911,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<number>2</number>
</property>
<property name="maximum">
<number>2000</number>
<number>2147483647</number>
</property>
<property name="value">
<number>500</number>
@@ -1944,7 +1947,7 @@ readme[0-9].txt: filter 'readme1.txt', 'readme2.txt' but not 'readme10.txt'.</st
<number>2</number>
</property>
<property name="maximum">
<number>2000</number>
<number>2147483647</number>
</property>
<property name="value">
<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">
<widget class="QSpinBox" name="spinMaxUploads">
<property name="maximum">
<number>2000</number>
<number>2147483647</number>
</property>
<property name="value">
<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">
<widget class="QSpinBox" name="spinMaxUploadsPerTorrent">
<property name="maximum">
<number>500</number>
<number>2147483647</number>
</property>
<property name="value">
<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">
<item>
<widget class="QLineEdit" name="textWebUIAPIKey">
<property name="enabled">
<bool>false</bool>
</property>
<property name="readOnly">
<bool>true</bool>
</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">
<bool>false</bool>
</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">
<string>Copy API key</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<iconset>
<normaloff>:/icons/edit-copy.svg</normaloff>:/icons/edit-copy.svg</iconset>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<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">
<string>Generate API key</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<iconset>
<normaloff>:/icons/view-refresh.svg</normaloff>:/icons/view-refresh.svg</iconset>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<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">
<string>Delete API key</string>
</property>
<property name="text">
<string/>
</property>
<property name="icon">
<iconset resource="../icons.qrc">
<iconset>
<normaloff>:/icons/list-remove.svg</normaloff>:/icons/list-remove.svg</iconset>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
</layout>

View File

@@ -85,6 +85,8 @@ namespace
}
}
using Utils::Misc::SizeUnit;
SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidget *parent)
: GUIApplicationComponent(app, parent)
, m_nameFilteringMode {u"Search/FilteringMode"_s}
@@ -99,6 +101,8 @@ SearchJobWidget::SearchJobWidget(const QString &id, IGUIApplication *app, QWidge
header()->setStretchLastSection(false);
header()->setTextElideMode(Qt::ElideRight);
fillFilterComboBoxes();
// Set Search results list model
m_searchListModel = new QStandardItemModel(0, SearchSortModel::NB_SEARCH_COLUMNS, this);
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->setDynamicSortFilter(true);
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->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::sortIndicatorChanged, this, &SearchJobWidget::saveSettings);
fillFilterComboBoxes();
m_lineEditSearchResultsFilter = new LineEdit(this);
m_lineEditSearchResultsFilter->setPlaceholderText(tr("Filter search results..."));
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);
m_ui->horizontalLayout->insertWidget(0, m_lineEditSearchResultsFilter);
connect(m_ui->filterMode, qOverload<int>(&QComboBox::currentIndexChanged)
, this, &SearchJobWidget::updateFilter);
connect(m_ui->minSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
connect(m_ui->minSeeds, qOverload<int>(&QSpinBox::valueChanged)
, this, &SearchJobWidget::updateFilter);
connect(m_ui->maxSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
connect(m_ui->maxSeeds, qOverload<int>(&QSpinBox::valueChanged)
, this, &SearchJobWidget::updateFilter);
connect(m_ui->minSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateFilter);
connect(m_ui->minSize, qOverload<double>(&QDoubleSpinBox::valueChanged)
, this, &SearchJobWidget::updateFilter);
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->filterMode, qOverload<int>(&QComboBox::currentIndexChanged), this, &SearchJobWidget::updateNameFilter);
connect(m_ui->minSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSeedsFilter);
connect(m_ui->minSeeds, qOverload<int>(&QSpinBox::valueChanged), this, &SearchJobWidget::updateSeedsFilter);
connect(m_ui->maxSeeds, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSeedsFilter);
connect(m_ui->maxSeeds, qOverload<int>(&QSpinBox::valueChanged), this, &SearchJobWidget::updateSeedsFilter);
connect(m_ui->minSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->minSize, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->maxSize, &QAbstractSpinBox::editingFinished, this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->maxSize, qOverload<double>(&QDoubleSpinBox::valueChanged), this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->minSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged), this, &SearchJobWidget::updateSizeFilter);
connect(m_ui->maxSizeUnit, qOverload<int>(&QComboBox::currentIndexChanged), this, &SearchJobWidget::updateSizeFilter);
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_proxyModel->setNameFilter(m_searchPattern);
updateFilter();
appendSearchResults(searchResults);
}
@@ -301,8 +302,8 @@ void SearchJobWidget::assignSearchHandler(SearchHandler *searchHandler)
m_searchPattern = m_searchHandler->pattern();
m_proxyModel->setNameFilter(m_searchPattern);
updateFilter();
updateResultsCount();
setStatus(Status::Ongoing);
}
@@ -454,26 +455,34 @@ void SearchJobWidget::updateResultsCount()
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
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();
}
void SearchJobWidget::fillFilterComboBoxes()
{
using Utils::Misc::SizeUnit;
using Utils::Misc::unitString;
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("Everywhere"), static_cast<int>(NameFilteringMode::Everywhere));
const QVariant selectedMode = static_cast<int>(m_nameFilteringMode.get(NameFilteringMode::OnlyNames));
const int index = m_ui->filterMode->findData(selectedMode);
const auto selectedFilteringMode = static_cast<int>(m_nameFilteringMode.get(NameFilteringMode::OnlyNames));
const int index = m_ui->filterMode->findData(selectedFilteringMode);
m_ui->filterMode->setCurrentIndex((index == -1) ? 0 : index);
}
void SearchJobWidget::filterSearchResults(const QString &name)
{
const QString pattern = (Preferences::instance()->getRegexAsFilteringPatternForSearchJob()
? name : Utils::String::wildcardToRegexPattern(name));
? name : Utils::String::wildcardToRegexPattern(name));
m_proxyModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
updateResultsCount();
}
@@ -557,11 +565,6 @@ void SearchJobWidget::contextMenuEvent(QContextMenuEvent *event)
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()
{
header()->restoreState(Preferences::instance()->getSearchTabHeaderState());

View File

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

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2013 sledgehammer999 <hammered999@gmail.com>
*
* 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)
{
if (m_isNameFilterEnabled == enabled)
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
m_isNameFilterEnabled = enabled;
endFilterChange(Direction::Rows);
#else
m_isNameFilterEnabled = enabled;
invalidateRowsFilter();
#endif
}
void SearchSortModel::setNameFilter(const QString &searchTerm)
{
if (m_searchTerm == searchTerm)
return;
#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
beginFilterChange();
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);
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);
m_maxSize = std::max(static_cast<qint64>(-1), maxSize);
minSize = std::max<qint64>(0, minSize);
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);
m_maxSeeds = std::max(-1, maxSeeds);
minSeeds = std::max(0, minSeeds);
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);
m_maxLeeches = std::max(-1, maxLeeches);
minLeeches = std::max(0, minLeeches);
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

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2013 sledgehammer999 <hammered999@gmail.com>
*
* 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"));
setWindowTitle(tr("Hibernate confirmation"));
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';

View File

@@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@@ -32,11 +32,12 @@
#include <QPushButton>
#include "base/bittorrent/session.h"
#include "base/utils/fs.h"
#include "base/bittorrent/torrent.h"
#include "torrentsharelimitswidget.h"
#include "ui_torrentcategorydialog.h"
TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent)
: QDialog {parent}
: QDialog(parent)
, m_ui {new Ui::TorrentCategoryDialog}
{
m_ui->setupUi(this);
@@ -56,6 +57,15 @@ TorrentCategoryDialog::TorrentCategoryDialog(QWidget *parent)
connect(m_ui->textCategoryName, &QLineEdit::textChanged, this, &TorrentCategoryDialog::categoryNameChanged);
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()
@@ -73,8 +83,7 @@ QString TorrentCategoryDialog::createCategory(QWidget *parent, const QString &pa
newCategoryName += u'/';
newCategoryName += tr("New Category");
TorrentCategoryDialog dialog {parent};
dialog.setCategoryName(newCategoryName);
TorrentCategoryDialog dialog {parent, newCategoryName, {}};
while (dialog.exec() == TorrentCategoryDialog::Accepted)
{
newCategoryName = dialog.categoryName();
@@ -110,14 +119,12 @@ void TorrentCategoryDialog::editCategory(QWidget *parent, const QString &categor
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->setCategoryNameEditable(false);
dialog->setCategoryName(categoryName);
dialog->setCategoryOptions(Session::instance()->categoryOptions(categoryName));
connect(dialog, &TorrentCategoryDialog::accepted, parent, [dialog, categoryName]()
{
Session::instance()->editCategory(categoryName, dialog->categoryOptions());
Session::instance()->setCategoryOptions(categoryName, dialog->categoryOptions());
});
dialog->open();
}
@@ -149,6 +156,11 @@ BitTorrent::CategoryOptions TorrentCategoryDialog::categoryOptions() const
else if (m_ui->comboUseDownloadPath->currentIndex() == 2)
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;
}
@@ -165,6 +177,11 @@ void TorrentCategoryDialog::setCategoryOptions(const BitTorrent::CategoryOptions
m_ui->comboUseDownloadPath->setCurrentIndex(0);
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)
@@ -177,6 +194,13 @@ void TorrentCategoryDialog::categoryNameChanged(const QString &categoryName)
if (useDownloadPath)
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());
}
@@ -195,3 +219,11 @@ void TorrentCategoryDialog::useDownloadPathChanged(const int index)
const Path categoryPath = btSession->categoryDownloadPath(categoryName, categoryOptions());
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.
* 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
* modify it under the terms of the GNU General Public License
@@ -52,6 +52,7 @@ public:
static void editCategory(QWidget *parent, const QString &categoryName);
explicit TorrentCategoryDialog(QWidget *parent = nullptr);
TorrentCategoryDialog(QWidget *parent, const QString &categoryName, const BitTorrent::CategoryOptions &categoryOptions);
~TorrentCategoryDialog() override;
void setCategoryNameEditable(bool editable);
@@ -65,6 +66,9 @@ private slots:
void useDownloadPathChanged(int index);
private:
void resetShareLimitsWidgetDefaults();
Ui::TorrentCategoryDialog *m_ui = nullptr;
Path m_lastEnteredDownloadPath;
QString m_parentCategoryName;
};

View File

@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>493</width>
<height>208</height>
<height>268</height>
</rect>
</property>
<property name="windowTitle">
@@ -140,6 +140,18 @@
</layout>
</widget>
</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>
<spacer name="verticalSpacer_2">
<property name="orientation">
@@ -172,6 +184,12 @@
<header>gui/fspathedit.h</header>
<container>1</container>
</customwidget>
<customwidget>
<class>TorrentShareLimitsWidget</class>
<extends>QWidget</extends>
<header>gui/torrentsharelimitswidget.h</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>

View File

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

View File

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

View File

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

View File

@@ -31,13 +31,17 @@
#include "torrentcreatordialog.h"
#include <functional>
#include <QCloseEvent>
#include <QFileDialog>
#include <QMessageBox>
#include <QMimeData>
#include <QThread>
#include <QUrl>
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrentcreator.h"
#include "base/bittorrent/torrentdescriptor.h"
#include "base/global.h"
#include "base/utils/fs.h"
@@ -57,6 +61,37 @@ namespace
#else
const QFileDialog::Options FILE_DIALOG_OPTIONS {};
#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)
@@ -98,7 +133,7 @@ TorrentCreatorDialog::TorrentCreatorDialog(QWidget *parent, const Path &defaultP
connect(m_ui->addFolderButton, &QPushButton::clicked, this, &TorrentCreatorDialog::onAddFolderButtonClicked);
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &TorrentCreatorDialog::onCreateButtonClicked);
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);
loadSettings();
@@ -192,6 +227,41 @@ void TorrentCreatorDialog::dragEnterEvent(QDragEnterEvent *event)
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
void TorrentCreatorDialog::onCreateButtonClicked()
{
@@ -277,9 +347,9 @@ void TorrentCreatorDialog::handleCreationSuccess(const BitTorrent::TorrentCreato
params.skipChecking = true;
if (m_ui->checkIgnoreShareLimits->isChecked())
{
params.ratioLimit = BitTorrent::Torrent::NO_RATIO_LIMIT;
params.seedingTimeLimit = BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT;
params.inactiveSeedingTimeLimit = BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT;
params.ratioLimit = BitTorrent::NO_RATIO_LIMIT;
params.seedingTimeLimit = BitTorrent::NO_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
@@ -298,20 +368,6 @@ void TorrentCreatorDialog::updateProgressBar(int 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
{
m_ui->textInputPath->setEnabled(enabled);
@@ -382,3 +438,5 @@ void TorrentCreatorDialog::loadSettings()
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize);
}
#include "torrentcreatordialog.moc"

View File

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

View File

@@ -1,6 +1,6 @@
/*
* 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) 2011 Christian Kandeler
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
@@ -289,7 +289,12 @@ TorrentOptionsDialog::TorrentOptionsDialog(QWidget *parent, const QList<BitTorre
, 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)
m_ui->torrentShareLimitsWidget->setRatioLimit(firstTorrentRatio);
if (allSameSeedingTime)
@@ -477,30 +482,37 @@ void TorrentOptionsDialog::accept()
void TorrentOptionsDialog::handleCategoryChanged([[maybe_unused]] const int index)
{
if (m_ui->checkAutoTMM->checkState() == Qt::Checked)
{
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());
}
}
const auto *btSession = BitTorrent::Session::instance();
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->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
{
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->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.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2024-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2020 thalieht
* Copyright (C) 2011 Christian Kandeler
* Copyright (C) 2011 Christophe Dumez <chris@qbittorrent.org>
@@ -35,7 +35,7 @@
#include <QDialog>
#include "base/bittorrent/sharelimitaction.h"
#include "base/bittorrent/sharelimits.h"
#include "base/path.h"
#include "base/settingvalue.h"

View File

@@ -28,6 +28,8 @@
#include "torrentsharelimitswidget.h"
#include <limits>
#include "base/bittorrent/torrent.h"
#include "ui_torrentsharelimitswidget.h"
@@ -50,6 +52,29 @@ namespace
RemoveWithContentActionIndex,
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)
@@ -58,7 +83,32 @@ TorrentShareLimitsWidget::TorrentShareLimitsWidget(QWidget *parent)
{
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->setMaximum(std::numeric_limits<int>::max());
m_ui->spinBoxRatioValue->setSuffix({});
m_ui->spinBoxRatioValue->clear();
m_ui->spinBoxSeedingTimeValue->setEnabled(false);
@@ -68,9 +118,25 @@ TorrentShareLimitsWidget::TorrentShareLimitsWidget(QWidget *parent)
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix({});
m_ui->spinBoxInactiveSeedingTimeValue->clear();
connect(m_ui->comboBoxRatioMode, &QComboBox::currentIndexChanged, this, &TorrentShareLimitsWidget::refreshRatioLimitControls);
connect(m_ui->comboBoxSeedingTimeMode, &QComboBox::currentIndexChanged, this, &TorrentShareLimitsWidget::refreshSeedingTimeLimitControls);
connect(m_ui->comboBoxInactiveSeedingTimeMode, &QComboBox::currentIndexChanged, this, &TorrentShareLimitsWidget::refreshInactiveSeedingTimeLimitControls);
int prevIndex = UninitializedModeIndex;
connect(m_ui->comboBoxRatioMode, &QComboBox::currentIndexChanged, this
, [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()
@@ -80,11 +146,11 @@ TorrentShareLimitsWidget::~TorrentShareLimitsWidget()
void TorrentShareLimitsWidget::setRatioLimit(const qreal ratioLimit)
{
if (ratioLimit == BitTorrent::Torrent::USE_GLOBAL_RATIO)
if (ratioLimit == BitTorrent::DEFAULT_RATIO_LIMIT)
{
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);
}
@@ -97,11 +163,11 @@ void TorrentShareLimitsWidget::setRatioLimit(const qreal ratioLimit)
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);
}
else if (seedingTimeLimit == BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT)
else if (seedingTimeLimit == BitTorrent::NO_SEEDING_TIME_LIMIT)
{
m_ui->comboBoxSeedingTimeMode->setCurrentIndex(UnlimitedModeIndex);
}
@@ -114,11 +180,11 @@ void TorrentShareLimitsWidget::setSeedingTimeLimit(const int seedingTimeLimit)
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);
}
else if (inactiveSeedingTimeLimit == BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT)
else if (inactiveSeedingTimeLimit == BitTorrent::NO_SEEDING_TIME_LIMIT)
{
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;
refreshRatioLimitControls();
if (m_defaultRatioLimit >= 0)
{
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;
refreshSeedingTimeLimitControls();
if (m_defaultSeedingTimeLimit >= 0)
{
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;
refreshInactiveSeedingTimeLimitControls();
if (m_defaultInactiveSeedingTimeLimit >= 0)
{
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
@@ -178,9 +275,9 @@ std::optional<qreal> TorrentShareLimitsWidget::ratioLimit() const
switch (m_ui->comboBoxRatioMode->currentIndex())
{
case DefaultModeIndex:
return BitTorrent::Torrent::USE_GLOBAL_RATIO;
return BitTorrent::DEFAULT_RATIO_LIMIT;
case UnlimitedModeIndex:
return BitTorrent::Torrent::NO_RATIO_LIMIT;
return BitTorrent::NO_RATIO_LIMIT;
case AssignedModeIndex:
return m_ui->spinBoxRatioValue->value();
default:
@@ -193,9 +290,9 @@ std::optional<int> TorrentShareLimitsWidget::seedingTimeLimit() const
switch (m_ui->comboBoxSeedingTimeMode->currentIndex())
{
case DefaultModeIndex:
return BitTorrent::Torrent::USE_GLOBAL_SEEDING_TIME;
return BitTorrent::DEFAULT_SEEDING_TIME_LIMIT;
case UnlimitedModeIndex:
return BitTorrent::Torrent::NO_SEEDING_TIME_LIMIT;
return BitTorrent::NO_SEEDING_TIME_LIMIT;
case AssignedModeIndex:
return m_ui->spinBoxSeedingTimeValue->value();
default:
@@ -208,9 +305,9 @@ std::optional<int> TorrentShareLimitsWidget::inactiveSeedingTimeLimit() const
switch (m_ui->comboBoxInactiveSeedingTimeMode->currentIndex())
{
case DefaultModeIndex:
return BitTorrent::Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME;
return BitTorrent::DEFAULT_SEEDING_TIME_LIMIT;
case UnlimitedModeIndex:
return BitTorrent::Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT;
return BitTorrent::NO_SEEDING_TIME_LIMIT;
case AssignedModeIndex:
return m_ui->spinBoxInactiveSeedingTimeValue->value();
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 (index == AssignedModeIndex)
if (previousIndex == AssignedModeIndex)
m_ratioLimit = m_ui->spinBoxRatioValue->value();
if (currentIndex == AssignedModeIndex)
{
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);
}
else
{
m_ratioLimit = m_ui->spinBoxRatioValue->value();
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 (index == AssignedModeIndex)
if (previousIndex == AssignedModeIndex)
m_seedingTimeLimit = m_ui->spinBoxSeedingTimeValue->value();
if (currentIndex == AssignedModeIndex)
{
m_ui->spinBoxSeedingTimeValue->setValue(m_seedingTimeLimit);
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->setSuffix(tr(" min"));
}
else
{
m_seedingTimeLimit = m_ui->spinBoxSeedingTimeValue->value();
m_ui->spinBoxSeedingTimeValue->setSuffix({});
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 (index == AssignedModeIndex)
if (previousIndex == AssignedModeIndex)
m_inactiveSeedingTimeLimit = m_ui->spinBoxInactiveSeedingTimeValue->value();
if (currentIndex == AssignedModeIndex)
{
m_ui->spinBoxInactiveSeedingTimeValue->setValue(m_inactiveSeedingTimeLimit);
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->setSuffix(tr(" min"));
}
else
{
m_inactiveSeedingTimeLimit = m_ui->spinBoxInactiveSeedingTimeValue->value();
m_ui->spinBoxInactiveSeedingTimeValue->setSuffix({});
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 "base/bittorrent/sharelimitaction.h"
#include "base/bittorrent/sharelimits.h"
namespace Ui
{
@@ -45,6 +45,12 @@ class TorrentShareLimitsWidget final : public QWidget
Q_DISABLE_COPY_MOVE(TorrentShareLimitsWidget)
public:
enum class UsedDefaults
{
Global,
Category
};
explicit TorrentShareLimitsWidget(QWidget *parent = nullptr);
~TorrentShareLimitsWidget() override;
@@ -53,7 +59,8 @@ public:
void setInactiveSeedingTimeLimit(int inactiveSeedingTimeLimit);
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<int> seedingTimeLimit() const;
@@ -61,9 +68,10 @@ public:
std::optional<BitTorrent::ShareLimitAction> shareLimitAction() const;
private:
void refreshRatioLimitControls();
void refreshSeedingTimeLimitControls();
void refreshInactiveSeedingTimeLimitControls();
void onRatioLimitModeChanged(int currentIndex, int previousIndex);
void onSeedingTimeLimitModeChanged(int currentIndex, int previousIndex);
void onInactiveSeedingTimeLimitModeChanged(int currentIndex, int previousIndex);
void resetDefaultItemsText();
Ui::TorrentShareLimitsWidget *m_ui = nullptr;
@@ -74,4 +82,7 @@ private:
int m_defaultSeedingTimeLimit = -1;
int m_defaultInactiveSeedingTimeLimit = -1;
qreal m_defaultRatioLimit = -1;
BitTorrent::ShareLimitAction m_defaultShareLimitAction = BitTorrent::ShareLimitAction::Default;
UsedDefaults m_usedDefaults = UsedDefaults::Global;
};

View File

@@ -21,26 +21,7 @@
</widget>
</item>
<item row="0" column="1">
<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>
<widget class="QComboBox" name="comboBoxRatioMode"/>
</item>
<item row="0" column="2">
<widget class="QDoubleSpinBox" name="spinBoxRatioValue">
@@ -63,26 +44,7 @@
</widget>
</item>
<item row="1" column="1">
<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>
<widget class="QComboBox" name="comboBoxSeedingTimeMode"/>
</item>
<item row="1" column="2">
<widget class="QSpinBox" name="spinBoxSeedingTimeValue">
@@ -108,26 +70,7 @@
</widget>
</item>
<item row="2" column="1">
<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>
<widget class="QComboBox" name="comboBoxInactiveSeedingTimeMode"/>
</item>
<item row="2" column="2">
<widget class="QSpinBox" name="spinBoxInactiveSeedingTimeValue">
@@ -157,36 +100,7 @@
</widget>
</item>
<item>
<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>
<widget class="QComboBox" name="comboBoxAction"/>
</item>
<item>
<spacer name="actionLayoutSpacer">

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@@ -72,6 +72,5 @@ private:
QModelIndex index(CategoryModelItem *item) const;
CategoryModelItem *findItem(const QString &fullName) const;
bool m_isSubcategoriesEnabled = false;
CategoryModelItem *m_rootItem = nullptr;
};

View File

@@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@@ -72,9 +72,6 @@ CategoryFilterWidget::CategoryFilterWidget(QWidget *parent)
#ifdef Q_OS_MACOS
setAttribute(Qt::WA_MacShowFocusRect, false);
#endif
m_defaultIndentation = indentation();
if (!BitTorrent::Session::instance()->isSubcategoriesEnabled())
setIndentation(0);
setContextMenuPolicy(Qt::CustomContextMenu);
sortByColumn(0, Qt::AscendingOrder);
setCurrentIndex(model()->index(0, 0));
@@ -84,7 +81,18 @@ CategoryFilterWidget::CategoryFilterWidget(QWidget *parent)
connect(this, &QWidget::customContextMenuRequested, this, &CategoryFilterWidget::showMenu);
connect(selectionModel(), &QItemSelectionModel::currentRowChanged
, 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
@@ -113,12 +121,8 @@ void CategoryFilterWidget::showMenu()
const auto selectedRows = selectionModel()->selectedRows();
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...")
, this, &CategoryFilterWidget::editCategory);
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()
{
if (!BitTorrent::Session::instance()->isSubcategoriesEnabled())
setIndentation(0);
else
setIndentation(m_defaultIndentation);
updateGeometry();
}
@@ -168,10 +167,13 @@ QSize CategoryFilterWidget::minimumSizeHint() const
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);
if (parent.isValid())
adjustIndentation();
// Expand all parents if the parent(s) of the node are not expanded.
QModelIndex p = parent;
while (p.isValid())
@@ -184,6 +186,26 @@ void CategoryFilterWidget::rowsInserted(const QModelIndex &parent, int start, in
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()
{
TorrentCategoryDialog::createCategory(this);

View File

@@ -1,6 +1,6 @@
/*
* 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
* modify it under the terms of the GNU General Public License
@@ -60,6 +60,6 @@ private:
QSize sizeHint() const override;
QSize minimumSizeHint() const override;
void rowsInserted(const QModelIndex &parent, int start, int end) override;
int m_defaultIndentation;
bool hasAnySubcategory() const;
void adjustIndentation();
};

View File

@@ -99,8 +99,6 @@ StatusFilterWidget::StatusFilterWidget(QWidget *parent, TransferListWidget *tran
setCurrentRow(TorrentFilter::All, QItemSelectionModel::SelectCurrent);
else
setCurrentRow(storedRow, QItemSelectionModel::SelectCurrent);
toggleFilter(pref->getStatusFilterState());
}
StatusFilterWidget::~StatusFilterWidget()
@@ -128,7 +126,7 @@ void StatusFilterWidget::updateTorrentStatus(const BitTorrent::Torrent *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 needStatus = TorrentFilter(status).match(torrent);

View File

@@ -1,6 +1,6 @@
/*
* 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>
*
* This program is free software; you can redistribute it and/or
@@ -36,13 +36,13 @@
#include <QMessageBox>
#include <QUrl>
#include "base/algorithm.h"
#include "base/bittorrent/session.h"
#include "base/bittorrent/trackerentry.h"
#include "base/bittorrent/trackerentrystatus.h"
#include "base/global.h"
#include "base/net/downloadmanager.h"
#include "base/preferences.h"
#include "base/torrentfilter.h"
#include "base/utils/compare.h"
#include "base/utils/fs.h"
#include "gui/transferlistwidget.h"
@@ -69,16 +69,35 @@ namespace
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.
// If failed to parse the domain, original input should be returned
return getTrackerHost(t.url);
}
const QString host = QUrl(url).host();
if (host.isEmpty())
return url;
template <typename T>
QSet<QString> extractTrackerHosts(const T &trackerEntries)
{
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)
@@ -123,7 +142,7 @@ namespace
TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, const bool downloadFavicon)
: BaseFilterWidget(parent, transferList)
, m_downloadTrackerFavicon(downloadFavicon)
, m_downloadTrackerFavicon {downloadFavicon}
{
auto *allTrackersItem = new QListWidgetItem(this);
allTrackersItem->setData(Qt::DisplayRole, formatItemText(ALL_ROW, 0));
@@ -131,22 +150,34 @@ TrackersFilterWidget::TrackersFilterWidget(QWidget *parent, TransferListWidget *
auto *trackerlessItem = new QListWidgetItem(this);
trackerlessItem->setData(Qt::DisplayRole, formatItemText(TRACKERLESS_ROW, 0));
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);
toggleFilter(Preferences::instance()->getTrackerFilterState());
}
TrackersFilterWidget::~TrackersFilterWidget()
@@ -155,83 +186,64 @@ TrackersFilterWidget::~TrackersFilterWidget()
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)
addItems(tracker.url, {torrentID});
for (const QString &trackerHost : addedTrackerHosts)
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)
removeItem(tracker, torrentID);
if (currentTrackerEntries.isEmpty())
increaseTorrentsCount(NULL_HOST, 1);
if (torrent->trackers().isEmpty())
addItems(NULL_HOST, {torrentID});
if (m_handleTrackerStatuses)
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();
m_errors.remove(torrentID);
m_trackerErrors.remove(torrentID);
m_warnings.remove(torrentID);
Algorithm::removeIf(m_trackers, [this, &torrentID](const QString &host, TrackerData &trackerData)
if (oldEntries.isEmpty())
{
QSet<BitTorrent::TorrentID> &torrentIDs = trackerData.torrents;
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});
decreaseTorrentsCount(NULL_HOST);
}
else
{
for (const BitTorrent::TrackerEntryStatus &status : trackers)
addItems(status.url, {torrentID});
for (const QString &trackerHost : asConst(extractTrackerHosts(oldEntries)))
decreaseTorrentsCount(trackerHost);
}
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))
if (newEntries.isEmpty())
{
applyFilter(row);
increaseTorrentsCount(NULL_HOST, 1);
}
else
{
for (const QString &trackerHost : asConst(extractTrackerHosts(newEntries)))
increaseTorrentsCount(trackerHost, 1);
}
if (m_handleTrackerStatuses)
refreshStatusItems(torrent);
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(host);
auto trackersIt = m_trackers.find(trackerHost);
const bool exists = (trackersIt != m_trackers.end());
QListWidgetItem *trackerItem = nullptr;
@@ -244,33 +256,27 @@ void TrackersFilterWidget::addItems(const QString &trackerURL, const QList<BitTo
trackerItem = new QListWidgetItem();
trackerItem->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"trackers"_s, u"network-server"_s));
const TrackerData trackerData {{}, trackerItem};
trackersIt = m_trackers.insert(host, trackerData);
const TrackerData trackerData {0, trackerItem};
trackersIt = m_trackers.insert(trackerHost, trackerData);
const QString scheme = getScheme(trackerURL);
downloadFavicon(host, u"%1://%2/favicon.ico"_s.arg((scheme.startsWith(u"http") ? scheme : u"http"_s), getFaviconHost(host)));
const QString scheme = getScheme(trackerHost);
downloadFavicon(trackerHost, u"%1://%2/favicon.ico"_s.arg((scheme.startsWith(u"http") ? scheme : u"http"_s), getFaviconHost(trackerHost)));
}
Q_ASSERT(trackerItem);
QSet<BitTorrent::TorrentID> &torrentIDs = trackersIt->torrents;
for (const BitTorrent::TorrentID &torrentID : torrents)
torrentIDs.insert(torrentID);
trackersIt->torrentsCount += torrentsCount;
trackerItem->setText(formatItemText(host, torrentIDs.size()));
trackerItem->setText(formatItemText(trackerHost, trackersIt->torrentsCount));
if (exists)
{
if (item(currentRow()) == trackerItem)
applyFilter(currentRow());
return;
}
Q_ASSERT(count() >= NUM_SPECIAL_ROWS);
Q_ASSERT(count() >= numSpecialRows());
const Utils::Compare::NaturalLessThan<Qt::CaseSensitive> naturalLessThan {};
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;
break;
@@ -280,88 +286,59 @@ void TrackersFilterWidget::addItems(const QString &trackerURL, const QList<BitTo
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;
torrentIDs.remove(id);
TrackerData &trackerData = iter.value();
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 (const auto errorHashesIt = m_errors.find(id)
; errorHashesIt != m_errors.end())
{
QSet<QString> &errored = *errorHashesIt;
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())));
if (currentItem() == trackerData.item)
setCurrentRow(0, QItemSelectionModel::SelectCurrent);
delete trackerData.item;
m_trackers.erase(iter);
updateGeometry();
}
else
{
trackerItem = item(TRACKERLESS_ROW);
trackerItem->setText(formatItemText(TRACKERLESS_ROW, torrentIDs.size()));
trackerData.item->setText(formatItemText(trackerHost, trackerData.torrentsCount));
}
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;
m_downloadTrackerFavicon = value;
@@ -381,107 +358,11 @@ void TrackersFilterWidget::setDownloadTrackerFavicon(bool value)
}
}
void TrackersFilterWidget::handleTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
void TrackersFilterWidget::handleTorrentTrackerStatusesUpdated(const BitTorrent::Torrent *torrent
, [[maybe_unused]] const QHash<QString, BitTorrent::TrackerEntryStatus> &updatedTrackers)
{
const BitTorrent::TorrentID id = torrent->id();
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);
}
if (m_handleTrackerStatuses)
refreshStatusItems(torrent);
}
void TrackersFilterWidget::downloadFavicon(const QString &trackerHost, const QString &faviconURL)
@@ -500,19 +381,14 @@ void TrackersFilterWidget::downloadFavicon(const QString &trackerHost, const QSt
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;
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);
}
@@ -522,6 +398,72 @@ void TrackersFilterWidget::removeTracker(const QString &tracker)
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)
{
const QSet<QString> trackerHosts = m_downloadingFavicons.take(result.url);
@@ -590,7 +532,7 @@ void TrackersFilterWidget::showMenu()
QMenu *menu = new QMenu(this);
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")
, this, &TrackersFilterWidget::onRemoveTrackerTriggered);
@@ -609,30 +551,80 @@ void TrackersFilterWidget::showMenu()
void TrackersFilterWidget::applyFilter(const int row)
{
if (row == ALL_ROW)
transferList()->applyTrackerFilterAll();
else if (isVisible())
transferList()->applyTrackerFilter(getTorrentIDs(row));
if (m_handleTrackerStatuses)
{
switch (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)
{
QHash<QString, QList<BitTorrent::TorrentID>> torrentsPerTracker;
QHash<QString, qsizetype> torrentsPerTrackerHost;
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
if (trackers.isEmpty())
torrentsPerTracker[NULL_HOST].append(torrentID);
if (const QList<BitTorrent::TrackerEntryStatus> trackers = torrent->trackers(); trackers.isEmpty())
{
++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();
@@ -641,22 +633,35 @@ void TrackersFilterWidget::handleTorrentsLoaded(const QList<BitTorrent::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
if (trackers.isEmpty())
removeItem(NULL_HOST, torrentID);
if (const QList<BitTorrent::TrackerEntryStatus> trackers = torrent->trackers(); trackers.isEmpty())
{
decreaseTorrentsCount(NULL_HOST);
}
else
{
for (const QString &trackerHost : asConst(extractTrackerHosts(trackers)))
decreaseTorrentsCount(trackerHost);
}
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()
{
const int row = currentRow();
if (row < NUM_SPECIAL_ROWS)
if (row < numSpecialRows())
return;
const QString &tracker = trackerFromRow(row);
@@ -694,27 +699,10 @@ QString TrackersFilterWidget::trackerFromRow(int row) const
int TrackersFilterWidget::rowFromTracker(const QString &tracker) const
{
Q_ASSERT(!tracker.isEmpty());
for (int i = NUM_SPECIAL_ROWS; i < count(); ++i)
for (int i = numSpecialRows(); i < count(); ++i)
{
if (tracker == trackerFromRow(i))
return i;
}
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.
* 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>
*
* This program is free software; you can redistribute it and/or
@@ -57,11 +57,6 @@ public:
TrackersFilterWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon);
~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);
private slots:
@@ -75,28 +70,40 @@ private:
void handleTorrentsLoaded(const QList<BitTorrent::Torrent *> &torrents) 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 addItems(const QString &trackerURL, const QList<BitTorrent::TorrentID> &torrents);
void removeItem(const QString &trackerURL, const BitTorrent::TorrentID &id);
void increaseTorrentsCount(const QString &trackerHost, qsizetype torrentsCount);
void decreaseTorrentsCount(const QString &trackerHost);
void refreshStatusItems(const BitTorrent::Torrent *torrent);
QString trackerFromRow(int row) const;
int rowFromTracker(const QString &tracker) const;
QSet<BitTorrent::TorrentID> getTorrentIDs(int row) const;
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
{
QSet<BitTorrent::TorrentID> torrents;
qsizetype torrentsCount = 0;
QListWidgetItem *item = nullptr;
};
QHash<QString, TrackerData> m_trackers; // <tracker host, tracker data>
QHash<BitTorrent::TorrentID, QSet<QString>> m_errors; // <torrent ID, tracker hosts>
QHash<BitTorrent::TorrentID, QSet<QString>> m_trackerErrors; // <torrent ID, tracker hosts>
QHash<BitTorrent::TorrentID, QSet<QString>> m_warnings; // <torrent ID, tracker hosts>
QSet<const BitTorrent::Torrent *> m_errors;
QSet<const BitTorrent::Torrent *> m_trackerErrors;
QSet<const BitTorrent::Torrent *> m_warnings;
PathList m_iconPaths;
int m_totalTorrents = 0;
bool m_downloadTrackerFavicon = false;
bool m_handleTrackerStatuses = false;
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.
* 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>
*
* This program is free software; you can redistribute it and/or
@@ -29,61 +29,26 @@
#include "transferlistfilterswidget.h"
#include <QCheckBox>
#include <QIcon>
#include <QListWidgetItem>
#include <QMenu>
#include <QPainter>
#include <QScrollArea>
#include <QStyleOptionButton>
#include <QUrl>
#include <QVBoxLayout>
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h"
#include "base/bittorrent/trackerentrystatus.h"
#include "base/global.h"
#include "base/logger.h"
#include "base/net/downloadmanager.h"
#include "base/preferences.h"
#include "base/torrentfilter.h"
#include "base/utils/compare.h"
#include "base/utils/fs.h"
#include "transferlistfilters/categoryfilterwidget.h"
#include "transferlistfilters/statusfilterwidget.h"
#include "transferlistfilters/tagfilterwidget.h"
#include "transferlistfilters/trackersfilterwidget.h"
#include "transferlistfilters/trackerstatusfilterwidget.h"
#include "transferlistfilterswidgetitem.h"
#include "transferlistwidget.h"
#include "uithememanager.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)
: QWidget(parent)
, m_transferList {transferList}
@@ -99,66 +64,65 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi
mainWidgetLayout->setSpacing(2);
mainWidgetLayout->setAlignment(Qt::AlignLeft | Qt::AlignTop);
QFont font;
font.setBold(true);
font.setCapitalization(QFont::AllUppercase);
{
auto *item = new TransferListFiltersWidgetItem(tr("Status"), new StatusFilterWidget(this, transferList), this);
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());
statusLabel->setFont(font);
connect(statusLabel, &QCheckBox::toggled, pref, &Preferences::setStatusFilterState);
mainWidgetLayout->addWidget(statusLabel);
{
auto *categoryFilterWidget = new CategoryFilterWidget(this);
connect(categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered
, transferList, &TransferListWidget::deleteVisibleTorrents);
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);
connect(statusLabel, &QCheckBox::toggled, statusFilters, &StatusFilterWidget::toggleFilter);
mainWidgetLayout->addWidget(statusFilters);
auto *item = new TransferListFiltersWidgetItem(tr("Categories"), categoryFilterWidget, this);
item->setChecked(pref->getCategoryFilterState());
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());
categoryLabel->setFont(font);
connect(categoryLabel, &QCheckBox::toggled, this
, &TransferListFiltersWidget::onCategoryFilterStateChanged);
mainWidgetLayout->addWidget(categoryLabel);
{
auto *tagFilterWidget = new TagFilterWidget(this);
connect(tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered
, transferList, &TransferListWidget::deleteVisibleTorrents);
connect(tagFilterWidget, &TagFilterWidget::actionStopTorrentsTriggered
, transferList, &TransferListWidget::stopVisibleTorrents);
connect(tagFilterWidget, &TagFilterWidget::actionStartTorrentsTriggered
, transferList, &TransferListWidget::startVisibleTorrents);
connect(tagFilterWidget, &TagFilterWidget::tagChanged
, transferList, &TransferListWidget::applyTagFilter);
m_categoryFilterWidget = new CategoryFilterWidget(this);
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionDeleteTorrentsTriggered
, transferList, &TransferListWidget::deleteVisibleTorrents);
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionStopTorrentsTriggered
, transferList, &TransferListWidget::stopVisibleTorrents);
connect(m_categoryFilterWidget, &CategoryFilterWidget::actionStartTorrentsTriggered
, transferList, &TransferListWidget::startVisibleTorrents);
connect(m_categoryFilterWidget, &CategoryFilterWidget::categoryChanged
, transferList, &TransferListWidget::applyCategoryFilter);
toggleCategoryFilter(pref->getCategoryFilterState());
mainWidgetLayout->addWidget(m_categoryFilterWidget);
auto *item = new TransferListFiltersWidgetItem(tr("Tags"), tagFilterWidget, this);
item->setChecked(pref->getTagFilterState());
connect(item, &TransferListFiltersWidgetItem::toggled, this, [this, tagFilterWidget](const bool enabled)
{
m_transferList->applyTagFilter(enabled ? tagFilterWidget->currentTag() : std::nullopt);
});
connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTagFilterState);
mainWidgetLayout->addWidget(item);
}
QCheckBox *tagsLabel = new ArrowCheckBox(tr("Tags"), this);
tagsLabel->setChecked(pref->getTagFilterState());
tagsLabel->setFont(font);
connect(tagsLabel, &QCheckBox::toggled, this, &TransferListFiltersWidget::onTagFilterStateChanged);
mainWidgetLayout->addWidget(tagsLabel);
const int trackerStatusItemPos = mainWidgetLayout->count();
m_tagFilterWidget = new TagFilterWidget(this);
connect(m_tagFilterWidget, &TagFilterWidget::actionDeleteTorrentsTriggered
, 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);
{
m_trackersFilterWidget = new TrackersFilterWidget(this, transferList, downloadFavicon);
QCheckBox *trackerLabel = new ArrowCheckBox(tr("Trackers"), this);
trackerLabel->setChecked(pref->getTrackerFilterState());
trackerLabel->setFont(font);
connect(trackerLabel, &QCheckBox::toggled, pref, &Preferences::setTrackerFilterState);
mainWidgetLayout->addWidget(trackerLabel);
m_trackersFilterWidget = new TrackersFilterWidget(this, transferList, downloadFavicon);
connect(trackerLabel, &QCheckBox::toggled, m_trackersFilterWidget, &TrackersFilterWidget::toggleFilter);
mainWidgetLayout->addWidget(m_trackersFilterWidget);
auto *item = new TransferListFiltersWidgetItem(tr("Trackers"), m_trackersFilterWidget, this);
item->setChecked(pref->getTrackerFilterState());
connect(item, &TransferListFiltersWidgetItem::toggled, pref, &Preferences::setTrackerFilterState);
mainWidgetLayout->addWidget(item);
}
auto *scroll = new QScrollArea(this);
scroll->setWidgetResizable(true);
@@ -169,54 +133,40 @@ TransferListFiltersWidget::TransferListFiltersWidget(QWidget *parent, TransferLi
auto *vLayout = new QVBoxLayout(this);
vLayout->setContentsMargins(0, 0, 0, 0);
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)
{
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.
* 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>
*
* This program is free software; you can redistribute it and/or
@@ -32,11 +32,6 @@
#include <QtContainerFwd>
#include <QWidget>
#include "base/bittorrent/trackerentry.h"
class CategoryFilterWidget;
class StatusFilterWidget;
class TagFilterWidget;
class TrackersFilterWidget;
class TransferListWidget;
@@ -55,23 +50,8 @@ public:
TransferListFiltersWidget(QWidget *parent, TransferListWidget *transferList, bool downloadFavicon);
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:
void toggleCategoryFilter(bool enabled);
void toggleTagFilter(bool enabled);
TransferListWidget *m_transferList = nullptr;
TrackersFilterWidget *m_trackersFilterWidget = nullptr;
CategoryFilterWidget *m_categoryFilterWidget = nullptr;
TagFilterWidget *m_tagFilterWidget = nullptr;
bool m_useSeparateTrackerStatusFilter = false;
};

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)
: QAbstractListModel {parent}
, m_statusStrings
{
{BitTorrent::TorrentState::Downloading, tr("Downloading")},
{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::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::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::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::QueuedUploading, tr("Queued", "Torrent is queued")},
{BitTorrent::TorrentState::CheckingDownloading, 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::StoppedDownloading, tr("Stopped")},
{BitTorrent::TorrentState::StoppedUploading, tr("Completed")},
{BitTorrent::TorrentState::Moving, tr("Moving", "Torrent local data are being moved/relocated")},
{BitTorrent::TorrentState::MissingFiles, tr("Missing Files")},
{BitTorrent::TorrentState::Error, tr("Errored", "Torrent status, the torrent has an error")}
}
, m_statusStrings {
{BitTorrent::TorrentState::Downloading, tr("Downloading")},
{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::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::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::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::QueuedUploading, tr("Queued", "Torrent is queued")},
{BitTorrent::TorrentState::CheckingDownloading, 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::StoppedDownloading, tr("Stopped")},
{BitTorrent::TorrentState::StoppedUploading, tr("Completed")},
{BitTorrent::TorrentState::Moving, tr("Moving", "Torrent local data are being moved/relocated")},
{BitTorrent::TorrentState::MissingFiles, tr("Missing Files")},
{BitTorrent::TorrentState::Error, tr("Errored", "Torrent status, the torrent has an error")}}
{
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::torrentStopped, 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
@@ -392,7 +392,7 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
case TR_RATIO:
return ratioString(torrent->realRatio());
case TR_RATIO_LIMIT:
return ratioString(torrent->maxRatio());
return ratioString(torrent->effectiveRatioLimit());
case TR_POPULARITY:
return ratioString(torrent->popularity());
case TR_CATEGORY:
@@ -509,7 +509,7 @@ QVariant TransferListModel::internalValue(const BitTorrent::Torrent *torrent, co
case TR_COMPLETED:
return torrent->completedSize();
case TR_RATIO_LIMIT:
return torrent->maxRatio();
return torrent->effectiveRatioLimit();
case TR_SEEN_COMPLETE_DATE:
return torrent->lastSeenComplete();
case TR_LAST_ACTIVITY:

View File

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

View File

@@ -49,13 +49,13 @@ public:
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 disableCategoryFilter();
void setTagFilter(const Tag &tag);
void disableTagFilter();
void setTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs);
void disableTrackerFilter();
void setTrackerFilter(const std::optional<QString> &trackerHost);
void setAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus);
private:
int compare(const QModelIndex &left, const QModelIndex &right) const;

View File

@@ -104,20 +104,6 @@ namespace
return false;
}
void openDestinationFolder(const BitTorrent::Torrent *const torrent)
{
const Path contentPath = torrent->contentPath();
const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
#ifdef Q_OS_MACOS
MacUtils::openFiles({openedPath});
#else
if (torrent->filesCount() == 1)
Utils::Gui::openFolderSelect(openedPath);
else
Utils::Gui::openPath(openedPath);
#endif
}
void removeTorrents(const QList<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
{
auto *session = BitTorrent::Session::instance();
@@ -592,7 +578,7 @@ void TransferListWidget::hideQueuePosColumn(bool hide)
resizeColumnToContents(TransferListModel::TR_QUEUE_POSITION);
}
void TransferListWidget::openSelectedTorrentsFolder() const
void TransferListWidget::openSelectedTorrentsFolder()
{
QSet<Path> paths;
#ifdef Q_OS_MACOS
@@ -612,7 +598,7 @@ void TransferListWidget::openSelectedTorrentsFolder() const
if (!paths.contains(openedPath))
{
if (torrent->filesCount() == 1)
Utils::Gui::openFolderSelect(openedPath);
Utils::Gui::openFolderSelect(openedPath, this);
else
Utils::Gui::openPath(openedPath);
}
@@ -621,6 +607,20 @@ void TransferListWidget::openSelectedTorrentsFolder() const
#endif // Q_OS_MACOS
}
void TransferListWidget::openDestinationFolder(const BitTorrent::Torrent *const torrent)
{
const Path contentPath = torrent->contentPath();
const Path openedPath = (!contentPath.isEmpty() ? contentPath : torrent->savePath());
#ifdef Q_OS_MACOS
MacUtils::openFiles({openedPath});
#else
if (torrent->filesCount() == 1)
Utils::Gui::openFolderSelect(openedPath, this);
else
Utils::Gui::openPath(openedPath);
#endif
}
void TransferListWidget::previewSelectedTorrents()
{
for (const BitTorrent::Torrent *torrent : asConst(getSelectedTorrents()))
@@ -1342,14 +1342,14 @@ void TransferListWidget::applyTagFilter(const std::optional<Tag> &tag)
m_sortFilterModel->setTagFilter(*tag);
}
void TransferListWidget::applyTrackerFilterAll()
void TransferListWidget::applyTrackerFilter(const std::optional<QString> &trackerHost)
{
m_sortFilterModel->disableTrackerFilter();
m_sortFilterModel->setTrackerFilter(trackerHost);
}
void TransferListWidget::applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs)
void TransferListWidget::applyAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus)
{
m_sortFilterModel->setTrackerFilter(torrentIDs);
m_sortFilterModel->setAnnounceStatusFilter(announceStatus);
}
void TransferListWidget::applyFilter(const QString &name, const TransferListModel::Column &type)
@@ -1362,7 +1362,7 @@ void TransferListWidget::applyFilter(const QString &name, const TransferListMode
void TransferListWidget::applyStatusFilter(const int filterIndex)
{
const auto filterType = static_cast<TorrentFilter::Type>(filterIndex);
const auto filterType = static_cast<TorrentFilter::Status>(filterIndex);
m_sortFilterModel->setStatusFilter(((filterType >= TorrentFilter::All) && (filterType < TorrentFilter::_Count)) ? filterType : TorrentFilter::All);
// Select first item if nothing is selected
if (selectionModel()->selectedRows(0).empty() && (m_sortFilterModel->rowCount() > 0))

View File

@@ -89,7 +89,8 @@ public slots:
void copySelectedInfohashes(CopyInfohashPolicy policy) const;
void copySelectedIDs() const;
void copySelectedComments() const;
void openSelectedTorrentsFolder() const;
void openSelectedTorrentsFolder();
void openDestinationFolder(const BitTorrent::Torrent *torrent);
void recheckSelectedTorrents();
void reannounceSelectedTorrents();
void setTorrentOptions();
@@ -99,8 +100,8 @@ public slots:
void applyStatusFilter(int filterIndex);
void applyCategoryFilter(const QString &category);
void applyTagFilter(const std::optional<Tag> &tag);
void applyTrackerFilterAll();
void applyTrackerFilter(const QSet<BitTorrent::TorrentID> &torrentIDs);
void applyTrackerFilter(const std::optional<QString> &trackerHost);
void applyAnnounceStatusFilter(const std::optional<BitTorrent::TorrentAnnounceStatus> &announceStatus);
void previewFile(const Path &filePath);
void renameSelectedTorrent();

View File

@@ -76,13 +76,11 @@ UIThemeManager::UIThemeManager()
, m_useSystemIcons {Preferences::instance()->useSystemIcons()}
#endif
{
#ifdef Q_OS_WIN
if (const QString styleName = Preferences::instance()->getStyle(); styleName.compare(u"system", Qt::CaseInsensitive) != 0)
{
if (!QApplication::setStyle(styleName.isEmpty() ? u"Fusion"_s : styleName))
if (!QApplication::setStyle(styleName))
LogMsg(tr("Set app style failed. Unknown style: \"%1\"").arg(styleName), Log::WARNING);
}
#endif
#ifdef QBT_HAS_COLORSCHEME_OPTION
applyColorScheme();

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