mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-04-23 22:17:23 -04:00
Compare commits
154 commits
master
...
release-5.
Author | SHA1 | Date | |
---|---|---|---|
|
100ee5dbe0 | ||
|
310a9d8e1a | ||
|
677cabcbdf | ||
|
b86079974c | ||
|
ca6a89e238 | ||
|
505c1e1c0a | ||
|
ecde201ec5 | ||
|
730bf957a4 | ||
|
069cd029eb | ||
|
375e6800e9 | ||
|
09fb92466a | ||
|
69321f0e94 | ||
|
f39e066672 | ||
|
6a5ea93c92 | ||
|
35dce07c63 | ||
|
0188e11dd7 | ||
|
1dc348539b | ||
|
241a0e91bf | ||
|
68f7295500 | ||
|
53adb7bfa8 | ||
|
6128f6eecc | ||
|
d156a44f8d | ||
|
c3c7f28bad | ||
|
9ac14cdf9f | ||
|
b899ea8c40 | ||
|
0d7c367332 | ||
|
22826499d5 | ||
|
dbfd830b56 | ||
|
ad3348b95f | ||
|
44b08fcb74 | ||
|
71b752baf3 | ||
|
15b6091261 | ||
|
abe457389d | ||
|
abce4cd1bc | ||
|
2bfb336905 | ||
|
2dee65fa52 | ||
|
423b3ed9bf | ||
|
3454f064f0 | ||
|
ac9ca4f452 | ||
|
09899a7d0d | ||
|
9ab3c573dc | ||
|
993eb25323 | ||
|
1e27e6504e | ||
|
330dce6aa2 | ||
|
39b965af48 | ||
|
5e105b0348 | ||
|
f2b2a2b034 | ||
|
10499dffe9 | ||
|
eea01b94a3 | ||
|
374951f6f2 | ||
|
6d6f9bc619 | ||
|
84ee620fdc | ||
|
6079b25419 | ||
|
fe24bc825b | ||
|
94136262a8 | ||
|
f52947e27e | ||
|
315e88aee9 | ||
|
565c6d843a | ||
|
9104351c89 | ||
|
e58b0a65d2 | ||
|
878d829904 | ||
|
063f77bc6c | ||
|
2a4077414f | ||
|
2a44253802 | ||
|
4712eba0dc | ||
|
983b7814aa | ||
|
e082a21751 | ||
|
7dd1d1bac8 | ||
|
49f57b1049 | ||
|
fbf68a0649 | ||
|
39229dc06a | ||
|
bb314e1555 | ||
|
a3a8b15828 | ||
|
b579afe1aa | ||
|
93096dba56 | ||
|
6379c33964 | ||
|
84372de675 | ||
|
403b7c7c35 | ||
|
b2fab43865 | ||
|
387821267f | ||
|
dd7ef8e934 | ||
|
cce295faeb | ||
|
db5479ea01 | ||
|
e1216c4c9a | ||
|
f4a0868426 | ||
|
59a5fcf7d0 | ||
|
f9a2b02a8d | ||
|
04f6a565f3 | ||
|
3e96048ee4 | ||
|
d4ccf3001c | ||
|
64506f16bd | ||
|
24a7a835af | ||
|
93b9bf9552 | ||
|
f4125601de | ||
|
2d67729617 | ||
|
878ebbed41 | ||
|
c61c3d7cd8 | ||
|
978fbbdc0d | ||
|
63689cf763 | ||
|
cebc72d3cf | ||
|
a67bd271c6 | ||
|
a8cffbb205 | ||
|
7dfb0110d4 | ||
|
3ad8fcbdd2 | ||
|
195eae5f3d | ||
|
920ae26f7b | ||
|
09ed0d6b66 | ||
|
4f0cc8aa11 | ||
|
4d490c84e7 | ||
|
96607ce874 | ||
|
418edc7471 | ||
|
bd01b7c4df | ||
|
b0ac763048 | ||
|
127d2d6f0b | ||
|
4149609e78 | ||
|
78c549f83e | ||
|
a3a53e2e0e | ||
|
5aaa43e01d | ||
|
86745d7b07 | ||
|
210650a5ee | ||
|
fe93b6d0d8 | ||
|
e8b585acd8 | ||
|
cea20141a9 | ||
|
0f5a27ed50 | ||
|
c2cf898ccd | ||
|
5e5aa8a563 | ||
|
12a4c3fda2 | ||
|
5f50b701d2 | ||
|
9f20d9c3aa | ||
|
05e3130baa | ||
|
683492648f | ||
|
2f2e158877 | ||
|
e60e96cb0e | ||
|
5f31208bf1 | ||
|
fa58e58e70 | ||
|
671943a9a6 | ||
|
8bad80bcdd | ||
|
c44e300507 | ||
|
318a677e8f | ||
|
0246df790a | ||
|
782fbc1425 | ||
|
7deccd5592 | ||
|
4a36fe7278 | ||
|
1c5af96ad8 | ||
|
3bb47a5410 | ||
|
d7abeb4bf0 | ||
|
a19d623ead | ||
|
1ef21bc2b7 | ||
|
4687b4e8e4 | ||
|
d2e5163861 | ||
|
8a15ea8026 | ||
|
2b99554813 | ||
|
e6638f9c19 | ||
|
ec6eac2ba1 |
747 changed files with 135686 additions and 116777 deletions
7
.github/workflows/ci_macos.yaml
vendored
7
.github/workflows/ci_macos.yaml
vendored
|
@ -23,7 +23,6 @@ jobs:
|
|||
|
||||
env:
|
||||
boost_path: "${{ github.workspace }}/../boost"
|
||||
openssl_root: "$(brew --prefix openssl@3)"
|
||||
libtorrent_path: "${{ github.workspace }}/../libtorrent"
|
||||
|
||||
steps:
|
||||
|
@ -70,7 +69,7 @@ jobs:
|
|||
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ matrix.qt_version }}
|
||||
archives: qtbase qtdeclarative qtsvg qttools
|
||||
|
@ -94,8 +93,7 @@ jobs:
|
|||
-DCMAKE_CXX_STANDARD=17 \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-Ddeprecated-functions=OFF \
|
||||
-DOPENSSL_ROOT_DIR="${{ env.openssl_root }}"
|
||||
-Ddeprecated-functions=OFF
|
||||
cmake --build build
|
||||
sudo cmake --install build
|
||||
|
||||
|
@ -109,7 +107,6 @@ jobs:
|
|||
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
|
||||
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
|
||||
-DBOOST_ROOT="${{ env.boost_path }}" \
|
||||
-DOPENSSL_ROOT_DIR="${{ env.openssl_root }}" \
|
||||
-DTESTING=ON \
|
||||
-DVERBOSE_CONFIGURE=ON \
|
||||
-D${{ matrix.qbt_gui }}
|
||||
|
|
12
.github/workflows/ci_python.yaml
vendored
12
.github/workflows/ci_python.yaml
vendored
|
@ -53,7 +53,7 @@ jobs:
|
|||
python-version: '3.7'
|
||||
|
||||
- name: Install tools (search engine)
|
||||
run: pip install bandit pycodestyle pyflakes
|
||||
run: pip install bandit mypy pycodestyle pyflakes pyright
|
||||
|
||||
- name: Gather files (search engine)
|
||||
run: |
|
||||
|
@ -61,6 +61,16 @@ jobs:
|
|||
echo $PY_FILES
|
||||
echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Check typings (search engine)
|
||||
run: |
|
||||
MYPYPATH="src/searchengine/nova3" \
|
||||
mypy \
|
||||
--follow-imports skip \
|
||||
--strict \
|
||||
$PY_FILES
|
||||
pyright \
|
||||
$PY_FILES
|
||||
|
||||
- name: Lint code (search engine)
|
||||
run: |
|
||||
pyflakes $PY_FILES
|
||||
|
|
3
.github/workflows/ci_ubuntu.yaml
vendored
3
.github/workflows/ci_ubuntu.yaml
vendored
|
@ -64,7 +64,7 @@ jobs:
|
|||
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ matrix.qt_version }}
|
||||
archives: icu qtbase qtdeclarative qtsvg qttools
|
||||
|
@ -134,7 +134,6 @@ jobs:
|
|||
|
||||
- name: Install AppImage
|
||||
run: |
|
||||
sudo apt install libfuse2
|
||||
curl \
|
||||
-L \
|
||||
-Z \
|
||||
|
|
32
.github/workflows/ci_windows.yaml
vendored
32
.github/workflows/ci_windows.yaml
vendored
|
@ -93,9 +93,9 @@ jobs:
|
|||
move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}"
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.7.0"
|
||||
version: "6.7.3"
|
||||
archives: qtbase qtsvg qttools
|
||||
cache: true
|
||||
|
||||
|
@ -153,26 +153,26 @@ jobs:
|
|||
copy build/qbittorrent.pdb upload/qBittorrent
|
||||
copy dist/windows/qt.conf upload/qBittorrent
|
||||
# runtimes
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
|
||||
mkdir upload/qBittorrent/plugins/iconengines
|
||||
copy "${{ env.Qt6_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
|
||||
mkdir upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
|
||||
mkdir upload/qBittorrent/plugins/platforms
|
||||
copy "${{ env.Qt6_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
|
||||
mkdir upload/qBittorrent/plugins/sqldrivers
|
||||
copy "${{ env.Qt6_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers
|
||||
mkdir upload/qBittorrent/plugins/styles
|
||||
copy "${{ env.Qt6_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles
|
||||
mkdir upload/qBittorrent/plugins/tls
|
||||
copy "${{ env.Qt6_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls
|
||||
# cmake additionals
|
||||
mkdir upload/cmake
|
||||
copy build/compile_commands.json upload/cmake
|
||||
|
|
2
.github/workflows/coverity-scan.yaml
vendored
2
.github/workflows/coverity-scan.yaml
vendored
|
@ -52,7 +52,7 @@ jobs:
|
|||
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: ${{ matrix.qt_version }}
|
||||
archives: icu qtbase qtdeclarative qtsvg qttools
|
||||
|
|
|
@ -78,11 +78,7 @@ repos:
|
|||
m4/.* |
|
||||
src/base/3rdparty/.* |
|
||||
src/searchengine/nova3/socks.py |
|
||||
src/webui/www/private/lang/.* |
|
||||
src/webui/www/private/scripts/lib/.* |
|
||||
src/webui/www/public/lang/.* |
|
||||
src/webui/www/public/scripts/lib/.* |
|
||||
src/webui/www/transifex/.*
|
||||
src/webui/www/private/scripts/lib/.*
|
||||
)$
|
||||
exclude_types:
|
||||
- ts
|
||||
|
@ -106,11 +102,7 @@ repos:
|
|||
m4/.* |
|
||||
src/base/3rdparty/.* |
|
||||
src/searchengine/nova3/socks.py |
|
||||
src/webui/www/private/lang/.* |
|
||||
src/webui/www/private/scripts/lib/.* |
|
||||
src/webui/www/public/lang/.* |
|
||||
src/webui/www/public/scripts/lib/.* |
|
||||
src/webui/www/transifex/.*
|
||||
src/webui/www/private/scripts/lib/.*
|
||||
)$
|
||||
exclude_types:
|
||||
- svg
|
||||
|
|
12
.tx/config
12
.tx/config
|
@ -1,7 +1,7 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_master]
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_v50x]
|
||||
file_filter = src/lang/qbittorrent_<lang>.ts
|
||||
source_file = src/lang/qbittorrent_en.ts
|
||||
source_lang = en
|
||||
|
@ -9,7 +9,7 @@ type = QT
|
|||
minimum_perc = 23
|
||||
lang_map = pt: pt_PT, zh: zh_CN
|
||||
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui]
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui_v50x]
|
||||
file_filter = src/webui/www/translations/webui_<lang>.ts
|
||||
source_file = src/webui/www/translations/webui_en.ts
|
||||
source_lang = en
|
||||
|
@ -17,14 +17,6 @@ type = QT
|
|||
minimum_perc = 23
|
||||
lang_map = pt: pt_PT, zh: zh_CN
|
||||
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui_json]
|
||||
file_filter = src/webui/www/transifex/<lang>.json
|
||||
source_file = src/webui/www/transifex/en.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
minimum_perc = 23
|
||||
lang_map = pt: pt_PT, zh: zh_CN
|
||||
|
||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrentdesktop_master]
|
||||
source_file = dist/unix/org.qbittorrent.qBittorrent.desktop
|
||||
source_lang = en
|
||||
|
|
100
Changelog
100
Changelog
|
@ -1,4 +1,61 @@
|
|||
Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
|
||||
Tue Feb 18th 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.4
|
||||
- BUGFIX: Fix cannot remove trackers via WebAPI (Chocobo1)
|
||||
- BUGFIX: Fix torrent content checkbox state under certain conditions (thalieht)
|
||||
- BUGFIX: Hide zero and infinity values in peer list only when that setting is set to `Always` (thalieht)
|
||||
- BUGFIX: Remove stopped torrent from "error" tracker filter (glassez)
|
||||
- WEBUI: Fix memory leak in context menus (skomerko)
|
||||
- WEBAPI: Don't trim string parameters (glassez)
|
||||
- WINDOWS: Handle Qt style options uniformly (glassez)
|
||||
- WINDOWS: NSIS: Update Portuguese translation (Hugo Carvalho)
|
||||
- MACOS: Avoid memory leak (Chocobo1)
|
||||
|
||||
Mon Dec 16th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.3
|
||||
- BUGFIX: Discard obsolete "state update" events after torrent is reloaded (glassez)
|
||||
- BUGFIX: Fix incorrect SQL column definition (glassez)
|
||||
- BUGFIX: Avoid redundant requests of announce entries from libtorrent (glassez)
|
||||
- WEBUI: Fix removing tracker URL with '|' character (Thomas Piccirello)
|
||||
- WEBUI: Fix reloading page after login (Evgenii Ryshkov)
|
||||
- WEBAPI: Fix incorrect key in torrent creator (Bartu Özen)
|
||||
- RSS: Don't add duplicate episodes to previously matched (wavygecko)
|
||||
- RSS: Use cached current time when parsing RSS feed (glassez)
|
||||
- WINDOWS: Don't follow symlink when creating torrents on Windows (Chocobo1)
|
||||
- WINDOWS: NSIS: Update Italian translation (Giacomo411)
|
||||
|
||||
Sun Nov 17th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.2
|
||||
- BUGFIX: Remove trackers from previous category when moved to new one (glassez)
|
||||
- BUGFIX: Fix `.torrent` file could not be deleted when torrent is canceled (glassez)
|
||||
- BUGFIX: Reset tracker entries when pausing the session (glassez)
|
||||
- BUGFIX: Check real palette darkness to detect "dark theme" (glassez)
|
||||
- BUGFIX: Correctly handle "torrent finished" events (glassez)
|
||||
- BUGFIX: Preserve initial torrent progress while checking resume data (glassez)
|
||||
- BUGFIX: Avoid reapplying Mark-of-the-Web when it already exists (Chocobo1)
|
||||
- BUGFIX: Don't apply Mark-of-the-Web on existing files (Chocobo1)
|
||||
- WEBUI: Add color scheme switcher (sledgehammer999)
|
||||
- SEARCH: Correctly delete the moved search tab (glassez)
|
||||
- WINDOWS: Correctly save and restore Qt style setting (glassez)
|
||||
- WINDOWS: NSIS: update Luxembourgish, Simplified Chinese and Traditional Chinese translations (Ikko Eltociear Ashimine, 3gf8jv4dv)
|
||||
|
||||
Mon Oct 28th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.1
|
||||
- FEATURE: Add "Simple pread/pwrite" disk IO type (Hanabishi)
|
||||
- BUGFIX: Don't ignore SSL errors (sledgehammer999)
|
||||
- BUGFIX: Don't try to apply Mark-of-the-Web to nonexistent files (glassez)
|
||||
- BUGFIX: Disable "Move to trash" option by default (glassez)
|
||||
- BUGFIX: Disable the ability to create torrents with a piece size of 256MiB (stalkerok)
|
||||
- BUGFIX: Allow to choose Qt style (glassez)
|
||||
- BUGFIX: Always notify user about duplicate torrent (glassez)
|
||||
- BUGFIX: Correctly handle "torrent finished after move" event (glassez)
|
||||
- BUGFIX: Correctly apply filename filter when `!qB` extension is enabled (glassez)
|
||||
- BUGFIX: Improve color scheme change detection (glassez)
|
||||
- BUGFIX: Fix button state for SSL certificate check (Chocobo1)
|
||||
- WEBUI: Fix CSS that results in hidden torrent list in some browsers (skomerko)
|
||||
- WEBUI: Use proper text color to highlight items in all filter lists (skomerko)
|
||||
- WEBUI: Fix 'rename files' dialog cannot be opened more than once (Chocobo1)
|
||||
- WEBUI: Fix UI of Advanced Settings to show all settings (glassez)
|
||||
- WEBUI: Free resources allocated by web session once it is destructed (dyseg)
|
||||
- SEARCH: Import correct libraries (Chocobo1)
|
||||
- OTHER: Sync flag icons with upstream (xavier2k6)
|
||||
|
||||
Sun Sep 29th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
|
||||
- FEATURE: Support creating .torrent with larger piece size (Chocobo1)
|
||||
- FEATURE: Improve tracker entries handling (glassez)
|
||||
- FEATURE: Add separate filter item for tracker errors (glassez)
|
||||
|
@ -12,14 +69,30 @@ Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
|
|||
- FEATURE: Enable Ctrl+F hotkey for more inputs (thalieht)
|
||||
- FEATURE: Add seeding limits to RSS and Watched folders options UI (glassez)
|
||||
- FEATURE: Subcategories implicitly follow the parent category options (glassez)
|
||||
- FEATURE: Add support for SSL torrents (Chocobo1, Radu Carpa)
|
||||
- FEATURE: Add option to name each qbittorrent instance (Chocobo1)
|
||||
- FEATURE: Add button for sending test email (Thomas Piccirello)
|
||||
- FEATURE: Allow torrents to override default share limit action (glassez)
|
||||
- FEATURE: Use Start/Stop instead of Resume/Pause (thalieht)
|
||||
- FEATURE: Add the Popularity metric (Aliaksei Urbanski)
|
||||
- FEATURE: Focus on Download button if torrent link retrieved from the clipboard (glassez)
|
||||
- FEATURE: Add ability to pause/resume entire BitTorrent session (glassez)
|
||||
- FEATURE: Add an option to set BitTorrent session shutdown timeout (glassez)
|
||||
- FEATURE: Apply "Excluded file names" to folder names as well (glassez)
|
||||
- FEATURE: Allow to use regular expression to filter torrent content (glassez)
|
||||
- FEATURE: Allow to move content files to Trash instead of deleting them (glassez)
|
||||
- FEATURE: Add ability to display torrent "privateness" in UI (ManiMatter)
|
||||
- FEATURE: Add a flag in `Peers` tab denoting a connection using NAT hole punching (stalkerok)
|
||||
- BUGFIX: Display error message when unrecoverable error occurred (glassez)
|
||||
- BUGFIX: Update size of selected files when selection is changed (glassez)
|
||||
- BUGFIX: Normalize tags by trimming leading/trailing whitespace (glassez)
|
||||
- BUGFIX: Correctly handle share limits in torrent options dialog (glassez)
|
||||
- BUGFIX: Adjust tracker tier when adding additional trackers (Chocobo1)
|
||||
- BUGFIX: Fix inconsistent naming between `Done/Progress` column (luzpaz)
|
||||
- BUGFIX: Sanitize peer client names (Hanabishi)
|
||||
- BUGFIX: Apply share limits immediately when torrent downloading is finished (glassez)
|
||||
- BUGFIX: Show download progress for folders with zero byte size as 100 instead of 0 (vikas_c)
|
||||
- BUGFIX: Fix highlighted piece color (Prince Gupta)
|
||||
- BUGFIX: Apply "merge trackers" logic regardless of way the torrent is added (glassez)
|
||||
- WEBUI: Improve WebUI responsiveness (Chocobo1)
|
||||
- WEBUI: Do not exit the app when WebUI has failed to start (Hanabishi)
|
||||
- WEBUI: Add `Moving` filter to side panel (xavier2k6)
|
||||
|
@ -28,14 +101,37 @@ Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
|
|||
- WEBUI: Leave the fields empty when value is invalid (Chocobo1)
|
||||
- WEBUI: Use natural sorting (Chocobo1)
|
||||
- WEBUI: Improve WebUI login behavior (JayRet)
|
||||
- WEBUI: Conditionally show filters sidebar (Thomas Piccirello)
|
||||
- WEBUI: Add support for running concurrent searches (Thomas Piccirello)
|
||||
- WEBUI: Improve accuracy of trackers list (Thomas Piccirello)
|
||||
- WEBUI: Fix error when category doesn't exist (Thomas Piccirello)
|
||||
- WEBUI: Improve table scrolling and selection on mobile (Thomas Piccirello)
|
||||
- WEBUI: Restore search tabs on load (Thomas Piccirello)
|
||||
- WEBUI: Restore previously used tab on load (Thomas Piccirello)
|
||||
- WEBUI: Increase default height of `Share ratio limit` dialog (thalieht)
|
||||
- WEBUI: Use enabled search plugins by default (Thomas Piccirello)
|
||||
- WEBUI: Add columns `Incomplete Save Path`, `Info Hash v1`, `Info Hash v2` (thalieht)
|
||||
- WEBUI: Always create generic filter items (skomerko)
|
||||
- WEBUI: Provide `Use Category paths in Manual Mode` option (skomerko)
|
||||
- WEBUI: Provide `Merge trackers to existing torrent` option (skomerko)
|
||||
- WEBAPI: Fix wrong timestamp values (Chocobo1)
|
||||
- WEBAPI: Send binary data with filename and mime type specified (glassez)
|
||||
- WEBAPI: Expose API for the torrent creator (glassez, Radu Carpa)
|
||||
- WEBAPI: Add support for SSL torrents (Chocobo1, Radu Carpa)
|
||||
- WEBAPI: Provide endpoint for listing directory content (Paweł Kotiuk)
|
||||
- WEBAPI: Provide "private" flag via "torrents/info" endpoint (ManiMatter)
|
||||
- WEBAPI: Add a way to download .torrent file using search plugin (glassez)
|
||||
- WEBAPI: Add "private" filter for "torrents/info" endpoint (ManiMatter)
|
||||
- WEBAPI: Add root_path to "torrents/info" result (David Newhall)
|
||||
- RSS: Show RSS feed title in HTML browser (Jay)
|
||||
- RSS: Allow to set delay between requests to the same host (jNullj)
|
||||
- SEARCH: Allow users to specify Python executable path (Chocobo1)
|
||||
- SEARCH: Lazy load search plugins (milahu)
|
||||
- SEARCH: Add date column to the built-in search engine (ducalex)
|
||||
- SEARCH: Allow to rearrange search tabs (glassez)
|
||||
- WINDOWS: Use Fusion style on Windows 10+. It has better compatibility with dark mode (glassez)
|
||||
- WINDOWS: Allow to set qBittorrent as default program (glassez)
|
||||
- WINDOWS: Don't access "Favorites" folder unexpectedly (glassez)
|
||||
- LINUX: Add support for systemd power management (Chocobo1)
|
||||
- LINUX: Add support for localized man pages (Victor Chernyakin)
|
||||
- LINUX: Specify a locale if none is set (Chocobo1)
|
||||
|
|
4
dist/mac/Info.plist
vendored
4
dist/mac/Info.plist
vendored
|
@ -55,7 +55,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>5.0.0</string>
|
||||
<string>5.0.4</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>${EXECUTABLE_NAME}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
|
@ -67,7 +67,7 @@
|
|||
<key>NSAppleScriptEnabled</key>
|
||||
<string>YES</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2006-2024 The qBittorrent project</string>
|
||||
<string>Copyright © 2006-2025 The qBittorrent project</string>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
|
|
146
dist/unix/org.qbittorrent.qBittorrent.desktop
vendored
146
dist/unix/org.qbittorrent.qBittorrent.desktop
vendored
|
@ -14,216 +14,220 @@ Keywords=bittorrent;torrent;magnet;download;p2p;
|
|||
SingleMainWindow=true
|
||||
|
||||
# Translations
|
||||
Comment[af]=Aflaai en deel lêers oor BitTorrent
|
||||
GenericName[af]=BitTorrent kliënt
|
||||
Comment[af]=Aflaai en deel lêers oor BitTorrent
|
||||
Name[af]=qBittorrent
|
||||
Comment[ar]=نزّل وشارك الملفات عبر كيوبتتورنت
|
||||
GenericName[ar]=عميل بتتورنت
|
||||
Comment[ar]=نزّل وشارك الملفات عبر كيوبتتورنت
|
||||
Name[ar]=qBittorrent
|
||||
Comment[be]=Спампоўванне і раздача файлаў праз пратакол BitTorrent
|
||||
GenericName[be]=Кліент BitTorrent
|
||||
Comment[be]=Спампоўванне і раздача файлаў праз пратакол BitTorrent
|
||||
Name[be]=qBittorrent
|
||||
Comment[bg]=Сваляне и споделяне на файлове чрез BitTorrent
|
||||
GenericName[bg]=BitTorrent клиент
|
||||
Comment[bg]=Сваляне и споделяне на файлове чрез BitTorrent
|
||||
Name[bg]=qBittorrent
|
||||
Comment[bn]=বিটটরেন্টে ফাইল ডাউনলোড এবং শেয়ার করুন
|
||||
GenericName[bn]=বিটটরেন্ট ক্লায়েন্ট
|
||||
Comment[bn]=বিটটরেন্টে ফাইল ডাউনলোড এবং শেয়ার করুন
|
||||
Name[bn]=qBittorrent
|
||||
Comment[zh]=通过 BitTorrent 下载和分享文件
|
||||
GenericName[zh]=BitTorrent 客户端
|
||||
Comment[zh]=通过 BitTorrent 下载和分享文件
|
||||
Name[zh]=qBittorrent
|
||||
Comment[bs]=Preuzmi i dijeli datoteke preko BitTorrent-a
|
||||
GenericName[bs]=BitTorrent klijent
|
||||
Comment[bs]=Preuzmi i dijeli datoteke preko BitTorrent-a
|
||||
Name[bs]=qBittorrent
|
||||
Comment[ca]=Baixeu i compartiu fitxers amb el BitTorrent
|
||||
GenericName[ca]=Client de BitTorrent
|
||||
Comment[ca]=Baixeu i compartiu fitxers amb el BitTorrent
|
||||
Name[ca]=qBittorrent
|
||||
Comment[cs]=Stahování a sdílení souborů přes síť BitTorrent
|
||||
GenericName[cs]=BitTorrent klient
|
||||
Comment[cs]=Stahování a sdílení souborů přes síť BitTorrent
|
||||
Name[cs]=qBittorrent
|
||||
Comment[da]=Download og del filer over BitTorrent
|
||||
GenericName[da]=BitTorrent-klient
|
||||
Comment[da]=Download og del filer over BitTorrent
|
||||
Name[da]=qBittorrent
|
||||
Comment[de]=Über BitTorrent Dateien herunterladen und teilen
|
||||
GenericName[de]=BitTorrent Client
|
||||
Comment[de]=Über BitTorrent Dateien herunterladen und teilen
|
||||
Name[de]=qBittorrent
|
||||
Comment[el]=Κάντε λήψη και μοιραστείτε αρχεία μέσω BitTorrent
|
||||
GenericName[el]=BitTorrent client
|
||||
Comment[el]=Κάντε λήψη και μοιραστείτε αρχεία μέσω BitTorrent
|
||||
Name[el]=qBittorrent
|
||||
Comment[en_GB]=Download and share files over BitTorrent
|
||||
GenericName[en_GB]=BitTorrent client
|
||||
Comment[en_GB]=Download and share files over BitTorrent
|
||||
Name[en_GB]=qBittorrent
|
||||
Comment[es]=Descargue y comparta archivos por BitTorrent
|
||||
GenericName[es]=Cliente BitTorrent
|
||||
Comment[es]=Descargue y comparta archivos por BitTorrent
|
||||
Name[es]=qBittorrent
|
||||
Comment[et]=Lae alla ja jaga faile üle BitTorrenti
|
||||
GenericName[et]=BitTorrent klient
|
||||
Comment[et]=Lae alla ja jaga faile üle BitTorrenti
|
||||
Name[et]=qBittorrent
|
||||
Comment[eu]=Jeitsi eta elkarbanatu agiriak BitTorrent bidez
|
||||
GenericName[eu]=BitTorrent bezeroa
|
||||
Comment[eu]=Jeitsi eta elkarbanatu agiriak BitTorrent bidez
|
||||
Name[eu]=qBittorrent
|
||||
Comment[fa]=دانلود و به اشتراک گذاری فایل های بوسیله بیت تورنت
|
||||
GenericName[fa]=بیت تورنت نسخه کلاینت
|
||||
Comment[fa]=دانلود و به اشتراک گذاری فایل های بوسیله بیت تورنت
|
||||
Name[fa]=qBittorrent
|
||||
Comment[fi]=Lataa ja jaa tiedostoja BitTorrentia käyttäen
|
||||
GenericName[fi]=BitTorrent-asiakasohjelma
|
||||
Comment[fi]=Lataa ja jaa tiedostoja BitTorrentia käyttäen
|
||||
Name[fi]=qBittorrent
|
||||
Comment[fr]=Télécharger et partager des fichiers sur BitTorrent
|
||||
GenericName[fr]=Client BitTorrent
|
||||
Comment[fr]=Télécharger et partager des fichiers sur BitTorrent
|
||||
Name[fr]=qBittorrent
|
||||
Comment[gl]=Descargar e compartir ficheiros co protocolo BitTorrent
|
||||
GenericName[gl]=Cliente BitTorrent
|
||||
Comment[gl]=Descargar e compartir ficheiros co protocolo BitTorrent
|
||||
Name[gl]=qBittorrent
|
||||
Comment[gu]=બિટ્ટોરેંટ પર ફાઈલો ડાઉનલોડ અને શેર કરો
|
||||
GenericName[gu]=બિટ્ટોરેંટ ક્લાયન્ટ
|
||||
Comment[gu]=બિટ્ટોરેંટ પર ફાઈલો ડાઉનલોડ અને શેર કરો
|
||||
Name[gu]=qBittorrent
|
||||
Comment[he]=הורד ושתף קבצים על גבי ביטורנט
|
||||
GenericName[he]=לקוח ביטורנט
|
||||
Comment[he]=הורד ושתף קבצים על גבי ביטורנט
|
||||
Name[he]=qBittorrent
|
||||
Comment[hr]=Preuzmite i dijelite datoteke putem BitTorrenta
|
||||
GenericName[hr]=BitTorrent klijent
|
||||
Comment[hr]=Preuzmite i dijelite datoteke putem BitTorrenta
|
||||
Name[hr]=qBittorrent
|
||||
Comment[hu]=Fájlok letöltése és megosztása a BitTorrent hálózaton keresztül
|
||||
GenericName[hu]=BitTorrent kliens
|
||||
Comment[hu]=Fájlok letöltése és megosztása a BitTorrent hálózaton keresztül
|
||||
Name[hu]=qBittorrent
|
||||
Comment[hy]=Նիշքերի փոխանցում BitTorrent-ի միջոցով
|
||||
GenericName[hy]=BitTorrent սպասառու
|
||||
Comment[hy]=Նիշքերի փոխանցում BitTorrent-ի միջոցով
|
||||
Name[hy]=qBittorrent
|
||||
Comment[id]=Unduh dan berbagi berkas melalui BitTorrent
|
||||
GenericName[id]=Klien BitTorrent
|
||||
Comment[id]=Unduh dan berbagi berkas melalui BitTorrent
|
||||
Name[id]=qBittorrent
|
||||
Comment[is]=Sækja og deila skrám yfir BitTorrent
|
||||
GenericName[is]=BitTorrent biðlarar
|
||||
Comment[is]=Sækja og deila skrám yfir BitTorrent
|
||||
Name[is]=qBittorrent
|
||||
Comment[it]=Scarica e condividi file tramite BitTorrent
|
||||
GenericName[it]=Client BitTorrent
|
||||
Comment[it]=Scarica e condividi file tramite BitTorrent
|
||||
Name[it]=qBittorrent
|
||||
Comment[ja]=BitTorrentでファイルのダウンロードと共有
|
||||
GenericName[ja]=BitTorrentクライアント
|
||||
Comment[ja]=BitTorrentでファイルのダウンロードと共有
|
||||
Name[ja]=qBittorrent
|
||||
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
|
||||
GenericName[ka]=BitTorrent კლიენტი
|
||||
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
|
||||
Name[ka]=qBittorrent
|
||||
Comment[ko]=BitTorrent를 통한 파일 내려받기 및 공유
|
||||
GenericName[ko]=BitTorrent 클라이언트
|
||||
Comment[ko]=BitTorrent를 통한 파일 다운로드 및 공유
|
||||
Name[ko]=qBittorrent
|
||||
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
|
||||
GenericName[lt]=BitTorrent klientas
|
||||
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
|
||||
Name[lt]=qBittorrent
|
||||
Comment[mk]=Превземајте и споделувајте фајлови преку BitTorrent
|
||||
GenericName[mk]=BitTorrent клиент
|
||||
Comment[mk]=Превземајте и споделувајте фајлови преку BitTorrent
|
||||
Name[mk]=qBittorrent
|
||||
Comment[my]=တောရန့်ဖြင့်ဖိုင်များဒေါင်းလုဒ်ဆွဲရန်နှင့်မျှဝေရန်
|
||||
GenericName[my]=တောရန့်စီမံခန့်ခွဲသည့်အရာ
|
||||
Comment[my]=တောရန့်ဖြင့်ဖိုင်များဒေါင်းလုဒ်ဆွဲရန်နှင့်မျှဝေရန်
|
||||
Name[my]=qBittorrent
|
||||
Comment[nb]=Last ned og del filer over BitTorrent
|
||||
GenericName[nb]=BitTorrent-klient
|
||||
Comment[nb]=Last ned og del filer over BitTorrent
|
||||
Name[nb]=qBittorrent
|
||||
Comment[nl]=Bestanden downloaden en delen via BitTorrent
|
||||
GenericName[nl]=BitTorrent-client
|
||||
Comment[nl]=Bestanden downloaden en delen via BitTorrent
|
||||
Name[nl]=qBittorrent
|
||||
Comment[pl]=Pobieraj i dziel się plikami przez BitTorrent
|
||||
GenericName[pl]=Klient BitTorrent
|
||||
Comment[pl]=Pobieraj i dziel się plikami przez BitTorrent
|
||||
Name[pl]=qBittorrent
|
||||
Comment[pt]=Transferir e partilhar ficheiros por BitTorrent
|
||||
GenericName[pt]=Cliente BitTorrent
|
||||
Comment[pt]=Transferir e partilhar ficheiros por BitTorrent
|
||||
Name[pt]=qBittorrent
|
||||
Comment[pt_BR]=Baixe e compartilhe arquivos pelo BitTorrent
|
||||
GenericName[pt_BR]=Cliente BitTorrent
|
||||
Comment[pt_BR]=Baixe e compartilhe arquivos pelo BitTorrent
|
||||
Name[pt_BR]=qBittorrent
|
||||
Comment[ro]=Descărcați și partajați fișiere prin BitTorrent
|
||||
GenericName[ro]=Client BitTorrent
|
||||
Comment[ro]=Descărcați și partajați fișiere prin BitTorrent
|
||||
Name[ro]=qBittorrent
|
||||
Comment[ru]=Обмен файлами по сети БитТоррент
|
||||
GenericName[ru]=Клиент сети БитТоррент
|
||||
Comment[ru]=Обмен файлами по сети БитТоррент
|
||||
Name[ru]=qBittorrent
|
||||
Comment[sk]=Sťahovanie a zdieľanie súborov prostredníctvom siete BitTorrent
|
||||
GenericName[sk]=Klient siete BitTorrent
|
||||
Comment[sk]=Sťahovanie a zdieľanie súborov prostredníctvom siete BitTorrent
|
||||
Name[sk]=qBittorrent
|
||||
Comment[sl]=Prenesite in delite datoteke preko BitTorrenta
|
||||
GenericName[sl]=BitTorrent odjemalec
|
||||
Comment[sl]=Prenesite in delite datoteke preko BitTorrenta
|
||||
Name[sl]=qBittorrent
|
||||
GenericName[sq]=Klienti BitTorrent
|
||||
Comment[sq]=Shkarko dhe shpërndaj skedarë në BitTorrent
|
||||
Name[sq]=qBittorrent
|
||||
Comment[sr]=Преузимајте и делите фајлове преко BitTorrent протокола
|
||||
GenericName[sr]=BitTorrent-клијент
|
||||
GenericName[sr]=BitTorrent клијент
|
||||
Comment[sr]=Преузимајте и делите фајлове преко BitTorrent-а
|
||||
Name[sr]=qBittorrent
|
||||
Comment[sr@latin]=Preuzimanje i deljenje fajlova preko BitTorrent-a
|
||||
GenericName[sr@latin]=BitTorrent klijent
|
||||
Comment[sr@latin]=Preuzimanje i deljenje fajlova preko BitTorrent-a
|
||||
Name[sr@latin]=qBittorrent
|
||||
Comment[sv]=Hämta och dela filer över BitTorrent
|
||||
GenericName[sv]=BitTorrent-klient
|
||||
Comment[sv]=Hämta och dela filer över BitTorrent
|
||||
Name[sv]=qBittorrent
|
||||
Comment[ta]=BitTorrent வழியாக கோப்புகளை பதிவிறக்க மற்றும் பகிர
|
||||
GenericName[ta]=BitTorrent வாடிக்கையாளர்
|
||||
Comment[ta]=BitTorrent வழியாக கோப்புகளை பதிவிறக்க மற்றும் பகிர
|
||||
Name[ta]=qBittorrent
|
||||
Comment[te]=క్యు బిట్ టొరెంట్ తో ఫైల్స్ దిగుమతి చేసుకోండి , పంచుకోండి
|
||||
GenericName[te]=క్యు బిట్ టొరెంట్ క్లయింట్
|
||||
Comment[te]=క్యు బిట్ టొరెంట్ తో ఫైల్స్ దిగుమతి చేసుకోండి , పంచుకోండి
|
||||
Name[te]=qBittorrent
|
||||
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่าน BitTorrent
|
||||
GenericName[th]=โปรแกรมบิททอเร้นท์
|
||||
GenericName[th]=ไคลเอนต์บิททอร์เรนต์
|
||||
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่านบิตทอร์เรนต์
|
||||
Name[th]=qBittorrent
|
||||
Comment[tr]=Dosyaları BitTorrent üzerinden indirin ve paylaşın
|
||||
GenericName[tr]=BitTorrent istemcisi
|
||||
Comment[tr]=Dosyaları BitTorrent üzerinden indirin ve paylaşın
|
||||
Name[tr]=qBittorrent
|
||||
Comment[ur]=BitTorrent پر فائلوں کو ڈاؤن لوڈ کریں اور اشتراک کریں
|
||||
GenericName[ur]=قیو بٹ ٹورنٹ کلائنٹ
|
||||
Comment[ur]=BitTorrent پر فائلوں کو ڈاؤن لوڈ کریں اور اشتراک کریں
|
||||
Name[ur]=qBittorrent
|
||||
Comment[uk]=Завантажуйте та поширюйте файли через BitTorrent
|
||||
GenericName[uk]=BitTorrent-клієнт
|
||||
Comment[uk]=Завантажуйте та поширюйте файли через BitTorrent
|
||||
Name[uk]=qBittorrent
|
||||
Comment[vi]=Tải xuống và chia sẻ tệp qua BitTorrent
|
||||
GenericName[vi]=Máy khách BitTorrent
|
||||
Comment[vi]=Tải xuống và chia sẻ tệp qua BitTorrent
|
||||
Name[vi]=qBittorrent
|
||||
Comment[zh_HK]=經由BitTorrent下載並分享檔案
|
||||
GenericName[zh_HK]=BitTorrent用戶端
|
||||
Comment[zh_HK]=經由BitTorrent下載並分享檔案
|
||||
Name[zh_HK]=qBittorrent
|
||||
Comment[zh_TW]=經由 BitTorrent 下載並分享檔案
|
||||
GenericName[zh_TW]=BitTorrent 用戶端
|
||||
Comment[zh_TW]=使用 BitTorrent 下載並分享檔案
|
||||
Name[zh_TW]=qBittorrent
|
||||
Comment[eo]=Elŝutu kaj kunhavigu dosierojn per BitTorrent
|
||||
GenericName[eo]=BitTorrent-kliento
|
||||
Comment[eo]=Elŝutu kaj kunhavigu dosierojn per BitTorrent
|
||||
Name[eo]=qBittorrent
|
||||
Comment[kk]=BitTorrent арқылы файл жүктеу және бөлісу
|
||||
GenericName[kk]=BitTorrent клиенті
|
||||
Comment[kk]=BitTorrent арқылы файл жүктеу және бөлісу
|
||||
Name[kk]=qBittorrent
|
||||
Comment[en_AU]=Download and share files over BitTorrent
|
||||
GenericName[en_AU]=BitTorrent client
|
||||
Comment[en_AU]=Download and share files over BitTorrent
|
||||
Name[en_AU]=qBittorrent
|
||||
Name[rm]=qBittorrent
|
||||
Name[jv]=qBittorrent
|
||||
Comment[oc]=Telecargar e partejar de fichièrs amb BitTorrent
|
||||
GenericName[oc]=Client BitTorrent
|
||||
Comment[oc]=Telecargar e partejar de fichièrs amb BitTorrent
|
||||
Name[oc]=qBittorrent
|
||||
Name[ug]=qBittorrent
|
||||
Name[yi]=qBittorrent
|
||||
Comment[nqo]=ߞߐߕߐ߯ߘߐ ߟߎ߬ ߟߊߖߌ߰ ߞߊ߬ ߓߊ߲߫ ߞߵߊ߬ߟߎ߬ ߘߐߕߟߊ߫ ߓߌߙߏߙߍ߲ߕ ߞߊ߲߬
|
||||
GenericName[nqo]=ߓߌߙߏߙߍ߲ߕ ߕߣߐ߬ߓߐ߬ߟߊ
|
||||
Comment[nqo]=ߞߐߕߐ߯ߘߐ ߟߎ߬ ߟߊߖߌ߰ ߞߊ߬ ߓߊ߲߫ ߞߵߊ߬ߟߎ߬ ߘߐߕߟߊ߫ ߓߌߙߏߙߍ߲ߕ ߞߊ߲߬
|
||||
Name[nqo]=qBittorrent
|
||||
Comment[uz@Latn]=BitTorrent orqali fayllarni yuklab olish va baham ko‘rish
|
||||
GenericName[uz@Latn]=BitTorrent mijozi
|
||||
Comment[uz@Latn]=BitTorrent orqali fayllarni yuklab olish va baham ko‘rish
|
||||
Name[uz@Latn]=qBittorrent
|
||||
Comment[ltg]=Atsasyuteit i daleit failus ar BitTorrent
|
||||
GenericName[ltg]=BitTorrent klients
|
||||
Comment[ltg]=Atsasyuteit i daleit failus ar BitTorrent
|
||||
Name[ltg]=qBittorrent
|
||||
Comment[hi_IN]=BitTorrent द्वारा फाइल डाउनलोड व सहभाजन
|
||||
GenericName[hi_IN]=Bittorrent साधन
|
||||
Comment[hi_IN]=BitTorrent द्वारा फाइल डाउनलोड व सहभाजन
|
||||
Name[hi_IN]=qBittorrent
|
||||
Comment[az@latin]=Faylları BitTorrent vasitəsilə endirin və paylaşın
|
||||
GenericName[az@latin]=BitTorrent client
|
||||
Comment[az@latin]=Faylları BitTorrent vasitəsilə endirin və paylaşın
|
||||
Name[az@latin]=qBittorrent
|
||||
Comment[lv_LV]=Lejupielādēt un koplietot failus ar BitTorrent
|
||||
GenericName[lv_LV]=BitTorrent klients
|
||||
Comment[lv_LV]=Lejupielādēt un koplietot failus ar BitTorrent
|
||||
Name[lv_LV]=qBittorrent
|
||||
Comment[ms_MY]=Muat turun dan kongsi fail melalui BitTorrent
|
||||
GenericName[ms_MY]=Klien BitTorrent
|
||||
Comment[ms_MY]=Muat turun dan kongsi fail melalui BitTorrent
|
||||
Name[ms_MY]=qBittorrent
|
||||
Comment[mn_MN]=BitTorrent-оор файлуудаа тат, түгээ
|
||||
GenericName[mn_MN]=BitTorrent татагч
|
||||
Comment[mn_MN]=BitTorrent-оор файлуудаа тат, түгээ
|
||||
Name[mn_MN]=qBittorrent
|
||||
Comment[ne_NP]=फाइलहरू डाउनलोड गर्नुहोस् र BitTorrent मा साझा गर्नुहोस्
|
||||
GenericName[ne_NP]=BitTorrent क्लाइन्ट
|
||||
Comment[ne_NP]=फाइलहरू डाउनलोड गर्नुहोस् र BitTorrent मा साझा गर्नुहोस्
|
||||
Name[ne_NP]=qBittorrent
|
||||
Comment[pt_PT]=Transferir e partilhar ficheiros por BitTorrent
|
||||
GenericName[pt_PT]=Cliente BitTorrent
|
||||
Comment[pt_PT]=Transferir e partilhar ficheiros por BitTorrent
|
||||
Name[pt_PT]=qBittorrent
|
||||
GenericName[si_LK]=BitTorrent සේවාදායකයා
|
||||
Comment[si_LK]=BitTorrent හරහා ගොනු බාගත කර බෙදාගන්න.
|
||||
Name[si_LK]=qBittorrent
|
||||
|
|
|
@ -62,6 +62,6 @@
|
|||
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
|
||||
<content_rating type="oars-1.1"/>
|
||||
<releases>
|
||||
<release version="5.0.0~beta1" date="2024-03-19"/>
|
||||
<release version="5.0.4" date="2025-02-18"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
|
4
dist/windows/config.nsh
vendored
4
dist/windows/config.nsh
vendored
|
@ -14,7 +14,7 @@
|
|||
; 4.5.1.3 -> good
|
||||
; 4.5.1.3.2 -> bad
|
||||
; 4.5.0beta -> bad
|
||||
!define /ifndef QBT_VERSION "5.0.0"
|
||||
!define /ifndef QBT_VERSION "5.0.4"
|
||||
|
||||
; Option that controls the installer's window name
|
||||
; If set, its value will be used like this:
|
||||
|
@ -86,7 +86,7 @@ OutFile "qbittorrent_${QBT_INSTALLER_FILENAME}_setup.exe"
|
|||
;Installer Version Information
|
||||
VIAddVersionKey "ProductName" "qBittorrent"
|
||||
VIAddVersionKey "CompanyName" "The qBittorrent project"
|
||||
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2024 The qBittorrent project"
|
||||
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2025 The qBittorrent project"
|
||||
VIAddVersionKey "FileDescription" "qBittorrent - A Bittorrent Client"
|
||||
VIAddVersionKey "FileVersion" "${QBT_VERSION}"
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ LangString launch_qbt ${LANG_ITALIAN} "Esegui qBittorrent."
|
|||
;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions."
|
||||
LangString inst_requires_64bit ${LANG_ITALIAN} "Questo installer funziona solo con versioni di Windows a 64bit."
|
||||
;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_ITALIAN} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_ITALIAN} "Questo installer richiede almeno Windows 10 (1809) / Windows Server 2019."
|
||||
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
|
||||
LangString inst_uninstall_link_description ${LANG_ITALIAN} "Disinstalla qBittorrent"
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ LangString inst_magnet ${LANG_LUXEMBOURGISH} "Magnet-Linken mat qBittorrent opma
|
|||
;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule"
|
||||
LangString inst_firewall ${LANG_LUXEMBOURGISH} "Reegel an der Windows Firewall dobäisetzen"
|
||||
;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_LUXEMBOURGISH} "D'Windows path lenght (Padlängtbeschränkung) desaktivéieren (260 Zeechen MAX_PATH Beschränkung, erfuerdert min. Windows 10 1607)"
|
||||
LangString inst_pathlimit ${LANG_LUXEMBOURGISH} "D'Windows path length (Padlängtbeschränkung) desaktivéieren (260 Zeechen MAX_PATH Beschränkung, erfuerdert min. Windows 10 1607)"
|
||||
;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule"
|
||||
LangString inst_firewallinfo ${LANG_LUXEMBOURGISH} "Reegel an der Windows Firewall dobäisetzen"
|
||||
;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing."
|
||||
|
|
|
@ -7,7 +7,7 @@ LangString inst_desktop ${LANG_PORTUGUESE} "Criar atalho no ambiente de trabalho
|
|||
;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut"
|
||||
LangString inst_startmenu ${LANG_PORTUGUESE} "Criar atalho no menu Iniciar"
|
||||
;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up"
|
||||
LangString inst_startup ${LANG_PORTUGUESE} "Iniciar o qBittorrent na inicialização do Windows"
|
||||
LangString inst_startup ${LANG_PORTUGUESE} "Iniciar o qBittorrent no arranque do Windows"
|
||||
;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent"
|
||||
LangString inst_torrent ${LANG_PORTUGUESE} "Abrir ficheiros .torrent com o qBittorrent"
|
||||
;LangString inst_magnet ${LANG_ENGLISH} "Open magnet links with qBittorrent"
|
||||
|
@ -29,7 +29,7 @@ LangString launch_qbt ${LANG_PORTUGUESE} "Iniciar qBittorrent."
|
|||
;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions."
|
||||
LangString inst_requires_64bit ${LANG_PORTUGUESE} "Este instalador funciona apenas em versões Windows de 64 bits."
|
||||
;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_PORTUGUESE} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_PORTUGUESE} "Este instalador requer, pelo menos, o Windows 10 (1809) / Windows Server 2019."
|
||||
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
|
||||
LangString inst_uninstall_link_description ${LANG_PORTUGUESE} "Desinstalar qBittorrent"
|
||||
|
||||
|
|
|
@ -23,13 +23,13 @@ LangString inst_warning ${LANG_SIMPCHINESE} "qBittorrent 正在运行。 安装
|
|||
;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact."
|
||||
LangString inst_uninstall_question ${LANG_SIMPCHINESE} "当前版本会被卸载。 用户设置和种子会被完整保留。"
|
||||
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
|
||||
LangString inst_unist ${LANG_SIMPCHINESE} "卸载以前的版本。"
|
||||
LangString inst_unist ${LANG_SIMPCHINESE} "正在卸载以前的版本。"
|
||||
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
|
||||
LangString launch_qbt ${LANG_SIMPCHINESE} "启动 qBittorrent。"
|
||||
;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions."
|
||||
LangString inst_requires_64bit ${LANG_SIMPCHINESE} "此安装程序仅支持 64 位 Windows 系统。"
|
||||
;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_SIMPCHINESE} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_SIMPCHINESE} "此安装程序仅支持 Windows 10 (1809) / Windows Server 2019 或更新的系统。"
|
||||
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
|
||||
LangString inst_uninstall_link_description ${LANG_SIMPCHINESE} "卸载 qBittorrent"
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ LangString launch_qbt ${LANG_TRADCHINESE} "啟動 qBittorrent"
|
|||
;LangString inst_requires_64bit ${LANG_ENGLISH} "This installer works only in 64-bit Windows versions."
|
||||
LangString inst_requires_64bit ${LANG_TRADCHINESE} "此安裝程式僅支援 64 位元版本的 Windows。"
|
||||
;LangString inst_requires_win10 ${LANG_ENGLISH} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_TRADCHINESE} "This installer requires at least Windows 10 (1809) / Windows Server 2019."
|
||||
LangString inst_requires_win10 ${LANG_TRADCHINESE} "此安裝程式僅支援 Windows 10 (1809) / Windows Server 2019 以上的系統。"
|
||||
;LangString inst_uninstall_link_description ${LANG_ENGLISH} "Uninstall qBittorrent"
|
||||
LangString inst_uninstall_link_description ${LANG_TRADCHINESE} "移除 qBittorrent"
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2014-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2014-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -58,10 +58,6 @@
|
|||
#include <QSplashScreen>
|
||||
#include <QTimer>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <QOperatingSystemVersion>
|
||||
#endif
|
||||
|
||||
#ifdef QBT_STATIC_QT
|
||||
#include <QtPlugin>
|
||||
Q_IMPORT_PLUGIN(QICOPlugin)
|
||||
|
@ -189,11 +185,6 @@ int main(int argc, char *argv[])
|
|||
// We must save it here because QApplication constructor may change it
|
||||
const bool isOneArg = (argc == 2);
|
||||
|
||||
#if !defined(DISABLE_GUI) && defined(Q_OS_WIN)
|
||||
if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::Windows10)
|
||||
QApplication::setStyle(u"Fusion"_s);
|
||||
#endif
|
||||
|
||||
// `app` must be declared out of try block to allow display message box in case of exception
|
||||
std::unique_ptr<Application> app;
|
||||
try
|
||||
|
|
|
@ -38,6 +38,8 @@ add_library(qbt_base STATIC
|
|||
bittorrent/torrent.h
|
||||
bittorrent/torrentcontenthandler.h
|
||||
bittorrent/torrentcontentlayout.h
|
||||
bittorrent/torrentcontentremoveoption.h
|
||||
bittorrent/torrentcontentremover.h
|
||||
bittorrent/torrentcreationmanager.h
|
||||
bittorrent/torrentcreationtask.h
|
||||
bittorrent/torrentcreator.h
|
||||
|
@ -145,6 +147,7 @@ add_library(qbt_base STATIC
|
|||
bittorrent/sslparameters.cpp
|
||||
bittorrent/torrent.cpp
|
||||
bittorrent/torrentcontenthandler.cpp
|
||||
bittorrent/torrentcontentremover.cpp
|
||||
bittorrent/torrentcreationmanager.cpp
|
||||
bittorrent/torrentcreationtask.cpp
|
||||
bittorrent/torrentcreator.cpp
|
||||
|
|
|
@ -157,10 +157,36 @@ void AddTorrentManager::handleAddTorrentFailed(const QString &source, const QStr
|
|||
emit addTorrentFailed(source, reason);
|
||||
}
|
||||
|
||||
void AddTorrentManager::handleDuplicateTorrent(const QString &source, BitTorrent::Torrent *torrent, const QString &message)
|
||||
void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
||||
, const BitTorrent::TorrentDescriptor &torrentDescr, BitTorrent::Torrent *existingTorrent)
|
||||
{
|
||||
const bool hasMetadata = torrentDescr.info().has_value();
|
||||
if (hasMetadata)
|
||||
{
|
||||
// Trying to set metadata to existing torrent in case if it has none
|
||||
existingTorrent->setMetadata(*torrentDescr.info());
|
||||
}
|
||||
|
||||
const bool isPrivate = existingTorrent->isPrivate() || (hasMetadata && torrentDescr.info()->isPrivate());
|
||||
QString message;
|
||||
if (!btSession()->isMergeTrackersEnabled())
|
||||
{
|
||||
message = tr("Merging of trackers is disabled");
|
||||
}
|
||||
else if (isPrivate)
|
||||
{
|
||||
message = tr("Trackers cannot be merged because it is a private torrent");
|
||||
}
|
||||
else
|
||||
{
|
||||
// merge trackers and web seeds
|
||||
existingTorrent->addTrackers(torrentDescr.trackers());
|
||||
existingTorrent->addUrlSeeds(torrentDescr.urlSeeds());
|
||||
message = tr("Trackers are merged from new source");
|
||||
}
|
||||
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: %2. Result: %3")
|
||||
.arg(source, torrent->name(), message));
|
||||
.arg(source, existingTorrent->name(), message));
|
||||
emit addTorrentFailed(source, message);
|
||||
}
|
||||
|
||||
|
@ -169,11 +195,9 @@ void AddTorrentManager::setTorrentFileGuard(const QString &source, std::shared_p
|
|||
m_guardedTorrentFiles.emplace(source, std::move(torrentFileGuard));
|
||||
}
|
||||
|
||||
void AddTorrentManager::releaseTorrentFileGuard(const QString &source)
|
||||
std::shared_ptr<TorrentFileGuard> AddTorrentManager::releaseTorrentFileGuard(const QString &source)
|
||||
{
|
||||
auto torrentFileGuard = m_guardedTorrentFiles.take(source);
|
||||
if (torrentFileGuard)
|
||||
torrentFileGuard->setAutoRemove(false);
|
||||
return m_guardedTorrentFiles.take(source);
|
||||
}
|
||||
|
||||
bool AddTorrentManager::processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||
|
@ -184,32 +208,7 @@ bool AddTorrentManager::processTorrent(const QString &source, const BitTorrent::
|
|||
if (BitTorrent::Torrent *torrent = btSession()->findTorrent(infoHash))
|
||||
{
|
||||
// a duplicate torrent is being added
|
||||
|
||||
const bool hasMetadata = torrentDescr.info().has_value();
|
||||
if (hasMetadata)
|
||||
{
|
||||
// Trying to set metadata to existing torrent in case if it has none
|
||||
torrent->setMetadata(*torrentDescr.info());
|
||||
}
|
||||
|
||||
if (!btSession()->isMergeTrackersEnabled())
|
||||
{
|
||||
handleDuplicateTorrent(source, torrent, tr("Merging of trackers is disabled"));
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isPrivate = torrent->isPrivate() || (hasMetadata && torrentDescr.info()->isPrivate());
|
||||
if (isPrivate)
|
||||
{
|
||||
handleDuplicateTorrent(source, torrent, tr("Trackers cannot be merged because it is a private torrent"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// merge trackers and web seeds
|
||||
torrent->addTrackers(torrentDescr.trackers());
|
||||
torrent->addUrlSeeds(torrentDescr.urlSeeds());
|
||||
|
||||
handleDuplicateTorrent(source, torrent, tr("Trackers are merged from new source"));
|
||||
handleDuplicateTorrent(source, torrentDescr, torrent);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -72,9 +72,9 @@ protected:
|
|||
bool addTorrentToSession(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||
, const BitTorrent::AddTorrentParams &addTorrentParams);
|
||||
void handleAddTorrentFailed(const QString &source, const QString &reason);
|
||||
void handleDuplicateTorrent(const QString &source, BitTorrent::Torrent *torrent, const QString &message);
|
||||
void handleDuplicateTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr, BitTorrent::Torrent *existingTorrent);
|
||||
void setTorrentFileGuard(const QString &source, std::shared_ptr<TorrentFileGuard> torrentFileGuard);
|
||||
void releaseTorrentFileGuard(const QString &source);
|
||||
std::shared_ptr<TorrentFileGuard> releaseTorrentFileGuard(const QString &source);
|
||||
|
||||
private:
|
||||
void onDownloadFinished(const Net::DownloadResult &result);
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
#include <QRegularExpression>
|
||||
#include <QThread>
|
||||
|
||||
#include "base/algorithm.h"
|
||||
#include "base/exceptions.h"
|
||||
#include "base/global.h"
|
||||
#include "base/logger.h"
|
||||
|
|
|
@ -240,11 +240,11 @@ void CustomDiskIOThread::handleCompleteFiles(lt::storage_index_t storage, const
|
|||
|
||||
lt::storage_interface *customStorageConstructor(const lt::storage_params ¶ms, lt::file_pool &pool)
|
||||
{
|
||||
return new CustomStorage {params, pool};
|
||||
return new CustomStorage(params, pool);
|
||||
}
|
||||
|
||||
CustomStorage::CustomStorage(const lt::storage_params ¶ms, lt::file_pool &filePool)
|
||||
: lt::default_storage {params, filePool}
|
||||
: lt::default_storage(params, filePool)
|
||||
, m_savePath {params.path}
|
||||
{
|
||||
}
|
||||
|
|
|
@ -67,7 +67,7 @@ namespace
|
|||
{
|
||||
const QString DB_CONNECTION_NAME = u"ResumeDataStorage"_s;
|
||||
|
||||
const int DB_VERSION = 7;
|
||||
const int DB_VERSION = 8;
|
||||
|
||||
const QString DB_TABLE_META = u"meta"_s;
|
||||
const QString DB_TABLE_TORRENTS = u"torrents"_s;
|
||||
|
@ -628,7 +628,31 @@ void BitTorrent::DBResumeDataStorage::updateDB(const int fromVersion) const
|
|||
}
|
||||
|
||||
if (fromVersion <= 6)
|
||||
addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SHARE_LIMIT_ACTION, "TEXTNOT NULL DEFAULT `Default`");
|
||||
addColumn(DB_TABLE_TORRENTS, DB_COLUMN_SHARE_LIMIT_ACTION, "TEXT NOT NULL DEFAULT `Default`");
|
||||
|
||||
if (fromVersion == 7)
|
||||
{
|
||||
const QString TEMP_COLUMN_NAME = DB_COLUMN_SHARE_LIMIT_ACTION.name + u"_temp";
|
||||
|
||||
auto queryStr = u"ALTER TABLE %1 ADD %2 %3"_s
|
||||
.arg(quoted(DB_TABLE_TORRENTS), TEMP_COLUMN_NAME, u"TEXT NOT NULL DEFAULT `Default`");
|
||||
if (!query.exec(queryStr))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
queryStr = u"UPDATE %1 SET %2 = %3"_s
|
||||
.arg(quoted(DB_TABLE_TORRENTS), quoted(TEMP_COLUMN_NAME), quoted(DB_COLUMN_SHARE_LIMIT_ACTION.name));
|
||||
if (!query.exec(queryStr))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
queryStr = u"ALTER TABLE %1 DROP %2"_s.arg(quoted(DB_TABLE_TORRENTS), quoted(DB_COLUMN_SHARE_LIMIT_ACTION.name));
|
||||
if (!query.exec(queryStr))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
|
||||
queryStr = u"ALTER TABLE %1 RENAME %2 TO %3"_s
|
||||
.arg(quoted(DB_TABLE_TORRENTS), quoted(TEMP_COLUMN_NAME), quoted(DB_COLUMN_SHARE_LIMIT_ACTION.name));
|
||||
if (!query.exec(queryStr))
|
||||
throw RuntimeError(query.lastError().text());
|
||||
}
|
||||
|
||||
const QString updateMetaVersionQuery = makeUpdateStatement(DB_TABLE_META, {DB_COLUMN_NAME, DB_COLUMN_VALUE});
|
||||
if (!query.prepare(updateMetaVersionQuery))
|
||||
|
|
|
@ -367,6 +367,10 @@ void PeerInfo::determineFlags()
|
|||
if (useUTPSocket())
|
||||
updateFlags(u'P', C_UTP);
|
||||
|
||||
// h = Peer is using NAT hole punching
|
||||
if (isHolepunched())
|
||||
updateFlags(u'h', tr("Peer is using NAT hole punching"));
|
||||
|
||||
m_flags.chop(1);
|
||||
m_flagsDescription.chop(1);
|
||||
}
|
||||
|
|
|
@ -37,17 +37,12 @@
|
|||
#include "addtorrentparams.h"
|
||||
#include "categoryoptions.h"
|
||||
#include "sharelimitaction.h"
|
||||
#include "torrentcontentremoveoption.h"
|
||||
#include "trackerentry.h"
|
||||
#include "trackerentrystatus.h"
|
||||
|
||||
class QString;
|
||||
|
||||
enum DeleteOption
|
||||
{
|
||||
DeleteTorrent,
|
||||
DeleteTorrentAndFiles
|
||||
};
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
class InfoHash;
|
||||
|
@ -58,6 +53,12 @@ namespace BitTorrent
|
|||
struct CacheStatus;
|
||||
struct SessionStatus;
|
||||
|
||||
enum class TorrentRemoveOption
|
||||
{
|
||||
KeepContent,
|
||||
RemoveContent
|
||||
};
|
||||
|
||||
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
|
||||
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
|
||||
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
|
||||
|
@ -91,7 +92,8 @@ namespace BitTorrent
|
|||
{
|
||||
Default = 0,
|
||||
MMap = 1,
|
||||
Posix = 2
|
||||
Posix = 2,
|
||||
SimplePreadPwrite = 3
|
||||
};
|
||||
Q_ENUM_NS(DiskIOType)
|
||||
|
||||
|
@ -425,7 +427,7 @@ namespace BitTorrent
|
|||
virtual void setExcludedFileNamesEnabled(bool enabled) = 0;
|
||||
virtual QStringList excludedFileNames() const = 0;
|
||||
virtual void setExcludedFileNames(const QStringList &newList) = 0;
|
||||
virtual bool isFilenameExcluded(const QString &fileName) const = 0;
|
||||
virtual void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) = 0;
|
||||
virtual QStringList bannedIPs() const = 0;
|
||||
virtual void setBannedIPs(const QStringList &newList) = 0;
|
||||
virtual ResumeDataStorageType resumeDataStorageType() const = 0;
|
||||
|
@ -434,6 +436,8 @@ namespace BitTorrent
|
|||
virtual void setMergeTrackersEnabled(bool enabled) = 0;
|
||||
virtual bool isStartPaused() const = 0;
|
||||
virtual void setStartPaused(bool value) = 0;
|
||||
virtual TorrentContentRemoveOption torrentContentRemoveOption() const = 0;
|
||||
virtual void setTorrentContentRemoveOption(TorrentContentRemoveOption option) = 0;
|
||||
|
||||
virtual bool isRestored() const = 0;
|
||||
|
||||
|
@ -453,7 +457,7 @@ namespace BitTorrent
|
|||
|
||||
virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0;
|
||||
virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) = 0;
|
||||
virtual bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteOption::DeleteTorrent) = 0;
|
||||
virtual bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) = 0;
|
||||
virtual bool downloadMetadata(const TorrentDescriptor &torrentDescr) = 0;
|
||||
virtual bool cancelDownloadMetadata(const TorrentID &id) = 0;
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@
|
|||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonValue>
|
||||
#include <QMutexLocker>
|
||||
#include <QNetworkAddressEntry>
|
||||
#include <QNetworkInterface>
|
||||
#include <QRegularExpression>
|
||||
|
@ -101,6 +102,7 @@
|
|||
#include "nativesessionextension.h"
|
||||
#include "portforwarderimpl.h"
|
||||
#include "resumedatastorage.h"
|
||||
#include "torrentcontentremover.h"
|
||||
#include "torrentdescriptor.h"
|
||||
#include "torrentimpl.h"
|
||||
#include "tracker.h"
|
||||
|
@ -525,6 +527,7 @@ SessionImpl::SessionImpl(QObject *parent)
|
|||
, m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3}
|
||||
, m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3}
|
||||
, m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3}
|
||||
, m_torrentContentRemoveOption {BITTORRENT_SESSION_KEY(u"TorrentContentRemoveOption"_s), TorrentContentRemoveOption::Delete}
|
||||
, m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)}
|
||||
, m_seedingLimitTimer {new QTimer(this)}
|
||||
, m_resumeDataTimer {new QTimer(this)}
|
||||
|
@ -550,7 +553,14 @@ SessionImpl::SessionImpl(QObject *parent)
|
|||
, this, [this]() { m_recentErroredTorrents.clear(); });
|
||||
|
||||
m_seedingLimitTimer->setInterval(10s);
|
||||
connect(m_seedingLimitTimer, &QTimer::timeout, this, &SessionImpl::processShareLimits);
|
||||
connect(m_seedingLimitTimer, &QTimer::timeout, this, [this]
|
||||
{
|
||||
// We shouldn't iterate over `m_torrents` in the loop below
|
||||
// since `deleteTorrent()` modifies it indirectly
|
||||
const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
|
||||
for (TorrentImpl *torrent : torrents)
|
||||
processTorrentShareLimits(torrent);
|
||||
});
|
||||
|
||||
initializeNativeSession();
|
||||
configureComponents();
|
||||
|
@ -586,6 +596,11 @@ SessionImpl::SessionImpl(QObject *parent)
|
|||
connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater);
|
||||
connect(m_fileSearcher, &FileSearcher::searchFinished, this, &SessionImpl::fileSearchFinished);
|
||||
|
||||
m_torrentContentRemover = new TorrentContentRemover;
|
||||
m_torrentContentRemover->moveToThread(m_ioThread.get());
|
||||
connect(m_ioThread.get(), &QThread::finished, m_torrentContentRemover, &QObject::deleteLater);
|
||||
connect(m_torrentContentRemover, &TorrentContentRemover::jobFinished, this, &SessionImpl::torrentContentRemovingFinished);
|
||||
|
||||
m_ioThread->start();
|
||||
|
||||
initMetrics();
|
||||
|
@ -604,7 +619,7 @@ SessionImpl::~SessionImpl()
|
|||
{
|
||||
m_nativeSession->pause();
|
||||
|
||||
const qint64 timeout = (m_shutdownTimeout >= 0) ? (m_shutdownTimeout * 1000) : -1;
|
||||
const auto timeout = (m_shutdownTimeout >= 0) ? (static_cast<qint64>(m_shutdownTimeout) * 1000) : -1;
|
||||
const QDeadlineTimer shutdownDeadlineTimer {timeout};
|
||||
|
||||
if (m_torrentsQueueChanged)
|
||||
|
@ -1592,7 +1607,7 @@ void SessionImpl::endStartup(ResumeSessionContext *context)
|
|||
reannounceToAllTrackers();
|
||||
}
|
||||
|
||||
m_wakeupCheckTimestamp = QDateTime::currentDateTime();
|
||||
m_wakeupCheckTimestamp = now;
|
||||
});
|
||||
m_wakeupCheckTimestamp = QDateTime::currentDateTime();
|
||||
m_wakeupCheckTimer->start(30s);
|
||||
|
@ -1624,6 +1639,13 @@ void SessionImpl::initializeNativeSession()
|
|||
#ifdef QBT_USES_LIBTORRENT2
|
||||
// preserve the same behavior as in earlier libtorrent versions
|
||||
pack.set_bool(lt::settings_pack::enable_set_file_valid_data, true);
|
||||
|
||||
// This is a special case. We use MMap disk IO but tweak it to always fallback to pread/pwrite.
|
||||
if (diskIOType() == DiskIOType::SimplePreadPwrite)
|
||||
{
|
||||
pack.set_int(lt::settings_pack::mmap_file_size_cutoff, std::numeric_limits<int>::max());
|
||||
pack.set_int(lt::settings_pack::disk_write_mode, lt::settings_pack::mmap_write_mode_t::always_pwrite);
|
||||
}
|
||||
#endif
|
||||
|
||||
lt::session_params sessionParams {std::move(pack), {}};
|
||||
|
@ -1634,6 +1656,7 @@ void SessionImpl::initializeNativeSession()
|
|||
sessionParams.disk_io_constructor = customPosixDiskIOConstructor;
|
||||
break;
|
||||
case DiskIOType::MMap:
|
||||
case DiskIOType::SimplePreadPwrite:
|
||||
sessionParams.disk_io_constructor = customMMapDiskIOConstructor;
|
||||
break;
|
||||
default:
|
||||
|
@ -2236,72 +2259,66 @@ void SessionImpl::populateAdditionalTrackers()
|
|||
m_additionalTrackerEntries = parseTrackerEntries(additionalTrackers());
|
||||
}
|
||||
|
||||
void SessionImpl::processShareLimits()
|
||||
void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
|
||||
{
|
||||
if (!torrent->isFinished() || torrent->isForced())
|
||||
return;
|
||||
|
||||
const auto effectiveLimit = []<typename T>(const T limit, const T useGlobalLimit, const T globalLimit) -> T
|
||||
{
|
||||
return (limit == useGlobalLimit) ? globalLimit : limit;
|
||||
};
|
||||
|
||||
// We shouldn't iterate over `m_torrents` in the loop below
|
||||
// since `deleteTorrent()` modifies it indirectly
|
||||
const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
|
||||
for (const auto &[torrentID, torrent] : torrents.asKeyValueRange())
|
||||
const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio());
|
||||
const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes());
|
||||
const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes());
|
||||
|
||||
bool reached = false;
|
||||
QString description;
|
||||
|
||||
if (const qreal ratio = torrent->realRatio();
|
||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
||||
{
|
||||
if (!torrent->isFinished() || torrent->isForced())
|
||||
continue;
|
||||
reached = true;
|
||||
description = tr("Torrent reached the share ratio limit.");
|
||||
}
|
||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the seeding time limit.");
|
||||
}
|
||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the inactive seeding time limit.");
|
||||
}
|
||||
|
||||
const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio());
|
||||
const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes());
|
||||
const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes());
|
||||
if (reached)
|
||||
{
|
||||
const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
|
||||
const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction();
|
||||
|
||||
bool reached = false;
|
||||
QString description;
|
||||
|
||||
if (const qreal ratio = torrent->realRatio();
|
||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
||||
if (shareLimitAction == ShareLimitAction::Remove)
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the share ratio limit.");
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
|
||||
removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent);
|
||||
}
|
||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the seeding time limit.");
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
|
||||
removeTorrent(torrent->id(), TorrentRemoveOption::RemoveContent);
|
||||
}
|
||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the inactive seeding time limit.");
|
||||
torrent->stop();
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName));
|
||||
}
|
||||
|
||||
if (reached)
|
||||
else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding())
|
||||
{
|
||||
const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
|
||||
const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction();
|
||||
|
||||
if (shareLimitAction == ShareLimitAction::Remove)
|
||||
{
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
|
||||
deleteTorrent(torrentID);
|
||||
}
|
||||
else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
|
||||
{
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
|
||||
deleteTorrent(torrentID, DeleteTorrentAndFiles);
|
||||
}
|
||||
else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
|
||||
{
|
||||
torrent->stop();
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName));
|
||||
}
|
||||
else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding())
|
||||
{
|
||||
torrent->setSuperSeeding(true);
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
|
||||
}
|
||||
torrent->setSuperSeeding(true);
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2331,6 +2348,19 @@ void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath,
|
|||
}
|
||||
}
|
||||
|
||||
void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage)
|
||||
{
|
||||
if (errorMessage.isEmpty())
|
||||
{
|
||||
LogMsg(tr("Torrent content removed. Torrent: \"%1\"").arg(torrentName));
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMsg(tr("Failed to remove torrent content. Torrent: \"%1\". Error: \"%2\"")
|
||||
.arg(torrentName, errorMessage), Log::WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
Torrent *SessionImpl::getTorrent(const TorrentID &id) const
|
||||
{
|
||||
return m_torrents.value(id);
|
||||
|
@ -2377,26 +2407,29 @@ void SessionImpl::banIP(const QString &ip)
|
|||
|
||||
// Delete a torrent from the session, given its hash
|
||||
// and from the disk, if the corresponding deleteOption is chosen
|
||||
bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption)
|
||||
bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption deleteOption)
|
||||
{
|
||||
TorrentImpl *const torrent = m_torrents.take(id);
|
||||
if (!torrent)
|
||||
return false;
|
||||
|
||||
qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrent->id().toString()));
|
||||
const TorrentID torrentID = torrent->id();
|
||||
const QString torrentName = torrent->name();
|
||||
|
||||
qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrentID.toString()));
|
||||
emit torrentAboutToBeRemoved(torrent);
|
||||
|
||||
if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid())
|
||||
m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1()));
|
||||
|
||||
// Remove it from session
|
||||
if (deleteOption == DeleteTorrent)
|
||||
if (deleteOption == TorrentRemoveOption::KeepContent)
|
||||
{
|
||||
m_removingTorrents[torrent->id()] = {torrent->name(), {}, deleteOption};
|
||||
m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), {}, deleteOption};
|
||||
|
||||
const lt::torrent_handle nativeHandle {torrent->nativeHandle()};
|
||||
const auto iter = std::find_if(m_moveStorageQueue.begin(), m_moveStorageQueue.end()
|
||||
, [&nativeHandle](const MoveStorageJob &job)
|
||||
, [&nativeHandle](const MoveStorageJob &job)
|
||||
{
|
||||
return job.torrentHandle == nativeHandle;
|
||||
});
|
||||
|
@ -2414,14 +2447,14 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp
|
|||
}
|
||||
else
|
||||
{
|
||||
m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption};
|
||||
m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), torrent->actualFilePaths(), deleteOption};
|
||||
|
||||
if (m_moveStorageQueue.size() > 1)
|
||||
{
|
||||
// Delete "move storage job" for the deleted torrent
|
||||
// (note: we shouldn't delete active job)
|
||||
const auto iter = std::find_if((m_moveStorageQueue.begin() + 1), m_moveStorageQueue.end()
|
||||
, [torrent](const MoveStorageJob &job)
|
||||
, [torrent](const MoveStorageJob &job)
|
||||
{
|
||||
return job.torrentHandle == torrent->nativeHandle();
|
||||
});
|
||||
|
@ -2429,12 +2462,13 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp
|
|||
m_moveStorageQueue.erase(iter);
|
||||
}
|
||||
|
||||
m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_files);
|
||||
m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_partfile);
|
||||
}
|
||||
|
||||
// Remove it from torrent resume directory
|
||||
m_resumeDataStorage->remove(torrent->id());
|
||||
m_resumeDataStorage->remove(torrentID);
|
||||
|
||||
LogMsg(tr("Torrent removed. Torrent: \"%1\"").arg(torrentName));
|
||||
delete torrent;
|
||||
return true;
|
||||
}
|
||||
|
@ -2462,7 +2496,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
|
|||
}
|
||||
#endif
|
||||
|
||||
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files);
|
||||
m_nativeSession->remove_torrent(nativeHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -2686,8 +2720,39 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
|||
if (m_loadingTorrents.contains(id) || (infoHash.isHybrid() && m_loadingTorrents.contains(altID)))
|
||||
return false;
|
||||
|
||||
if (findTorrent(infoHash))
|
||||
if (Torrent *torrent = findTorrent(infoHash))
|
||||
{
|
||||
// a duplicate torrent is being added
|
||||
|
||||
if (hasMetadata)
|
||||
{
|
||||
// Trying to set metadata to existing torrent in case if it has none
|
||||
torrent->setMetadata(*source.info());
|
||||
}
|
||||
|
||||
if (!isMergeTrackersEnabled())
|
||||
{
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
||||
.arg(torrent->name(), tr("Merging of trackers is disabled")));
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate());
|
||||
if (isPrivate)
|
||||
{
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
||||
.arg(torrent->name(), tr("Trackers cannot be merged because it is a private torrent")));
|
||||
return false;
|
||||
}
|
||||
|
||||
// merge trackers and web seeds
|
||||
torrent->addTrackers(source.trackers());
|
||||
torrent->addUrlSeeds(source.urlSeeds());
|
||||
|
||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
||||
.arg(torrent->name(), tr("Trackers are merged from new source")));
|
||||
return false;
|
||||
}
|
||||
|
||||
// It looks illogical that we don't just use an existing handle,
|
||||
// but as previous experience has shown, it actually creates unnecessary
|
||||
|
@ -2751,6 +2816,19 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
|||
loadTorrentParams.name = contentName;
|
||||
}
|
||||
|
||||
const auto nativeIndexes = torrentInfo.nativeIndexes();
|
||||
|
||||
Q_ASSERT(p.file_priorities.empty());
|
||||
Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size()));
|
||||
QList<DownloadPriority> filePriorities = addTorrentParams.filePriorities;
|
||||
|
||||
// Filename filter should be applied before `findIncompleteFiles()` is called.
|
||||
if (filePriorities.isEmpty() && isExcludedFileNamesEnabled())
|
||||
{
|
||||
// Check file name blacklist when priorities are not explicitly set
|
||||
applyFilenameFilter(filePaths, filePriorities);
|
||||
}
|
||||
|
||||
if (!loadTorrentParams.hasFinishedStatus)
|
||||
{
|
||||
const Path actualDownloadPath = useAutoTMM
|
||||
|
@ -2759,36 +2837,20 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
|||
isFindingIncompleteFiles = true;
|
||||
}
|
||||
|
||||
const auto nativeIndexes = torrentInfo.nativeIndexes();
|
||||
if (!isFindingIncompleteFiles)
|
||||
{
|
||||
for (int index = 0; index < filePaths.size(); ++index)
|
||||
p.renamed_files[nativeIndexes[index]] = filePaths.at(index).toString().toStdString();
|
||||
}
|
||||
|
||||
Q_ASSERT(p.file_priorities.empty());
|
||||
Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size()));
|
||||
|
||||
const int internalFilesCount = torrentInfo.nativeInfo()->files().num_files(); // including .pad files
|
||||
// Use qBittorrent default priority rather than libtorrent's (4)
|
||||
p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal));
|
||||
|
||||
if (addTorrentParams.filePriorities.isEmpty())
|
||||
if (!filePriorities.isEmpty())
|
||||
{
|
||||
if (isExcludedFileNamesEnabled())
|
||||
{
|
||||
// Check file name blacklist when priorities are not explicitly set
|
||||
for (int i = 0; i < filePaths.size(); ++i)
|
||||
{
|
||||
if (isFilenameExcluded(filePaths.at(i).filename()))
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = lt::dont_download;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < addTorrentParams.filePriorities.size(); ++i)
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(addTorrentParams.filePriorities[i]);
|
||||
for (int i = 0; i < filePriorities.size(); ++i)
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(filePriorities[i]);
|
||||
}
|
||||
|
||||
Q_ASSERT(p.ti);
|
||||
|
@ -3874,21 +3936,41 @@ void SessionImpl::populateExcludedFileNamesRegExpList()
|
|||
|
||||
for (const QString &str : excludedNames)
|
||||
{
|
||||
const QString pattern = QRegularExpression::anchoredPattern(QRegularExpression::wildcardToRegularExpression(str));
|
||||
const QString pattern = QRegularExpression::wildcardToRegularExpression(str);
|
||||
const QRegularExpression re {pattern, QRegularExpression::CaseInsensitiveOption};
|
||||
m_excludedFileNamesRegExpList.append(re);
|
||||
}
|
||||
}
|
||||
|
||||
bool SessionImpl::isFilenameExcluded(const QString &fileName) const
|
||||
void SessionImpl::applyFilenameFilter(const PathList &files, QList<DownloadPriority> &priorities)
|
||||
{
|
||||
if (!isExcludedFileNamesEnabled())
|
||||
return false;
|
||||
return;
|
||||
|
||||
return std::any_of(m_excludedFileNamesRegExpList.begin(), m_excludedFileNamesRegExpList.end(), [&fileName](const QRegularExpression &re)
|
||||
const auto isFilenameExcluded = [patterns = m_excludedFileNamesRegExpList](const Path &fileName)
|
||||
{
|
||||
return re.match(fileName).hasMatch();
|
||||
});
|
||||
return std::any_of(patterns.begin(), patterns.end(), [&fileName](const QRegularExpression &re)
|
||||
{
|
||||
Path path = fileName;
|
||||
while (!re.match(path.filename()).hasMatch())
|
||||
{
|
||||
path = path.parentPath();
|
||||
if (path.isEmpty())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
priorities.resize(files.count(), DownloadPriority::Normal);
|
||||
for (int i = 0; i < priorities.size(); ++i)
|
||||
{
|
||||
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
|
||||
continue;
|
||||
|
||||
if (isFilenameExcluded(files.at(i)))
|
||||
priorities[i] = BitTorrent::DownloadPriority::Ignored;
|
||||
}
|
||||
}
|
||||
|
||||
void SessionImpl::setBannedIPs(const QStringList &newList)
|
||||
|
@ -3957,6 +4039,16 @@ void SessionImpl::setStartPaused(const bool value)
|
|||
m_startPaused = value;
|
||||
}
|
||||
|
||||
TorrentContentRemoveOption SessionImpl::torrentContentRemoveOption() const
|
||||
{
|
||||
return m_torrentContentRemoveOption;
|
||||
}
|
||||
|
||||
void SessionImpl::setTorrentContentRemoveOption(const TorrentContentRemoveOption option)
|
||||
{
|
||||
m_torrentContentRemoveOption = option;
|
||||
}
|
||||
|
||||
QStringList SessionImpl::bannedIPs() const
|
||||
{
|
||||
return m_bannedIPs;
|
||||
|
@ -3974,14 +4066,29 @@ bool SessionImpl::isPaused() const
|
|||
|
||||
void SessionImpl::pause()
|
||||
{
|
||||
if (!m_isPaused)
|
||||
{
|
||||
if (isRestored())
|
||||
m_nativeSession->pause();
|
||||
if (m_isPaused)
|
||||
return;
|
||||
|
||||
m_isPaused = true;
|
||||
emit paused();
|
||||
if (isRestored())
|
||||
{
|
||||
m_nativeSession->pause();
|
||||
|
||||
for (TorrentImpl *torrent : asConst(m_torrents))
|
||||
{
|
||||
torrent->resetTrackerEntryStatuses();
|
||||
|
||||
const QList<TrackerEntryStatus> trackers = torrent->trackers();
|
||||
QHash<QString, TrackerEntryStatus> updatedTrackers;
|
||||
updatedTrackers.reserve(trackers.size());
|
||||
|
||||
for (const TrackerEntryStatus &status : trackers)
|
||||
updatedTrackers.emplace(status.url, status);
|
||||
emit trackerEntryStatusesUpdated(torrent, updatedTrackers);
|
||||
}
|
||||
}
|
||||
|
||||
m_isPaused = true;
|
||||
emit paused();
|
||||
}
|
||||
|
||||
void SessionImpl::resume()
|
||||
|
@ -4890,7 +4997,7 @@ void SessionImpl::updateSeedingLimitTimer()
|
|||
if ((globalMaxRatio() == Torrent::NO_RATIO_LIMIT) && !hasPerTorrentRatioLimit()
|
||||
&& (globalMaxSeedingMinutes() == Torrent::NO_SEEDING_TIME_LIMIT) && !hasPerTorrentSeedingTimeLimit()
|
||||
&& (globalMaxInactiveSeedingMinutes() == Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT) && !hasPerTorrentInactiveSeedingTimeLimit())
|
||||
{
|
||||
{
|
||||
if (m_seedingLimitTimer->isActive())
|
||||
m_seedingLimitTimer->stop();
|
||||
}
|
||||
|
@ -5002,18 +5109,7 @@ void SessionImpl::handleTorrentChecked(TorrentImpl *const torrent)
|
|||
|
||||
void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent)
|
||||
{
|
||||
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
|
||||
emit torrentFinished(torrent);
|
||||
|
||||
if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
|
||||
exportTorrentFile(torrent, exportPath);
|
||||
|
||||
const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent)
|
||||
{
|
||||
return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored());
|
||||
});
|
||||
if (!hasUnfinishedTorrents)
|
||||
emit allTorrentsFinished();
|
||||
m_pendingFinishedTorrents.append(torrent);
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentResumeDataReady(TorrentImpl *const torrent, const LoadTorrentParams &data)
|
||||
|
@ -5141,11 +5237,37 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath)
|
|||
// Last job is completed for torrent that being removing, so actually remove it
|
||||
const lt::torrent_handle nativeHandle {finishedJob.torrentHandle};
|
||||
const RemovingTorrentData &removingTorrentData = m_removingTorrents[nativeHandle.info_hash()];
|
||||
if (removingTorrentData.deleteOption == DeleteTorrent)
|
||||
if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent)
|
||||
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionImpl::processPendingFinishedTorrents()
|
||||
{
|
||||
if (m_pendingFinishedTorrents.isEmpty())
|
||||
return;
|
||||
|
||||
for (TorrentImpl *torrent : asConst(m_pendingFinishedTorrents))
|
||||
{
|
||||
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
|
||||
emit torrentFinished(torrent);
|
||||
|
||||
if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
|
||||
exportTorrentFile(torrent, exportPath);
|
||||
|
||||
processTorrentShareLimits(torrent);
|
||||
}
|
||||
|
||||
m_pendingFinishedTorrents.clear();
|
||||
|
||||
const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent)
|
||||
{
|
||||
return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored());
|
||||
});
|
||||
if (!hasUnfinishedTorrents)
|
||||
emit allTorrentsFinished();
|
||||
}
|
||||
|
||||
void SessionImpl::storeCategories() const
|
||||
{
|
||||
QJsonObject jsonObj;
|
||||
|
@ -5372,6 +5494,11 @@ void SessionImpl::setTorrentContentLayout(const TorrentContentLayout value)
|
|||
// Read alerts sent by libtorrent session
|
||||
void SessionImpl::readAlerts()
|
||||
{
|
||||
// cache current datetime of Qt and libtorrent clocks in order
|
||||
// to optimize conversion of time points from lt to Qt clocks
|
||||
m_ltNow = lt::clock_type::now();
|
||||
m_qNow = QDateTime::currentDateTime();
|
||||
|
||||
const std::vector<lt::alert *> alerts = getPendingAlerts();
|
||||
|
||||
Q_ASSERT(m_loadedTorrents.isEmpty());
|
||||
|
@ -5398,7 +5525,8 @@ void SessionImpl::readAlerts()
|
|||
}
|
||||
}
|
||||
|
||||
processTrackerStatuses();
|
||||
// Some torrents may become "finished" after different alerts handling.
|
||||
processPendingFinishedTorrents();
|
||||
}
|
||||
|
||||
void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
|
||||
|
@ -5660,74 +5788,32 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle,
|
|||
return torrent;
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert *alert)
|
||||
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/)
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
#else
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hash);
|
||||
#endif
|
||||
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(id);
|
||||
if (removingTorrentDataIter != m_removingTorrents.end())
|
||||
{
|
||||
if (removingTorrentDataIter->deleteOption == DeleteTorrent)
|
||||
{
|
||||
LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
}
|
||||
}
|
||||
// We cannot consider `torrent_removed_alert` as a starting point for removing content,
|
||||
// because it has an inconsistent posting time between different versions of libtorrent,
|
||||
// so files may still be in use in some cases.
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert)
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
#else
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hash);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
|
||||
#endif
|
||||
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(id);
|
||||
if (removingTorrentDataIter == m_removingTorrents.end())
|
||||
return;
|
||||
|
||||
// torrent_deleted_alert can also be posted due to deletion of partfile. Ignore it in such a case.
|
||||
if (removingTorrentDataIter->deleteOption == DeleteTorrent)
|
||||
return;
|
||||
|
||||
Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove);
|
||||
LogMsg(tr("Removed torrent and deleted its content. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
handleRemovedTorrent(torrentID);
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert)
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
#else
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hash);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
|
||||
#endif
|
||||
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(id);
|
||||
if (removingTorrentDataIter == m_removingTorrents.end())
|
||||
return;
|
||||
|
||||
if (alert->error)
|
||||
{
|
||||
// libtorrent won't delete the directory if it contains files not listed in the torrent,
|
||||
// so we remove the directory ourselves
|
||||
Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove);
|
||||
|
||||
LogMsg(tr("Removed torrent but failed to delete its content and/or partfile. Torrent: \"%1\". Error: \"%2\"")
|
||||
.arg(removingTorrentDataIter->name, QString::fromLocal8Bit(alert->error.message().c_str()))
|
||||
, Log::WARNING);
|
||||
}
|
||||
else // torrent without metadata, hence no files on disk
|
||||
{
|
||||
LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
|
||||
}
|
||||
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
const auto errorMessage = alert->error ? QString::fromLocal8Bit(alert->error.message().c_str()) : QString();
|
||||
handleRemovedTorrent(torrentID, errorMessage);
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert)
|
||||
|
@ -6116,7 +6202,12 @@ void SessionImpl::handleTrackerAlert(const lt::tracker_alert *alert)
|
|||
if (!torrent)
|
||||
return;
|
||||
|
||||
[[maybe_unused]] const QMutexLocker updatedTrackerStatusesLocker {&m_updatedTrackerStatusesMutex};
|
||||
|
||||
const auto prevSize = m_updatedTrackerStatuses.size();
|
||||
QMap<int, int> &updateInfo = m_updatedTrackerStatuses[torrent->nativeHandle()][std::string(alert->tracker_url())][alert->local_endpoint];
|
||||
if (prevSize < m_updatedTrackerStatuses.size())
|
||||
updateTrackerEntryStatuses(torrent->nativeHandle());
|
||||
|
||||
if (alert->type() == lt::tracker_reply_alert::alert_type)
|
||||
{
|
||||
|
@ -6140,7 +6231,7 @@ void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *a
|
|||
if (torrent2)
|
||||
{
|
||||
if (torrent1)
|
||||
deleteTorrent(torrentIDv1);
|
||||
removeTorrent(torrentIDv1);
|
||||
else
|
||||
cancelDownloadMetadata(torrentIDv1);
|
||||
|
||||
|
@ -6178,17 +6269,6 @@ void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *a
|
|||
}
|
||||
#endif
|
||||
|
||||
void SessionImpl::processTrackerStatuses()
|
||||
{
|
||||
if (m_updatedTrackerStatuses.isEmpty())
|
||||
return;
|
||||
|
||||
for (auto it = m_updatedTrackerStatuses.cbegin(); it != m_updatedTrackerStatuses.cend(); ++it)
|
||||
updateTrackerEntryStatuses(it.key(), it.value());
|
||||
|
||||
m_updatedTrackerStatuses.clear();
|
||||
}
|
||||
|
||||
void SessionImpl::saveStatistics() const
|
||||
{
|
||||
if (!m_isStatisticsDirty)
|
||||
|
@ -6213,15 +6293,19 @@ void SessionImpl::loadStatistics()
|
|||
m_previouslyUploaded = value[u"AlltimeUL"_s].toLongLong();
|
||||
}
|
||||
|
||||
void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers)
|
||||
void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle)
|
||||
{
|
||||
invokeAsync([this, torrentHandle = std::move(torrentHandle), updatedTrackers = std::move(updatedTrackers)]() mutable
|
||||
invokeAsync([this, torrentHandle = std::move(torrentHandle)]() mutable
|
||||
{
|
||||
try
|
||||
{
|
||||
std::vector<lt::announce_entry> nativeTrackers = torrentHandle.trackers();
|
||||
invoke([this, torrentHandle, nativeTrackers = std::move(nativeTrackers)
|
||||
, updatedTrackers = std::move(updatedTrackers)]
|
||||
|
||||
QMutexLocker updatedTrackerStatusesLocker {&m_updatedTrackerStatusesMutex};
|
||||
QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers = m_updatedTrackerStatuses.take(torrentHandle);
|
||||
updatedTrackerStatusesLocker.unlock();
|
||||
|
||||
invoke([this, torrentHandle, nativeTrackers = std::move(nativeTrackers), updatedTrackers = std::move(updatedTrackers)]
|
||||
{
|
||||
TorrentImpl *torrent = m_torrents.value(torrentHandle.info_hash());
|
||||
if (!torrent || torrent->isStopped())
|
||||
|
@ -6249,3 +6333,35 @@ void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, Q
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError)
|
||||
{
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(torrentID);
|
||||
if (removingTorrentDataIter == m_removingTorrents.end())
|
||||
return;
|
||||
|
||||
if (!partfileRemoveError.isEmpty())
|
||||
{
|
||||
LogMsg(tr("Failed to remove partfile. Torrent: \"%1\". Reason: \"%2\".")
|
||||
.arg(removingTorrentDataIter->name, partfileRemoveError)
|
||||
, Log::WARNING);
|
||||
}
|
||||
|
||||
if ((removingTorrentDataIter->removeOption == TorrentRemoveOption::RemoveContent)
|
||||
&& !removingTorrentDataIter->contentStoragePath.isEmpty())
|
||||
{
|
||||
QMetaObject::invokeMethod(m_torrentContentRemover, [this, jobData = *removingTorrentDataIter]
|
||||
{
|
||||
m_torrentContentRemover->performJob(jobData.name, jobData.contentStoragePath
|
||||
, jobData.fileNames, m_torrentContentRemoveOption);
|
||||
});
|
||||
}
|
||||
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
}
|
||||
|
||||
QDateTime SessionImpl::fromLTTimePoint32(const lt::time_point32 &timePoint) const
|
||||
{
|
||||
const auto secsSinceNow = lt::duration_cast<lt::seconds>(timePoint - m_ltNow + lt::milliseconds(500)).count();
|
||||
return m_qNow.addSecs(secsSinceNow);
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
#include <QElapsedTimer>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
#include <QMutex>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
#include <QVector>
|
||||
|
@ -75,6 +76,7 @@ namespace BitTorrent
|
|||
class InfoHash;
|
||||
class ResumeDataStorage;
|
||||
class Torrent;
|
||||
class TorrentContentRemover;
|
||||
class TorrentDescriptor;
|
||||
class TorrentImpl;
|
||||
class Tracker;
|
||||
|
@ -402,7 +404,7 @@ namespace BitTorrent
|
|||
void setExcludedFileNamesEnabled(bool enabled) override;
|
||||
QStringList excludedFileNames() const override;
|
||||
void setExcludedFileNames(const QStringList &excludedFileNames) override;
|
||||
bool isFilenameExcluded(const QString &fileName) const override;
|
||||
void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) override;
|
||||
QStringList bannedIPs() const override;
|
||||
void setBannedIPs(const QStringList &newList) override;
|
||||
ResumeDataStorageType resumeDataStorageType() const override;
|
||||
|
@ -411,6 +413,8 @@ namespace BitTorrent
|
|||
void setMergeTrackersEnabled(bool enabled) override;
|
||||
bool isStartPaused() const override;
|
||||
void setStartPaused(bool value) override;
|
||||
TorrentContentRemoveOption torrentContentRemoveOption() const override;
|
||||
void setTorrentContentRemoveOption(TorrentContentRemoveOption option) override;
|
||||
|
||||
bool isRestored() const override;
|
||||
|
||||
|
@ -430,7 +434,7 @@ namespace BitTorrent
|
|||
|
||||
bool isKnownTorrent(const InfoHash &infoHash) const override;
|
||||
bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶ms = {}) override;
|
||||
bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteTorrent) override;
|
||||
bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) override;
|
||||
bool downloadMetadata(const TorrentDescriptor &torrentDescr) override;
|
||||
bool cancelDownloadMetadata(const TorrentID &id) override;
|
||||
|
||||
|
@ -472,6 +476,8 @@ namespace BitTorrent
|
|||
void addMappedPorts(const QSet<quint16> &ports);
|
||||
void removeMappedPorts(const QSet<quint16> &ports);
|
||||
|
||||
QDateTime fromLTTimePoint32(const lt::time_point32 &timePoint) const;
|
||||
|
||||
template <typename Func>
|
||||
void invoke(Func &&func)
|
||||
{
|
||||
|
@ -487,11 +493,11 @@ namespace BitTorrent
|
|||
void configureDeferred();
|
||||
void readAlerts();
|
||||
void enqueueRefresh();
|
||||
void processShareLimits();
|
||||
void generateResumeData();
|
||||
void handleIPFilterParsed(int ruleCount);
|
||||
void handleIPFilterError();
|
||||
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
|
||||
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
|
||||
|
||||
private:
|
||||
struct ResumeSessionContext;
|
||||
|
@ -507,8 +513,9 @@ namespace BitTorrent
|
|||
struct RemovingTorrentData
|
||||
{
|
||||
QString name;
|
||||
Path pathToRemove;
|
||||
DeleteOption deleteOption {};
|
||||
Path contentStoragePath;
|
||||
PathList fileNames;
|
||||
TorrentRemoveOption removeOption {};
|
||||
};
|
||||
|
||||
explicit SessionImpl(QObject *parent = nullptr);
|
||||
|
@ -535,7 +542,7 @@ namespace BitTorrent
|
|||
void populateAdditionalTrackers();
|
||||
void enableIPFilter();
|
||||
void disableIPFilter();
|
||||
void processTrackerStatuses();
|
||||
void processTorrentShareLimits(TorrentImpl *torrent);
|
||||
void populateExcludedFileNamesRegExpList();
|
||||
void prepareStartup();
|
||||
void handleLoadedResumeData(ResumeSessionContext *context);
|
||||
|
@ -588,6 +595,7 @@ namespace BitTorrent
|
|||
|
||||
void moveTorrentStorage(const MoveStorageJob &job) const;
|
||||
void handleMoveTorrentStorageJobFinished(const Path &newPath);
|
||||
void processPendingFinishedTorrents();
|
||||
|
||||
void loadCategories();
|
||||
void storeCategories() const;
|
||||
|
@ -597,15 +605,9 @@ namespace BitTorrent
|
|||
void saveStatistics() const;
|
||||
void loadStatistics();
|
||||
|
||||
void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers);
|
||||
void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle);
|
||||
|
||||
// BitTorrent
|
||||
lt::session *m_nativeSession = nullptr;
|
||||
NativeSessionExtension *m_nativeSessionExtension = nullptr;
|
||||
|
||||
bool m_deferredConfigureScheduled = false;
|
||||
bool m_IPFilteringConfigured = false;
|
||||
mutable bool m_listenInterfaceConfigured = false;
|
||||
void handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError = {});
|
||||
|
||||
CachedSettingValue<QString> m_DHTBootstrapNodes;
|
||||
CachedSettingValue<bool> m_isDHTEnabled;
|
||||
|
@ -731,8 +733,16 @@ namespace BitTorrent
|
|||
CachedSettingValue<int> m_I2POutboundQuantity;
|
||||
CachedSettingValue<int> m_I2PInboundLength;
|
||||
CachedSettingValue<int> m_I2POutboundLength;
|
||||
CachedSettingValue<TorrentContentRemoveOption> m_torrentContentRemoveOption;
|
||||
SettingValue<bool> m_startPaused;
|
||||
|
||||
lt::session *m_nativeSession = nullptr;
|
||||
NativeSessionExtension *m_nativeSessionExtension = nullptr;
|
||||
|
||||
bool m_deferredConfigureScheduled = false;
|
||||
bool m_IPFilteringConfigured = false;
|
||||
mutable bool m_listenInterfaceConfigured = false;
|
||||
|
||||
bool m_isRestored = false;
|
||||
bool m_isPaused = isStartPaused();
|
||||
|
||||
|
@ -766,6 +776,7 @@ namespace BitTorrent
|
|||
QThreadPool *m_asyncWorker = nullptr;
|
||||
ResumeDataStorage *m_resumeDataStorage = nullptr;
|
||||
FileSearcher *m_fileSearcher = nullptr;
|
||||
TorrentContentRemover *m_torrentContentRemover = nullptr;
|
||||
|
||||
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
|
||||
|
||||
|
@ -783,6 +794,7 @@ namespace BitTorrent
|
|||
// This field holds amounts of peers reported by trackers in their responses to announces
|
||||
// (torrent.tracker_name.tracker_local_endpoint.protocol_version.num_peers)
|
||||
QHash<lt::torrent_handle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>>> m_updatedTrackerStatuses;
|
||||
QMutex m_updatedTrackerStatusesMutex;
|
||||
|
||||
// I/O errored torrents
|
||||
QSet<TorrentID> m_recentErroredTorrents;
|
||||
|
@ -809,6 +821,11 @@ namespace BitTorrent
|
|||
QTimer *m_wakeupCheckTimer = nullptr;
|
||||
QDateTime m_wakeupCheckTimestamp;
|
||||
|
||||
QList<TorrentImpl *> m_pendingFinishedTorrents;
|
||||
|
||||
QDateTime m_qNow;
|
||||
lt::clock_type::time_point m_ltNow;
|
||||
|
||||
friend void Session::initInstance();
|
||||
friend void Session::freeInstance();
|
||||
friend Session *Session::instance();
|
||||
|
|
|
@ -215,7 +215,15 @@ namespace BitTorrent
|
|||
virtual int piecesCount() const = 0;
|
||||
virtual int piecesHave() const = 0;
|
||||
virtual qreal progress() const = 0;
|
||||
|
||||
virtual QDateTime addedTime() const = 0;
|
||||
virtual QDateTime completedTime() const = 0;
|
||||
virtual QDateTime lastSeenComplete() const = 0;
|
||||
virtual qlonglong activeTime() const = 0;
|
||||
virtual qlonglong finishedTime() const = 0;
|
||||
virtual qlonglong timeSinceUpload() const = 0;
|
||||
virtual qlonglong timeSinceDownload() const = 0;
|
||||
virtual qlonglong timeSinceActivity() const = 0;
|
||||
|
||||
// Share limits
|
||||
virtual qreal ratioLimit() const = 0;
|
||||
|
@ -228,6 +236,7 @@ namespace BitTorrent
|
|||
virtual void setShareLimitAction(ShareLimitAction action) = 0;
|
||||
|
||||
virtual PathList filePaths() const = 0;
|
||||
virtual PathList actualFilePaths() const = 0;
|
||||
|
||||
virtual TorrentInfo info() const = 0;
|
||||
virtual bool isFinished() const = 0;
|
||||
|
@ -253,8 +262,6 @@ namespace BitTorrent
|
|||
virtual QString error() const = 0;
|
||||
virtual qlonglong totalDownload() const = 0;
|
||||
virtual qlonglong totalUpload() const = 0;
|
||||
virtual qlonglong activeTime() const = 0;
|
||||
virtual qlonglong finishedTime() const = 0;
|
||||
virtual qlonglong eta() const = 0;
|
||||
virtual int seedsCount() const = 0;
|
||||
virtual int peersCount() const = 0;
|
||||
|
@ -262,11 +269,6 @@ namespace BitTorrent
|
|||
virtual int totalSeedsCount() const = 0;
|
||||
virtual int totalPeersCount() const = 0;
|
||||
virtual int totalLeechersCount() const = 0;
|
||||
virtual QDateTime lastSeenComplete() const = 0;
|
||||
virtual QDateTime completedTime() const = 0;
|
||||
virtual qlonglong timeSinceUpload() const = 0;
|
||||
virtual qlonglong timeSinceDownload() const = 0;
|
||||
virtual qlonglong timeSinceActivity() const = 0;
|
||||
virtual int downloadLimit() const = 0;
|
||||
virtual int uploadLimit() const = 0;
|
||||
virtual bool superSeeding() const = 0;
|
||||
|
|
50
src/base/bittorrent/torrentcontentremoveoption.h
Normal file
50
src/base/bittorrent/torrentcontentremoveoption.h
Normal file
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QMetaEnum>
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
|
||||
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
|
||||
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
|
||||
inline namespace TorrentContentRemoveOptionNS
|
||||
{
|
||||
Q_NAMESPACE
|
||||
|
||||
enum class TorrentContentRemoveOption
|
||||
{
|
||||
Delete,
|
||||
MoveToTrash
|
||||
};
|
||||
|
||||
Q_ENUM_NS(TorrentContentRemoveOption)
|
||||
}
|
||||
}
|
61
src/base/bittorrent/torrentcontentremover.cpp
Normal file
61
src/base/bittorrent/torrentcontentremover.cpp
Normal file
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#include "torrentcontentremover.h"
|
||||
|
||||
#include "base/utils/fs.h"
|
||||
|
||||
void BitTorrent::TorrentContentRemover::performJob(const QString &torrentName, const Path &basePath
|
||||
, const PathList &fileNames, const TorrentContentRemoveOption option)
|
||||
{
|
||||
QString errorMessage;
|
||||
|
||||
if (!fileNames.isEmpty())
|
||||
{
|
||||
const auto removeFileFn = [&option](const Path &filePath)
|
||||
{
|
||||
return ((option == TorrentContentRemoveOption::MoveToTrash)
|
||||
? Utils::Fs::moveFileToTrash : Utils::Fs::removeFile)(filePath);
|
||||
};
|
||||
|
||||
for (const Path &fileName : fileNames)
|
||||
{
|
||||
if (const auto result = removeFileFn(basePath / fileName)
|
||||
; !result && errorMessage.isEmpty())
|
||||
{
|
||||
errorMessage = result.error();
|
||||
}
|
||||
}
|
||||
|
||||
const Path rootPath = Path::findRootFolder(fileNames);
|
||||
if (!rootPath.isEmpty())
|
||||
Utils::Fs::smartRemoveEmptyFolderTree(basePath / rootPath);
|
||||
}
|
||||
|
||||
emit jobFinished(torrentName, errorMessage);
|
||||
}
|
53
src/base/bittorrent/torrentcontentremover.h
Normal file
53
src/base/bittorrent/torrentcontentremover.h
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include "base/path.h"
|
||||
#include "torrentcontentremoveoption.h"
|
||||
|
||||
namespace BitTorrent
|
||||
{
|
||||
class TorrentContentRemover final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(TorrentContentRemover)
|
||||
|
||||
public:
|
||||
using QObject::QObject;
|
||||
|
||||
public slots:
|
||||
void performJob(const QString &torrentName, const Path &basePath
|
||||
, const PathList &fileNames, TorrentContentRemoveOption option);
|
||||
|
||||
signals:
|
||||
void jobFinished(const QString &torrentName, const QString &errorMessage);
|
||||
};
|
||||
}
|
|
@ -36,6 +36,7 @@
|
|||
#include <libtorrent/file_storage.hpp>
|
||||
#include <libtorrent/torrent_info.hpp>
|
||||
|
||||
#include <QtSystemDetection>
|
||||
#include <QDirIterator>
|
||||
#include <QFileInfo>
|
||||
#include <QHash>
|
||||
|
@ -123,7 +124,14 @@ void TorrentCreator::run()
|
|||
// need to sort the file names by natural sort order
|
||||
QStringList dirs = {m_params.sourcePath.data()};
|
||||
|
||||
QDirIterator dirIter {m_params.sourcePath.data(), (QDir::AllDirs | QDir::NoDotAndDotDot), QDirIterator::Subdirectories};
|
||||
#ifdef Q_OS_WIN
|
||||
// libtorrent couldn't handle .lnk files on Windows
|
||||
// Also, Windows users do not expect torrent creator to traverse into .lnk files so skip over them
|
||||
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot | QDir::NoSymLinks};
|
||||
#else
|
||||
const QDir::Filters dirFilters {QDir::AllDirs | QDir::NoDotAndDotDot};
|
||||
#endif
|
||||
QDirIterator dirIter {m_params.sourcePath.data(), dirFilters, QDirIterator::Subdirectories};
|
||||
while (dirIter.hasNext())
|
||||
{
|
||||
const QString filePath = dirIter.next();
|
||||
|
@ -138,7 +146,12 @@ void TorrentCreator::run()
|
|||
{
|
||||
QStringList tmpNames; // natural sort files within each dir
|
||||
|
||||
QDirIterator fileIter {dir, QDir::Files};
|
||||
#ifdef Q_OS_WIN
|
||||
const QDir::Filters fileFilters {QDir::Files | QDir::NoSymLinks};
|
||||
#else
|
||||
const QDir::Filters fileFilters {QDir::Files};
|
||||
#endif
|
||||
QDirIterator fileIter {dir, fileFilters};
|
||||
while (fileIter.hasNext())
|
||||
{
|
||||
const QFileInfo fileInfo = fileIter.nextFileInfo();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
|
@ -35,9 +35,7 @@
|
|||
#include <libtorrent/write_resume_data.hpp>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QDateTime>
|
||||
#include <QRegularExpression>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
|
||||
#include "base/global.h"
|
||||
|
@ -147,7 +145,13 @@ BitTorrent::TorrentDescriptor::TorrentDescriptor(lt::add_torrent_params ltAddTor
|
|||
: m_ltAddTorrentParams {std::move(ltAddTorrentParams)}
|
||||
{
|
||||
if (m_ltAddTorrentParams.ti && m_ltAddTorrentParams.ti->is_valid())
|
||||
{
|
||||
m_info.emplace(*m_ltAddTorrentParams.ti);
|
||||
if (m_ltAddTorrentParams.ti->creation_date() > 0)
|
||||
m_creationDate = QDateTime::fromSecsSinceEpoch(m_ltAddTorrentParams.ti->creation_date());
|
||||
m_creator = QString::fromStdString(m_ltAddTorrentParams.ti->creator());
|
||||
m_comment = QString::fromStdString(m_ltAddTorrentParams.ti->comment());
|
||||
}
|
||||
}
|
||||
|
||||
BitTorrent::InfoHash BitTorrent::TorrentDescriptor::infoHash() const
|
||||
|
@ -166,18 +170,17 @@ QString BitTorrent::TorrentDescriptor::name() const
|
|||
|
||||
QDateTime BitTorrent::TorrentDescriptor::creationDate() const
|
||||
{
|
||||
return ((m_ltAddTorrentParams.ti->creation_date() != 0)
|
||||
? QDateTime::fromSecsSinceEpoch(m_ltAddTorrentParams.ti->creation_date()) : QDateTime());
|
||||
return m_creationDate;
|
||||
}
|
||||
|
||||
QString BitTorrent::TorrentDescriptor::creator() const
|
||||
{
|
||||
return QString::fromStdString(m_ltAddTorrentParams.ti->creator());
|
||||
return m_creator;
|
||||
}
|
||||
|
||||
QString BitTorrent::TorrentDescriptor::comment() const
|
||||
{
|
||||
return QString::fromStdString(m_ltAddTorrentParams.ti->comment());
|
||||
return m_comment;
|
||||
}
|
||||
|
||||
const std::optional<BitTorrent::TorrentInfo> &BitTorrent::TorrentDescriptor::info() const
|
||||
|
|
|
@ -33,7 +33,9 @@
|
|||
#include <libtorrent/add_torrent_params.hpp>
|
||||
|
||||
#include <QtContainerFwd>
|
||||
#include <QDateTime>
|
||||
#include <QMetaType>
|
||||
#include <QString>
|
||||
|
||||
#include "base/3rdparty/expected.hpp"
|
||||
#include "base/path.h"
|
||||
|
@ -41,8 +43,6 @@
|
|||
#include "torrentinfo.h"
|
||||
|
||||
class QByteArray;
|
||||
class QDateTime;
|
||||
class QString;
|
||||
class QUrl;
|
||||
|
||||
namespace BitTorrent
|
||||
|
@ -78,6 +78,9 @@ namespace BitTorrent
|
|||
|
||||
lt::add_torrent_params m_ltAddTorrentParams;
|
||||
std::optional<TorrentInfo> m_info;
|
||||
QDateTime m_creationDate;
|
||||
QString m_creator;
|
||||
QString m_comment;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -49,6 +49,7 @@
|
|||
|
||||
#include <QtSystemDetection>
|
||||
#include <QByteArray>
|
||||
#include <QCache>
|
||||
#include <QDebug>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
|
@ -77,6 +78,10 @@
|
|||
#include "base/utils/os.h"
|
||||
#endif // Q_OS_MACOS || Q_OS_WIN
|
||||
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
#include "customstorage.h"
|
||||
#endif
|
||||
|
||||
using namespace BitTorrent;
|
||||
|
||||
namespace
|
||||
|
@ -88,37 +93,28 @@ namespace
|
|||
return entry;
|
||||
}
|
||||
|
||||
QDateTime fromLTTimePoint32(const lt::time_point32 &timePoint)
|
||||
{
|
||||
const auto ltNow = lt::clock_type::now();
|
||||
const auto qNow = QDateTime::currentDateTime();
|
||||
const auto secsSinceNow = lt::duration_cast<lt::seconds>(timePoint - ltNow + lt::milliseconds(500)).count();
|
||||
|
||||
return qNow.addSecs(secsSinceNow);
|
||||
}
|
||||
|
||||
QString toString(const lt::tcp::endpoint <TCPEndpoint)
|
||||
{
|
||||
return QString::fromStdString((std::stringstream() << ltTCPEndpoint).str());
|
||||
static QCache<lt::tcp::endpoint, QString> cache;
|
||||
|
||||
if (const QString *endpointName = cache.object(ltTCPEndpoint))
|
||||
return *endpointName;
|
||||
|
||||
const std::string tmp = (std::ostringstream() << ltTCPEndpoint).str();
|
||||
const auto endpointName = QString::fromLatin1(tmp.c_str(), tmp.size());
|
||||
cache.insert(ltTCPEndpoint, new QString(endpointName));
|
||||
return endpointName;
|
||||
}
|
||||
|
||||
template <typename FromLTTimePoint32Func>
|
||||
void updateTrackerEntryStatus(TrackerEntryStatus &trackerEntryStatus, const lt::announce_entry &nativeEntry
|
||||
, const QSet<int> &btProtocols, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
|
||||
, const QSet<int> &btProtocols, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo
|
||||
, const FromLTTimePoint32Func &fromLTTimePoint32)
|
||||
{
|
||||
Q_ASSERT(trackerEntryStatus.url == QString::fromStdString(nativeEntry.url));
|
||||
|
||||
trackerEntryStatus.tier = nativeEntry.tier;
|
||||
|
||||
// remove outdated endpoints
|
||||
trackerEntryStatus.endpoints.removeIf([&nativeEntry](const QHash<std::pair<QString, int>, TrackerEndpointStatus>::iterator &iter)
|
||||
{
|
||||
return std::none_of(nativeEntry.endpoints.cbegin(), nativeEntry.endpoints.cend()
|
||||
, [&endpointName = std::get<0>(iter.key())](const auto &existingEndpoint)
|
||||
{
|
||||
return (endpointName == toString(existingEndpoint.local_endpoint));
|
||||
});
|
||||
});
|
||||
|
||||
const auto numEndpoints = static_cast<qsizetype>(nativeEntry.endpoints.size()) * btProtocols.size();
|
||||
|
||||
int numUpdating = 0;
|
||||
|
@ -201,6 +197,19 @@ namespace
|
|||
}
|
||||
}
|
||||
|
||||
if (trackerEntryStatus.endpoints.size() > numEndpoints)
|
||||
{
|
||||
// remove outdated endpoints
|
||||
trackerEntryStatus.endpoints.removeIf([&nativeEntry](const QHash<std::pair<QString, int>, TrackerEndpointStatus>::iterator &iter)
|
||||
{
|
||||
return std::none_of(nativeEntry.endpoints.cbegin(), nativeEntry.endpoints.cend()
|
||||
, [&endpointName = std::get<0>(iter.key())](const auto &existingEndpoint)
|
||||
{
|
||||
return (endpointName == toString(existingEndpoint.local_endpoint));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (numEndpoints > 0)
|
||||
{
|
||||
if (numUpdating > 0)
|
||||
|
@ -313,6 +322,11 @@ TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession
|
|||
{
|
||||
if (m_ltAddTorrentParams.ti)
|
||||
{
|
||||
if (const std::time_t creationDate = m_ltAddTorrentParams.ti->creation_date(); creationDate > 0)
|
||||
m_creationDate = QDateTime::fromSecsSinceEpoch(creationDate);
|
||||
m_creator = QString::fromStdString(m_ltAddTorrentParams.ti->creator());
|
||||
m_comment = QString::fromStdString(m_ltAddTorrentParams.ti->comment());
|
||||
|
||||
// Initialize it only if torrent is added with metadata.
|
||||
// Otherwise it should be initialized in "Metadata received" handler.
|
||||
m_torrentInfo = TorrentInfo(*m_ltAddTorrentParams.ti);
|
||||
|
@ -356,6 +370,12 @@ TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession
|
|||
m_urlSeeds.append(QString::fromStdString(urlSeed));
|
||||
m_nativeStatus = extensionData->status;
|
||||
|
||||
m_addedTime = QDateTime::fromSecsSinceEpoch(m_nativeStatus.added_time);
|
||||
if (m_nativeStatus.completed_time > 0)
|
||||
m_completedTime = QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time);
|
||||
if (m_nativeStatus.last_seen_complete > 0)
|
||||
m_lastSeenComplete = QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete);
|
||||
|
||||
if (hasMetadata())
|
||||
updateProgress();
|
||||
|
||||
|
@ -399,17 +419,17 @@ QString TorrentImpl::name() const
|
|||
|
||||
QDateTime TorrentImpl::creationDate() const
|
||||
{
|
||||
return m_torrentInfo.creationDate();
|
||||
return m_creationDate;
|
||||
}
|
||||
|
||||
QString TorrentImpl::creator() const
|
||||
{
|
||||
return m_torrentInfo.creator();
|
||||
return m_creator;
|
||||
}
|
||||
|
||||
QString TorrentImpl::comment() const
|
||||
{
|
||||
return m_torrentInfo.comment();
|
||||
return m_comment;
|
||||
}
|
||||
|
||||
bool TorrentImpl::isPrivate() const
|
||||
|
@ -456,6 +476,8 @@ Path TorrentImpl::savePath() const
|
|||
void TorrentImpl::setSavePath(const Path &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
if (isAutoTMMEnabled()) [[unlikely]]
|
||||
return;
|
||||
|
||||
const Path basePath = m_session->useCategoryPathsInManualMode()
|
||||
? m_session->categorySavePath(category()) : m_session->savePath();
|
||||
|
@ -483,6 +505,8 @@ Path TorrentImpl::downloadPath() const
|
|||
void TorrentImpl::setDownloadPath(const Path &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
if (isAutoTMMEnabled()) [[unlikely]]
|
||||
return;
|
||||
|
||||
const Path basePath = m_session->useCategoryPathsInManualMode()
|
||||
? m_session->categoryDownloadPath(category()) : m_session->downloadPath();
|
||||
|
@ -934,7 +958,52 @@ void TorrentImpl::removeAllTags()
|
|||
|
||||
QDateTime TorrentImpl::addedTime() const
|
||||
{
|
||||
return QDateTime::fromSecsSinceEpoch(m_nativeStatus.added_time);
|
||||
return m_addedTime;
|
||||
}
|
||||
|
||||
QDateTime TorrentImpl::completedTime() const
|
||||
{
|
||||
return m_completedTime;
|
||||
}
|
||||
|
||||
QDateTime TorrentImpl::lastSeenComplete() const
|
||||
{
|
||||
return m_lastSeenComplete;
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::activeTime() const
|
||||
{
|
||||
return lt::total_seconds(m_nativeStatus.active_duration);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::finishedTime() const
|
||||
{
|
||||
return lt::total_seconds(m_nativeStatus.finished_duration);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::timeSinceUpload() const
|
||||
{
|
||||
if (m_nativeStatus.last_upload.time_since_epoch().count() == 0)
|
||||
return -1;
|
||||
|
||||
return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_upload);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::timeSinceDownload() const
|
||||
{
|
||||
if (m_nativeStatus.last_download.time_since_epoch().count() == 0)
|
||||
return -1;
|
||||
|
||||
return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_download);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::timeSinceActivity() const
|
||||
{
|
||||
const qlonglong upTime = timeSinceUpload();
|
||||
const qlonglong downTime = timeSinceDownload();
|
||||
return ((upTime < 0) != (downTime < 0))
|
||||
? std::max(upTime, downTime)
|
||||
: std::min(upTime, downTime);
|
||||
}
|
||||
|
||||
qreal TorrentImpl::ratioLimit() const
|
||||
|
@ -982,6 +1051,21 @@ PathList TorrentImpl::filePaths() const
|
|||
return m_filePaths;
|
||||
}
|
||||
|
||||
PathList TorrentImpl::actualFilePaths() const
|
||||
{
|
||||
if (!hasMetadata())
|
||||
return {};
|
||||
|
||||
PathList paths;
|
||||
paths.reserve(filesCount());
|
||||
|
||||
const lt::file_storage files = nativeTorrentInfo()->files();
|
||||
for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes()))
|
||||
paths.emplaceBack(files.file_path(nativeIndex));
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
QVector<DownloadPriority> TorrentImpl::filePriorities() const
|
||||
{
|
||||
return m_filePriorities;
|
||||
|
@ -1238,16 +1322,6 @@ qlonglong TorrentImpl::totalUpload() const
|
|||
return m_nativeStatus.all_time_upload;
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::activeTime() const
|
||||
{
|
||||
return lt::total_seconds(m_nativeStatus.active_duration);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::finishedTime() const
|
||||
{
|
||||
return lt::total_seconds(m_nativeStatus.finished_duration);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::eta() const
|
||||
{
|
||||
if (isStopped()) return MAX_ETA;
|
||||
|
@ -1357,45 +1431,6 @@ int TorrentImpl::totalLeechersCount() const
|
|||
return (m_nativeStatus.num_incomplete > -1) ? m_nativeStatus.num_incomplete : (m_nativeStatus.list_peers - m_nativeStatus.list_seeds);
|
||||
}
|
||||
|
||||
QDateTime TorrentImpl::lastSeenComplete() const
|
||||
{
|
||||
if (m_nativeStatus.last_seen_complete > 0)
|
||||
return QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete);
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
QDateTime TorrentImpl::completedTime() const
|
||||
{
|
||||
if (m_nativeStatus.completed_time > 0)
|
||||
return QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time);
|
||||
else
|
||||
return {};
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::timeSinceUpload() const
|
||||
{
|
||||
if (m_nativeStatus.last_upload.time_since_epoch().count() == 0)
|
||||
return -1;
|
||||
return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_upload);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::timeSinceDownload() const
|
||||
{
|
||||
if (m_nativeStatus.last_download.time_since_epoch().count() == 0)
|
||||
return -1;
|
||||
return lt::total_seconds(lt::clock_type::now() - m_nativeStatus.last_download);
|
||||
}
|
||||
|
||||
qlonglong TorrentImpl::timeSinceActivity() const
|
||||
{
|
||||
const qlonglong upTime = timeSinceUpload();
|
||||
const qlonglong downTime = timeSinceDownload();
|
||||
return ((upTime < 0) != (downTime < 0))
|
||||
? std::max(upTime, downTime)
|
||||
: std::min(upTime, downTime);
|
||||
}
|
||||
|
||||
int TorrentImpl::downloadLimit() const
|
||||
{
|
||||
return m_downloadLimit;;
|
||||
|
@ -1447,11 +1482,13 @@ QBitArray TorrentImpl::pieces() const
|
|||
|
||||
QBitArray TorrentImpl::downloadingPieces() const
|
||||
{
|
||||
QBitArray result(piecesCount());
|
||||
if (!hasMetadata())
|
||||
return {};
|
||||
|
||||
std::vector<lt::partial_piece_info> queue;
|
||||
m_nativeHandle.get_download_queue(queue);
|
||||
|
||||
QBitArray result {piecesCount()};
|
||||
for (const lt::partial_piece_info &info : queue)
|
||||
result.setBit(LT::toUnderlyingType(info.piece_index));
|
||||
|
||||
|
@ -1607,9 +1644,19 @@ void TorrentImpl::forceRecheck()
|
|||
return;
|
||||
|
||||
m_nativeHandle.force_recheck();
|
||||
|
||||
// We have to force update the cached state, otherwise someone will be able to get
|
||||
// an incorrect one during the interval until the cached state is updated in a regular way.
|
||||
m_nativeStatus.state = lt::torrent_status::checking_resume_data;
|
||||
m_nativeStatus.pieces.clear_all();
|
||||
m_nativeStatus.num_pieces = 0;
|
||||
m_ltAddTorrentParams.have_pieces.clear();
|
||||
m_ltAddTorrentParams.verified_pieces.clear();
|
||||
m_ltAddTorrentParams.unfinished_pieces.clear();
|
||||
m_completedFiles.fill(false);
|
||||
m_filesProgress.fill(0);
|
||||
m_pieces.fill(false);
|
||||
m_unchecked = false;
|
||||
|
||||
if (m_hasMissingFiles)
|
||||
{
|
||||
|
@ -1622,14 +1669,6 @@ void TorrentImpl::forceRecheck()
|
|||
}
|
||||
}
|
||||
|
||||
m_unchecked = false;
|
||||
|
||||
m_completedFiles.fill(false);
|
||||
m_filesProgress.fill(0);
|
||||
m_pieces.fill(false);
|
||||
m_nativeStatus.pieces.clear_all();
|
||||
m_nativeStatus.num_pieces = 0;
|
||||
|
||||
if (isStopped())
|
||||
{
|
||||
// When "force recheck" is applied on Stopped torrent, we start them to perform checking
|
||||
|
@ -1734,7 +1773,13 @@ TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entr
|
|||
#else
|
||||
const QSet<int> btProtocols {1};
|
||||
#endif
|
||||
::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo);
|
||||
|
||||
const auto fromLTTimePoint32 = [this](const lt::time_point32 &timePoint)
|
||||
{
|
||||
return m_session->fromLTTimePoint32(timePoint);
|
||||
};
|
||||
::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo, fromLTTimePoint32);
|
||||
|
||||
return *it;
|
||||
}
|
||||
|
||||
|
@ -1791,12 +1836,13 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
|
|||
const Path filePath = actualFilePath.removedExtension(QB_EXT);
|
||||
m_filePaths.append(filePath);
|
||||
|
||||
lt::download_priority_t &nativePriority = p.file_priorities[LT::toUnderlyingType(nativeIndex)];
|
||||
if ((nativePriority != lt::dont_download) && m_session->isFilenameExcluded(filePath.filename()))
|
||||
nativePriority = lt::dont_download;
|
||||
const auto priority = LT::fromNative(nativePriority);
|
||||
m_filePriorities.append(priority);
|
||||
m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
|
||||
}
|
||||
|
||||
m_session->applyFilenameFilter(m_filePaths, m_filePriorities);
|
||||
for (int i = 0; i < m_filePriorities.size(); ++i)
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]);
|
||||
|
||||
p.save_path = savePath.toString().toStdString();
|
||||
p.ti = metadata;
|
||||
|
||||
|
@ -1859,6 +1905,9 @@ void TorrentImpl::reload()
|
|||
|
||||
auto *const extensionData = new ExtensionData;
|
||||
p.userdata = LTClientData(extensionData);
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
p.storage = customStorageConstructor;
|
||||
#endif
|
||||
m_nativeHandle = m_nativeSession->add_torrent(p);
|
||||
|
||||
m_nativeStatus = extensionData->status;
|
||||
|
@ -1933,8 +1982,17 @@ void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext cont
|
|||
{
|
||||
if (!hasMetadata())
|
||||
{
|
||||
m_savePath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
if (context == MoveStorageContext::ChangeSavePath)
|
||||
{
|
||||
m_savePath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
else if (context == MoveStorageContext::ChangeDownloadPath)
|
||||
{
|
||||
m_downloadPath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2104,6 +2162,7 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
|
|||
|
||||
m_ltAddTorrentParams.have_pieces.clear();
|
||||
m_ltAddTorrentParams.verified_pieces.clear();
|
||||
m_ltAddTorrentParams.unfinished_pieces.clear();
|
||||
|
||||
m_nativeStatus.torrent_file = m_ltAddTorrentParams.ti;
|
||||
|
||||
|
@ -2146,23 +2205,37 @@ void TorrentImpl::handleSaveResumeDataAlert(const lt::save_resume_data_alert *p)
|
|||
|
||||
void TorrentImpl::prepareResumeData(const lt::add_torrent_params ¶ms)
|
||||
{
|
||||
if (m_hasMissingFiles)
|
||||
{
|
||||
const auto havePieces = m_ltAddTorrentParams.have_pieces;
|
||||
const auto unfinishedPieces = m_ltAddTorrentParams.unfinished_pieces;
|
||||
const auto verifiedPieces = m_ltAddTorrentParams.verified_pieces;
|
||||
decltype(params.have_pieces) havePieces;
|
||||
decltype(params.unfinished_pieces) unfinishedPieces;
|
||||
decltype(params.verified_pieces) verifiedPieces;
|
||||
|
||||
// The resume data obtained from libtorrent contains an empty "progress" in the following cases:
|
||||
// 1. when it was requested at a time when the initial resume data has not yet been checked,
|
||||
// 2. when initial resume data was rejected
|
||||
// We should preserve the initial "progress" in such cases.
|
||||
const bool needPreserveProgress = m_hasMissingFiles
|
||||
|| (!m_ltAddTorrentParams.have_pieces.empty() && params.have_pieces.empty());
|
||||
const bool preserveSeedMode = !m_hasMissingFiles && !hasMetadata()
|
||||
&& (m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode);
|
||||
|
||||
if (needPreserveProgress)
|
||||
{
|
||||
havePieces = std::move(m_ltAddTorrentParams.have_pieces);
|
||||
unfinishedPieces = std::move(m_ltAddTorrentParams.unfinished_pieces);
|
||||
verifiedPieces = std::move(m_ltAddTorrentParams.verified_pieces);
|
||||
}
|
||||
|
||||
// Update recent resume data but preserve existing progress
|
||||
m_ltAddTorrentParams = params;
|
||||
m_ltAddTorrentParams.have_pieces = havePieces;
|
||||
m_ltAddTorrentParams.unfinished_pieces = unfinishedPieces;
|
||||
m_ltAddTorrentParams.verified_pieces = verifiedPieces;
|
||||
}
|
||||
else
|
||||
{
|
||||
const bool preserveSeedMode = (!hasMetadata() && (m_ltAddTorrentParams.flags & lt::torrent_flags::seed_mode));
|
||||
// Update recent resume data
|
||||
m_ltAddTorrentParams = params;
|
||||
|
||||
if (needPreserveProgress)
|
||||
{
|
||||
m_ltAddTorrentParams.have_pieces = std::move(havePieces);
|
||||
m_ltAddTorrentParams.unfinished_pieces = std::move(unfinishedPieces);
|
||||
m_ltAddTorrentParams.verified_pieces = std::move(verifiedPieces);
|
||||
}
|
||||
|
||||
if (preserveSeedMode)
|
||||
m_ltAddTorrentParams.flags |= lt::torrent_flags::seed_mode;
|
||||
}
|
||||
|
@ -2312,7 +2385,8 @@ void TorrentImpl::handleFileCompletedAlert(const lt::file_completed_alert *p)
|
|||
|
||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
// only apply Mark-of-the-Web to new download files
|
||||
if (Preferences::instance()->isMarkOfTheWebEnabled() && isDownloading())
|
||||
if (Preferences::instance()->isMarkOfTheWebEnabled()
|
||||
&& (m_nativeStatus.state == lt::torrent_status::downloading))
|
||||
{
|
||||
const Path fullpath = actualStorageLocation() / actualPath;
|
||||
Utils::OS::applyMarkOfTheWeb(fullpath);
|
||||
|
@ -2558,11 +2632,23 @@ bool TorrentImpl::isMoveInProgress() const
|
|||
|
||||
void TorrentImpl::updateStatus(const lt::torrent_status &nativeStatus)
|
||||
{
|
||||
// Since libtorrent alerts are handled asynchronously there can be obsolete
|
||||
// "state update" event reached here after torrent was reloaded in libtorrent.
|
||||
// Just discard such events.
|
||||
if (nativeStatus.handle != m_nativeHandle) [[unlikely]]
|
||||
return;
|
||||
|
||||
const lt::torrent_status oldStatus = std::exchange(m_nativeStatus, nativeStatus);
|
||||
|
||||
if (m_nativeStatus.num_pieces != oldStatus.num_pieces)
|
||||
updateProgress();
|
||||
|
||||
if (m_nativeStatus.completed_time != oldStatus.completed_time)
|
||||
m_completedTime = (m_nativeStatus.completed_time > 0) ? QDateTime::fromSecsSinceEpoch(m_nativeStatus.completed_time) : QDateTime();
|
||||
|
||||
if (m_nativeStatus.last_seen_complete != oldStatus.last_seen_complete)
|
||||
m_lastSeenComplete = QDateTime::fromSecsSinceEpoch(m_nativeStatus.last_seen_complete);
|
||||
|
||||
updateState();
|
||||
|
||||
m_payloadRateMonitor.addSample({nativeStatus.download_payload_rate
|
||||
|
|
|
@ -138,7 +138,15 @@ namespace BitTorrent
|
|||
int piecesCount() const override;
|
||||
int piecesHave() const override;
|
||||
qreal progress() const override;
|
||||
|
||||
QDateTime addedTime() const override;
|
||||
QDateTime completedTime() const override;
|
||||
QDateTime lastSeenComplete() const override;
|
||||
qlonglong activeTime() const override;
|
||||
qlonglong finishedTime() const override;
|
||||
qlonglong timeSinceUpload() const override;
|
||||
qlonglong timeSinceDownload() const override;
|
||||
qlonglong timeSinceActivity() const override;
|
||||
|
||||
qreal ratioLimit() const override;
|
||||
void setRatioLimit(qreal limit) override;
|
||||
|
@ -153,6 +161,7 @@ namespace BitTorrent
|
|||
Path actualFilePath(int index) const override;
|
||||
qlonglong fileSize(int index) const override;
|
||||
PathList filePaths() const override;
|
||||
PathList actualFilePaths() const override;
|
||||
QVector<DownloadPriority> filePriorities() const override;
|
||||
|
||||
TorrentInfo info() const override;
|
||||
|
@ -180,8 +189,6 @@ namespace BitTorrent
|
|||
QString error() const override;
|
||||
qlonglong totalDownload() const override;
|
||||
qlonglong totalUpload() const override;
|
||||
qlonglong activeTime() const override;
|
||||
qlonglong finishedTime() const override;
|
||||
qlonglong eta() const override;
|
||||
QVector<qreal> filesProgress() const override;
|
||||
int seedsCount() const override;
|
||||
|
@ -190,11 +197,6 @@ namespace BitTorrent
|
|||
int totalSeedsCount() const override;
|
||||
int totalPeersCount() const override;
|
||||
int totalLeechersCount() const override;
|
||||
QDateTime lastSeenComplete() const override;
|
||||
QDateTime completedTime() const override;
|
||||
qlonglong timeSinceUpload() const override;
|
||||
qlonglong timeSinceDownload() const override;
|
||||
qlonglong timeSinceActivity() const override;
|
||||
int downloadLimit() const override;
|
||||
int uploadLimit() const override;
|
||||
bool superSeeding() const override;
|
||||
|
@ -341,6 +343,14 @@ namespace BitTorrent
|
|||
|
||||
InfoHash m_infoHash;
|
||||
|
||||
QDateTime m_creationDate;
|
||||
QString m_creator;
|
||||
QString m_comment;
|
||||
|
||||
QDateTime m_addedTime;
|
||||
QDateTime m_completedTime;
|
||||
QDateTime m_lastSeenComplete;
|
||||
|
||||
// m_moveFinishedTriggers is activated only when the following conditions are met:
|
||||
// all file rename jobs complete, all file move jobs complete
|
||||
QQueue<EventTrigger> m_moveFinishedTriggers;
|
||||
|
|
|
@ -44,6 +44,7 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
|
|||
, m_requestHandler(requestHandler)
|
||||
{
|
||||
m_socket->setParent(this);
|
||||
connect(m_socket, &QAbstractSocket::disconnected, this, &Connection::closed);
|
||||
|
||||
// reserve common size for requests, don't use the max allowed size which is too big for
|
||||
// memory constrained platforms
|
||||
|
@ -62,11 +63,6 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
|
|||
});
|
||||
}
|
||||
|
||||
Connection::~Connection()
|
||||
{
|
||||
m_socket->close();
|
||||
}
|
||||
|
||||
void Connection::read()
|
||||
{
|
||||
// reuse existing buffer and avoid unnecessary memory allocation/relocation
|
||||
|
@ -182,11 +178,6 @@ bool Connection::hasExpired(const qint64 timeout) const
|
|||
&& m_idleTimer.hasExpired(timeout);
|
||||
}
|
||||
|
||||
bool Connection::isClosed() const
|
||||
{
|
||||
return (m_socket->state() == QAbstractSocket::UnconnectedState);
|
||||
}
|
||||
|
||||
bool Connection::acceptsGzipEncoding(QString codings)
|
||||
{
|
||||
// [rfc7231] 5.3.4. Accept-Encoding
|
||||
|
|
|
@ -47,10 +47,11 @@ namespace Http
|
|||
|
||||
public:
|
||||
Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent = nullptr);
|
||||
~Connection();
|
||||
|
||||
bool hasExpired(qint64 timeout) const;
|
||||
bool isClosed() const;
|
||||
|
||||
signals:
|
||||
void closed();
|
||||
|
||||
private:
|
||||
static bool acceptsGzipEncoding(QString codings);
|
||||
|
|
|
@ -32,7 +32,10 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
|
||||
#include <QtLogging>
|
||||
#include <QNetworkProxy>
|
||||
#include <QSslCipher>
|
||||
#include <QSslConfiguration>
|
||||
|
@ -40,7 +43,6 @@
|
|||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
#include "base/algorithm.h"
|
||||
#include "base/global.h"
|
||||
#include "base/utils/net.h"
|
||||
#include "base/utils/sslkey.h"
|
||||
|
@ -113,32 +115,38 @@ Server::Server(IRequestHandler *requestHandler, QObject *parent)
|
|||
|
||||
void Server::incomingConnection(const qintptr socketDescriptor)
|
||||
{
|
||||
if (m_connections.size() >= CONNECTIONS_LIMIT) return;
|
||||
|
||||
QTcpSocket *serverSocket = nullptr;
|
||||
if (m_https)
|
||||
serverSocket = new QSslSocket(this);
|
||||
else
|
||||
serverSocket = new QTcpSocket(this);
|
||||
|
||||
std::unique_ptr<QTcpSocket> serverSocket = m_https ? std::make_unique<QSslSocket>(this) : std::make_unique<QTcpSocket>(this);
|
||||
if (!serverSocket->setSocketDescriptor(socketDescriptor))
|
||||
return;
|
||||
|
||||
if (m_connections.size() >= CONNECTIONS_LIMIT)
|
||||
{
|
||||
delete serverSocket;
|
||||
qWarning("Too many connections. Exceeded CONNECTIONS_LIMIT (%d). Connection closed.", CONNECTIONS_LIMIT);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_https)
|
||||
try
|
||||
{
|
||||
static_cast<QSslSocket *>(serverSocket)->setProtocol(QSsl::SecureProtocols);
|
||||
static_cast<QSslSocket *>(serverSocket)->setPrivateKey(m_key);
|
||||
static_cast<QSslSocket *>(serverSocket)->setLocalCertificateChain(m_certificates);
|
||||
static_cast<QSslSocket *>(serverSocket)->setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||
static_cast<QSslSocket *>(serverSocket)->startServerEncryption();
|
||||
}
|
||||
if (m_https)
|
||||
{
|
||||
auto *sslSocket = static_cast<QSslSocket *>(serverSocket.get());
|
||||
sslSocket->setProtocol(QSsl::SecureProtocols);
|
||||
sslSocket->setPrivateKey(m_key);
|
||||
sslSocket->setLocalCertificateChain(m_certificates);
|
||||
sslSocket->setPeerVerifyMode(QSslSocket::VerifyNone);
|
||||
sslSocket->startServerEncryption();
|
||||
}
|
||||
|
||||
auto *c = new Connection(serverSocket, m_requestHandler, this);
|
||||
m_connections.insert(c);
|
||||
connect(serverSocket, &QAbstractSocket::disconnected, this, [c, this]() { removeConnection(c); });
|
||||
auto *connection = new Connection(serverSocket.release(), m_requestHandler, this);
|
||||
m_connections.insert(connection);
|
||||
connect(connection, &Connection::closed, this, [this, connection] { removeConnection(connection); });
|
||||
}
|
||||
catch (const std::bad_alloc &exception)
|
||||
{
|
||||
// drop the connection instead of throwing exception and crash
|
||||
qWarning("Failed to allocate memory for HTTP connection. Connection closed.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void Server::removeConnection(Connection *connection)
|
||||
|
|
|
@ -148,10 +148,20 @@ Net::DownloadManager::DownloadManager(QObject *parent)
|
|||
QStringList errorList;
|
||||
for (const QSslError &error : errors)
|
||||
errorList += error.errorString();
|
||||
LogMsg(tr("Ignoring SSL error, URL: \"%1\", errors: \"%2\"").arg(reply->url().toString(), errorList.join(u". ")), Log::WARNING);
|
||||
|
||||
// Ignore all SSL errors
|
||||
reply->ignoreSslErrors();
|
||||
QString errorMsg;
|
||||
if (!Preferences::instance()->isIgnoreSSLErrors())
|
||||
{
|
||||
errorMsg = tr("SSL error, URL: \"%1\", errors: \"%2\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
errorMsg = tr("Ignoring SSL error, URL: \"%1\", errors: \"%2\"");
|
||||
// Ignore all SSL errors
|
||||
reply->ignoreSslErrors();
|
||||
}
|
||||
|
||||
LogMsg(errorMsg.arg(reply->url().toString(), errorList.join(u". ")), Log::WARNING);
|
||||
});
|
||||
|
||||
connect(ProxyConfigurationManager::instance(), &ProxyConfigurationManager::proxyConfigurationChanged
|
||||
|
|
|
@ -43,7 +43,6 @@ class QSslSocket;
|
|||
#else
|
||||
class QTcpSocket;
|
||||
#endif
|
||||
class QTextCodec;
|
||||
|
||||
namespace Net
|
||||
{
|
||||
|
|
|
@ -134,17 +134,17 @@ void Preferences::setCustomUIThemePath(const Path &path)
|
|||
setValue(u"Preferences/General/CustomUIThemePath"_s, path);
|
||||
}
|
||||
|
||||
bool Preferences::deleteTorrentFilesAsDefault() const
|
||||
bool Preferences::removeTorrentContent() const
|
||||
{
|
||||
return value(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, false);
|
||||
}
|
||||
|
||||
void Preferences::setDeleteTorrentFilesAsDefault(const bool del)
|
||||
void Preferences::setRemoveTorrentContent(const bool remove)
|
||||
{
|
||||
if (del == deleteTorrentFilesAsDefault())
|
||||
if (remove == removeTorrentContent())
|
||||
return;
|
||||
|
||||
setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, del);
|
||||
setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, remove);
|
||||
}
|
||||
|
||||
bool Preferences::confirmOnExit() const
|
||||
|
@ -429,6 +429,19 @@ void Preferences::setWinStartup(const bool b)
|
|||
settings.remove(profileID);
|
||||
}
|
||||
}
|
||||
|
||||
QString Preferences::getStyle() const
|
||||
{
|
||||
return value<QString>(u"Appearance/Style"_s);
|
||||
}
|
||||
|
||||
void Preferences::setStyle(const QString &styleName)
|
||||
{
|
||||
if (styleName == getStyle())
|
||||
return;
|
||||
|
||||
setValue(u"Appearance/Style"_s, styleName);
|
||||
}
|
||||
#endif // Q_OS_WIN
|
||||
|
||||
// Downloads
|
||||
|
@ -1330,6 +1343,19 @@ void Preferences::setMarkOfTheWebEnabled(const bool enabled)
|
|||
setValue(u"Preferences/Advanced/markOfTheWeb"_s, enabled);
|
||||
}
|
||||
|
||||
bool Preferences::isIgnoreSSLErrors() const
|
||||
{
|
||||
return value(u"Preferences/Advanced/IgnoreSSLErrors"_s, false);
|
||||
}
|
||||
|
||||
void Preferences::setIgnoreSSLErrors(const bool enabled)
|
||||
{
|
||||
if (enabled == isIgnoreSSLErrors())
|
||||
return;
|
||||
|
||||
setValue(u"Preferences/Advanced/IgnoreSSLErrors"_s, enabled);
|
||||
}
|
||||
|
||||
Path Preferences::getPythonExecutablePath() const
|
||||
{
|
||||
return value(u"Preferences/Search/pythonExecutablePath"_s, Path());
|
||||
|
|
|
@ -105,8 +105,8 @@ public:
|
|||
void setUseCustomUITheme(bool use);
|
||||
Path customUIThemePath() const;
|
||||
void setCustomUIThemePath(const Path &path);
|
||||
bool deleteTorrentFilesAsDefault() const;
|
||||
void setDeleteTorrentFilesAsDefault(bool del);
|
||||
bool removeTorrentContent() const;
|
||||
void setRemoveTorrentContent(bool remove);
|
||||
bool confirmOnExit() const;
|
||||
void setConfirmOnExit(bool confirm);
|
||||
bool speedInTitleBar() const;
|
||||
|
@ -130,6 +130,8 @@ public:
|
|||
#ifdef Q_OS_WIN
|
||||
bool WinStartup() const;
|
||||
void setWinStartup(bool b);
|
||||
QString getStyle() const;
|
||||
void setStyle(const QString &styleName);
|
||||
#endif
|
||||
|
||||
// Downloads
|
||||
|
@ -293,6 +295,8 @@ public:
|
|||
void setTrackerPortForwardingEnabled(bool enabled);
|
||||
bool isMarkOfTheWebEnabled() const;
|
||||
void setMarkOfTheWebEnabled(bool enabled);
|
||||
bool isIgnoreSSLErrors() const;
|
||||
void setIgnoreSSLErrors(bool enabled);
|
||||
Path getPythonExecutablePath() const;
|
||||
void setPythonExecutablePath(const Path &path);
|
||||
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
|
||||
|
|
|
@ -396,6 +396,8 @@ bool AutoDownloadRule::matchesSmartEpisodeFilter(const QString &articleTitle) co
|
|||
m_dataPtr->lastComputedEpisodes.append(episodeStr + u"-REPACK");
|
||||
m_dataPtr->lastComputedEpisodes.append(episodeStr + u"-PROPER");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
m_dataPtr->lastComputedEpisodes.append(episodeStr);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -29,11 +29,8 @@
|
|||
|
||||
#include "rss_parser.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QGlobalStatic>
|
||||
#include <QHash>
|
||||
#include <QMetaObject>
|
||||
#include <QRegularExpression>
|
||||
#include <QStringList>
|
||||
#include <QVariant>
|
||||
|
@ -359,7 +356,7 @@ namespace
|
|||
};
|
||||
|
||||
// Ported to Qt from KDElibs4
|
||||
QDateTime parseDate(const QString &string)
|
||||
QDateTime parseDate(const QString &string, const QDateTime &fallbackDate)
|
||||
{
|
||||
const char16_t shortDay[][4] =
|
||||
{
|
||||
|
@ -382,7 +379,7 @@ namespace
|
|||
|
||||
const QString str = string.trimmed();
|
||||
if (str.isEmpty())
|
||||
return QDateTime::currentDateTime();
|
||||
return fallbackDate;
|
||||
|
||||
int nyear = 6; // indexes within string to values
|
||||
int nmonth = 4;
|
||||
|
@ -402,14 +399,14 @@ namespace
|
|||
const bool h1 = (parts[3] == u"-");
|
||||
const bool h2 = (parts[5] == u"-");
|
||||
if (h1 != h2)
|
||||
return QDateTime::currentDateTime();
|
||||
return fallbackDate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check for the obsolete form "Wdy Mon DD HH:MM:SS YYYY"
|
||||
rx = QRegularExpression {u"^([A-Z][a-z]+)\\s+(\\S+)\\s+(\\d\\d)\\s+(\\d\\d):(\\d\\d):(\\d\\d)\\s+(\\d\\d\\d\\d)$"_s};
|
||||
if (str.indexOf(rx, 0, &rxMatch) != 0)
|
||||
return QDateTime::currentDateTime();
|
||||
return fallbackDate;
|
||||
|
||||
nyear = 7;
|
||||
nmonth = 2;
|
||||
|
@ -427,14 +424,14 @@ namespace
|
|||
const int hour = parts[nhour].toInt(&ok[2]);
|
||||
const int minute = parts[nmin].toInt(&ok[3]);
|
||||
if (!ok[0] || !ok[1] || !ok[2] || !ok[3])
|
||||
return QDateTime::currentDateTime();
|
||||
return fallbackDate;
|
||||
|
||||
int second = 0;
|
||||
if (!parts[nsec].isEmpty())
|
||||
{
|
||||
second = parts[nsec].toInt(&ok[0]);
|
||||
if (!ok[0])
|
||||
return QDateTime::currentDateTime();
|
||||
return fallbackDate;
|
||||
}
|
||||
|
||||
const bool leapSecond = (second == 60);
|
||||
|
@ -518,21 +515,21 @@ namespace
|
|||
|
||||
const QDate qDate(year, month + 1, day); // convert date, and check for out-of-range
|
||||
if (!qDate.isValid())
|
||||
return QDateTime::currentDateTime();
|
||||
return fallbackDate;
|
||||
|
||||
const QTime qTime(hour, minute, second);
|
||||
QDateTime result(qDate, qTime, Qt::UTC);
|
||||
if (offset)
|
||||
result = result.addSecs(-offset);
|
||||
if (!result.isValid())
|
||||
return QDateTime::currentDateTime(); // invalid date/time
|
||||
return fallbackDate; // invalid date/time
|
||||
|
||||
if (leapSecond)
|
||||
{
|
||||
// Validate a leap second time. Leap seconds are inserted after 23:59:59 UTC.
|
||||
// Convert the time to UTC and check that it is 00:00:00.
|
||||
if ((hour*3600 + minute*60 + 60 - offset + 86400*5) % 86400) // (max abs(offset) is 100 hours)
|
||||
return QDateTime::currentDateTime(); // the time isn't the last second of the day
|
||||
return fallbackDate; // the time isn't the last second of the day
|
||||
}
|
||||
|
||||
return result;
|
||||
|
@ -550,6 +547,7 @@ RSS::Private::Parser::Parser(const QString &lastBuildDate)
|
|||
void RSS::Private::Parser::parse(const QByteArray &feedData)
|
||||
{
|
||||
QXmlStreamReader xml {feedData};
|
||||
m_fallbackDate = QDateTime::currentDateTime();
|
||||
XmlStreamEntityResolver resolver;
|
||||
xml.setEntityResolver(&resolver);
|
||||
bool foundChannel = false;
|
||||
|
@ -641,7 +639,7 @@ void RSS::Private::Parser::parseRssArticle(QXmlStreamReader &xml)
|
|||
}
|
||||
else if (name == u"pubDate")
|
||||
{
|
||||
article[Article::KeyDate] = parseDate(xml.readElementText().trimmed());
|
||||
article[Article::KeyDate] = parseDate(xml.readElementText().trimmed(), m_fallbackDate);
|
||||
}
|
||||
else if (name == u"author")
|
||||
{
|
||||
|
@ -755,7 +753,7 @@ void RSS::Private::Parser::parseAtomArticle(QXmlStreamReader &xml)
|
|||
{
|
||||
// ATOM uses standard compliant date, don't do fancy stuff
|
||||
const QDateTime articleDate = QDateTime::fromString(xml.readElementText().trimmed(), Qt::ISODate);
|
||||
article[Article::KeyDate] = (articleDate.isValid() ? articleDate : QDateTime::currentDateTime());
|
||||
article[Article::KeyDate] = (articleDate.isValid() ? articleDate : m_fallbackDate);
|
||||
}
|
||||
else if (name == u"author")
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -29,6 +29,7 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
|
@ -66,6 +67,7 @@ namespace RSS::Private
|
|||
void parseAtomChannel(QXmlStreamReader &xml);
|
||||
void addArticle(QVariantHash article);
|
||||
|
||||
QDateTime m_fallbackDate;
|
||||
QString m_baseUrl;
|
||||
ParsingResult m_result;
|
||||
QSet<QString> m_articleIDs;
|
||||
|
|
|
@ -52,19 +52,21 @@ const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored);
|
|||
using BitTorrent::Torrent;
|
||||
|
||||
TorrentFilter::TorrentFilter(const Type type, const std::optional<TorrentIDSet> &idSet
|
||||
, const std::optional<QString> &category, const std::optional<Tag> &tag)
|
||||
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
|
||||
: m_type {type}
|
||||
, m_category {category}
|
||||
, m_tag {tag}
|
||||
, m_idSet {idSet}
|
||||
, m_private {isPrivate}
|
||||
{
|
||||
}
|
||||
|
||||
TorrentFilter::TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet
|
||||
, const std::optional<QString> &category, const std::optional<Tag> &tag)
|
||||
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
|
||||
: m_category {category}
|
||||
, m_tag {tag}
|
||||
, m_idSet {idSet}
|
||||
, m_private {isPrivate}
|
||||
{
|
||||
setTypeByName(filter);
|
||||
}
|
||||
|
@ -147,11 +149,22 @@ bool TorrentFilter::setTag(const std::optional<Tag> &tag)
|
|||
return false;
|
||||
}
|
||||
|
||||
bool TorrentFilter::setPrivate(const std::optional<bool> isPrivate)
|
||||
{
|
||||
if (m_private != isPrivate)
|
||||
{
|
||||
m_private = isPrivate;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool TorrentFilter::match(const Torrent *const torrent) const
|
||||
{
|
||||
if (!torrent) return false;
|
||||
|
||||
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent));
|
||||
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent) && matchPrivate(torrent));
|
||||
}
|
||||
|
||||
bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
|
||||
|
@ -224,3 +237,11 @@ bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const
|
|||
|
||||
return torrent->hasTag(*m_tag);
|
||||
}
|
||||
|
||||
bool TorrentFilter::matchPrivate(const BitTorrent::Torrent *const torrent) const
|
||||
{
|
||||
if (!m_private)
|
||||
return true;
|
||||
|
||||
return m_private == torrent->isPrivate();
|
||||
}
|
||||
|
|
|
@ -87,16 +87,24 @@ public:
|
|||
|
||||
TorrentFilter() = default;
|
||||
// category & tags: pass empty string for uncategorized / untagged torrents.
|
||||
TorrentFilter(Type type, const std::optional<TorrentIDSet> &idSet = AnyID
|
||||
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tag = AnyTag);
|
||||
TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet = AnyID
|
||||
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tags = AnyTag);
|
||||
TorrentFilter(Type type
|
||||
, const std::optional<TorrentIDSet> &idSet = AnyID
|
||||
, const std::optional<QString> &category = AnyCategory
|
||||
, const std::optional<Tag> &tag = AnyTag
|
||||
, std::optional<bool> isPrivate = {});
|
||||
TorrentFilter(const QString &filter
|
||||
, const std::optional<TorrentIDSet> &idSet = AnyID
|
||||
, const std::optional<QString> &category = AnyCategory
|
||||
, const std::optional<Tag> &tags = AnyTag
|
||||
, std::optional<bool> isPrivate = {});
|
||||
|
||||
|
||||
bool setType(Type type);
|
||||
bool setTypeByName(const QString &filter);
|
||||
bool setTorrentIDSet(const std::optional<TorrentIDSet> &idSet);
|
||||
bool setCategory(const std::optional<QString> &category);
|
||||
bool setTag(const std::optional<Tag> &tag);
|
||||
bool setPrivate(std::optional<bool> isPrivate);
|
||||
|
||||
bool match(const BitTorrent::Torrent *torrent) const;
|
||||
|
||||
|
@ -105,9 +113,11 @@ private:
|
|||
bool matchHash(const BitTorrent::Torrent *torrent) const;
|
||||
bool matchCategory(const BitTorrent::Torrent *torrent) const;
|
||||
bool matchTag(const BitTorrent::Torrent *torrent) const;
|
||||
bool matchPrivate(const BitTorrent::Torrent *torrent) const;
|
||||
|
||||
Type m_type {All};
|
||||
std::optional<QString> m_category;
|
||||
std::optional<Tag> m_tag;
|
||||
std::optional<TorrentIDSet> m_idSet;
|
||||
std::optional<bool> m_private;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -29,8 +29,6 @@
|
|||
|
||||
#include "fs.h"
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
|
@ -52,6 +50,7 @@
|
|||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QDir>
|
||||
|
@ -311,20 +310,42 @@ bool Utils::Fs::renameFile(const Path &from, const Path &to)
|
|||
*
|
||||
* This function will try to fix the file permissions before removing it.
|
||||
*/
|
||||
bool Utils::Fs::removeFile(const Path &path)
|
||||
nonstd::expected<void, QString> Utils::Fs::removeFile(const Path &path)
|
||||
{
|
||||
if (QFile::remove(path.data()))
|
||||
return true;
|
||||
|
||||
QFile file {path.data()};
|
||||
if (file.remove())
|
||||
return {};
|
||||
|
||||
if (!file.exists())
|
||||
return true;
|
||||
return {};
|
||||
|
||||
// Make sure we have read/write permissions
|
||||
file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
|
||||
return file.remove();
|
||||
if (file.remove())
|
||||
return {};
|
||||
|
||||
return nonstd::make_unexpected(file.errorString());
|
||||
}
|
||||
|
||||
nonstd::expected<void, QString> Utils::Fs::moveFileToTrash(const Path &path)
|
||||
{
|
||||
QFile file {path.data()};
|
||||
if (file.moveToTrash())
|
||||
return {};
|
||||
|
||||
if (!file.exists())
|
||||
return {};
|
||||
|
||||
// Make sure we have read/write permissions
|
||||
file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
|
||||
if (file.moveToTrash())
|
||||
return {};
|
||||
|
||||
const QString errorMessage = file.errorString();
|
||||
return nonstd::make_unexpected(!errorMessage.isEmpty() ? errorMessage : QCoreApplication::translate("fs", "Unknown error"));
|
||||
}
|
||||
|
||||
|
||||
bool Utils::Fs::isReadable(const Path &path)
|
||||
{
|
||||
return QFileInfo(path.data()).isReadable();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -35,6 +35,7 @@
|
|||
|
||||
#include <QString>
|
||||
|
||||
#include "base/3rdparty/expected.hpp"
|
||||
#include "base/global.h"
|
||||
#include "base/pathfwd.h"
|
||||
|
||||
|
@ -60,7 +61,8 @@ namespace Utils::Fs
|
|||
|
||||
bool copyFile(const Path &from, const Path &to);
|
||||
bool renameFile(const Path &from, const Path &to);
|
||||
bool removeFile(const Path &path);
|
||||
nonstd::expected<void, QString> removeFile(const Path &path);
|
||||
nonstd::expected<void, QString> moveFileToTrash(const Path &path);
|
||||
bool mkdir(const Path &dirPath);
|
||||
bool mkpath(const Path &dirPath);
|
||||
bool rmdir(const Path &dirPath);
|
||||
|
|
|
@ -31,6 +31,8 @@
|
|||
#include "os.h"
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <algorithm>
|
||||
|
||||
#include <windows.h>
|
||||
#include <powrprof.h>
|
||||
#include <shlobj.h>
|
||||
|
@ -42,6 +44,8 @@
|
|||
#include <CoreServices/CoreServices.h>
|
||||
#endif // Q_OS_MACOS
|
||||
|
||||
#include <QScopeGuard>
|
||||
|
||||
#ifdef QBT_USES_DBUS
|
||||
#include <QDBusInterface>
|
||||
#endif // QBT_USES_DBUS
|
||||
|
@ -271,6 +275,11 @@ Path Utils::OS::windowsSystemPath()
|
|||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
bool Utils::OS::applyMarkOfTheWeb(const Path &file, const QString &url)
|
||||
{
|
||||
// Trying to apply this to a non-existent file is unacceptable,
|
||||
// as it may unexpectedly create such a file.
|
||||
if (!file.exists())
|
||||
return false;
|
||||
|
||||
Q_ASSERT(url.isEmpty() || url.startsWith(u"http:") || url.startsWith(u"https:"));
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
|
@ -278,34 +287,53 @@ bool Utils::OS::applyMarkOfTheWeb(const Path &file, const QString &url)
|
|||
// https://searchfox.org/mozilla-central/rev/ffdc4971dc18e1141cb2a90c2b0b776365650270/xpcom/io/CocoaFileUtils.mm#230
|
||||
// https://github.com/transmission/transmission/blob/f62f7427edb1fd5c430e0ef6956bbaa4f03ae597/macosx/Torrent.mm#L1945-L1955
|
||||
|
||||
const CFStringRef fileString = file.toString().toCFString();
|
||||
[[maybe_unused]] const auto fileStringGuard = qScopeGuard([&fileString] { ::CFRelease(fileString); });
|
||||
const CFURLRef fileURL = ::CFURLCreateWithFileSystemPath(kCFAllocatorDefault
|
||||
, fileString, kCFURLPOSIXPathStyle, false);
|
||||
[[maybe_unused]] const auto fileURLGuard = qScopeGuard([&fileURL] { ::CFRelease(fileURL); });
|
||||
|
||||
if (CFDictionaryRef currentProperties = nullptr;
|
||||
::CFURLCopyResourcePropertyForKey(fileURL, kCFURLQuarantinePropertiesKey, ¤tProperties, NULL)
|
||||
&& currentProperties)
|
||||
{
|
||||
[[maybe_unused]] const auto currentPropertiesGuard = qScopeGuard([¤tProperties] { ::CFRelease(currentProperties); });
|
||||
|
||||
if (CFStringRef quarantineType = nullptr;
|
||||
::CFDictionaryGetValueIfPresent(currentProperties, kLSQuarantineTypeKey, reinterpret_cast<const void **>(&quarantineType))
|
||||
&& quarantineType)
|
||||
{
|
||||
if (::CFStringCompare(quarantineType, kLSQuarantineTypeOtherDownload, 0) == kCFCompareEqualTo)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
CFMutableDictionaryRef properties = ::CFDictionaryCreateMutable(kCFAllocatorDefault, 0
|
||||
, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
|
||||
if (properties == NULL)
|
||||
if (!properties)
|
||||
return false;
|
||||
[[maybe_unused]] const auto propertiesGuard = qScopeGuard([&properties] { ::CFRelease(properties); });
|
||||
|
||||
::CFDictionarySetValue(properties, kLSQuarantineTypeKey, kLSQuarantineTypeOtherDownload);
|
||||
if (!url.isEmpty())
|
||||
::CFDictionarySetValue(properties, kLSQuarantineDataURLKey, url.toCFString());
|
||||
|
||||
const CFStringRef fileString = file.toString().toCFString();
|
||||
const CFURLRef fileURL = ::CFURLCreateWithFileSystemPath(kCFAllocatorDefault
|
||||
, fileString, kCFURLPOSIXPathStyle, false);
|
||||
{
|
||||
const CFStringRef urlCFString = url.toCFString();
|
||||
[[maybe_unused]] const auto urlStringGuard = qScopeGuard([&urlCFString] { ::CFRelease(urlCFString); });
|
||||
::CFDictionarySetValue(properties, kLSQuarantineDataURLKey, urlCFString);
|
||||
}
|
||||
|
||||
const Boolean success = ::CFURLSetResourcePropertyForKey(fileURL, kCFURLQuarantinePropertiesKey
|
||||
, properties, NULL);
|
||||
|
||||
::CFRelease(fileURL);
|
||||
::CFRelease(fileString);
|
||||
::CFRelease(properties);
|
||||
|
||||
return success;
|
||||
#elif defined(Q_OS_WIN)
|
||||
const QString zoneIDStream = file.toString() + u":Zone.Identifier";
|
||||
HANDLE handle = ::CreateFileW(zoneIDStream.toStdWString().c_str(), GENERIC_WRITE
|
||||
|
||||
HANDLE handle = ::CreateFileW(zoneIDStream.toStdWString().c_str(), (GENERIC_READ | GENERIC_WRITE)
|
||||
, (FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE)
|
||||
, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (handle == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
[[maybe_unused]] const auto handleGuard = qScopeGuard([&handle] { ::CloseHandle(handle); });
|
||||
|
||||
// 5.6.1 Zone.Identifier Stream Name
|
||||
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6e3f7352-d11c-4d76-8c39-2516a9df36e8
|
||||
|
@ -313,10 +341,27 @@ 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};
|
||||
::GetFileSizeEx(handle, &streamSize) && (streamSize.QuadPart > 0))
|
||||
{
|
||||
const DWORD expectedReadSize = std::min<LONGLONG>(streamSize.QuadPart, 1024);
|
||||
QByteArray buf {expectedReadSize, '\0'};
|
||||
|
||||
if (DWORD actualReadSize = 0;
|
||||
::ReadFile(handle, buf.data(), expectedReadSize, &actualReadSize, nullptr) && (actualReadSize == expectedReadSize))
|
||||
{
|
||||
if (buf.startsWith("[ZoneTransfer]\r\n") && buf.contains("\r\nZoneId=3\r\n") && buf.contains("\r\nHostUrl="))
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!::SetFilePointerEx(handle, {0}, nullptr, FILE_BEGIN))
|
||||
return false;
|
||||
if (!::SetEndOfFile(handle))
|
||||
return false;
|
||||
|
||||
DWORD written = 0;
|
||||
const BOOL writeResult = ::WriteFile(handle, zoneID.constData(), zoneID.size(), &written, nullptr);
|
||||
::CloseHandle(handle);
|
||||
|
||||
return writeResult && (written == zoneID.size());
|
||||
#endif
|
||||
}
|
||||
|
|
|
@ -30,9 +30,9 @@
|
|||
|
||||
#define QBT_VERSION_MAJOR 5
|
||||
#define QBT_VERSION_MINOR 0
|
||||
#define QBT_VERSION_BUGFIX 0
|
||||
#define QBT_VERSION_BUGFIX 4
|
||||
#define QBT_VERSION_BUILD 0
|
||||
#define QBT_VERSION_STATUS "beta1" // Should be empty for stable releases!
|
||||
#define QBT_VERSION_STATUS "" // Should be empty for stable releases!
|
||||
|
||||
#define QBT__STRINGIFY(x) #x
|
||||
#define QBT_STRINGIFY(x) QBT__STRINGIFY(x)
|
||||
|
|
|
@ -52,6 +52,8 @@ add_library(qbt_gui STATIC
|
|||
desktopintegration.h
|
||||
downloadfromurldialog.h
|
||||
executionlogwidget.h
|
||||
filterpatternformat.h
|
||||
filterpatternformatmenu.h
|
||||
flowlayout.h
|
||||
fspathedit.h
|
||||
fspathedit_p.h
|
||||
|
@ -151,6 +153,7 @@ add_library(qbt_gui STATIC
|
|||
desktopintegration.cpp
|
||||
downloadfromurldialog.cpp
|
||||
executionlogwidget.cpp
|
||||
filterpatternformatmenu.cpp
|
||||
flowlayout.cpp
|
||||
fspathedit.cpp
|
||||
fspathedit_p.cpp
|
||||
|
|
|
@ -67,7 +67,7 @@ AboutDialog::AboutDialog(QWidget *parent)
|
|||
u"</p>"_s
|
||||
.arg(tr("An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.")
|
||||
.replace(u"C++"_s, u"C\u2060+\u2060+"_s) // make C++ non-breaking
|
||||
, tr("Copyright %1 2006-2024 The qBittorrent project").arg(C_COPYRIGHT)
|
||||
, tr("Copyright %1 2006-2025 The qBittorrent project").arg(C_COPYRIGHT)
|
||||
, tr("Home Page:")
|
||||
, tr("Forum:")
|
||||
, tr("Bug Tracker:"));
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -64,6 +64,7 @@
|
|||
#include "base/utils/fs.h"
|
||||
#include "base/utils/misc.h"
|
||||
#include "base/utils/string.h"
|
||||
#include "filterpatternformatmenu.h"
|
||||
#include "lineedit.h"
|
||||
#include "torrenttagsdialog.h"
|
||||
|
||||
|
@ -181,6 +182,11 @@ public:
|
|||
return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index));
|
||||
}
|
||||
|
||||
PathList filePaths() const
|
||||
{
|
||||
return (m_filePaths.isEmpty() ? m_torrentInfo.filePaths() : m_filePaths);
|
||||
}
|
||||
|
||||
void renameFile(const int index, const Path &newFilePath) override
|
||||
{
|
||||
Q_ASSERT((index >= 0) && (index < filesCount()));
|
||||
|
@ -290,6 +296,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
|||
, m_storeRememberLastSavePath {SETTINGS_KEY(u"RememberLastSavePath"_s)}
|
||||
, m_storeTreeHeaderState {u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_s)}
|
||||
, m_storeSplitterState {u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_s)}
|
||||
, m_storeFilterPatternFormat {u"GUI/" SETTINGS_KEY(u"FilterPatternFormat"_s)}
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
|
@ -316,6 +323,8 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
|||
// Torrent content filtering
|
||||
m_filterLine->setPlaceholderText(tr("Filter files..."));
|
||||
m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
m_filterLine->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_filterLine, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::showContentFilterContextMenu);
|
||||
m_ui->contentFilterLayout->insertWidget(3, m_filterLine);
|
||||
const auto *focusSearchHotkey = new QShortcut(QKeySequence::Find, this);
|
||||
connect(focusSearchHotkey, &QShortcut::activated, this, [this]()
|
||||
|
@ -360,7 +369,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
|||
});
|
||||
dlg->open();
|
||||
});
|
||||
connect(m_filterLine, &LineEdit::textChanged, m_ui->contentTreeView, &TorrentContentWidget::setFilterPattern);
|
||||
connect(m_filterLine, &LineEdit::textChanged, this, &AddNewTorrentDialog::setContentFilterPattern);
|
||||
connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll);
|
||||
connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone);
|
||||
connect(Preferences::instance(), &Preferences::changed, []
|
||||
|
@ -691,6 +700,28 @@ void AddNewTorrentDialog::saveTorrentFile()
|
|||
}
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::showContentFilterContextMenu()
|
||||
{
|
||||
QMenu *menu = m_filterLine->createStandardContextMenu();
|
||||
|
||||
auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu);
|
||||
connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format)
|
||||
{
|
||||
m_storeFilterPatternFormat = format;
|
||||
setContentFilterPattern();
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
menu->addMenu(formatMenu);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
menu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::setContentFilterPattern()
|
||||
{
|
||||
m_ui->contentTreeView->setFilterPattern(m_filterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards));
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::populateSavePaths()
|
||||
{
|
||||
Q_ASSERT(m_currentContext);
|
||||
|
@ -790,6 +821,8 @@ void AddNewTorrentDialog::reject()
|
|||
if (!m_currentContext) [[unlikely]]
|
||||
return;
|
||||
|
||||
emit torrentRejected(m_currentContext->torrentDescr);
|
||||
|
||||
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
|
||||
const bool hasMetadata = torrentDescr.info().has_value();
|
||||
if (!hasMetadata)
|
||||
|
@ -886,15 +919,7 @@ void AddNewTorrentDialog::setupTreeview()
|
|||
{
|
||||
// Check file name blacklist for torrents that are manually added
|
||||
QVector<BitTorrent::DownloadPriority> priorities = m_contentAdaptor->filePriorities();
|
||||
for (int i = 0; i < priorities.size(); ++i)
|
||||
{
|
||||
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
|
||||
continue;
|
||||
|
||||
if (BitTorrent::Session::instance()->isFilenameExcluded(torrentInfo.filePath(i).filename()))
|
||||
priorities[i] = BitTorrent::DownloadPriority::Ignored;
|
||||
}
|
||||
|
||||
BitTorrent::Session::instance()->applyFilenameFilter(m_contentAdaptor->filePaths(), priorities);
|
||||
m_contentAdaptor->prioritizeFiles(priorities);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -35,6 +35,7 @@
|
|||
|
||||
#include "base/path.h"
|
||||
#include "base/settingvalue.h"
|
||||
#include "filterpatternformat.h"
|
||||
|
||||
class LineEdit;
|
||||
|
||||
|
@ -65,6 +66,7 @@ public:
|
|||
|
||||
signals:
|
||||
void torrentAccepted(const BitTorrent::TorrentDescriptor &torrentDescriptor, const BitTorrent::AddTorrentParams &addTorrentParams);
|
||||
void torrentRejected(const BitTorrent::TorrentDescriptor &torrentDescriptor);
|
||||
|
||||
private slots:
|
||||
void updateDiskSpaceLabel();
|
||||
|
@ -92,6 +94,8 @@ private:
|
|||
void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {});
|
||||
void setupTreeview();
|
||||
void saveTorrentFile();
|
||||
void showContentFilterContextMenu();
|
||||
void setContentFilterPattern();
|
||||
|
||||
Ui::AddNewTorrentDialog *m_ui = nullptr;
|
||||
std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
|
||||
|
@ -107,4 +111,5 @@ private:
|
|||
SettingValue<bool> m_storeRememberLastSavePath;
|
||||
SettingValue<QByteArray> m_storeTreeHeaderState;
|
||||
SettingValue<QByteArray> m_storeSplitterState;
|
||||
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
|
||||
};
|
||||
|
|
|
@ -63,6 +63,7 @@ namespace
|
|||
// qBittorrent section
|
||||
QBITTORRENT_HEADER,
|
||||
RESUME_DATA_STORAGE,
|
||||
TORRENT_CONTENT_REMOVE_OPTION,
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
MEMORY_WORKING_SET_LIMIT,
|
||||
#endif
|
||||
|
@ -104,6 +105,7 @@ namespace
|
|||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||
ENABLE_MARK_OF_THE_WEB,
|
||||
#endif // Q_OS_MACOS || Q_OS_WIN
|
||||
IGNORE_SSL_ERRORS,
|
||||
PYTHON_EXECUTABLE_PATH,
|
||||
START_SESSION_PAUSED,
|
||||
SESSION_SHUTDOWN_TIMEOUT,
|
||||
|
@ -331,6 +333,8 @@ void AdvancedSettings::saveAdvancedSettings() const
|
|||
// Mark-of-the-Web
|
||||
pref->setMarkOfTheWebEnabled(m_checkBoxMarkOfTheWeb.isChecked());
|
||||
#endif // Q_OS_MACOS || Q_OS_WIN
|
||||
// Ignore SSL errors
|
||||
pref->setIgnoreSSLErrors(m_checkBoxIgnoreSSLErrors.isChecked());
|
||||
// Python executable path
|
||||
pref->setPythonExecutablePath(Path(m_pythonExecutablePath.text().trimmed()));
|
||||
// Start session paused
|
||||
|
@ -364,6 +368,8 @@ void AdvancedSettings::saveAdvancedSettings() const
|
|||
session->setI2PInboundLength(m_spinBoxI2PInboundLength.value());
|
||||
session->setI2POutboundLength(m_spinBoxI2POutboundLength.value());
|
||||
#endif
|
||||
|
||||
session->setTorrentContentRemoveOption(m_comboBoxTorrentContentRemoveOption.currentData().value<BitTorrent::TorrentContentRemoveOption>());
|
||||
}
|
||||
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
|
@ -472,6 +478,11 @@ void AdvancedSettings::loadAdvancedSettings()
|
|||
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
|
||||
addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage);
|
||||
|
||||
m_comboBoxTorrentContentRemoveOption.addItem(tr("Delete files permanently"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::Delete));
|
||||
m_comboBoxTorrentContentRemoveOption.addItem(tr("Move files to trash (if possible)"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::MoveToTrash));
|
||||
m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption())));
|
||||
addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption);
|
||||
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
// Physical memory (RAM) usage limit
|
||||
m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
|
||||
|
@ -577,6 +588,7 @@ void AdvancedSettings::loadAdvancedSettings()
|
|||
m_comboBoxDiskIOType.addItem(tr("Default"), QVariant::fromValue(BitTorrent::DiskIOType::Default));
|
||||
m_comboBoxDiskIOType.addItem(tr("Memory mapped files"), QVariant::fromValue(BitTorrent::DiskIOType::MMap));
|
||||
m_comboBoxDiskIOType.addItem(tr("POSIX-compliant"), QVariant::fromValue(BitTorrent::DiskIOType::Posix));
|
||||
m_comboBoxDiskIOType.addItem(tr("Simple pread/pwrite"), QVariant::fromValue(BitTorrent::DiskIOType::SimplePreadPwrite));
|
||||
m_comboBoxDiskIOType.setCurrentIndex(m_comboBoxDiskIOType.findData(QVariant::fromValue(session->diskIOType())));
|
||||
addRow(DISK_IO_TYPE, tr("Disk IO type (requires restart)") + u' ' + makeLink(u"https://www.libtorrent.org/single-page-ref.html#default-disk-io-constructor", u"(?)")
|
||||
, &m_comboBoxDiskIOType);
|
||||
|
@ -845,6 +857,10 @@ void AdvancedSettings::loadAdvancedSettings()
|
|||
m_checkBoxMarkOfTheWeb.setChecked(pref->isMarkOfTheWebEnabled());
|
||||
addRow(ENABLE_MARK_OF_THE_WEB, motwLabel, &m_checkBoxMarkOfTheWeb);
|
||||
#endif // Q_OS_MACOS || Q_OS_WIN
|
||||
// Ignore SSL errors
|
||||
m_checkBoxIgnoreSSLErrors.setChecked(pref->isIgnoreSSLErrors());
|
||||
m_checkBoxIgnoreSSLErrors.setToolTip(tr("Affects certificate validation and non-torrent protocol activities (e.g. RSS feeds, program updates, torrent files, geoip db, etc)"));
|
||||
addRow(IGNORE_SSL_ERRORS, tr("Ignore SSL errors"), &m_checkBoxIgnoreSSLErrors);
|
||||
// Python executable path
|
||||
m_pythonExecutablePath.setPlaceholderText(tr("(Auto detect if empty)"));
|
||||
m_pythonExecutablePath.setText(pref->getPythonExecutablePath().toString());
|
||||
|
|
|
@ -77,11 +77,12 @@ private:
|
|||
m_spinBoxSavePathHistoryLength, m_spinBoxPeerTurnover, m_spinBoxPeerTurnoverCutoff, m_spinBoxPeerTurnoverInterval, m_spinBoxRequestQueueSize;
|
||||
QCheckBox m_checkBoxOsCache, m_checkBoxRecheckCompleted, m_checkBoxResolveCountries, m_checkBoxResolveHosts,
|
||||
m_checkBoxProgramNotifications, m_checkBoxTorrentAddedNotifications, m_checkBoxReannounceWhenAddressChanged, m_checkBoxTrackerFavicon, m_checkBoxTrackerStatus,
|
||||
m_checkBoxTrackerPortForwarding, m_checkBoxConfirmTorrentRecheck, m_checkBoxConfirmRemoveAllTags, m_checkBoxAnnounceAllTrackers, m_checkBoxAnnounceAllTiers,
|
||||
m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity,
|
||||
m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused;
|
||||
m_checkBoxTrackerPortForwarding, m_checkBoxIgnoreSSLErrors, m_checkBoxConfirmTorrentRecheck, m_checkBoxConfirmRemoveAllTags, m_checkBoxAnnounceAllTrackers,
|
||||
m_checkBoxAnnounceAllTiers, m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts,
|
||||
m_checkBoxPieceExtentAffinity, m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents,
|
||||
m_checkBoxStartSessionPaused;
|
||||
QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxDiskIOReadMode, m_comboBoxDiskIOWriteMode, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm,
|
||||
m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage;
|
||||
m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage, m_comboBoxTorrentContentRemoveOption;
|
||||
QLineEdit m_lineEditAppInstanceName, m_pythonExecutablePath, m_lineEditAnnounceIP, m_lineEditDHTBootstrapNodes;
|
||||
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -30,6 +31,7 @@
|
|||
|
||||
#include <QPushButton>
|
||||
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/global.h"
|
||||
#include "base/preferences.h"
|
||||
#include "uithememanager.h"
|
||||
|
@ -53,8 +55,8 @@ DeletionConfirmationDialog::DeletionConfirmationDialog(QWidget *parent, const in
|
|||
m_ui->rememberBtn->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s));
|
||||
m_ui->rememberBtn->setIconSize(Utils::Gui::mediumIconSize());
|
||||
|
||||
m_ui->checkPermDelete->setChecked(defaultDeleteFiles || Preferences::instance()->deleteTorrentFilesAsDefault());
|
||||
connect(m_ui->checkPermDelete, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState);
|
||||
m_ui->checkRemoveContent->setChecked(defaultDeleteFiles || Preferences::instance()->removeTorrentContent());
|
||||
connect(m_ui->checkRemoveContent, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState);
|
||||
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Remove"));
|
||||
m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus();
|
||||
|
||||
|
@ -67,18 +69,18 @@ DeletionConfirmationDialog::~DeletionConfirmationDialog()
|
|||
delete m_ui;
|
||||
}
|
||||
|
||||
bool DeletionConfirmationDialog::isDeleteFileSelected() const
|
||||
bool DeletionConfirmationDialog::isRemoveContentSelected() const
|
||||
{
|
||||
return m_ui->checkPermDelete->isChecked();
|
||||
return m_ui->checkRemoveContent->isChecked();
|
||||
}
|
||||
|
||||
void DeletionConfirmationDialog::updateRememberButtonState()
|
||||
{
|
||||
m_ui->rememberBtn->setEnabled(m_ui->checkPermDelete->isChecked() != Preferences::instance()->deleteTorrentFilesAsDefault());
|
||||
m_ui->rememberBtn->setEnabled(m_ui->checkRemoveContent->isChecked() != Preferences::instance()->removeTorrentContent());
|
||||
}
|
||||
|
||||
void DeletionConfirmationDialog::on_rememberBtn_clicked()
|
||||
{
|
||||
Preferences::instance()->setDeleteTorrentFilesAsDefault(m_ui->checkPermDelete->isChecked());
|
||||
Preferences::instance()->setRemoveTorrentContent(m_ui->checkRemoveContent->isChecked());
|
||||
m_ui->rememberBtn->setEnabled(false);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -37,16 +38,16 @@ namespace Ui
|
|||
class DeletionConfirmationDialog;
|
||||
}
|
||||
|
||||
class DeletionConfirmationDialog : public QDialog
|
||||
class DeletionConfirmationDialog final : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(DeletionConfirmationDialog)
|
||||
|
||||
public:
|
||||
DeletionConfirmationDialog(QWidget *parent, int size, const QString &name, bool defaultDeleteFiles);
|
||||
~DeletionConfirmationDialog();
|
||||
~DeletionConfirmationDialog() override;
|
||||
|
||||
bool isDeleteFileSelected() const;
|
||||
bool isRemoveContentSelected() const;
|
||||
|
||||
private slots:
|
||||
void updateRememberButtonState();
|
||||
|
|
|
@ -75,7 +75,7 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkPermDelete">
|
||||
<widget class="QCheckBox" name="checkRemoveContent">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
|
||||
<horstretch>0</horstretch>
|
||||
|
@ -88,7 +88,7 @@
|
|||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Also permanently delete the files</string>
|
||||
<string>Also remove the content files</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
|
48
src/gui/filterpatternformat.h
Normal file
48
src/gui/filterpatternformat.h
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QMetaEnum>
|
||||
|
||||
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
|
||||
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
|
||||
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
|
||||
inline namespace FilterPatternFormatNS
|
||||
{
|
||||
Q_NAMESPACE
|
||||
|
||||
enum class FilterPatternFormat
|
||||
{
|
||||
PlainText,
|
||||
Wildcards,
|
||||
Regex
|
||||
};
|
||||
|
||||
Q_ENUM_NS(FilterPatternFormat)
|
||||
}
|
82
src/gui/filterpatternformatmenu.cpp
Normal file
82
src/gui/filterpatternformatmenu.cpp
Normal file
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#include "filterpatternformatmenu.h"
|
||||
|
||||
#include <QActionGroup>
|
||||
|
||||
FilterPatternFormatMenu::FilterPatternFormatMenu(const FilterPatternFormat format, QWidget *parent)
|
||||
: QMenu(parent)
|
||||
{
|
||||
setTitle(tr("Pattern Format"));
|
||||
|
||||
auto *patternFormatGroup = new QActionGroup(this);
|
||||
patternFormatGroup->setExclusive(true);
|
||||
|
||||
QAction *plainTextAction = addAction(tr("Plain text"));
|
||||
plainTextAction->setCheckable(true);
|
||||
patternFormatGroup->addAction(plainTextAction);
|
||||
|
||||
QAction *wildcardsAction = addAction(tr("Wildcards"));
|
||||
wildcardsAction->setCheckable(true);
|
||||
patternFormatGroup->addAction(wildcardsAction);
|
||||
|
||||
QAction *regexAction = addAction(tr("Regular expression"));
|
||||
regexAction->setCheckable(true);
|
||||
patternFormatGroup->addAction(regexAction);
|
||||
|
||||
switch (format)
|
||||
{
|
||||
case FilterPatternFormat::Wildcards:
|
||||
default:
|
||||
wildcardsAction->setChecked(true);
|
||||
break;
|
||||
case FilterPatternFormat::PlainText:
|
||||
plainTextAction->setChecked(true);
|
||||
break;
|
||||
case FilterPatternFormat::Regex:
|
||||
regexAction->setChecked(true);
|
||||
break;
|
||||
}
|
||||
|
||||
connect(plainTextAction, &QAction::toggled, this, [this](const bool checked)
|
||||
{
|
||||
if (checked)
|
||||
emit patternFormatChanged(FilterPatternFormat::PlainText);
|
||||
});
|
||||
connect(wildcardsAction, &QAction::toggled, this, [this](const bool checked)
|
||||
{
|
||||
if (checked)
|
||||
emit patternFormatChanged(FilterPatternFormat::Wildcards);
|
||||
});
|
||||
connect(regexAction, &QAction::toggled, this, [this](const bool checked)
|
||||
{
|
||||
if (checked)
|
||||
emit patternFormatChanged(FilterPatternFormat::Regex);
|
||||
});
|
||||
}
|
45
src/gui/filterpatternformatmenu.h
Normal file
45
src/gui/filterpatternformatmenu.h
Normal file
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
*
|
||||
* In addition, as a special exception, the copyright holders give permission to
|
||||
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||
* and distribute the linked executables. You must obey the GNU General Public
|
||||
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||
* modify file(s), you may extend this exception to your version of the file(s),
|
||||
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||
* exception statement from your version.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QMenu>
|
||||
|
||||
#include "filterpatternformat.h"
|
||||
|
||||
class FilterPatternFormatMenu final : public QMenu
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY_MOVE(FilterPatternFormatMenu)
|
||||
|
||||
public:
|
||||
explicit FilterPatternFormatMenu(FilterPatternFormat format, QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void patternFormatChanged(FilterPatternFormat format);
|
||||
};
|
|
@ -175,7 +175,8 @@ void GUIAddTorrentManager::onMetadataDownloaded(const BitTorrent::TorrentInfo &m
|
|||
}
|
||||
}
|
||||
|
||||
bool GUIAddTorrentManager::processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr, const BitTorrent::AddTorrentParams ¶ms)
|
||||
bool GUIAddTorrentManager::processTorrent(const QString &source
|
||||
, const BitTorrent::TorrentDescriptor &torrentDescr, const BitTorrent::AddTorrentParams ¶ms)
|
||||
{
|
||||
const bool hasMetadata = torrentDescr.info().has_value();
|
||||
const BitTorrent::InfoHash infoHash = torrentDescr.infoHash();
|
||||
|
@ -183,32 +184,39 @@ bool GUIAddTorrentManager::processTorrent(const QString &source, const BitTorren
|
|||
// Prevent showing the dialog if download is already present
|
||||
if (BitTorrent::Torrent *torrent = btSession()->findTorrent(infoHash))
|
||||
{
|
||||
if (hasMetadata)
|
||||
if (Preferences::instance()->confirmMergeTrackers())
|
||||
{
|
||||
// Trying to set metadata to existing torrent in case if it has none
|
||||
torrent->setMetadata(*torrentDescr.info());
|
||||
}
|
||||
if (hasMetadata)
|
||||
{
|
||||
// Trying to set metadata to existing torrent in case if it has none
|
||||
torrent->setMetadata(*torrentDescr.info());
|
||||
}
|
||||
|
||||
if (torrent->isPrivate() || (hasMetadata && torrentDescr.info()->isPrivate()))
|
||||
{
|
||||
handleDuplicateTorrent(source, torrent, tr("Trackers cannot be merged because it is a private torrent"));
|
||||
const bool isPrivate = torrent->isPrivate() || (hasMetadata && torrentDescr.info()->isPrivate());
|
||||
const QString dialogCaption = tr("Torrent is already present");
|
||||
if (isPrivate)
|
||||
{
|
||||
// We cannot merge trackers for private torrent but we still notify user
|
||||
// about duplicate torrent if confirmation dialog is enabled.
|
||||
RaisedMessageBox::warning(app()->mainWindow(), dialogCaption
|
||||
, tr("Trackers cannot be merged because it is a private torrent."));
|
||||
}
|
||||
else
|
||||
{
|
||||
const bool mergeTrackers = btSession()->isMergeTrackersEnabled();
|
||||
const QMessageBox::StandardButton btn = RaisedMessageBox::question(app()->mainWindow(), dialogCaption
|
||||
, tr("Torrent '%1' is already in the transfer list. Do you want to merge trackers from new source?").arg(torrent->name())
|
||||
, (QMessageBox::Yes | QMessageBox::No), (mergeTrackers ? QMessageBox::Yes : QMessageBox::No));
|
||||
if (btn == QMessageBox::Yes)
|
||||
{
|
||||
torrent->addTrackers(torrentDescr.trackers());
|
||||
torrent->addUrlSeeds(torrentDescr.urlSeeds());
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
bool mergeTrackers = btSession()->isMergeTrackersEnabled();
|
||||
if (Preferences::instance()->confirmMergeTrackers())
|
||||
{
|
||||
const QMessageBox::StandardButton btn = RaisedMessageBox::question(app()->mainWindow(), tr("Torrent is already present")
|
||||
, tr("Torrent '%1' is already in the transfer list. Do you want to merge trackers from new source?").arg(torrent->name())
|
||||
, (QMessageBox::Yes | QMessageBox::No), QMessageBox::Yes);
|
||||
mergeTrackers = (btn == QMessageBox::Yes);
|
||||
}
|
||||
|
||||
if (mergeTrackers)
|
||||
{
|
||||
torrent->addTrackers(torrentDescr.trackers());
|
||||
torrent->addUrlSeeds(torrentDescr.urlSeeds());
|
||||
}
|
||||
handleDuplicateTorrent(source, torrentDescr, torrent);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -227,15 +235,22 @@ bool GUIAddTorrentManager::processTorrent(const QString &source, const BitTorren
|
|||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||
m_dialogs[infoHash] = dlg;
|
||||
connect(dlg, &AddNewTorrentDialog::torrentAccepted, this
|
||||
, [this, source](const BitTorrent::TorrentDescriptor &torrentDescr, const BitTorrent::AddTorrentParams &addTorrentParams)
|
||||
{
|
||||
addTorrentToSession(source, torrentDescr, addTorrentParams);
|
||||
});
|
||||
connect(dlg, &QDialog::finished, this, [this, source, infoHash, dlg]
|
||||
, [this, source, dlg](const BitTorrent::TorrentDescriptor &torrentDescr, const BitTorrent::AddTorrentParams &addTorrentParams)
|
||||
{
|
||||
if (dlg->isDoNotDeleteTorrentChecked())
|
||||
releaseTorrentFileGuard(source);
|
||||
{
|
||||
if (auto torrentFileGuard = releaseTorrentFileGuard(source))
|
||||
torrentFileGuard->setAutoRemove(false);
|
||||
}
|
||||
|
||||
addTorrentToSession(source, torrentDescr, addTorrentParams);
|
||||
});
|
||||
connect(dlg, &AddNewTorrentDialog::torrentRejected, this, [this, source]
|
||||
{
|
||||
releaseTorrentFileGuard(source);
|
||||
});
|
||||
connect(dlg, &QDialog::finished, this, [this, source, infoHash]
|
||||
{
|
||||
m_dialogs.remove(infoHash);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2023-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2024 Jonathan Ketchker
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
|
@ -30,6 +30,7 @@
|
|||
|
||||
#include "optionsdialog.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <limits>
|
||||
|
@ -44,6 +45,10 @@
|
|||
#include <QSystemTrayIcon>
|
||||
#include <QTranslator>
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
#include <QStyleFactory>
|
||||
#endif
|
||||
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/bittorrent/sharelimitaction.h"
|
||||
#include "base/exceptions.h"
|
||||
|
@ -56,6 +61,7 @@
|
|||
#include "base/rss/rss_session.h"
|
||||
#include "base/torrentfileguard.h"
|
||||
#include "base/torrentfileswatcher.h"
|
||||
#include "base/utils/compare.h"
|
||||
#include "base/utils/io.h"
|
||||
#include "base/utils/misc.h"
|
||||
#include "base/utils/net.h"
|
||||
|
@ -236,6 +242,8 @@ void OptionsDialog::loadBehaviorTabOptions()
|
|||
initializeLanguageCombo();
|
||||
setLocale(pref->getLocale());
|
||||
|
||||
initializeStyleCombo();
|
||||
|
||||
m_ui->checkUseCustomTheme->setChecked(Preferences::instance()->useCustomUITheme());
|
||||
m_ui->customThemeFilePath->setSelectedPath(Preferences::instance()->customUIThemePath());
|
||||
m_ui->customThemeFilePath->setMode(FileSystemPathEdit::Mode::FileOpen);
|
||||
|
@ -345,7 +353,11 @@ void OptionsDialog::loadBehaviorTabOptions()
|
|||
|
||||
m_ui->checkBoxPerformanceWarning->setChecked(session->isPerformanceWarningEnabled());
|
||||
|
||||
connect(m_ui->comboI18n, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
|
||||
connect(m_ui->comboLanguage, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
connect(m_ui->comboStyle, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
|
||||
#endif
|
||||
|
||||
#if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
|
||||
connect(m_ui->checkUseSystemIcon, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
|
||||
|
@ -443,6 +455,10 @@ void OptionsDialog::saveBehaviorTabOptions() const
|
|||
}
|
||||
pref->setLocale(locale);
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
pref->setStyle(m_ui->comboStyle->currentData().toString());
|
||||
#endif
|
||||
|
||||
#if (defined(Q_OS_UNIX) && !defined(Q_OS_MACOS))
|
||||
pref->useSystemIcons(m_ui->checkUseSystemIcon->isChecked());
|
||||
#endif
|
||||
|
@ -1387,7 +1403,7 @@ void OptionsDialog::initializeLanguageCombo()
|
|||
for (const QString &langFile : langFiles)
|
||||
{
|
||||
const QString langCode = QStringView(langFile).sliced(12).chopped(3).toString(); // remove "qbittorrent_" and ".qm"
|
||||
m_ui->comboI18n->addItem(Utils::Misc::languageToLocalizedString(langCode), langCode);
|
||||
m_ui->comboLanguage->addItem(Utils::Misc::languageToLocalizedString(langCode), langCode);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1672,6 +1688,34 @@ bool OptionsDialog::isSplashScreenDisabled() const
|
|||
return !m_ui->checkShowSplash->isChecked();
|
||||
}
|
||||
|
||||
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));
|
||||
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);
|
||||
|
||||
QStringList styleNames = QStyleFactory::keys();
|
||||
std::sort(styleNames.begin(), styleNames.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
|
||||
for (const QString &styleName : asConst(styleNames))
|
||||
m_ui->comboStyle->addItem(styleName, styleName);
|
||||
|
||||
const QString prefStyleName = Preferences::instance()->getStyle();
|
||||
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->UISettingsBoxLayout->removeWidget(m_ui->labelStyle);
|
||||
m_ui->UISettingsBoxLayout->removeWidget(m_ui->comboStyle);
|
||||
m_ui->UISettingsBoxLayout->removeWidget(m_ui->labelStyleHint);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
bool OptionsDialog::WinStartup() const
|
||||
{
|
||||
|
@ -1721,7 +1765,7 @@ QString OptionsDialog::getProxyPassword() const
|
|||
// Locale Settings
|
||||
QString OptionsDialog::getLocale() const
|
||||
{
|
||||
return m_ui->comboI18n->itemData(m_ui->comboI18n->currentIndex(), Qt::UserRole).toString();
|
||||
return m_ui->comboLanguage->itemData(m_ui->comboLanguage->currentIndex(), Qt::UserRole).toString();
|
||||
}
|
||||
|
||||
void OptionsDialog::setLocale(const QString &localeStr)
|
||||
|
@ -1746,7 +1790,7 @@ void OptionsDialog::setLocale(const QString &localeStr)
|
|||
name = locale.name();
|
||||
}
|
||||
// Attempt to find exact match
|
||||
int index = m_ui->comboI18n->findData(name, Qt::UserRole);
|
||||
int index = m_ui->comboLanguage->findData(name, Qt::UserRole);
|
||||
if (index < 0)
|
||||
{
|
||||
//Attempt to find a language match without a country
|
||||
|
@ -1754,16 +1798,16 @@ void OptionsDialog::setLocale(const QString &localeStr)
|
|||
if (pos > -1)
|
||||
{
|
||||
QString lang = name.left(pos);
|
||||
index = m_ui->comboI18n->findData(lang, Qt::UserRole);
|
||||
index = m_ui->comboLanguage->findData(lang, Qt::UserRole);
|
||||
}
|
||||
}
|
||||
if (index < 0)
|
||||
{
|
||||
// Unrecognized, use US English
|
||||
index = m_ui->comboI18n->findData(u"en"_s, Qt::UserRole);
|
||||
index = m_ui->comboLanguage->findData(u"en"_s, Qt::UserRole);
|
||||
Q_ASSERT(index >= 0);
|
||||
}
|
||||
m_ui->comboI18n->setCurrentIndex(index);
|
||||
m_ui->comboLanguage->setCurrentIndex(index);
|
||||
}
|
||||
|
||||
Path OptionsDialog::getTorrentExportDir() const
|
||||
|
@ -1869,7 +1913,7 @@ Path OptionsDialog::getFilter() const
|
|||
void OptionsDialog::webUIHttpsCertChanged(const Path &path)
|
||||
{
|
||||
const auto readResult = Utils::IO::readFile(path, Utils::Net::MAX_SSL_FILE_SIZE);
|
||||
const bool isCertValid = !Utils::SSLKey::load(readResult.value_or(QByteArray())).isNull();
|
||||
const bool isCertValid = Utils::Net::isSSLCertificatesValid(readResult.value_or(QByteArray()));
|
||||
|
||||
m_ui->textWebUIHttpsCert->setSelectedPath(path);
|
||||
m_ui->lblSslCertStatus->setPixmap(UIThemeManager::instance()->getScaledPixmap(
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -143,6 +143,7 @@ private:
|
|||
|
||||
// General options
|
||||
void initializeLanguageCombo();
|
||||
void initializeStyleCombo();
|
||||
QString getLocale() const;
|
||||
bool isSplashScreenDisabled() const;
|
||||
#ifdef Q_OS_WIN
|
||||
|
|
|
@ -132,9 +132,9 @@
|
|||
<property name="title">
|
||||
<string>Interface</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_81">
|
||||
<layout class="QGridLayout" name="UISettingsBoxLayout">
|
||||
<item row="0" column="0" colspan="3">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<widget class="QLabel" name="labelRestartRequired">
|
||||
<property name="font">
|
||||
<font>
|
||||
<italic>true</italic>
|
||||
|
@ -146,17 +146,17 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<widget class="QLabel" name="labelLanguage">
|
||||
<property name="text">
|
||||
<string>Language:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="comboI18n"/>
|
||||
<widget class="QComboBox" name="comboLanguage"/>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<spacer name="horizontalSpacer_111">
|
||||
<spacer name="spacerLanguage">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
|
@ -168,7 +168,26 @@
|
|||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="3">
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="labelStyle">
|
||||
<property name="text">
|
||||
<string>Style:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="comboStyle"/>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QLabel" name="labelStyleHint">
|
||||
<property name="font">
|
||||
<font>
|
||||
<italic>true</italic>
|
||||
</font>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="3">
|
||||
<widget class="QGroupBox" name="checkUseCustomTheme">
|
||||
<property name="title">
|
||||
<string>Use custom UI Theme</string>
|
||||
|
@ -190,14 +209,14 @@
|
|||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<item row="4" column="0" colspan="2">
|
||||
<widget class="QCheckBox" name="checkUseSystemIcon">
|
||||
<property name="text">
|
||||
<string>Use icons from system theme</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0" colspan="2">
|
||||
<item row="5" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="buttonCustomizeUITheme">
|
||||
<property name="text">
|
||||
<string>Customize UI Theme...</string>
|
||||
|
@ -3881,7 +3900,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
|
|||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>tabOption</tabstop>
|
||||
<tabstop>comboI18n</tabstop>
|
||||
<tabstop>comboLanguage</tabstop>
|
||||
<tabstop>checkUseCustomTheme</tabstop>
|
||||
<tabstop>customThemeFilePath</tabstop>
|
||||
<tabstop>checkAddStopped</tabstop>
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
#ifdef Q_OS_MACOS
|
||||
#include <IOKit/pwr_mgt/IOPMLib.h>
|
||||
#include <QScopeGuard>
|
||||
#endif
|
||||
|
||||
#ifdef Q_OS_WIN
|
||||
|
@ -74,8 +75,10 @@ void PowerManagement::setBusy()
|
|||
#elif defined(QBT_USES_DBUS)
|
||||
m_inhibitor->requestBusy();
|
||||
#elif defined(Q_OS_MACOS)
|
||||
const CFStringRef assertName = tr("qBittorrent is active").toCFString();
|
||||
[[maybe_unused]] const auto assertNameGuard = qScopeGuard([&assertName] { ::CFRelease(assertName); });
|
||||
const IOReturn success = ::IOPMAssertionCreateWithName(kIOPMAssertionTypeNoIdleSleep, kIOPMAssertionLevelOn
|
||||
, tr("qBittorrent is active").toCFString(), &m_assertionID);
|
||||
, assertName, &m_assertionID);
|
||||
if (success != kIOReturnSuccess)
|
||||
m_busy = false;
|
||||
#endif
|
||||
|
|
|
@ -29,6 +29,9 @@
|
|||
|
||||
#include "programupdater.h"
|
||||
|
||||
#include <libtorrent/version.hpp>
|
||||
|
||||
#include <QtCore/qconfig.h>
|
||||
#include <QtSystemDetection>
|
||||
#include <QDebug>
|
||||
#include <QDesktopServices>
|
||||
|
@ -61,6 +64,20 @@ namespace
|
|||
}
|
||||
return (newVersion > currentVersion);
|
||||
}
|
||||
|
||||
QString buildVariant()
|
||||
{
|
||||
#if defined(Q_OS_MACOS)
|
||||
const auto BASE_OS = u"Mac OS X"_s;
|
||||
#elif defined(Q_OS_WIN)
|
||||
const auto BASE_OS = u"Windows x64"_s;
|
||||
#endif
|
||||
|
||||
if constexpr ((QT_VERSION_MAJOR == 6) && (LIBTORRENT_VERSION_MAJOR == 1))
|
||||
return BASE_OS;
|
||||
|
||||
return u"%1 (qt%2 lt%3%4)"_s.arg(BASE_OS, QString::number(QT_VERSION_MAJOR), QString::number(LIBTORRENT_VERSION_MAJOR), QString::number(LIBTORRENT_VERSION_MINOR));
|
||||
}
|
||||
}
|
||||
|
||||
void ProgramUpdater::checkForUpdates() const
|
||||
|
@ -97,12 +114,7 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
|
|||
: QString {};
|
||||
};
|
||||
|
||||
#ifdef Q_OS_MACOS
|
||||
const QString OS_TYPE = u"Mac OS X"_s;
|
||||
#elif defined(Q_OS_WIN)
|
||||
const QString OS_TYPE = u"Windows x64"_s;
|
||||
#endif
|
||||
|
||||
const QString variant = buildVariant();
|
||||
bool inItem = false;
|
||||
QString version;
|
||||
QString updateLink;
|
||||
|
@ -128,7 +140,7 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
|
|||
{
|
||||
if (inItem && (xml.name() == u"item"))
|
||||
{
|
||||
if (type.compare(OS_TYPE, Qt::CaseInsensitive) == 0)
|
||||
if (type.compare(variant, Qt::CaseInsensitive) == 0)
|
||||
{
|
||||
qDebug("The last update available is %s", qUtf8Printable(version));
|
||||
if (!version.isEmpty())
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -46,9 +47,9 @@ namespace
|
|||
}
|
||||
|
||||
DownloadedPiecesBar::DownloadedPiecesBar(QWidget *parent)
|
||||
: base {parent}
|
||||
, m_dlPieceColor {dlPieceColor(pieceColor())}
|
||||
: base(parent)
|
||||
{
|
||||
updateColorsImpl();
|
||||
}
|
||||
|
||||
QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin, int reqSize)
|
||||
|
@ -128,25 +129,24 @@ QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin
|
|||
return result;
|
||||
}
|
||||
|
||||
bool DownloadedPiecesBar::updateImage(QImage &image)
|
||||
QImage DownloadedPiecesBar::renderImage()
|
||||
{
|
||||
// qDebug() << "updateImage";
|
||||
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888);
|
||||
if (image2.isNull())
|
||||
QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
|
||||
if (image.isNull())
|
||||
{
|
||||
qDebug() << "QImage image2() allocation failed, width():" << width();
|
||||
return false;
|
||||
qDebug() << "QImage allocation failed, width():" << width();
|
||||
return image;
|
||||
}
|
||||
|
||||
if (m_pieces.isEmpty())
|
||||
{
|
||||
image2.fill(backgroundColor());
|
||||
image = image2;
|
||||
return true;
|
||||
image.fill(backgroundColor());
|
||||
return image;
|
||||
}
|
||||
|
||||
QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image2.width());
|
||||
QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image2.width());
|
||||
QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image.width());
|
||||
QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image.width());
|
||||
|
||||
// filling image
|
||||
for (int x = 0; x < scaledPieces.size(); ++x)
|
||||
|
@ -161,15 +161,15 @@ bool DownloadedPiecesBar::updateImage(QImage &image)
|
|||
QRgb mixedColor = mixTwoColors(pieceColor().rgb(), m_dlPieceColor.rgb(), ratio);
|
||||
mixedColor = mixTwoColors(backgroundColor().rgb(), mixedColor, fillRatio);
|
||||
|
||||
image2.setPixel(x, 0, mixedColor);
|
||||
image.setPixel(x, 0, mixedColor);
|
||||
}
|
||||
else
|
||||
{
|
||||
image2.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
image.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
}
|
||||
}
|
||||
image = image2;
|
||||
return true;
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::setProgress(const QBitArray &pieces, const QBitArray &downloadedPieces)
|
||||
|
@ -177,7 +177,7 @@ void DownloadedPiecesBar::setProgress(const QBitArray &pieces, const QBitArray &
|
|||
m_pieces = pieces;
|
||||
m_downloadedPieces = downloadedPieces;
|
||||
|
||||
requestImageUpdate();
|
||||
redraw();
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::clear()
|
||||
|
@ -198,3 +198,14 @@ QString DownloadedPiecesBar::simpleToolTipText() const
|
|||
+ u"</table>";
|
||||
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::updateColors()
|
||||
{
|
||||
PiecesBar::updateColors();
|
||||
updateColorsImpl();
|
||||
}
|
||||
|
||||
void DownloadedPiecesBar::updateColorsImpl()
|
||||
{
|
||||
m_dlPieceColor = dlPieceColor(pieceColor());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -52,11 +53,13 @@ public:
|
|||
private:
|
||||
// scale bitfield vector to float vector
|
||||
QVector<float> bitfieldToFloatVector(const QBitArray &vecin, int reqSize);
|
||||
bool updateImage(QImage &image) override;
|
||||
QImage renderImage() override;
|
||||
QString simpleToolTipText() const override;
|
||||
void updateColors() override;
|
||||
void updateColorsImpl();
|
||||
|
||||
// incomplete piece color
|
||||
const QColor m_dlPieceColor;
|
||||
QColor m_dlPieceColor;
|
||||
// last used bitfields, uses to better resize redraw
|
||||
// TODO: make a diff pieces to new pieces and update only changed pixels, speedup when update > 20x faster
|
||||
QBitArray m_pieces;
|
||||
|
|
|
@ -411,7 +411,7 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
|
|||
return;
|
||||
|
||||
// Remove I2P peers since they will be completely reloaded.
|
||||
for (QStandardItem *item : asConst(m_I2PPeerItems))
|
||||
for (const QStandardItem *item : asConst(m_I2PPeerItems))
|
||||
m_listModel->removeRow(item->row());
|
||||
m_I2PPeerItems.clear();
|
||||
|
||||
|
@ -420,7 +420,8 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
|
|||
for (auto i = m_peerItems.cbegin(); i != m_peerItems.cend(); ++i)
|
||||
existingPeers.insert(i.key());
|
||||
|
||||
const bool hideZeroValues = Preferences::instance()->getHideZeroValues();
|
||||
const Preferences *pref = Preferences::instance();
|
||||
const bool hideZeroValues = (pref->getHideZeroValues() && (pref->getHideZeroComboValues() == 0));
|
||||
for (const BitTorrent::PeerInfo &peer : peers)
|
||||
{
|
||||
const PeerEndpoint peerEndpoint {peer.address(), peer.connectionType()};
|
||||
|
@ -466,10 +467,14 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
|
|||
{
|
||||
QStandardItem *item = m_peerItems.take(peerEndpoint);
|
||||
|
||||
QSet<QStandardItem *> &items = m_itemsByIP[peerEndpoint.address.ip];
|
||||
items.remove(item);
|
||||
if (items.isEmpty())
|
||||
m_itemsByIP.remove(peerEndpoint.address.ip);
|
||||
const auto items = m_itemsByIP.find(peerEndpoint.address.ip);
|
||||
Q_ASSERT(items != m_itemsByIP.end());
|
||||
if (items == m_itemsByIP.end()) [[unlikely]]
|
||||
continue;
|
||||
|
||||
items->remove(item);
|
||||
if (items->isEmpty())
|
||||
m_itemsByIP.erase(items);
|
||||
|
||||
m_listModel->removeRow(item->row());
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -126,39 +127,38 @@ QVector<float> PieceAvailabilityBar::intToFloatVector(const QVector<int> &vecin,
|
|||
return result;
|
||||
}
|
||||
|
||||
bool PieceAvailabilityBar::updateImage(QImage &image)
|
||||
QImage PieceAvailabilityBar::renderImage()
|
||||
{
|
||||
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888);
|
||||
if (image2.isNull())
|
||||
QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
|
||||
if (image.isNull())
|
||||
{
|
||||
qDebug() << "QImage image2() allocation failed, width():" << width();
|
||||
return false;
|
||||
qDebug() << "QImage allocation failed, width():" << width();
|
||||
return image;
|
||||
}
|
||||
|
||||
if (m_pieces.empty())
|
||||
{
|
||||
image2.fill(backgroundColor());
|
||||
image = image2;
|
||||
return true;
|
||||
image.fill(backgroundColor());
|
||||
return image;
|
||||
}
|
||||
|
||||
QVector<float> scaledPieces = intToFloatVector(m_pieces, image2.width());
|
||||
QVector<float> scaledPieces = intToFloatVector(m_pieces, image.width());
|
||||
|
||||
// filling image
|
||||
for (int x = 0; x < scaledPieces.size(); ++x)
|
||||
{
|
||||
float piecesToValue = scaledPieces.at(x);
|
||||
image2.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
image.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
|
||||
}
|
||||
image = image2;
|
||||
return true;
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void PieceAvailabilityBar::setAvailability(const QVector<int> &avail)
|
||||
{
|
||||
m_pieces = avail;
|
||||
|
||||
requestImageUpdate();
|
||||
redraw();
|
||||
}
|
||||
|
||||
void PieceAvailabilityBar::clear()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -46,7 +47,7 @@ public:
|
|||
void clear() override;
|
||||
|
||||
private:
|
||||
bool updateImage(QImage &image) override;
|
||||
QImage renderImage() override;
|
||||
QString simpleToolTipText() const override;
|
||||
|
||||
// last used int vector, uses to better resize redraw
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2016 Eugene Shalygin
|
||||
* Copyright (C) 2006 Christophe Dumez
|
||||
*
|
||||
|
@ -114,10 +115,10 @@ namespace
|
|||
}
|
||||
|
||||
PiecesBar::PiecesBar(QWidget *parent)
|
||||
: QWidget {parent}
|
||||
: QWidget(parent)
|
||||
{
|
||||
updatePieceColors();
|
||||
setMouseTracking(true);
|
||||
updateColorsImpl();
|
||||
}
|
||||
|
||||
void PiecesBar::setTorrent(const BitTorrent::Torrent *torrent)
|
||||
|
@ -135,12 +136,19 @@ void PiecesBar::clear()
|
|||
|
||||
bool PiecesBar::event(QEvent *e)
|
||||
{
|
||||
if (e->type() == QEvent::ToolTip)
|
||||
const QEvent::Type eventType = e->type();
|
||||
if (eventType == QEvent::ToolTip)
|
||||
{
|
||||
showToolTip(static_cast<QHelpEvent *>(e));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (eventType == QEvent::PaletteChange)
|
||||
{
|
||||
updateColors();
|
||||
redraw();
|
||||
}
|
||||
|
||||
return base::event(e);
|
||||
}
|
||||
|
||||
|
@ -154,7 +162,7 @@ void PiecesBar::leaveEvent(QEvent *e)
|
|||
{
|
||||
m_hovered = false;
|
||||
m_highlightedRegion = {};
|
||||
requestImageUpdate();
|
||||
redraw();
|
||||
base::leaveEvent(e);
|
||||
}
|
||||
|
||||
|
@ -178,16 +186,17 @@ void PiecesBar::paintEvent(QPaintEvent *)
|
|||
else
|
||||
{
|
||||
if (m_image.width() != imageRect.width())
|
||||
updateImage(m_image);
|
||||
{
|
||||
if (const QImage image = renderImage(); !image.isNull())
|
||||
m_image = image;
|
||||
}
|
||||
painter.drawImage(imageRect, m_image);
|
||||
}
|
||||
|
||||
if (!m_highlightedRegion.isNull())
|
||||
{
|
||||
QColor highlightColor {this->palette().color(QPalette::Active, QPalette::Highlight)};
|
||||
highlightColor.setAlphaF(0.35f);
|
||||
QRect targetHighlightRect {m_highlightedRegion.adjusted(borderWidth, borderWidth, borderWidth, height() - 2 * borderWidth)};
|
||||
painter.fillRect(targetHighlightRect, highlightColor);
|
||||
painter.fillRect(targetHighlightRect, highlightedPieceColor());
|
||||
}
|
||||
|
||||
QPainterPath border;
|
||||
|
@ -196,30 +205,40 @@ void PiecesBar::paintEvent(QPaintEvent *)
|
|||
painter.drawPath(border);
|
||||
}
|
||||
|
||||
void PiecesBar::requestImageUpdate()
|
||||
void PiecesBar::redraw()
|
||||
{
|
||||
if (updateImage(m_image))
|
||||
if (const QImage image = renderImage(); !image.isNull())
|
||||
{
|
||||
m_image = image;
|
||||
update();
|
||||
}
|
||||
}
|
||||
|
||||
QColor PiecesBar::backgroundColor() const
|
||||
{
|
||||
return palette().color(QPalette::Base);
|
||||
return palette().color(QPalette::Active, QPalette::Base);
|
||||
}
|
||||
|
||||
QColor PiecesBar::borderColor() const
|
||||
{
|
||||
return palette().color(QPalette::Dark);
|
||||
return palette().color(QPalette::Active, QPalette::Dark);
|
||||
}
|
||||
|
||||
QColor PiecesBar::pieceColor() const
|
||||
{
|
||||
return palette().color(QPalette::Highlight);
|
||||
return palette().color(QPalette::Active, QPalette::Highlight);
|
||||
}
|
||||
|
||||
QColor PiecesBar::highlightedPieceColor() const
|
||||
{
|
||||
QColor col = palette().color(QPalette::Highlight).darker();
|
||||
col.setAlphaF(0.35);
|
||||
return col;
|
||||
}
|
||||
|
||||
QColor PiecesBar::colorBoxBorderColor() const
|
||||
{
|
||||
return palette().color(QPalette::ToolTipText);
|
||||
return palette().color(QPalette::Active, QPalette::ToolTipText);
|
||||
}
|
||||
|
||||
const QVector<QRgb> &PiecesBar::pieceColors() const
|
||||
|
@ -325,12 +344,17 @@ void PiecesBar::highlightFile(int imagePos)
|
|||
}
|
||||
}
|
||||
|
||||
void PiecesBar::updatePieceColors()
|
||||
void PiecesBar::updateColors()
|
||||
{
|
||||
updateColorsImpl();
|
||||
}
|
||||
|
||||
void PiecesBar::updateColorsImpl()
|
||||
{
|
||||
m_pieceColors = QVector<QRgb>(256);
|
||||
for (int i = 0; i < 256; ++i)
|
||||
{
|
||||
float ratio = (i / 255.0);
|
||||
const float ratio = (i / 255.0);
|
||||
m_pieceColors[i] = mixTwoColors(backgroundColor().rgb(), pieceColor().rgb(), ratio);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2016 Eugene Shalygin
|
||||
* Copyright (C) 2006 Christophe Dumez
|
||||
*
|
||||
|
@ -54,22 +55,22 @@ public:
|
|||
|
||||
virtual void clear();
|
||||
|
||||
// QObject interface
|
||||
bool event(QEvent *e) override;
|
||||
|
||||
protected:
|
||||
// QWidget interface
|
||||
bool event(QEvent *e) override;
|
||||
void enterEvent(QEnterEvent *e) override;
|
||||
void leaveEvent(QEvent *e) override;
|
||||
void mouseMoveEvent(QMouseEvent *e) override;
|
||||
|
||||
void paintEvent(QPaintEvent *e) override;
|
||||
void requestImageUpdate();
|
||||
|
||||
virtual void updateColors();
|
||||
void redraw();
|
||||
|
||||
QColor backgroundColor() const;
|
||||
QColor borderColor() const;
|
||||
QColor pieceColor() const;
|
||||
QColor highlightedPieceColor() const;
|
||||
QColor colorBoxBorderColor() const;
|
||||
|
||||
const QVector<QRgb> &pieceColors() const;
|
||||
|
||||
// mix two colors by light model, ratio <0, 1>
|
||||
|
@ -82,11 +83,9 @@ private:
|
|||
void highlightFile(int imagePos);
|
||||
|
||||
virtual QString simpleToolTipText() const = 0;
|
||||
virtual QImage renderImage() = 0;
|
||||
|
||||
// draw new image to replace the actual image
|
||||
// returns true if image was successfully updated
|
||||
virtual bool updateImage(QImage &image) = 0;
|
||||
void updatePieceColors();
|
||||
void updateColorsImpl();
|
||||
|
||||
const BitTorrent::Torrent *m_torrent = nullptr;
|
||||
QImage m_image;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -52,6 +52,7 @@
|
|||
#include "base/utils/misc.h"
|
||||
#include "base/utils/string.h"
|
||||
#include "gui/autoexpandabledialog.h"
|
||||
#include "gui/filterpatternformatmenu.h"
|
||||
#include "gui/lineedit.h"
|
||||
#include "gui/trackerlist/trackerlistwidget.h"
|
||||
#include "gui/uithememanager.h"
|
||||
|
@ -66,6 +67,7 @@
|
|||
PropertiesWidget::PropertiesWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
, m_ui {new Ui::PropertiesWidget}
|
||||
, m_storeFilterPatternFormat {u"GUI/PropertiesWidget/FilterPatternFormat"_s}
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
#ifndef Q_OS_MACOS
|
||||
|
@ -78,7 +80,9 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
|
|||
m_contentFilterLine = new LineEdit(this);
|
||||
m_contentFilterLine->setPlaceholderText(tr("Filter files..."));
|
||||
m_contentFilterLine->setFixedWidth(300);
|
||||
connect(m_contentFilterLine, &LineEdit::textChanged, m_ui->filesList, &TorrentContentWidget::setFilterPattern);
|
||||
m_contentFilterLine->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_contentFilterLine, &QWidget::customContextMenuRequested, this, &PropertiesWidget::showContentFilterContextMenu);
|
||||
connect(m_contentFilterLine, &LineEdit::textChanged, this, &PropertiesWidget::setContentFilterPattern);
|
||||
m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
|
||||
|
||||
m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open);
|
||||
|
@ -206,6 +210,7 @@ void PropertiesWidget::clear()
|
|||
m_ui->labelSavePathVal->clear();
|
||||
m_ui->labelCreatedOnVal->clear();
|
||||
m_ui->labelTotalPiecesVal->clear();
|
||||
m_ui->labelPrivateVal->clear();
|
||||
m_ui->labelInfohash1Val->clear();
|
||||
m_ui->labelInfohash2Val->clear();
|
||||
m_ui->labelCommentVal->clear();
|
||||
|
@ -274,6 +279,28 @@ void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent)
|
|||
m_ui->labelSavePathVal->setText(m_torrent->savePath().toString());
|
||||
}
|
||||
|
||||
void PropertiesWidget::showContentFilterContextMenu()
|
||||
{
|
||||
QMenu *menu = m_contentFilterLine->createStandardContextMenu();
|
||||
|
||||
auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu);
|
||||
connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format)
|
||||
{
|
||||
m_storeFilterPatternFormat = format;
|
||||
setContentFilterPattern();
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
menu->addMenu(formatMenu);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
menu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
void PropertiesWidget::setContentFilterPattern()
|
||||
{
|
||||
m_ui->filesList->setFilterPattern(m_contentFilterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards));
|
||||
}
|
||||
|
||||
void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent)
|
||||
{
|
||||
if (torrent == m_torrent)
|
||||
|
@ -309,7 +336,14 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
|
|||
m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped()));
|
||||
|
||||
m_ui->labelCreatedByVal->setText(m_torrent->creator());
|
||||
|
||||
m_ui->labelPrivateVal->setText(m_torrent->isPrivate() ? tr("Yes") : tr("No"));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ui->labelPrivateVal->setText(tr("N/A"));
|
||||
}
|
||||
|
||||
// Load dynamic data
|
||||
loadDynamicData();
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -32,7 +32,8 @@
|
|||
#include <QList>
|
||||
#include <QWidget>
|
||||
|
||||
#include "base/pathfwd.h"
|
||||
#include "base/settingvalue.h"
|
||||
#include "gui/filterpatternformat.h"
|
||||
|
||||
class QPushButton;
|
||||
class QTreeView;
|
||||
|
@ -102,6 +103,8 @@ private slots:
|
|||
|
||||
private:
|
||||
QPushButton *getButtonFromIndex(int index);
|
||||
void showContentFilterContextMenu();
|
||||
void setContentFilterPattern();
|
||||
|
||||
Ui::PropertiesWidget *m_ui = nullptr;
|
||||
BitTorrent::Torrent *m_torrent = nullptr;
|
||||
|
@ -115,4 +118,6 @@ private:
|
|||
PropTabBar *m_tabBar = nullptr;
|
||||
LineEdit *m_contentFilterLine = nullptr;
|
||||
int m_handleWidth = -1;
|
||||
|
||||
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
|
||||
};
|
||||
|
|
|
@ -823,6 +823,38 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="labelPrivate">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Private:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="5">
|
||||
<widget class="QLabel" name="labelPrivateVal">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="labelInfohash1">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
|
@ -838,71 +870,7 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="labelInfohash2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Info Hash v2:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1" colspan="5">
|
||||
<widget class="QLabel" name="labelInfohash2Val">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="labelSavePath">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save Path:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="labelComment">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Comment:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1" colspan="5">
|
||||
<widget class="QLabel" name="labelInfohash1Val">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
|
@ -918,7 +886,55 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="labelInfohash2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Info Hash v2:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1" colspan="5">
|
||||
<widget class="QLabel" name="labelInfohash2Val">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
<property name="textInteractionFlags">
|
||||
<set>Qt::TextSelectableByMouse</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="labelSavePath">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Save Path:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1" colspan="5">
|
||||
<widget class="QLabel" name="labelSavePathVal">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
|
||||
|
@ -937,7 +953,23 @@
|
|||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1" colspan="5">
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="labelComment">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Comment:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1" colspan="5">
|
||||
<widget class="QLabel" name="labelCommentVal">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
|
|
|
@ -106,10 +106,12 @@ void HtmlBrowser::resourceLoaded(QNetworkReply *reply)
|
|||
atts[QNetworkRequest::HttpStatusCodeAttribute] = 200;
|
||||
atts[QNetworkRequest::HttpReasonPhraseAttribute] = u"Ok"_s;
|
||||
metaData.setAttributes(atts);
|
||||
metaData.setLastModified(QDateTime::currentDateTime());
|
||||
metaData.setExpirationDate(QDateTime::currentDateTime().addDays(1));
|
||||
const auto currentDateTime = QDateTime::currentDateTime();
|
||||
metaData.setLastModified(currentDateTime);
|
||||
metaData.setExpirationDate(currentDateTime.addDays(1));
|
||||
QIODevice *dev = m_diskCache->prepare(metaData);
|
||||
if (!dev) return;
|
||||
if (!dev)
|
||||
return;
|
||||
|
||||
QApplication::style()->standardIcon(QStyle::SP_MessageBoxWarning).pixmap(32, 32).save(dev, "PNG");
|
||||
m_diskCache->insert(dev);
|
||||
|
|
|
@ -126,6 +126,8 @@ RSSWidget::RSSWidget(IGUIApplication *app, QWidget *parent)
|
|||
, this, &RSSWidget::handleSessionProcessingStateChanged);
|
||||
connect(RSS::Session::instance()->rootFolder(), &RSS::Folder::unreadCountChanged
|
||||
, this, &RSSWidget::handleUnreadCountChanged);
|
||||
|
||||
m_ui->textBrowser->installEventFilter(this);
|
||||
}
|
||||
|
||||
RSSWidget::~RSSWidget()
|
||||
|
@ -494,60 +496,11 @@ void RSSWidget::handleCurrentArticleItemChanged(QListWidgetItem *currentItem, QL
|
|||
article->markAsRead();
|
||||
}
|
||||
|
||||
if (!currentItem) return;
|
||||
if (!currentItem)
|
||||
return;
|
||||
|
||||
auto *article = m_articleListWidget->getRSSArticle(currentItem);
|
||||
Q_ASSERT(article);
|
||||
|
||||
const QString highlightedBaseColor = m_ui->textBrowser->palette().color(QPalette::Highlight).name();
|
||||
const QString highlightedBaseTextColor = m_ui->textBrowser->palette().color(QPalette::HighlightedText).name();
|
||||
const QString alternateBaseColor = m_ui->textBrowser->palette().color(QPalette::AlternateBase).name();
|
||||
|
||||
QString html =
|
||||
u"<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>" +
|
||||
u"<div style='background-color: \"%1\"; font-weight: bold; color: \"%2\";'>%3</div>"_s.arg(highlightedBaseColor, highlightedBaseTextColor, article->title());
|
||||
if (article->date().isValid())
|
||||
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Date: "), QLocale::system().toString(article->date().toLocalTime()));
|
||||
if (m_feedListWidget->currentItem() == m_feedListWidget->stickyUnreadItem())
|
||||
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Feed: "), article->feed()->title());
|
||||
if (!article->author().isEmpty())
|
||||
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Author: "), article->author());
|
||||
html += u"</div>"
|
||||
u"<div style='margin-left: 5px; margin-right: 5px;'>";
|
||||
if (Qt::mightBeRichText(article->description()))
|
||||
{
|
||||
html += article->description();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString description = article->description();
|
||||
QRegularExpression rx;
|
||||
// If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice
|
||||
rx.setPatternOptions(QRegularExpression::InvertedGreedinessOption
|
||||
| QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
rx.setPattern(u"\\[img\\](.+)\\[/img\\]"_s);
|
||||
description = description.replace(rx, u"<img src=\"\\1\">"_s);
|
||||
|
||||
rx.setPattern(u"\\[url=(\")?(.+)\\1\\]"_s);
|
||||
description = description.replace(rx, u"<a href=\"\\2\">"_s);
|
||||
description = description.replace(u"[/url]"_s, u"</a>"_s, Qt::CaseInsensitive);
|
||||
|
||||
rx.setPattern(u"\\[(/)?([bius])\\]"_s);
|
||||
description = description.replace(rx, u"<\\1\\2>"_s);
|
||||
|
||||
rx.setPattern(u"\\[color=(\")?(.+)\\1\\]"_s);
|
||||
description = description.replace(rx, u"<span style=\"color:\\2\">"_s);
|
||||
description = description.replace(u"[/color]"_s, u"</span>"_s, Qt::CaseInsensitive);
|
||||
|
||||
rx.setPattern(u"\\[size=(\")?(.+)\\d\\1\\]"_s);
|
||||
description = description.replace(rx, u"<span style=\"font-size:\\2px\">"_s);
|
||||
description = description.replace(u"[/size]"_s, u"</span>"_s, Qt::CaseInsensitive);
|
||||
|
||||
html += u"<pre>" + description + u"</pre>";
|
||||
}
|
||||
html += u"</div>";
|
||||
m_ui->textBrowser->setHtml(html);
|
||||
renderArticle(article);
|
||||
}
|
||||
|
||||
void RSSWidget::saveSlidersPosition()
|
||||
|
@ -590,3 +543,73 @@ void RSSWidget::handleUnreadCountChanged()
|
|||
{
|
||||
emit unreadCountUpdated(RSS::Session::instance()->rootFolder()->unreadCount());
|
||||
}
|
||||
|
||||
bool RSSWidget::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if ((obj == m_ui->textBrowser) && (event->type() == QEvent::PaletteChange))
|
||||
{
|
||||
QListWidgetItem *currentItem = m_articleListWidget->currentItem();
|
||||
if (currentItem)
|
||||
{
|
||||
const RSS::Article *article = m_articleListWidget->getRSSArticle(currentItem);
|
||||
renderArticle(article);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void RSSWidget::renderArticle(const RSS::Article *article) const
|
||||
{
|
||||
Q_ASSERT(article);
|
||||
|
||||
const QString highlightedBaseColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::Highlight).name();
|
||||
const QString highlightedBaseTextColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::HighlightedText).name();
|
||||
const QString alternateBaseColor = m_ui->textBrowser->palette().color(QPalette::Active, QPalette::AlternateBase).name();
|
||||
|
||||
QString html =
|
||||
u"<div style='border: 2px solid red; margin-left: 5px; margin-right: 5px; margin-bottom: 5px;'>" +
|
||||
u"<div style='background-color: \"%1\"; font-weight: bold; color: \"%2\";'>%3</div>"_s.arg(highlightedBaseColor, highlightedBaseTextColor, article->title());
|
||||
if (article->date().isValid())
|
||||
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Date: "), QLocale::system().toString(article->date().toLocalTime()));
|
||||
if (m_feedListWidget->currentItem() == m_feedListWidget->stickyUnreadItem())
|
||||
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Feed: "), article->feed()->title());
|
||||
if (!article->author().isEmpty())
|
||||
html += u"<div style='background-color: \"%1\";'><b>%2</b>%3</div>"_s.arg(alternateBaseColor, tr("Author: "), article->author());
|
||||
html += u"</div>"
|
||||
u"<div style='margin-left: 5px; margin-right: 5px;'>";
|
||||
if (Qt::mightBeRichText(article->description()))
|
||||
{
|
||||
html += article->description();
|
||||
}
|
||||
else
|
||||
{
|
||||
QString description = article->description();
|
||||
QRegularExpression rx;
|
||||
// If description is plain text, replace BBCode tags with HTML and wrap everything in <pre></pre> so it looks nice
|
||||
rx.setPatternOptions(QRegularExpression::InvertedGreedinessOption
|
||||
| QRegularExpression::CaseInsensitiveOption);
|
||||
|
||||
rx.setPattern(u"\\[img\\](.+)\\[/img\\]"_s);
|
||||
description = description.replace(rx, u"<img src=\"\\1\">"_s);
|
||||
|
||||
rx.setPattern(u"\\[url=(\")?(.+)\\1\\]"_s);
|
||||
description = description.replace(rx, u"<a href=\"\\2\">"_s);
|
||||
description = description.replace(u"[/url]"_s, u"</a>"_s, Qt::CaseInsensitive);
|
||||
|
||||
rx.setPattern(u"\\[(/)?([bius])\\]"_s);
|
||||
description = description.replace(rx, u"<\\1\\2>"_s);
|
||||
|
||||
rx.setPattern(u"\\[color=(\")?(.+)\\1\\]"_s);
|
||||
description = description.replace(rx, u"<span style=\"color:\\2\">"_s);
|
||||
description = description.replace(u"[/color]"_s, u"</span>"_s, Qt::CaseInsensitive);
|
||||
|
||||
rx.setPattern(u"\\[size=(\")?(.+)\\d\\1\\]"_s);
|
||||
description = description.replace(rx, u"<span style=\"font-size:\\2px\">"_s);
|
||||
description = description.replace(u"[/size]"_s, u"</span>"_s, Qt::CaseInsensitive);
|
||||
|
||||
html += u"<pre>" + description + u"</pre>";
|
||||
}
|
||||
html += u"</div>";
|
||||
m_ui->textBrowser->setHtml(html);
|
||||
}
|
||||
|
|
|
@ -40,6 +40,11 @@ class QTreeWidgetItem;
|
|||
class ArticleListWidget;
|
||||
class FeedListWidget;
|
||||
|
||||
namespace RSS
|
||||
{
|
||||
class Article;
|
||||
}
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class RSSWidget;
|
||||
|
@ -85,6 +90,9 @@ private slots:
|
|||
void handleUnreadCountChanged();
|
||||
|
||||
private:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
void renderArticle(const RSS::Article *article) const;
|
||||
|
||||
Ui::RSSWidget *m_ui = nullptr;
|
||||
ArticleListWidget *m_articleListWidget = nullptr;
|
||||
FeedListWidget *m_feedListWidget = nullptr;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2018-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -50,6 +50,19 @@
|
|||
#include "searchsortmodel.h"
|
||||
#include "ui_searchjobwidget.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
enum DataRole
|
||||
{
|
||||
LinkVisitedRole = Qt::UserRole + 100
|
||||
};
|
||||
|
||||
QColor visitedRowColor()
|
||||
{
|
||||
return QApplication::palette().color(QPalette::Disabled, QPalette::WindowText);
|
||||
}
|
||||
}
|
||||
|
||||
SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, IGUIApplication *app, QWidget *parent)
|
||||
: GUIApplicationComponent(app, parent)
|
||||
, m_ui {new Ui::SearchJobWidget}
|
||||
|
@ -158,6 +171,8 @@ SearchJobWidget::SearchJobWidget(SearchHandler *searchHandler, IGUIApplication *
|
|||
connect(this, &QObject::destroyed, searchHandler, &QObject::deleteLater);
|
||||
|
||||
setStatusTip(statusText(m_status));
|
||||
|
||||
connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, &SearchJobWidget::onUIThemeChanged);
|
||||
}
|
||||
|
||||
SearchJobWidget::~SearchJobWidget()
|
||||
|
@ -179,9 +194,31 @@ QHeaderView *SearchJobWidget::header() const
|
|||
// Set the color of a row in data model
|
||||
void SearchJobWidget::setRowColor(int row, const QColor &color)
|
||||
{
|
||||
m_proxyModel->setDynamicSortFilter(false);
|
||||
for (int i = 0; i < m_proxyModel->columnCount(); ++i)
|
||||
m_proxyModel->setData(m_proxyModel->index(row, i), color, Qt::ForegroundRole);
|
||||
}
|
||||
|
||||
void SearchJobWidget::setRowVisited(const int row)
|
||||
{
|
||||
m_proxyModel->setDynamicSortFilter(false);
|
||||
|
||||
m_proxyModel->setData(m_proxyModel->index(row, 0), true, LinkVisitedRole);
|
||||
setRowColor(row, visitedRowColor());
|
||||
|
||||
m_proxyModel->setDynamicSortFilter(true);
|
||||
}
|
||||
|
||||
void SearchJobWidget::onUIThemeChanged()
|
||||
{
|
||||
m_proxyModel->setDynamicSortFilter(false);
|
||||
|
||||
for (int row = 0; row < m_proxyModel->rowCount(); ++row)
|
||||
{
|
||||
const QVariant userData = m_proxyModel->data(m_proxyModel->index(row, 0), LinkVisitedRole);
|
||||
const bool isVisited = userData.toBool();
|
||||
if (isVisited)
|
||||
setRowColor(row, visitedRowColor());
|
||||
}
|
||||
|
||||
m_proxyModel->setDynamicSortFilter(true);
|
||||
}
|
||||
|
@ -284,7 +321,8 @@ void SearchJobWidget::downloadTorrent(const QModelIndex &rowIndex, const AddTorr
|
|||
, this, [this, option](const QString &source) { addTorrentToSession(source, option); });
|
||||
connect(downloadHandler, &SearchDownloadHandler::downloadFinished, downloadHandler, &SearchDownloadHandler::deleteLater);
|
||||
}
|
||||
setRowColor(rowIndex.row(), QApplication::palette().color(QPalette::LinkVisited));
|
||||
|
||||
setRowVisited(rowIndex.row());
|
||||
}
|
||||
|
||||
void SearchJobWidget::addTorrentToSession(const QString &source, const AddTorrentOption option)
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2018-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -113,8 +113,10 @@ private:
|
|||
void fillFilterComboBoxes();
|
||||
NameFilteringMode filteringMode() const;
|
||||
QHeaderView *header() const;
|
||||
void setRowColor(int row, const QColor &color);
|
||||
int visibleColumnsCount() const;
|
||||
void setRowColor(int row, const QColor &color);
|
||||
void setRowVisited(int row);
|
||||
void onUIThemeChanged();
|
||||
|
||||
void downloadTorrents(AddTorrentOption option = AddTorrentOption::Default);
|
||||
void openTorrentPages() const;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2020, Will Da Silva <will@willdasilva.xyz>
|
||||
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -120,6 +120,7 @@ SearchWidget::SearchWidget(IGUIApplication *app, MainWindow *mainWindow)
|
|||
#endif
|
||||
connect(m_ui->tabWidget, &QTabWidget::tabCloseRequested, this, &SearchWidget::closeTab);
|
||||
connect(m_ui->tabWidget, &QTabWidget::currentChanged, this, &SearchWidget::tabChanged);
|
||||
connect(m_ui->tabWidget->tabBar(), &QTabBar::tabMoved, this, &SearchWidget::tabMoved);
|
||||
|
||||
const auto *searchManager = SearchPluginManager::instance();
|
||||
const auto onPluginChanged = [this]()
|
||||
|
@ -262,6 +263,11 @@ void SearchWidget::tabChanged(const int index)
|
|||
m_currentSearchTab = ((index < 0) ? nullptr : m_allTabs.at(m_ui->tabWidget->currentIndex()));
|
||||
}
|
||||
|
||||
void SearchWidget::tabMoved(const int from, const int to)
|
||||
{
|
||||
m_allTabs.move(from, to);
|
||||
}
|
||||
|
||||
void SearchWidget::selectMultipleBox([[maybe_unused]] const int index)
|
||||
{
|
||||
if (selectedPlugin() == u"multi")
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2020, Will Da Silva <will@willdasilva.xyz>
|
||||
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -66,6 +66,7 @@ private slots:
|
|||
private:
|
||||
bool eventFilter(QObject *object, QEvent *event) override;
|
||||
void tabChanged(int index);
|
||||
void tabMoved(int from, int to);
|
||||
void closeTab(int index);
|
||||
void closeAllTabs();
|
||||
void tabStatusChanged(QWidget *tab);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006-2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -37,17 +37,12 @@
|
|||
#include <QPointer>
|
||||
#include <QScopeGuard>
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
#include <windows.h>
|
||||
#include <shellapi.h>
|
||||
#else
|
||||
#include <QMimeDatabase>
|
||||
#include <QMimeType>
|
||||
#endif
|
||||
|
||||
#if defined Q_OS_WIN || defined Q_OS_MACOS
|
||||
#if defined(Q_OS_MACOS)
|
||||
#define QBT_PIXMAP_CACHE_FOR_FILE_ICONS
|
||||
#include <QPixmapCache>
|
||||
#elif !defined(Q_OS_WIN)
|
||||
#include <QMimeDatabase>
|
||||
#include <QMimeType>
|
||||
#endif
|
||||
|
||||
#include "base/bittorrent/downloadpriority.h"
|
||||
|
@ -116,27 +111,8 @@ namespace
|
|||
};
|
||||
#endif // QBT_PIXMAP_CACHE_FOR_FILE_ICONS
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
// See QTBUG-25319 for explanation why this is required
|
||||
class WinShellFileIconProvider final : public CachingFileIconProvider
|
||||
{
|
||||
QPixmap pixmapForExtension(const QString &ext) const override
|
||||
{
|
||||
const std::wstring extWStr = QString(u'.' + ext).toStdWString();
|
||||
|
||||
SHFILEINFOW sfi {};
|
||||
const HRESULT hr = ::SHGetFileInfoW(extWStr.c_str(),
|
||||
FILE_ATTRIBUTE_NORMAL, &sfi, sizeof(sfi), (SHGFI_ICON | SHGFI_USEFILEATTRIBUTES));
|
||||
if (FAILED(hr))
|
||||
return {};
|
||||
|
||||
const auto iconPixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon));
|
||||
::DestroyIcon(sfi.hIcon);
|
||||
return iconPixmap;
|
||||
}
|
||||
};
|
||||
#elif defined(Q_OS_MACOS)
|
||||
// There is a similar bug on macOS, to be reported to Qt
|
||||
#if defined(Q_OS_MACOS)
|
||||
// There is a bug on macOS, to be reported to Qt
|
||||
// https://github.com/qbittorrent/qBittorrent/pull/6156#issuecomment-316302615
|
||||
class MacFileIconProvider final : public CachingFileIconProvider
|
||||
{
|
||||
|
@ -145,7 +121,7 @@ namespace
|
|||
return MacUtils::pixmapForExtension(ext, QSize(32, 32));
|
||||
}
|
||||
};
|
||||
#else
|
||||
#elif !defined(Q_OS_WIN)
|
||||
/**
|
||||
* @brief Tests whether QFileIconProvider actually works
|
||||
*
|
||||
|
@ -189,7 +165,7 @@ TorrentContentModel::TorrentContentModel(QObject *parent)
|
|||
: QAbstractItemModel(parent)
|
||||
, m_rootItem(new TorrentContentModelFolder(QVector<QString>({ tr("Name"), tr("Total Size"), tr("Progress"), tr("Download Priority"), tr("Remaining"), tr("Availability") })))
|
||||
#if defined(Q_OS_WIN)
|
||||
, m_fileIconProvider {new WinShellFileIconProvider}
|
||||
, m_fileIconProvider {new QFileIconProvider}
|
||||
#elif defined(Q_OS_MACOS)
|
||||
, m_fileIconProvider {new MacFileIconProvider}
|
||||
#else
|
||||
|
@ -422,7 +398,9 @@ QVariant TorrentContentModel::data(const QModelIndex &index, const int role) con
|
|||
const bool hasIgnored = std::any_of(childItems.cbegin(), childItems.cend()
|
||||
, [](const TorrentContentModelItem *childItem)
|
||||
{
|
||||
return (childItem->priority() == BitTorrent::DownloadPriority::Ignored);
|
||||
const auto prio = childItem->priority();
|
||||
return ((prio == BitTorrent::DownloadPriority::Ignored)
|
||||
|| (prio == BitTorrent::DownloadPriority::Mixed));
|
||||
});
|
||||
|
||||
return hasIgnored ? Qt::PartiallyChecked : Qt::Checked;
|
||||
|
|
|
@ -147,10 +147,19 @@ void TorrentContentModelFolder::recalculateProgress()
|
|||
tRemaining += child->remaining();
|
||||
}
|
||||
|
||||
if (!isRootItem() && (tSize > 0))
|
||||
if (!isRootItem())
|
||||
{
|
||||
m_progress = tProgress / tSize;
|
||||
m_remaining = tRemaining;
|
||||
if (tSize > 0)
|
||||
{
|
||||
m_progress = tProgress / tSize;
|
||||
m_remaining = tRemaining;
|
||||
}
|
||||
else
|
||||
{
|
||||
m_progress = 1.0;
|
||||
m_remaining = 0;
|
||||
}
|
||||
|
||||
Q_ASSERT(m_progress <= 1.);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -56,6 +56,19 @@
|
|||
#include "gui/macutilities.h"
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
QList<QPersistentModelIndex> toPersistentIndexes(const QModelIndexList &indexes)
|
||||
{
|
||||
QList<QPersistentModelIndex> persistentIndexes;
|
||||
persistentIndexes.reserve(indexes.size());
|
||||
for (const QModelIndex &index : indexes)
|
||||
persistentIndexes.emplaceBack(index);
|
||||
|
||||
return persistentIndexes;
|
||||
}
|
||||
}
|
||||
|
||||
TorrentContentWidget::TorrentContentWidget(QWidget *parent)
|
||||
: QTreeView(parent)
|
||||
{
|
||||
|
@ -173,10 +186,20 @@ Path TorrentContentWidget::getItemPath(const QModelIndex &index) const
|
|||
return path;
|
||||
}
|
||||
|
||||
void TorrentContentWidget::setFilterPattern(const QString &patternText)
|
||||
void TorrentContentWidget::setFilterPattern(const QString &patternText, const FilterPatternFormat format)
|
||||
{
|
||||
const QString pattern = Utils::String::wildcardToRegexPattern(patternText);
|
||||
m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
if (format == FilterPatternFormat::PlainText)
|
||||
{
|
||||
m_filterModel->setFilterFixedString(patternText);
|
||||
m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
}
|
||||
else
|
||||
{
|
||||
const QString pattern = ((format == FilterPatternFormat::Regex)
|
||||
? patternText : Utils::String::wildcardToRegexPattern(patternText));
|
||||
m_filterModel->setFilterRegularExpression(QRegularExpression(pattern, QRegularExpression::CaseInsensitiveOption));
|
||||
}
|
||||
|
||||
if (patternText.isEmpty())
|
||||
{
|
||||
collapseAll();
|
||||
|
@ -219,9 +242,9 @@ void TorrentContentWidget::keyPressEvent(QKeyEvent *event)
|
|||
|
||||
const Qt::CheckState state = (static_cast<Qt::CheckState>(value.toInt()) == Qt::Checked)
|
||||
? Qt::Unchecked : Qt::Checked;
|
||||
const QModelIndexList selection = selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME);
|
||||
const QList<QPersistentModelIndex> selection = toPersistentIndexes(selectionModel()->selectedRows(TorrentContentModelItem::COL_NAME));
|
||||
|
||||
for (const QModelIndex &index : selection)
|
||||
for (const QPersistentModelIndex &index : selection)
|
||||
model()->setData(index, state, Qt::CheckStateRole);
|
||||
}
|
||||
|
||||
|
@ -248,10 +271,10 @@ void TorrentContentWidget::renameSelectedFile()
|
|||
|
||||
void TorrentContentWidget::applyPriorities(const BitTorrent::DownloadPriority priority)
|
||||
{
|
||||
const QModelIndexList selectedRows = selectionModel()->selectedRows(0);
|
||||
for (const QModelIndex &index : selectedRows)
|
||||
const QList<QPersistentModelIndex> selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
|
||||
for (const QPersistentModelIndex &index : selectedRows)
|
||||
{
|
||||
model()->setData(index.sibling(index.row(), Priority), static_cast<int>(priority));
|
||||
model()->setData(index, static_cast<int>(priority));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -261,7 +284,7 @@ void TorrentContentWidget::applyPrioritiesByOrder()
|
|||
// a download priority that will apply to each item. The number of groups depends on how
|
||||
// many "download priority" are available to be assigned
|
||||
|
||||
const QModelIndexList selectedRows = selectionModel()->selectedRows(0);
|
||||
const QList<QPersistentModelIndex> selectedRows = toPersistentIndexes(selectionModel()->selectedRows(Priority));
|
||||
|
||||
const qsizetype priorityGroups = 3;
|
||||
const auto priorityGroupSize = std::max<qsizetype>((selectedRows.length() / priorityGroups), 1);
|
||||
|
@ -283,8 +306,8 @@ void TorrentContentWidget::applyPrioritiesByOrder()
|
|||
break;
|
||||
}
|
||||
|
||||
const QModelIndex &index = selectedRows[i];
|
||||
model()->setData(index.sibling(index.row(), Priority), static_cast<int>(priority));
|
||||
const QPersistentModelIndex &index = selectedRows[i];
|
||||
model()->setData(index, static_cast<int>(priority));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2014 Ivan Sorokin <vanyacpp@gmail.com>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -33,6 +33,7 @@
|
|||
|
||||
#include "base/bittorrent/downloadpriority.h"
|
||||
#include "base/pathfwd.h"
|
||||
#include "filterpatternformat.h"
|
||||
|
||||
class QShortcut;
|
||||
|
||||
|
@ -92,7 +93,7 @@ public:
|
|||
int getFileIndex(const QModelIndex &index) const;
|
||||
Path getItemPath(const QModelIndex &index) const;
|
||||
|
||||
void setFilterPattern(const QString &patternText);
|
||||
void setFilterPattern(const QString &patternText, FilterPatternFormat format = FilterPatternFormat::Wildcards);
|
||||
|
||||
void checkAll();
|
||||
void checkNone();
|
||||
|
|
|
@ -84,11 +84,7 @@ TorrentCreatorDialog::TorrentCreatorDialog(QWidget *parent, const Path &defaultP
|
|||
m_ui->setupUi(this);
|
||||
|
||||
m_ui->comboPieceSize->addItem(tr("Auto"), 0);
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
for (int i = 4; i <= 18; ++i)
|
||||
#else
|
||||
for (int i = 4; i <= 17; ++i)
|
||||
#endif
|
||||
{
|
||||
const int size = 1024 << i;
|
||||
const QString displaySize = Utils::Misc::friendlyUnit(size, false, 0);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
|
@ -37,6 +37,7 @@
|
|||
#include "base/global.h"
|
||||
#include "autoexpandabledialog.h"
|
||||
#include "flowlayout.h"
|
||||
#include "utils.h"
|
||||
|
||||
#include "ui_torrenttagsdialog.h"
|
||||
|
||||
|
@ -52,10 +53,10 @@ TorrentTagsDialog::TorrentTagsDialog(const TagSet &initialTags, QWidget *parent)
|
|||
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *tagsLayout = new FlowLayout(m_ui->scrollArea);
|
||||
auto *tagsLayout = new FlowLayout(m_ui->scrollArea->widget());
|
||||
for (const Tag &tag : asConst(initialTags.united(BitTorrent::Session::instance()->tags())))
|
||||
{
|
||||
auto *tagWidget = new QCheckBox(tag.toString());
|
||||
auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
|
||||
if (initialTags.contains(tag))
|
||||
tagWidget->setChecked(true);
|
||||
tagsLayout->addWidget(tagWidget);
|
||||
|
@ -78,12 +79,12 @@ TorrentTagsDialog::~TorrentTagsDialog()
|
|||
TagSet TorrentTagsDialog::tags() const
|
||||
{
|
||||
TagSet tags;
|
||||
auto *layout = m_ui->scrollArea->layout();
|
||||
auto *layout = m_ui->scrollArea->widget()->layout();
|
||||
for (int i = 0; i < (layout->count() - 1); ++i)
|
||||
{
|
||||
const auto *tagWidget = static_cast<QCheckBox *>(layout->itemAt(i)->widget());
|
||||
if (tagWidget->isChecked())
|
||||
tags.insert(Tag(tagWidget->text()));
|
||||
tags.insert(Utils::Gui::widgetTextToTag(tagWidget->text()));
|
||||
}
|
||||
|
||||
return tags;
|
||||
|
@ -111,9 +112,9 @@ void TorrentTagsDialog::addNewTag()
|
|||
}
|
||||
else
|
||||
{
|
||||
auto *layout = m_ui->scrollArea->layout();
|
||||
auto *layout = m_ui->scrollArea->widget()->layout();
|
||||
auto *btn = layout->takeAt(layout->count() - 1);
|
||||
auto *tagWidget = new QCheckBox(tag.toString());
|
||||
auto *tagWidget = new QCheckBox(Utils::Gui::tagToWidgetText(tag));
|
||||
tagWidget->setChecked(true);
|
||||
layout->addWidget(tagWidget);
|
||||
layout->addItem(btn);
|
||||
|
|
|
@ -488,11 +488,11 @@ QVariant TrackerListModel::headerData(const int section, const Qt::Orientation o
|
|||
switch (section)
|
||||
{
|
||||
case COL_URL:
|
||||
return tr("URL/Announce endpoint");
|
||||
return tr("URL/Announce Endpoint");
|
||||
case COL_TIER:
|
||||
return tr("Tier");
|
||||
case COL_PROTOCOL:
|
||||
return tr("Protocol");
|
||||
return tr("BT Protocol");
|
||||
case COL_STATUS:
|
||||
return tr("Status");
|
||||
case COL_PEERS:
|
||||
|
@ -506,9 +506,9 @@ QVariant TrackerListModel::headerData(const int section, const Qt::Orientation o
|
|||
case COL_MSG:
|
||||
return tr("Message");
|
||||
case COL_NEXT_ANNOUNCE:
|
||||
return tr("Next announce");
|
||||
return tr("Next Announce");
|
||||
case COL_MIN_ANNOUNCE:
|
||||
return tr("Min announce");
|
||||
return tr("Min Announce");
|
||||
default:
|
||||
return {};
|
||||
}
|
||||
|
@ -585,7 +585,7 @@ QVariant TrackerListModel::data(const QModelIndex &index, const int role) const
|
|||
case COL_TIER:
|
||||
return (isEndpoint || (index.row() < STICKY_ROW_COUNT)) ? QString() : QString::number(itemPtr->tier);
|
||||
case COL_PROTOCOL:
|
||||
return isEndpoint ? tr("v%1").arg(itemPtr->btVersion) : QString();
|
||||
return isEndpoint ? (u'v' + QString::number(itemPtr->btVersion)) : QString();
|
||||
case COL_STATUS:
|
||||
if (isEndpoint)
|
||||
return toString(itemPtr->status);
|
||||
|
|
|
@ -235,10 +235,7 @@ void StatusFilterWidget::applyFilter(int row)
|
|||
|
||||
void StatusFilterWidget::handleTorrentsLoaded(const QVector<BitTorrent::Torrent *> &torrents)
|
||||
{
|
||||
for (const BitTorrent::Torrent *torrent : torrents)
|
||||
updateTorrentStatus(torrent);
|
||||
|
||||
updateTexts();
|
||||
update(torrents);
|
||||
}
|
||||
|
||||
void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torrent)
|
||||
|
@ -273,6 +270,12 @@ void StatusFilterWidget::torrentAboutToBeDeleted(BitTorrent::Torrent *const torr
|
|||
m_nbStalled = m_nbStalledUploading + m_nbStalledDownloading;
|
||||
|
||||
updateTexts();
|
||||
|
||||
if (Preferences::instance()->getHideZeroStatusFilters())
|
||||
{
|
||||
hideZeroItems();
|
||||
updateGeometry();
|
||||
}
|
||||
}
|
||||
|
||||
void StatusFilterWidget::configure()
|
||||
|
|
|
@ -394,15 +394,11 @@ void TrackersFilterWidget::handleTrackerStatusesUpdated(const BitTorrent::Torren
|
|||
{
|
||||
if (trackerEntryStatus.state == 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::all_of(trackerEntryStatus.endpoints.cbegin(), trackerEntryStatus.endpoints.cend()
|
||||
, [](const BitTorrent::TrackerEndpointStatus &endpointEntry)
|
||||
|
@ -426,16 +422,38 @@ void TrackersFilterWidget::handleTrackerStatusesUpdated(const BitTorrent::Torren
|
|||
else if ((trackerEntryStatus.state == BitTorrent::TrackerEndpointState::NotWorking)
|
||||
|| (trackerEntryStatus.state == 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);
|
||||
}
|
||||
else if (trackerEntryStatus.state == 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);
|
||||
}
|
||||
else if (trackerEntryStatus.state == 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);
|
||||
}
|
||||
}
|
||||
|
||||
if ((errorHashesIt != m_errors.end()) && errorHashesIt->isEmpty())
|
||||
|
|
|
@ -39,7 +39,6 @@
|
|||
#include <QUrl>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "base/algorithm.h"
|
||||
#include "base/bittorrent/session.h"
|
||||
#include "base/bittorrent/torrent.h"
|
||||
#include "base/bittorrent/trackerentrystatus.h"
|
||||
|
|
|
@ -193,6 +193,7 @@ QVariant TransferListModel::headerData(const int section, const Qt::Orientation
|
|||
case TR_INFOHASH_V1: return tr("Info Hash v1", "i.e: torrent info hash v1");
|
||||
case TR_INFOHASH_V2: return tr("Info Hash v2", "i.e: torrent info hash v2");
|
||||
case TR_REANNOUNCE: return tr("Reannounce In", "Indicates the time until next trackers reannounce");
|
||||
case TR_PRIVATE: return tr("Private", "Flags private torrents");
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
@ -357,6 +358,15 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
|
|||
return Utils::Misc::userFriendlyDuration(time);
|
||||
};
|
||||
|
||||
const auto privateString = [hideValues](const bool isPrivate, const bool hasMetadata) -> QString
|
||||
{
|
||||
if (hideValues && !isPrivate)
|
||||
return {};
|
||||
if (hasMetadata)
|
||||
return isPrivate ? tr("Yes") : tr("No");
|
||||
return tr("N/A");
|
||||
};
|
||||
|
||||
switch (column)
|
||||
{
|
||||
case TR_NAME:
|
||||
|
@ -431,6 +441,8 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
|
|||
return hashString(torrent->infoHash().v2());
|
||||
case TR_REANNOUNCE:
|
||||
return reannounceString(torrent->nextAnnounce());
|
||||
case TR_PRIVATE:
|
||||
return privateString(torrent->isPrivate(), torrent->hasMetadata());
|
||||
}
|
||||
|
||||
return {};
|
||||
|
@ -512,6 +524,8 @@ QVariant TransferListModel::internalValue(const BitTorrent::Torrent *torrent, co
|
|||
return QVariant::fromValue(torrent->infoHash().v2());
|
||||
case TR_REANNOUNCE:
|
||||
return torrent->nextAnnounce();
|
||||
case TR_PRIVATE:
|
||||
return (torrent->hasMetadata() ? torrent->isPrivate() : QVariant());
|
||||
}
|
||||
|
||||
return {};
|
||||
|
|
|
@ -86,6 +86,7 @@ public:
|
|||
TR_INFOHASH_V1,
|
||||
TR_INFOHASH_V2,
|
||||
TR_REANNOUNCE,
|
||||
TR_PRIVATE,
|
||||
|
||||
NB_COLUMNS
|
||||
};
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue