mirror of
https://github.com/qbittorrent/qBittorrent.git
synced 2025-04-21 13:07:26 -04:00
Compare commits
73 commits
master
...
release-5.
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
314 changed files with 108126 additions and 94427 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
|
||||
|
|
14
.github/workflows/ci_ubuntu.yaml
vendored
14
.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
|
||||
|
@ -138,12 +138,12 @@ jobs:
|
|||
curl \
|
||||
-L \
|
||||
-Z \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage \
|
||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||
chmod +x \
|
||||
linuxdeploy-x86_64.AppImage \
|
||||
linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||
linuxdeploy-static-x86_64.AppImage \
|
||||
linuxdeploy-plugin-qt-static-x86_64.AppImage \
|
||||
linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||
|
||||
- name: Prepare files for AppImage
|
||||
|
@ -156,12 +156,12 @@ jobs:
|
|||
|
||||
- name: Package AppImage
|
||||
run: |
|
||||
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
|
||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --plugin qt
|
||||
rm qbittorrent/apprun-hooks/*
|
||||
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh
|
||||
NO_APPSTREAM=1 \
|
||||
OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \
|
||||
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --output appimage
|
||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --output appimage
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
|
|
30
.github/workflows/ci_windows.yaml
vendored
30
.github/workflows/ci_windows.yaml
vendored
|
@ -93,7 +93,7 @@ jobs:
|
|||
move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}"
|
||||
|
||||
- name: Install Qt
|
||||
uses: jurplel/install-qt-action@v3
|
||||
uses: jurplel/install-qt-action@v4
|
||||
with:
|
||||
version: "6.7.0"
|
||||
archives: qtbase qtsvg qttools
|
||||
|
@ -153,26 +153,26 @@ jobs:
|
|||
copy build/qbittorrent.pdb upload/qBittorrent
|
||||
copy dist/windows/qt.conf upload/qBittorrent
|
||||
# runtimes
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt6_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
|
||||
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
|
||||
mkdir upload/qBittorrent/plugins/iconengines
|
||||
copy "${{ env.Qt6_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
|
||||
mkdir upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
|
||||
mkdir upload/qBittorrent/plugins/platforms
|
||||
copy "${{ env.Qt6_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
|
||||
mkdir upload/qBittorrent/plugins/sqldrivers
|
||||
copy "${{ env.Qt6_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers
|
||||
mkdir upload/qBittorrent/plugins/styles
|
||||
copy "${{ env.Qt6_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles
|
||||
mkdir upload/qBittorrent/plugins/tls
|
||||
copy "${{ env.Qt6_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls
|
||||
copy "${{ env.Qt_ROOT_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls
|
||||
# cmake additionals
|
||||
mkdir upload/cmake
|
||||
copy build/compile_commands.json upload/cmake
|
||||
|
|
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
|
||||
|
|
43
Changelog
43
Changelog
|
@ -1,4 +1,4 @@
|
|||
Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
|
||||
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 +12,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 +44,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)
|
||||
|
|
144
dist/unix/org.qbittorrent.qBittorrent.desktop
vendored
144
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]=โปรแกรมบิททอเร้นท์
|
||||
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่าน BitTorrent
|
||||
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.0" date="2024-09-29"/>
|
||||
</releases>
|
||||
</component>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
{
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -425,7 +426,7 @@ namespace BitTorrent
|
|||
virtual void setExcludedFileNamesEnabled(bool enabled) = 0;
|
||||
virtual QStringList excludedFileNames() const = 0;
|
||||
virtual void setExcludedFileNames(const QStringList &newList) = 0;
|
||||
virtual bool isFilenameExcluded(const QString &fileName) const = 0;
|
||||
virtual void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) = 0;
|
||||
virtual QStringList bannedIPs() const = 0;
|
||||
virtual void setBannedIPs(const QStringList &newList) = 0;
|
||||
virtual ResumeDataStorageType resumeDataStorageType() const = 0;
|
||||
|
@ -434,6 +435,8 @@ namespace BitTorrent
|
|||
virtual void setMergeTrackersEnabled(bool enabled) = 0;
|
||||
virtual bool isStartPaused() const = 0;
|
||||
virtual void setStartPaused(bool value) = 0;
|
||||
virtual TorrentContentRemoveOption torrentContentRemoveOption() const = 0;
|
||||
virtual void setTorrentContentRemoveOption(TorrentContentRemoveOption option) = 0;
|
||||
|
||||
virtual bool isRestored() const = 0;
|
||||
|
||||
|
@ -453,7 +456,7 @@ namespace BitTorrent
|
|||
|
||||
virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0;
|
||||
virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶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;
|
||||
|
||||
|
|
|
@ -101,6 +101,7 @@
|
|||
#include "nativesessionextension.h"
|
||||
#include "portforwarderimpl.h"
|
||||
#include "resumedatastorage.h"
|
||||
#include "torrentcontentremover.h"
|
||||
#include "torrentdescriptor.h"
|
||||
#include "torrentimpl.h"
|
||||
#include "tracker.h"
|
||||
|
@ -525,6 +526,7 @@ SessionImpl::SessionImpl(QObject *parent)
|
|||
, m_I2POutboundQuantity {BITTORRENT_SESSION_KEY(u"I2P/OutboundQuantity"_s), 3}
|
||||
, m_I2PInboundLength {BITTORRENT_SESSION_KEY(u"I2P/InboundLength"_s), 3}
|
||||
, m_I2POutboundLength {BITTORRENT_SESSION_KEY(u"I2P/OutboundLength"_s), 3}
|
||||
, m_torrentContentRemoveOption {BITTORRENT_SESSION_KEY(u"TorrentContentRemoveOption"_s), TorrentContentRemoveOption::MoveToTrash}
|
||||
, m_startPaused {BITTORRENT_SESSION_KEY(u"StartPaused"_s)}
|
||||
, m_seedingLimitTimer {new QTimer(this)}
|
||||
, m_resumeDataTimer {new QTimer(this)}
|
||||
|
@ -550,7 +552,14 @@ SessionImpl::SessionImpl(QObject *parent)
|
|||
, this, [this]() { m_recentErroredTorrents.clear(); });
|
||||
|
||||
m_seedingLimitTimer->setInterval(10s);
|
||||
connect(m_seedingLimitTimer, &QTimer::timeout, this, &SessionImpl::processShareLimits);
|
||||
connect(m_seedingLimitTimer, &QTimer::timeout, this, [this]
|
||||
{
|
||||
// We shouldn't iterate over `m_torrents` in the loop below
|
||||
// since `deleteTorrent()` modifies it indirectly
|
||||
const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
|
||||
for (TorrentImpl *torrent : torrents)
|
||||
processTorrentShareLimits(torrent);
|
||||
});
|
||||
|
||||
initializeNativeSession();
|
||||
configureComponents();
|
||||
|
@ -586,6 +595,11 @@ SessionImpl::SessionImpl(QObject *parent)
|
|||
connect(m_ioThread.get(), &QThread::finished, m_fileSearcher, &QObject::deleteLater);
|
||||
connect(m_fileSearcher, &FileSearcher::searchFinished, this, &SessionImpl::fileSearchFinished);
|
||||
|
||||
m_torrentContentRemover = new TorrentContentRemover;
|
||||
m_torrentContentRemover->moveToThread(m_ioThread.get());
|
||||
connect(m_ioThread.get(), &QThread::finished, m_torrentContentRemover, &QObject::deleteLater);
|
||||
connect(m_torrentContentRemover, &TorrentContentRemover::jobFinished, this, &SessionImpl::torrentContentRemovingFinished);
|
||||
|
||||
m_ioThread->start();
|
||||
|
||||
initMetrics();
|
||||
|
@ -604,7 +618,7 @@ SessionImpl::~SessionImpl()
|
|||
{
|
||||
m_nativeSession->pause();
|
||||
|
||||
const qint64 timeout = (m_shutdownTimeout >= 0) ? (m_shutdownTimeout * 1000) : -1;
|
||||
const auto timeout = (m_shutdownTimeout >= 0) ? (static_cast<qint64>(m_shutdownTimeout) * 1000) : -1;
|
||||
const QDeadlineTimer shutdownDeadlineTimer {timeout};
|
||||
|
||||
if (m_torrentsQueueChanged)
|
||||
|
@ -2236,72 +2250,66 @@ void SessionImpl::populateAdditionalTrackers()
|
|||
m_additionalTrackerEntries = parseTrackerEntries(additionalTrackers());
|
||||
}
|
||||
|
||||
void SessionImpl::processShareLimits()
|
||||
void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
|
||||
{
|
||||
if (!torrent->isFinished() || torrent->isForced())
|
||||
return;
|
||||
|
||||
const auto effectiveLimit = []<typename T>(const T limit, const T useGlobalLimit, const T globalLimit) -> T
|
||||
{
|
||||
return (limit == useGlobalLimit) ? globalLimit : limit;
|
||||
};
|
||||
|
||||
// We shouldn't iterate over `m_torrents` in the loop below
|
||||
// since `deleteTorrent()` modifies it indirectly
|
||||
const QHash<TorrentID, TorrentImpl *> torrents {m_torrents};
|
||||
for (const auto &[torrentID, torrent] : torrents.asKeyValueRange())
|
||||
const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio());
|
||||
const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes());
|
||||
const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes());
|
||||
|
||||
bool reached = false;
|
||||
QString description;
|
||||
|
||||
if (const qreal ratio = torrent->realRatio();
|
||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
||||
{
|
||||
if (!torrent->isFinished() || torrent->isForced())
|
||||
continue;
|
||||
reached = true;
|
||||
description = tr("Torrent reached the share ratio limit.");
|
||||
}
|
||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the seeding time limit.");
|
||||
}
|
||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the inactive seeding time limit.");
|
||||
}
|
||||
|
||||
const qreal ratioLimit = effectiveLimit(torrent->ratioLimit(), Torrent::USE_GLOBAL_RATIO, globalMaxRatio());
|
||||
const int seedingTimeLimit = effectiveLimit(torrent->seedingTimeLimit(), Torrent::USE_GLOBAL_SEEDING_TIME, globalMaxSeedingMinutes());
|
||||
const int inactiveSeedingTimeLimit = effectiveLimit(torrent->inactiveSeedingTimeLimit(), Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME, globalMaxInactiveSeedingMinutes());
|
||||
if (reached)
|
||||
{
|
||||
const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
|
||||
const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction();
|
||||
|
||||
bool reached = false;
|
||||
QString description;
|
||||
|
||||
if (const qreal ratio = torrent->realRatio();
|
||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
||||
if (shareLimitAction == ShareLimitAction::Remove)
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the share ratio limit.");
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
|
||||
removeTorrent(torrent->id(), TorrentRemoveOption::KeepContent);
|
||||
}
|
||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||
else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the seeding time limit.");
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
|
||||
removeTorrent(torrent->id(), TorrentRemoveOption::RemoveContent);
|
||||
}
|
||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||
else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
|
||||
{
|
||||
reached = true;
|
||||
description = tr("Torrent reached the inactive seeding time limit.");
|
||||
torrent->stop();
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName));
|
||||
}
|
||||
|
||||
if (reached)
|
||||
else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding())
|
||||
{
|
||||
const QString torrentName = tr("Torrent: \"%1\".").arg(torrent->name());
|
||||
const ShareLimitAction shareLimitAction = (torrent->shareLimitAction() == ShareLimitAction::Default) ? m_shareLimitAction : torrent->shareLimitAction();
|
||||
|
||||
if (shareLimitAction == ShareLimitAction::Remove)
|
||||
{
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent."), torrentName));
|
||||
deleteTorrent(torrentID);
|
||||
}
|
||||
else if (shareLimitAction == ShareLimitAction::RemoveWithContent)
|
||||
{
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Removing torrent and deleting its content."), torrentName));
|
||||
deleteTorrent(torrentID, DeleteTorrentAndFiles);
|
||||
}
|
||||
else if ((shareLimitAction == ShareLimitAction::Stop) && !torrent->isStopped())
|
||||
{
|
||||
torrent->stop();
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Torrent stopped."), torrentName));
|
||||
}
|
||||
else if ((shareLimitAction == ShareLimitAction::EnableSuperSeeding) && !torrent->isStopped() && !torrent->superSeeding())
|
||||
{
|
||||
torrent->setSuperSeeding(true);
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
|
||||
}
|
||||
torrent->setSuperSeeding(true);
|
||||
LogMsg(u"%1 %2 %3"_s.arg(description, tr("Super seeding enabled."), torrentName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2331,6 +2339,19 @@ void SessionImpl::fileSearchFinished(const TorrentID &id, const Path &savePath,
|
|||
}
|
||||
}
|
||||
|
||||
void SessionImpl::torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage)
|
||||
{
|
||||
if (errorMessage.isEmpty())
|
||||
{
|
||||
LogMsg(tr("Torrent content removed. Torrent: \"%1\"").arg(torrentName));
|
||||
}
|
||||
else
|
||||
{
|
||||
LogMsg(tr("Failed to remove torrent content. Torrent: \"%1\". Error: \"%2\"")
|
||||
.arg(torrentName, errorMessage), Log::WARNING);
|
||||
}
|
||||
}
|
||||
|
||||
Torrent *SessionImpl::getTorrent(const TorrentID &id) const
|
||||
{
|
||||
return m_torrents.value(id);
|
||||
|
@ -2377,26 +2398,29 @@ void SessionImpl::banIP(const QString &ip)
|
|||
|
||||
// Delete a torrent from the session, given its hash
|
||||
// and from the disk, if the corresponding deleteOption is chosen
|
||||
bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOption)
|
||||
bool SessionImpl::removeTorrent(const TorrentID &id, const TorrentRemoveOption deleteOption)
|
||||
{
|
||||
TorrentImpl *const torrent = m_torrents.take(id);
|
||||
if (!torrent)
|
||||
return false;
|
||||
|
||||
qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrent->id().toString()));
|
||||
const TorrentID torrentID = torrent->id();
|
||||
const QString torrentName = torrent->name();
|
||||
|
||||
qDebug("Deleting torrent with ID: %s", qUtf8Printable(torrentID.toString()));
|
||||
emit torrentAboutToBeRemoved(torrent);
|
||||
|
||||
if (const InfoHash infoHash = torrent->infoHash(); infoHash.isHybrid())
|
||||
m_hybridTorrentsByAltID.remove(TorrentID::fromSHA1Hash(infoHash.v1()));
|
||||
|
||||
// Remove it from session
|
||||
if (deleteOption == DeleteTorrent)
|
||||
if (deleteOption == TorrentRemoveOption::KeepContent)
|
||||
{
|
||||
m_removingTorrents[torrent->id()] = {torrent->name(), {}, deleteOption};
|
||||
m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), {}, deleteOption};
|
||||
|
||||
const lt::torrent_handle nativeHandle {torrent->nativeHandle()};
|
||||
const auto iter = std::find_if(m_moveStorageQueue.begin(), m_moveStorageQueue.end()
|
||||
, [&nativeHandle](const MoveStorageJob &job)
|
||||
, [&nativeHandle](const MoveStorageJob &job)
|
||||
{
|
||||
return job.torrentHandle == nativeHandle;
|
||||
});
|
||||
|
@ -2414,14 +2438,14 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp
|
|||
}
|
||||
else
|
||||
{
|
||||
m_removingTorrents[torrent->id()] = {torrent->name(), torrent->rootPath(), deleteOption};
|
||||
m_removingTorrents[torrentID] = {torrentName, torrent->actualStorageLocation(), torrent->actualFilePaths(), deleteOption};
|
||||
|
||||
if (m_moveStorageQueue.size() > 1)
|
||||
{
|
||||
// Delete "move storage job" for the deleted torrent
|
||||
// (note: we shouldn't delete active job)
|
||||
const auto iter = std::find_if((m_moveStorageQueue.begin() + 1), m_moveStorageQueue.end()
|
||||
, [torrent](const MoveStorageJob &job)
|
||||
, [torrent](const MoveStorageJob &job)
|
||||
{
|
||||
return job.torrentHandle == torrent->nativeHandle();
|
||||
});
|
||||
|
@ -2429,12 +2453,13 @@ bool SessionImpl::deleteTorrent(const TorrentID &id, const DeleteOption deleteOp
|
|||
m_moveStorageQueue.erase(iter);
|
||||
}
|
||||
|
||||
m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_files);
|
||||
m_nativeSession->remove_torrent(torrent->nativeHandle(), lt::session::delete_partfile);
|
||||
}
|
||||
|
||||
// Remove it from torrent resume directory
|
||||
m_resumeDataStorage->remove(torrent->id());
|
||||
m_resumeDataStorage->remove(torrentID);
|
||||
|
||||
LogMsg(tr("Torrent removed. Torrent: \"%1\"").arg(torrentName));
|
||||
delete torrent;
|
||||
return true;
|
||||
}
|
||||
|
@ -2462,7 +2487,7 @@ bool SessionImpl::cancelDownloadMetadata(const TorrentID &id)
|
|||
}
|
||||
#endif
|
||||
|
||||
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_files);
|
||||
m_nativeSession->remove_torrent(nativeHandle);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -2686,8 +2711,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
|
||||
|
@ -2769,26 +2825,22 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
|||
Q_ASSERT(p.file_priorities.empty());
|
||||
Q_ASSERT(addTorrentParams.filePriorities.isEmpty() || (addTorrentParams.filePriorities.size() == nativeIndexes.size()));
|
||||
|
||||
QList<DownloadPriority> filePriorities = addTorrentParams.filePriorities;
|
||||
|
||||
if (filePriorities.isEmpty() && isExcludedFileNamesEnabled())
|
||||
{
|
||||
// Check file name blacklist when priorities are not explicitly set
|
||||
applyFilenameFilter(filePaths, filePriorities);
|
||||
}
|
||||
|
||||
const int internalFilesCount = torrentInfo.nativeInfo()->files().num_files(); // including .pad files
|
||||
// Use qBittorrent default priority rather than libtorrent's (4)
|
||||
p.file_priorities = std::vector(internalFilesCount, LT::toNative(DownloadPriority::Normal));
|
||||
|
||||
if (addTorrentParams.filePriorities.isEmpty())
|
||||
if (!filePriorities.isEmpty())
|
||||
{
|
||||
if (isExcludedFileNamesEnabled())
|
||||
{
|
||||
// Check file name blacklist when priorities are not explicitly set
|
||||
for (int i = 0; i < filePaths.size(); ++i)
|
||||
{
|
||||
if (isFilenameExcluded(filePaths.at(i).filename()))
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = lt::dont_download;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < addTorrentParams.filePriorities.size(); ++i)
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(addTorrentParams.filePriorities[i]);
|
||||
for (int i = 0; i < filePriorities.size(); ++i)
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(filePriorities[i]);
|
||||
}
|
||||
|
||||
Q_ASSERT(p.ti);
|
||||
|
@ -3874,21 +3926,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 +4029,16 @@ void SessionImpl::setStartPaused(const bool value)
|
|||
m_startPaused = value;
|
||||
}
|
||||
|
||||
TorrentContentRemoveOption SessionImpl::torrentContentRemoveOption() const
|
||||
{
|
||||
return m_torrentContentRemoveOption;
|
||||
}
|
||||
|
||||
void SessionImpl::setTorrentContentRemoveOption(const TorrentContentRemoveOption option)
|
||||
{
|
||||
m_torrentContentRemoveOption = option;
|
||||
}
|
||||
|
||||
QStringList SessionImpl::bannedIPs() const
|
||||
{
|
||||
return m_bannedIPs;
|
||||
|
@ -4890,7 +4972,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 +5084,7 @@ void SessionImpl::handleTorrentChecked(TorrentImpl *const torrent)
|
|||
|
||||
void SessionImpl::handleTorrentFinished(TorrentImpl *const torrent)
|
||||
{
|
||||
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
|
||||
emit torrentFinished(torrent);
|
||||
|
||||
if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
|
||||
exportTorrentFile(torrent, exportPath);
|
||||
|
||||
const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent)
|
||||
{
|
||||
return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored());
|
||||
});
|
||||
if (!hasUnfinishedTorrents)
|
||||
emit allTorrentsFinished();
|
||||
m_pendingFinishedTorrents.append(torrent);
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentResumeDataReady(TorrentImpl *const torrent, const LoadTorrentParams &data)
|
||||
|
@ -5141,7 +5212,7 @@ void SessionImpl::handleMoveTorrentStorageJobFinished(const Path &newPath)
|
|||
// Last job is completed for torrent that being removing, so actually remove it
|
||||
const lt::torrent_handle nativeHandle {finishedJob.torrentHandle};
|
||||
const RemovingTorrentData &removingTorrentData = m_removingTorrents[nativeHandle.info_hash()];
|
||||
if (removingTorrentData.deleteOption == DeleteTorrent)
|
||||
if (removingTorrentData.removeOption == TorrentRemoveOption::KeepContent)
|
||||
m_nativeSession->remove_torrent(nativeHandle, lt::session::delete_partfile);
|
||||
}
|
||||
}
|
||||
|
@ -5660,74 +5731,32 @@ TorrentImpl *SessionImpl::createTorrent(const lt::torrent_handle &nativeHandle,
|
|||
return torrent;
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert *alert)
|
||||
void SessionImpl::handleTorrentRemovedAlert(const lt::torrent_removed_alert */*alert*/)
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
#else
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hash);
|
||||
#endif
|
||||
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(id);
|
||||
if (removingTorrentDataIter != m_removingTorrents.end())
|
||||
{
|
||||
if (removingTorrentDataIter->deleteOption == DeleteTorrent)
|
||||
{
|
||||
LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
}
|
||||
}
|
||||
// We cannot consider `torrent_removed_alert` as a starting point for removing content,
|
||||
// because it has an inconsistent posting time between different versions of libtorrent,
|
||||
// so files may still be in use in some cases.
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert)
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
#else
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hash);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
|
||||
#endif
|
||||
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(id);
|
||||
if (removingTorrentDataIter == m_removingTorrents.end())
|
||||
return;
|
||||
|
||||
// torrent_deleted_alert can also be posted due to deletion of partfile. Ignore it in such a case.
|
||||
if (removingTorrentDataIter->deleteOption == DeleteTorrent)
|
||||
return;
|
||||
|
||||
Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove);
|
||||
LogMsg(tr("Removed torrent and deleted its content. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
handleRemovedTorrent(torrentID);
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert)
|
||||
{
|
||||
#ifdef QBT_USES_LIBTORRENT2
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hashes);
|
||||
#else
|
||||
const auto id = TorrentID::fromInfoHash(alert->info_hash);
|
||||
const auto torrentID = TorrentID::fromInfoHash(alert->info_hash);
|
||||
#endif
|
||||
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(id);
|
||||
if (removingTorrentDataIter == m_removingTorrents.end())
|
||||
return;
|
||||
|
||||
if (alert->error)
|
||||
{
|
||||
// libtorrent won't delete the directory if it contains files not listed in the torrent,
|
||||
// so we remove the directory ourselves
|
||||
Utils::Fs::smartRemoveEmptyFolderTree(removingTorrentDataIter->pathToRemove);
|
||||
|
||||
LogMsg(tr("Removed torrent but failed to delete its content and/or partfile. Torrent: \"%1\". Error: \"%2\"")
|
||||
.arg(removingTorrentDataIter->name, QString::fromLocal8Bit(alert->error.message().c_str()))
|
||||
, Log::WARNING);
|
||||
}
|
||||
else // torrent without metadata, hence no files on disk
|
||||
{
|
||||
LogMsg(tr("Removed torrent. Torrent: \"%1\"").arg(removingTorrentDataIter->name));
|
||||
}
|
||||
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
const auto errorMessage = alert->error ? QString::fromLocal8Bit(alert->error.message().c_str()) : QString();
|
||||
handleRemovedTorrent(torrentID, errorMessage);
|
||||
}
|
||||
|
||||
void SessionImpl::handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert)
|
||||
|
@ -6079,6 +6108,29 @@ void SessionImpl::handleStateUpdateAlert(const lt::state_update_alert *alert)
|
|||
if (!updatedTorrents.isEmpty())
|
||||
emit torrentsUpdated(updatedTorrents);
|
||||
|
||||
if (!m_pendingFinishedTorrents.isEmpty())
|
||||
{
|
||||
for (TorrentImpl *torrent : m_pendingFinishedTorrents)
|
||||
{
|
||||
LogMsg(tr("Torrent download finished. Torrent: \"%1\"").arg(torrent->name()));
|
||||
emit torrentFinished(torrent);
|
||||
|
||||
if (const Path exportPath = finishedTorrentExportDirectory(); !exportPath.isEmpty())
|
||||
exportTorrentFile(torrent, exportPath);
|
||||
|
||||
processTorrentShareLimits(torrent);
|
||||
}
|
||||
|
||||
m_pendingFinishedTorrents.clear();
|
||||
|
||||
const bool hasUnfinishedTorrents = std::any_of(m_torrents.cbegin(), m_torrents.cend(), [](const TorrentImpl *torrent)
|
||||
{
|
||||
return !(torrent->isFinished() || torrent->isStopped() || torrent->isErrored());
|
||||
});
|
||||
if (!hasUnfinishedTorrents)
|
||||
emit allTorrentsFinished();
|
||||
}
|
||||
|
||||
if (m_needSaveTorrentsQueue)
|
||||
saveTorrentsQueue();
|
||||
|
||||
|
@ -6140,7 +6192,7 @@ void SessionImpl::handleTorrentConflictAlert(const lt::torrent_conflict_alert *a
|
|||
if (torrent2)
|
||||
{
|
||||
if (torrent1)
|
||||
deleteTorrent(torrentIDv1);
|
||||
removeTorrent(torrentIDv1);
|
||||
else
|
||||
cancelDownloadMetadata(torrentIDv1);
|
||||
|
||||
|
@ -6249,3 +6301,29 @@ void SessionImpl::updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, Q
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SessionImpl::handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError)
|
||||
{
|
||||
const auto removingTorrentDataIter = m_removingTorrents.find(torrentID);
|
||||
if (removingTorrentDataIter == m_removingTorrents.end())
|
||||
return;
|
||||
|
||||
if (!partfileRemoveError.isEmpty())
|
||||
{
|
||||
LogMsg(tr("Failed to remove partfile. Torrent: \"%1\". Reason: \"%2\".")
|
||||
.arg(removingTorrentDataIter->name, partfileRemoveError)
|
||||
, Log::WARNING);
|
||||
}
|
||||
|
||||
if ((removingTorrentDataIter->removeOption == TorrentRemoveOption::RemoveContent)
|
||||
&& !removingTorrentDataIter->contentStoragePath.isEmpty())
|
||||
{
|
||||
QMetaObject::invokeMethod(m_torrentContentRemover, [this, jobData = *removingTorrentDataIter]
|
||||
{
|
||||
m_torrentContentRemover->performJob(jobData.name, jobData.contentStoragePath
|
||||
, jobData.fileNames, m_torrentContentRemoveOption);
|
||||
});
|
||||
}
|
||||
|
||||
m_removingTorrents.erase(removingTorrentDataIter);
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ namespace BitTorrent
|
|||
class InfoHash;
|
||||
class ResumeDataStorage;
|
||||
class Torrent;
|
||||
class TorrentContentRemover;
|
||||
class TorrentDescriptor;
|
||||
class TorrentImpl;
|
||||
class Tracker;
|
||||
|
@ -402,7 +403,7 @@ namespace BitTorrent
|
|||
void setExcludedFileNamesEnabled(bool enabled) override;
|
||||
QStringList excludedFileNames() const override;
|
||||
void setExcludedFileNames(const QStringList &excludedFileNames) override;
|
||||
bool isFilenameExcluded(const QString &fileName) const override;
|
||||
void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) override;
|
||||
QStringList bannedIPs() const override;
|
||||
void setBannedIPs(const QStringList &newList) override;
|
||||
ResumeDataStorageType resumeDataStorageType() const override;
|
||||
|
@ -411,6 +412,8 @@ namespace BitTorrent
|
|||
void setMergeTrackersEnabled(bool enabled) override;
|
||||
bool isStartPaused() const override;
|
||||
void setStartPaused(bool value) override;
|
||||
TorrentContentRemoveOption torrentContentRemoveOption() const override;
|
||||
void setTorrentContentRemoveOption(TorrentContentRemoveOption option) override;
|
||||
|
||||
bool isRestored() const override;
|
||||
|
||||
|
@ -430,7 +433,7 @@ namespace BitTorrent
|
|||
|
||||
bool isKnownTorrent(const InfoHash &infoHash) const override;
|
||||
bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams ¶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;
|
||||
|
||||
|
@ -487,11 +490,11 @@ namespace BitTorrent
|
|||
void configureDeferred();
|
||||
void readAlerts();
|
||||
void enqueueRefresh();
|
||||
void processShareLimits();
|
||||
void generateResumeData();
|
||||
void handleIPFilterParsed(int ruleCount);
|
||||
void handleIPFilterError();
|
||||
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
|
||||
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
|
||||
|
||||
private:
|
||||
struct ResumeSessionContext;
|
||||
|
@ -507,8 +510,9 @@ namespace BitTorrent
|
|||
struct RemovingTorrentData
|
||||
{
|
||||
QString name;
|
||||
Path pathToRemove;
|
||||
DeleteOption deleteOption {};
|
||||
Path contentStoragePath;
|
||||
PathList fileNames;
|
||||
TorrentRemoveOption removeOption {};
|
||||
};
|
||||
|
||||
explicit SessionImpl(QObject *parent = nullptr);
|
||||
|
@ -536,6 +540,7 @@ namespace BitTorrent
|
|||
void enableIPFilter();
|
||||
void disableIPFilter();
|
||||
void processTrackerStatuses();
|
||||
void processTorrentShareLimits(TorrentImpl *torrent);
|
||||
void populateExcludedFileNamesRegExpList();
|
||||
void prepareStartup();
|
||||
void handleLoadedResumeData(ResumeSessionContext *context);
|
||||
|
@ -599,13 +604,7 @@ namespace BitTorrent
|
|||
|
||||
void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers);
|
||||
|
||||
// BitTorrent
|
||||
lt::session *m_nativeSession = nullptr;
|
||||
NativeSessionExtension *m_nativeSessionExtension = nullptr;
|
||||
|
||||
bool m_deferredConfigureScheduled = false;
|
||||
bool m_IPFilteringConfigured = false;
|
||||
mutable bool m_listenInterfaceConfigured = false;
|
||||
void handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError = {});
|
||||
|
||||
CachedSettingValue<QString> m_DHTBootstrapNodes;
|
||||
CachedSettingValue<bool> m_isDHTEnabled;
|
||||
|
@ -731,8 +730,16 @@ namespace BitTorrent
|
|||
CachedSettingValue<int> m_I2POutboundQuantity;
|
||||
CachedSettingValue<int> m_I2PInboundLength;
|
||||
CachedSettingValue<int> m_I2POutboundLength;
|
||||
CachedSettingValue<TorrentContentRemoveOption> m_torrentContentRemoveOption;
|
||||
SettingValue<bool> m_startPaused;
|
||||
|
||||
lt::session *m_nativeSession = nullptr;
|
||||
NativeSessionExtension *m_nativeSessionExtension = nullptr;
|
||||
|
||||
bool m_deferredConfigureScheduled = false;
|
||||
bool m_IPFilteringConfigured = false;
|
||||
mutable bool m_listenInterfaceConfigured = false;
|
||||
|
||||
bool m_isRestored = false;
|
||||
bool m_isPaused = isStartPaused();
|
||||
|
||||
|
@ -766,6 +773,7 @@ namespace BitTorrent
|
|||
QThreadPool *m_asyncWorker = nullptr;
|
||||
ResumeDataStorage *m_resumeDataStorage = nullptr;
|
||||
FileSearcher *m_fileSearcher = nullptr;
|
||||
TorrentContentRemover *m_torrentContentRemover = nullptr;
|
||||
|
||||
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
|
||||
|
||||
|
@ -809,6 +817,8 @@ namespace BitTorrent
|
|||
QTimer *m_wakeupCheckTimer = nullptr;
|
||||
QDateTime m_wakeupCheckTimestamp;
|
||||
|
||||
QList<TorrentImpl *> m_pendingFinishedTorrents;
|
||||
|
||||
friend void Session::initInstance();
|
||||
friend void Session::freeInstance();
|
||||
friend Session *Session::instance();
|
||||
|
|
|
@ -228,6 +228,7 @@ namespace BitTorrent
|
|||
virtual void setShareLimitAction(ShareLimitAction action) = 0;
|
||||
|
||||
virtual PathList filePaths() const = 0;
|
||||
virtual PathList actualFilePaths() const = 0;
|
||||
|
||||
virtual TorrentInfo info() const = 0;
|
||||
virtual bool isFinished() const = 0;
|
||||
|
|
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);
|
||||
};
|
||||
}
|
|
@ -77,6 +77,10 @@
|
|||
#include "base/utils/os.h"
|
||||
#endif // Q_OS_MACOS || Q_OS_WIN
|
||||
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
#include "customstorage.h"
|
||||
#endif
|
||||
|
||||
using namespace BitTorrent;
|
||||
|
||||
namespace
|
||||
|
@ -456,6 +460,8 @@ Path TorrentImpl::savePath() const
|
|||
void TorrentImpl::setSavePath(const Path &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
if (isAutoTMMEnabled()) [[unlikely]]
|
||||
return;
|
||||
|
||||
const Path basePath = m_session->useCategoryPathsInManualMode()
|
||||
? m_session->categorySavePath(category()) : m_session->savePath();
|
||||
|
@ -483,6 +489,8 @@ Path TorrentImpl::downloadPath() const
|
|||
void TorrentImpl::setDownloadPath(const Path &path)
|
||||
{
|
||||
Q_ASSERT(!isAutoTMMEnabled());
|
||||
if (isAutoTMMEnabled()) [[unlikely]]
|
||||
return;
|
||||
|
||||
const Path basePath = m_session->useCategoryPathsInManualMode()
|
||||
? m_session->categoryDownloadPath(category()) : m_session->downloadPath();
|
||||
|
@ -982,6 +990,21 @@ PathList TorrentImpl::filePaths() const
|
|||
return m_filePaths;
|
||||
}
|
||||
|
||||
PathList TorrentImpl::actualFilePaths() const
|
||||
{
|
||||
if (!hasMetadata())
|
||||
return {};
|
||||
|
||||
PathList paths;
|
||||
paths.reserve(filesCount());
|
||||
|
||||
const lt::file_storage files = nativeTorrentInfo()->files();
|
||||
for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes()))
|
||||
paths.emplaceBack(files.file_path(nativeIndex));
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
QVector<DownloadPriority> TorrentImpl::filePriorities() const
|
||||
{
|
||||
return m_filePriorities;
|
||||
|
@ -1447,11 +1470,13 @@ QBitArray TorrentImpl::pieces() const
|
|||
|
||||
QBitArray TorrentImpl::downloadingPieces() const
|
||||
{
|
||||
QBitArray result(piecesCount());
|
||||
if (!hasMetadata())
|
||||
return {};
|
||||
|
||||
std::vector<lt::partial_piece_info> queue;
|
||||
m_nativeHandle.get_download_queue(queue);
|
||||
|
||||
QBitArray result {piecesCount()};
|
||||
for (const lt::partial_piece_info &info : queue)
|
||||
result.setBit(LT::toUnderlyingType(info.piece_index));
|
||||
|
||||
|
@ -1791,12 +1816,13 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
|
|||
const Path filePath = actualFilePath.removedExtension(QB_EXT);
|
||||
m_filePaths.append(filePath);
|
||||
|
||||
lt::download_priority_t &nativePriority = p.file_priorities[LT::toUnderlyingType(nativeIndex)];
|
||||
if ((nativePriority != lt::dont_download) && m_session->isFilenameExcluded(filePath.filename()))
|
||||
nativePriority = lt::dont_download;
|
||||
const auto priority = LT::fromNative(nativePriority);
|
||||
m_filePriorities.append(priority);
|
||||
m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
|
||||
}
|
||||
|
||||
m_session->applyFilenameFilter(fileNames, m_filePriorities);
|
||||
for (int i = 0; i < m_filePriorities.size(); ++i)
|
||||
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]);
|
||||
|
||||
p.save_path = savePath.toString().toStdString();
|
||||
p.ti = metadata;
|
||||
|
||||
|
@ -1859,6 +1885,9 @@ void TorrentImpl::reload()
|
|||
|
||||
auto *const extensionData = new ExtensionData;
|
||||
p.userdata = LTClientData(extensionData);
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
p.storage = customStorageConstructor;
|
||||
#endif
|
||||
m_nativeHandle = m_nativeSession->add_torrent(p);
|
||||
|
||||
m_nativeStatus = extensionData->status;
|
||||
|
@ -1933,8 +1962,17 @@ void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext cont
|
|||
{
|
||||
if (!hasMetadata())
|
||||
{
|
||||
m_savePath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
if (context == MoveStorageContext::ChangeSavePath)
|
||||
{
|
||||
m_savePath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
else if (context == MoveStorageContext::ChangeDownloadPath)
|
||||
{
|
||||
m_downloadPath = newPath;
|
||||
m_session->handleTorrentSavePathChanged(this);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -153,6 +153,7 @@ namespace BitTorrent
|
|||
Path actualFilePath(int index) const override;
|
||||
qlonglong fileSize(int index) const override;
|
||||
PathList filePaths() const override;
|
||||
PathList actualFilePaths() const override;
|
||||
QVector<DownloadPriority> filePriorities() const override;
|
||||
|
||||
TorrentInfo info() const override;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
#define QBT_VERSION_MINOR 0
|
||||
#define QBT_VERSION_BUGFIX 0
|
||||
#define QBT_VERSION_BUILD 0
|
||||
#define QBT_VERSION_STATUS "beta1" // Should be empty for stable releases!
|
||||
#define QBT_VERSION_STATUS "" // 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
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -64,6 +64,7 @@
|
|||
#include "base/utils/fs.h"
|
||||
#include "base/utils/misc.h"
|
||||
#include "base/utils/string.h"
|
||||
#include "filterpatternformatmenu.h"
|
||||
#include "lineedit.h"
|
||||
#include "torrenttagsdialog.h"
|
||||
|
||||
|
@ -181,6 +182,11 @@ public:
|
|||
return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index));
|
||||
}
|
||||
|
||||
PathList filePaths() const
|
||||
{
|
||||
return (m_filePaths.isEmpty() ? m_torrentInfo.filePaths() : m_filePaths);
|
||||
}
|
||||
|
||||
void renameFile(const int index, const Path &newFilePath) override
|
||||
{
|
||||
Q_ASSERT((index >= 0) && (index < filesCount()));
|
||||
|
@ -290,6 +296,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
|||
, m_storeRememberLastSavePath {SETTINGS_KEY(u"RememberLastSavePath"_s)}
|
||||
, m_storeTreeHeaderState {u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_s)}
|
||||
, m_storeSplitterState {u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_s)}
|
||||
, m_storeFilterPatternFormat {u"GUI/" SETTINGS_KEY(u"FilterPatternFormat"_s)}
|
||||
{
|
||||
m_ui->setupUi(this);
|
||||
|
||||
|
@ -316,6 +323,8 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
|||
// Torrent content filtering
|
||||
m_filterLine->setPlaceholderText(tr("Filter files..."));
|
||||
m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
|
||||
m_filterLine->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_filterLine, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::showContentFilterContextMenu);
|
||||
m_ui->contentFilterLayout->insertWidget(3, m_filterLine);
|
||||
const auto *focusSearchHotkey = new QShortcut(QKeySequence::Find, this);
|
||||
connect(focusSearchHotkey, &QShortcut::activated, this, [this]()
|
||||
|
@ -360,7 +369,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
|||
});
|
||||
dlg->open();
|
||||
});
|
||||
connect(m_filterLine, &LineEdit::textChanged, m_ui->contentTreeView, &TorrentContentWidget::setFilterPattern);
|
||||
connect(m_filterLine, &LineEdit::textChanged, this, &AddNewTorrentDialog::setContentFilterPattern);
|
||||
connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll);
|
||||
connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone);
|
||||
connect(Preferences::instance(), &Preferences::changed, []
|
||||
|
@ -691,6 +700,28 @@ void AddNewTorrentDialog::saveTorrentFile()
|
|||
}
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::showContentFilterContextMenu()
|
||||
{
|
||||
QMenu *menu = m_filterLine->createStandardContextMenu();
|
||||
|
||||
auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu);
|
||||
connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format)
|
||||
{
|
||||
m_storeFilterPatternFormat = format;
|
||||
setContentFilterPattern();
|
||||
});
|
||||
|
||||
menu->addSeparator();
|
||||
menu->addMenu(formatMenu);
|
||||
menu->setAttribute(Qt::WA_DeleteOnClose);
|
||||
menu->popup(QCursor::pos());
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::setContentFilterPattern()
|
||||
{
|
||||
m_ui->contentTreeView->setFilterPattern(m_filterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards));
|
||||
}
|
||||
|
||||
void AddNewTorrentDialog::populateSavePaths()
|
||||
{
|
||||
Q_ASSERT(m_currentContext);
|
||||
|
@ -886,15 +917,7 @@ void AddNewTorrentDialog::setupTreeview()
|
|||
{
|
||||
// Check file name blacklist for torrents that are manually added
|
||||
QVector<BitTorrent::DownloadPriority> priorities = m_contentAdaptor->filePriorities();
|
||||
for (int i = 0; i < priorities.size(); ++i)
|
||||
{
|
||||
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
|
||||
continue;
|
||||
|
||||
if (BitTorrent::Session::instance()->isFilenameExcluded(torrentInfo.filePath(i).filename()))
|
||||
priorities[i] = BitTorrent::DownloadPriority::Ignored;
|
||||
}
|
||||
|
||||
BitTorrent::Session::instance()->applyFilenameFilter(m_contentAdaptor->filePaths(), priorities);
|
||||
m_contentAdaptor->prioritizeFiles(priorities);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -35,6 +35,7 @@
|
|||
|
||||
#include "base/path.h"
|
||||
#include "base/settingvalue.h"
|
||||
#include "filterpatternformat.h"
|
||||
|
||||
class LineEdit;
|
||||
|
||||
|
@ -92,6 +93,8 @@ private:
|
|||
void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {});
|
||||
void setupTreeview();
|
||||
void saveTorrentFile();
|
||||
void showContentFilterContextMenu();
|
||||
void setContentFilterPattern();
|
||||
|
||||
Ui::AddNewTorrentDialog *m_ui = nullptr;
|
||||
std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
|
||||
|
@ -107,4 +110,5 @@ private:
|
|||
SettingValue<bool> m_storeRememberLastSavePath;
|
||||
SettingValue<QByteArray> m_storeTreeHeaderState;
|
||||
SettingValue<QByteArray> m_storeSplitterState;
|
||||
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
|
||||
};
|
||||
|
|
|
@ -63,6 +63,7 @@ namespace
|
|||
// qBittorrent section
|
||||
QBITTORRENT_HEADER,
|
||||
RESUME_DATA_STORAGE,
|
||||
TORRENT_CONTENT_REMOVE_OPTION,
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
MEMORY_WORKING_SET_LIMIT,
|
||||
#endif
|
||||
|
@ -364,6 +365,8 @@ void AdvancedSettings::saveAdvancedSettings() const
|
|||
session->setI2PInboundLength(m_spinBoxI2PInboundLength.value());
|
||||
session->setI2POutboundLength(m_spinBoxI2POutboundLength.value());
|
||||
#endif
|
||||
|
||||
session->setTorrentContentRemoveOption(m_comboBoxTorrentContentRemoveOption.currentData().value<BitTorrent::TorrentContentRemoveOption>());
|
||||
}
|
||||
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
|
@ -472,6 +475,11 @@ void AdvancedSettings::loadAdvancedSettings()
|
|||
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
|
||||
addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage);
|
||||
|
||||
m_comboBoxTorrentContentRemoveOption.addItem(tr("Delete files permanently"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::Delete));
|
||||
m_comboBoxTorrentContentRemoveOption.addItem(tr("Move files to trash (if possible)"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::MoveToTrash));
|
||||
m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption())));
|
||||
addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption);
|
||||
|
||||
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
|
||||
// Physical memory (RAM) usage limit
|
||||
m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
|
||||
|
|
|
@ -81,7 +81,7 @@ private:
|
|||
m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity,
|
||||
m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused;
|
||||
QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxDiskIOReadMode, m_comboBoxDiskIOWriteMode, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm,
|
||||
m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage;
|
||||
m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage, m_comboBoxTorrentContentRemoveOption;
|
||||
QLineEdit m_lineEditAppInstanceName, m_pythonExecutablePath, m_lineEditAnnounceIP, m_lineEditDHTBootstrapNodes;
|
||||
|
||||
#ifndef QBT_USES_LIBTORRENT2
|
||||
|
|
|
@ -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);
|
||||
};
|
|
@ -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();
|
||||
|
||||
|
@ -466,10 +466,14 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
|
|||
{
|
||||
QStandardItem *item = m_peerItems.take(peerEndpoint);
|
||||
|
||||
QSet<QStandardItem *> &items = m_itemsByIP[peerEndpoint.address.ip];
|
||||
items.remove(item);
|
||||
if (items.isEmpty())
|
||||
m_itemsByIP.remove(peerEndpoint.address.ip);
|
||||
const auto items = m_itemsByIP.find(peerEndpoint.address.ip);
|
||||
Q_ASSERT(items != m_itemsByIP.end());
|
||||
if (items == m_itemsByIP.end()) [[unlikely]]
|
||||
continue;
|
||||
|
||||
items->remove(item);
|
||||
if (items->isEmpty())
|
||||
m_itemsByIP.erase(items);
|
||||
|
||||
m_listModel->removeRow(item->row());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
@ -41,6 +42,7 @@
|
|||
#include "base/indexrange.h"
|
||||
#include "base/path.h"
|
||||
#include "base/utils/misc.h"
|
||||
#include "gui/uithememanager.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
@ -114,10 +116,16 @@ namespace
|
|||
}
|
||||
|
||||
PiecesBar::PiecesBar(QWidget *parent)
|
||||
: QWidget {parent}
|
||||
: QWidget(parent)
|
||||
{
|
||||
updatePieceColors();
|
||||
setMouseTracking(true);
|
||||
|
||||
updateColorsImpl();
|
||||
connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, [this]
|
||||
{
|
||||
updateColors();
|
||||
redraw();
|
||||
});
|
||||
}
|
||||
|
||||
void PiecesBar::setTorrent(const BitTorrent::Torrent *torrent)
|
||||
|
@ -154,7 +162,7 @@ void PiecesBar::leaveEvent(QEvent *e)
|
|||
{
|
||||
m_hovered = false;
|
||||
m_highlightedRegion = {};
|
||||
requestImageUpdate();
|
||||
redraw();
|
||||
base::leaveEvent(e);
|
||||
}
|
||||
|
||||
|
@ -178,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">
|
||||
|
|
|
@ -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,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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -59,8 +59,8 @@ namespace
|
|||
int customCompare(const TagSet &left, const TagSet &right, const Utils::Compare::NaturalCompare<Qt::CaseInsensitive> &compare)
|
||||
{
|
||||
for (auto leftIter = left.cbegin(), rightIter = right.cbegin();
|
||||
(leftIter != left.cend()) && (rightIter != right.cend());
|
||||
++leftIter, ++rightIter)
|
||||
(leftIter != left.cend()) && (rightIter != right.cend());
|
||||
++leftIter, ++rightIter)
|
||||
{
|
||||
const int result = compare(leftIter->toString(), rightIter->toString());
|
||||
if (result != 0)
|
||||
|
@ -84,6 +84,17 @@ namespace
|
|||
return isLeftValid ? -1 : 1;
|
||||
}
|
||||
|
||||
int compareAsBool(const QVariant &left, const QVariant &right)
|
||||
{
|
||||
const bool leftValid = left.isValid();
|
||||
const bool rightValid = right.isValid();
|
||||
if (leftValid && rightValid)
|
||||
return threeWayCompare(left.toBool(), right.toBool());
|
||||
if (!leftValid && !rightValid)
|
||||
return 0;
|
||||
return leftValid ? -1 : 1;
|
||||
}
|
||||
|
||||
int adjustSubSortColumn(const int column)
|
||||
{
|
||||
return ((column >= 0) && (column < TransferListModel::NB_COLUMNS))
|
||||
|
@ -214,6 +225,9 @@ int TransferListSortModel::compare(const QModelIndex &left, const QModelIndex &r
|
|||
case TransferListModel::TR_UPSPEED:
|
||||
return customCompare(leftValue.toInt(), rightValue.toInt());
|
||||
|
||||
case TransferListModel::TR_PRIVATE:
|
||||
return compareAsBool(leftValue, rightValue);
|
||||
|
||||
case TransferListModel::TR_PEERS:
|
||||
case TransferListModel::TR_SEEDS:
|
||||
{
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2023 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2023-2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -116,9 +116,10 @@ namespace
|
|||
void removeTorrents(const QVector<BitTorrent::Torrent *> &torrents, const bool isDeleteFileSelected)
|
||||
{
|
||||
auto *session = BitTorrent::Session::instance();
|
||||
const DeleteOption deleteOption = isDeleteFileSelected ? DeleteTorrentAndFiles : DeleteTorrent;
|
||||
const BitTorrent::TorrentRemoveOption removeOption = isDeleteFileSelected
|
||||
? BitTorrent::TorrentRemoveOption::RemoveContent : BitTorrent::TorrentRemoveOption::KeepContent;
|
||||
for (const BitTorrent::Torrent *torrent : torrents)
|
||||
session->deleteTorrent(torrent->id(), deleteOption);
|
||||
session->removeTorrent(torrent->id(), removeOption);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -183,6 +184,7 @@ TransferListWidget::TransferListWidget(QWidget *parent, MainWindow *mainWindow)
|
|||
setColumnHidden(TransferListModel::TR_LAST_ACTIVITY, true);
|
||||
setColumnHidden(TransferListModel::TR_TOTAL_SIZE, true);
|
||||
setColumnHidden(TransferListModel::TR_REANNOUNCE, true);
|
||||
setColumnHidden(TransferListModel::TR_PRIVATE, true);
|
||||
}
|
||||
|
||||
//Ensure that at least one column is visible at all times
|
||||
|
@ -442,7 +444,7 @@ void TransferListWidget::deleteSelectedTorrents(const bool deleteLocalFiles)
|
|||
{
|
||||
// Some torrents might be removed when waiting for user input, so refetch the torrent list
|
||||
// NOTE: this will only work when dialog is modal
|
||||
removeTorrents(getSelectedTorrents(), dialog->isDeleteFileSelected());
|
||||
removeTorrents(getSelectedTorrents(), dialog->isRemoveContentSelected());
|
||||
});
|
||||
dialog->open();
|
||||
}
|
||||
|
@ -465,7 +467,7 @@ void TransferListWidget::deleteVisibleTorrents()
|
|||
{
|
||||
// Some torrents might be removed when waiting for user input, so refetch the torrent list
|
||||
// NOTE: this will only work when dialog is modal
|
||||
removeTorrents(getVisibleTorrents(), dialog->isDeleteFileSelected());
|
||||
removeTorrents(getVisibleTorrents(), dialog->isRemoveContentSelected());
|
||||
});
|
||||
dialog->open();
|
||||
}
|
||||
|
@ -1190,7 +1192,7 @@ void TransferListWidget::displayListMenu()
|
|||
const TagSet tags = BitTorrent::Session::instance()->tags();
|
||||
for (const Tag &tag : asConst(tags))
|
||||
{
|
||||
auto *action = new TriStateAction(tag.toString(), tagsMenu);
|
||||
auto *action = new TriStateAction(Utils::Gui::tagToWidgetText(tag), tagsMenu);
|
||||
action->setCloseOnInteraction(false);
|
||||
|
||||
const Qt::CheckState initialState = tagsInAll.contains(tag) ? Qt::Checked
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2017 Mike Tzou
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -54,6 +55,7 @@
|
|||
|
||||
#include "base/global.h"
|
||||
#include "base/path.h"
|
||||
#include "base/tag.h"
|
||||
#include "base/utils/fs.h"
|
||||
#include "base/utils/version.h"
|
||||
|
||||
|
@ -216,3 +218,29 @@ void Utils::Gui::openFolderSelect(const Path &path)
|
|||
openPath(path.parentPath());
|
||||
#endif
|
||||
}
|
||||
|
||||
QString Utils::Gui::tagToWidgetText(const Tag &tag)
|
||||
{
|
||||
return tag.toString().replace(u'&', u"&&"_s);
|
||||
}
|
||||
|
||||
Tag Utils::Gui::widgetTextToTag(const QString &text)
|
||||
{
|
||||
// replace pairs of '&' with single '&' and remove non-paired occurrences of '&'
|
||||
QString cleanedText;
|
||||
cleanedText.reserve(text.size());
|
||||
bool amp = false;
|
||||
for (const QChar c : text)
|
||||
{
|
||||
if (c == u'&')
|
||||
{
|
||||
amp = !amp;
|
||||
if (amp)
|
||||
continue;
|
||||
}
|
||||
|
||||
cleanedText.append(c);
|
||||
}
|
||||
|
||||
return Tag(cleanedText);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/*
|
||||
* Bittorrent Client using Qt and libtorrent.
|
||||
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
|
||||
* Copyright (C) 2017 Mike Tzou
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
|
@ -34,8 +35,11 @@ class QIcon;
|
|||
class QPixmap;
|
||||
class QPoint;
|
||||
class QSize;
|
||||
class QString;
|
||||
class QWidget;
|
||||
|
||||
class Tag;
|
||||
|
||||
namespace Utils::Gui
|
||||
{
|
||||
bool isDarkTheme();
|
||||
|
@ -51,4 +55,7 @@ namespace Utils::Gui
|
|||
|
||||
void openPath(const Path &path);
|
||||
void openFolderSelect(const Path &path);
|
||||
|
||||
QString tagToWidgetText(const Tag &tag);
|
||||
Tag widgetTextToTag(const QString &text);
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
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