mirror of
https://github.com/qbittorrent/qBittorrent
synced 2025-07-06 05:01:25 -07:00
Compare commits
67 commits
master
...
release-5.
Author | SHA1 | Date | |
---|---|---|---|
|
202ff8a099 | ||
|
c0585441fb | ||
|
a8b6cbceb0 | ||
|
6ad073e0bc | ||
|
ad68813fe8 | ||
|
22df0b45c5 | ||
|
bb34444ddc | ||
|
dd5c934103 | ||
|
3fca180e98 | ||
|
9b29d37d21 | ||
|
206d5abf84 | ||
|
101f35dcf2 | ||
|
13282d94ef | ||
|
1daa42e4fe | ||
|
ea9f3800ce | ||
|
af14584772 | ||
|
7d51524251 | ||
|
7a9aac79f9 | ||
|
085ae0d1c4 | ||
|
f748a682ca | ||
|
df987cc954 | ||
|
535fc42747 | ||
|
1da31bc2e1 | ||
|
9515ca59f2 | ||
|
eaf9017aa4 | ||
|
f51ad39ad9 | ||
|
9133b16431 | ||
|
909a3eb44e | ||
|
778aa64c54 | ||
|
7049f80a01 | ||
|
87b90b7fd7 | ||
|
b3690494ab | ||
|
f4e6b515c2 | ||
|
a721540e6c | ||
|
3fd05d001f | ||
|
f04b114b64 | ||
|
da87be2b12 | ||
|
891265b390 | ||
|
f46e44d3ed | ||
|
a4094a440d | ||
|
46c3da21e1 | ||
|
2f06ea2587 | ||
|
cfbf6b73ff | ||
|
c687a7d0d3 | ||
|
009cc71f9b | ||
|
de1cf208ce | ||
|
5f49472fa4 | ||
|
2076302170 | ||
|
2a33e187eb | ||
|
00149e03c0 | ||
|
57d529c17a | ||
|
d492fcf29a | ||
|
d0caa35b39 | ||
|
ec7a00af92 | ||
|
76a3aba7e0 | ||
|
7003ac3f4d | ||
|
964be0fa1c | ||
|
c1defceccf | ||
|
260394623d | ||
|
478c2d5b12 | ||
|
49cfbd9a49 | ||
|
d028f46fab | ||
|
57b24a200e | ||
|
269dfe87e0 | ||
|
6a1c465d85 | ||
|
bc7d5c1f8f | ||
|
8aabef423c |
206 changed files with 62087 additions and 78450 deletions
13
.github/workflows/ci_ubuntu.yaml
vendored
13
.github/workflows/ci_ubuntu.yaml
vendored
|
@ -138,16 +138,15 @@ jobs:
|
||||||
|
|
||||||
- name: Install AppImage
|
- name: Install AppImage
|
||||||
run: |
|
run: |
|
||||||
sudo apt install libfuse2
|
|
||||||
curl \
|
curl \
|
||||||
-L \
|
-L \
|
||||||
-Z \
|
-Z \
|
||||||
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage \
|
-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-static-x86_64.AppImage \
|
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||||
-O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
|
-O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||||
chmod +x \
|
chmod +x \
|
||||||
linuxdeploy-static-x86_64.AppImage \
|
linuxdeploy-x86_64.AppImage \
|
||||||
linuxdeploy-plugin-qt-static-x86_64.AppImage \
|
linuxdeploy-plugin-qt-x86_64.AppImage \
|
||||||
linuxdeploy-plugin-appimage-x86_64.AppImage
|
linuxdeploy-plugin-appimage-x86_64.AppImage
|
||||||
|
|
||||||
- name: Prepare files for AppImage
|
- name: Prepare files for AppImage
|
||||||
|
@ -160,12 +159,12 @@ jobs:
|
||||||
|
|
||||||
- name: Package AppImage
|
- name: Package AppImage
|
||||||
run: |
|
run: |
|
||||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --plugin qt
|
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
|
||||||
rm qbittorrent/apprun-hooks/*
|
rm qbittorrent/apprun-hooks/*
|
||||||
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh
|
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh
|
||||||
NO_APPSTREAM=1 \
|
NO_APPSTREAM=1 \
|
||||||
OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \
|
OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \
|
||||||
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --output appimage
|
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --output appimage
|
||||||
|
|
||||||
- name: Upload build artifacts
|
- name: Upload build artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[main]
|
[main]
|
||||||
host = https://www.transifex.com
|
host = https://www.transifex.com
|
||||||
|
|
||||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_master]
|
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_v51x]
|
||||||
file_filter = src/lang/qbittorrent_<lang>.ts
|
file_filter = src/lang/qbittorrent_<lang>.ts
|
||||||
source_file = src/lang/qbittorrent_en.ts
|
source_file = src/lang/qbittorrent_en.ts
|
||||||
source_lang = en
|
source_lang = en
|
||||||
|
@ -9,7 +9,7 @@ type = QT
|
||||||
minimum_perc = 23
|
minimum_perc = 23
|
||||||
lang_map = pt: pt_PT, zh: zh_CN
|
lang_map = pt: pt_PT, zh: zh_CN
|
||||||
|
|
||||||
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui]
|
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui_v51x]
|
||||||
file_filter = src/webui/www/translations/webui_<lang>.ts
|
file_filter = src/webui/www/translations/webui_<lang>.ts
|
||||||
source_file = src/webui/www/translations/webui_en.ts
|
source_file = src/webui/www/translations/webui_en.ts
|
||||||
source_lang = en
|
source_lang = en
|
||||||
|
|
119
Changelog
119
Changelog
|
@ -1,4 +1,121 @@
|
||||||
Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.0
|
Wed Jul 02nd 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.2
|
||||||
|
- BUGFIX: Don't expose palette colors in UI theme editor since they are not customizable (glassez)
|
||||||
|
- BUGFIX: Add fallback to update mechanism (sledgehammer999)
|
||||||
|
- WEBUI: Fix incorrectly backported changes (glassez)
|
||||||
|
- WEBAPI: Trim leading whitespaces on Run External Program fields (Chocobo1)
|
||||||
|
- RSS/SEARCH: Prevent opening local files if web page is expected (glassez)
|
||||||
|
- MACOS: Make qBittorrent quit on MacOS with main window closed (Ryu481)
|
||||||
|
|
||||||
|
Mon Jun 23rd 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.1
|
||||||
|
- BUGFIX: Don't interpret wildcard pattern as filepath globbing (glassez)
|
||||||
|
- BUGFIX: Fix appearance of search history length spinbox (glassez)
|
||||||
|
- BUGFIX: Remove dubious seeding time max value (glassez)
|
||||||
|
- BUGFIX: Fix ratio handling (glassez)
|
||||||
|
- BUGFIX: Fix compilation with Qt 6.6.0 (glassez)
|
||||||
|
- WEBUI: Make General tab text selectable by default (dezza)
|
||||||
|
- WEBUI: Add versioning to local preferences (Chocobo1)
|
||||||
|
- WEBUI: Make multi-rename search & replace fields use a monospace font (Atk)
|
||||||
|
- WEBUI: Fix wrong replacement sequence in IPv6 string (Chocobo1)
|
||||||
|
- WEBUI: Fix memory leak (bolshoytoster)
|
||||||
|
- WEBUI: Fix path autofill in set location and new category (tehcneko)
|
||||||
|
- RSS: Mark matched article as "read" if it refers to a duplicate torrent (glassez)
|
||||||
|
- WINDOWS: Update command line help message (KanishkaHalder1771)
|
||||||
|
- WINDOWS: NSIS: Don't require agreement on the license page (Chocobo1)
|
||||||
|
- LINUX: Fix preview not opening on Wayland (Isak05)
|
||||||
|
- LINUX: Add fallback for random number generator (Chocobo1)
|
||||||
|
|
||||||
|
Sun Apr 27th 2025 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.0
|
||||||
|
- FEATURE: Enable customizing the save statistics time interval (Burnerelu)
|
||||||
|
- FEATURE: Add drag support to torrent content widget (Chocobo1)
|
||||||
|
- FEATURE: Display External IP Address in status bar (Thomas Piccirello)
|
||||||
|
- FEATURE: Use modern functions to get random numbers under Linux/Windows (security related) (Chocobo1)
|
||||||
|
- FEATURE: Add eXact Length parameter when creating magnet URI (antanilol)
|
||||||
|
- FEATURE: Support fetching tracker list from URL (Thomas Piccirello)
|
||||||
|
- FEATURE: Add `announce_port` support (Maxime Thiebaut)
|
||||||
|
- BUGFIX: Enable adaptive step size for upload and download limits (Harald Nordgren)
|
||||||
|
- BUGFIX: Add URL link for reverse proxy setup examples (Chocobo1)
|
||||||
|
- BUGFIX: Allow drop action only on transfer list (Chocobo1)
|
||||||
|
- BUGFIX: Fix the tab order in dialogs (thalieht)
|
||||||
|
- BUGFIX: Fix filesize sorting in preview dialog (DoubleSpicy)
|
||||||
|
- BUGFIX: Improve the speed icons in the status bar (Mahdi Hosseinzadeh)
|
||||||
|
- BUGFIX: Update link to news (tinyboxvk)
|
||||||
|
- BUGFIX: Fix tab stop order in various dialogs and UI elements (Chocobo1)
|
||||||
|
- BUGFIX: Make links accessible by keyboard (Chocobo1)
|
||||||
|
- BUGFIX: Make tab key switch focus (Chocobo1)
|
||||||
|
- BUGFIX: Revise DHT bootstrap node list (stalkerok, Chocobo1)
|
||||||
|
- BUGFIX: Return first tracker as fallback for "current tracker" (glassez)
|
||||||
|
- BUGFIX: Prevent crash when exiting app with `Add torrent` dialogs opened (glassez)
|
||||||
|
- BUGFIX: Fix torrent relocating files when switching to "manual" mode (glassez)
|
||||||
|
- BUGFIX: Prevent crash due to corrupted resume data (glassez)
|
||||||
|
- WEBUI: Improvements that should help with assistive technologies (Chocobo1)
|
||||||
|
- WEBUI: Internal refactoring to migrate away from MooTools and towards native browser APIs (Chocobo1, skomerko)
|
||||||
|
- WEBUI: Implement path autocompletion (Paweł Kotiuk)
|
||||||
|
- WEBUI: Implement double-click behavior controls (Hanabishi)
|
||||||
|
- WEBUI: Add ability to toggle alternating row colors in tables (skomerko)
|
||||||
|
- WEBUI: Improve visibility of unread RSS articles (skomerko)
|
||||||
|
- WEBUI: Remove deleted torrents even if they are currently filtered out (Carmelo Scandaliato)
|
||||||
|
- WEBUI: Highlight torrent category in context menu (skomerko)
|
||||||
|
- WEBUI: Implement 'Auto hide zero status filters' (skomerko)
|
||||||
|
- WEBUI: Allow to filter torrent list by save path (skomerko)
|
||||||
|
- WEBUI: Handle regex syntax error for torrent filtering (HamletDuFromage)
|
||||||
|
- WEBUI: Add missing icons (skomerko)
|
||||||
|
- WEBUI: Add link to 'List of alternative WebUI' wiki page in Options (Chocobo1)
|
||||||
|
- WEBUI: Improve properties panel, torrent deletion dialog, filter list, subcategories, torrent deletion, statistics window (skomerko)
|
||||||
|
- WEBUI: Allow to display only hostname in the Tracker column (skomerko)
|
||||||
|
- WEBUI: Show country/region name next to its flag (skomerko)
|
||||||
|
- WEBUI: Improve hash copy actions in context menu (skomerko)
|
||||||
|
- WEBUI: Support removing tracker from all torrents in WebUI/WebAPI (Thomas Piccirello)
|
||||||
|
- WEBUI: Display DHT information in the Status bar only when DHT is enabled (skomerko)
|
||||||
|
- WEBUI: Add 'Confirm torrent recheck' option (skomerko)
|
||||||
|
- WEBUI: Support managing web seeds (Thomas Piccirello)
|
||||||
|
- WEBUI: Add colors to log table rows (skomerko)
|
||||||
|
- WEBUI: Prevent text selection within tabs, menu items (skomerko)
|
||||||
|
- WEBUI: Use correct text and background colors in RSS details view (skomerko)
|
||||||
|
- WEBUI: Reduce padding in torrents table (skomerko)
|
||||||
|
- WEBUI: Add WebAPI/WebUI for managing cookies (Thomas Piccirello)
|
||||||
|
- WEBUI: Support updating RSS feed URL (Thomas Piccirello)
|
||||||
|
- WEBUI: Add 'Engine' column to Search table (skomerko)
|
||||||
|
- WEBUI: Add confirm dialog for Auto TMM (skomerko)
|
||||||
|
- WEBUI: Add context menu to search tabs (skomerko)
|
||||||
|
- WEBUI: Show file filter when Content tab selected on load (Thomas Piccirello)
|
||||||
|
- WEBUI: DHT, PeX and LSD rows are now always on top in Trackers table (skomerko)
|
||||||
|
- WEBUI: Clear properties panel when torrent no longer selected (skomerko)
|
||||||
|
- WEBUI: Support auto resizing table columns (Thomas Piccirello)
|
||||||
|
- WEBUI: Fix displaying RSS panel on load (Thomas Piccirello)
|
||||||
|
- WEBUI: Add tooltip to regex filter button (Patrik Elfström)
|
||||||
|
- WEBUI: Hide context menu when clicking on a table row (Patrik Elfström)
|
||||||
|
- WEBUI: Display torrent progress percentage in General tab (skomerko)
|
||||||
|
- WEBUI: Use thin scrollbars (skomerko)
|
||||||
|
- WEBUI: Show 'Rename...' context menu item only when one torrent is selected (skomerko)
|
||||||
|
- WEBUI: Display error when download fails (Thomas Piccirello)
|
||||||
|
- WEBUI: Add colors to 'Status' column in Trackers table (skomerko)
|
||||||
|
- WEBUI: Add missing icon to 'Queue' context menu item (skomerko)
|
||||||
|
- WEBUI: Change filter inputs to type search (Patrik Elfström)
|
||||||
|
- WEBUI: Allow to move state icon to name column in torrents table (skomerko)
|
||||||
|
- WEBUI: Fix bug where the 'Tracker editing' dialog displays incorrect data (skomerko)
|
||||||
|
- WEBUI: Maintain row highlight after rearranging table columns (skomerko)
|
||||||
|
- WEBUI: Fix preferences not applied in magnet handler (Chocobo1)
|
||||||
|
- WEBUI: Update sort icon after changing column order (skomerko)
|
||||||
|
- WEBUI: Show 'Edit tracker URL...' only when one tracker is selected (skomerko)
|
||||||
|
- WEBUI: Set status filter to 'All' if selected filter is no longer visible (skomerko)
|
||||||
|
- WEBAPI: Don't reannounce when removing tracker via WebAPI (Thomas Piccirello)
|
||||||
|
- WEBAPI: Add WebAPI for managing torrent webseeds (Thomas Piccirello)
|
||||||
|
- WEBAPI: Add `forced` parameter to `torrents/add` (Chris B)
|
||||||
|
- WEBAPI: Optionally include trackers list in torrent info response (ze0s)
|
||||||
|
- WEBAPI: Add new method `setTags` to upsert tags on torrents (ze0s)
|
||||||
|
- RSS: Resolve relative URLs within RSS article description (Zentino)
|
||||||
|
- SEARCH: Provide SSL context field (Chocobo1)
|
||||||
|
- SEARCH: Allow to refresh existing search (glassez)
|
||||||
|
- SEARCH: Allow multiple simultaneous searches (glassez)
|
||||||
|
- SEARCH: Store opened search tabs (glassez)
|
||||||
|
- SEARCH: Store search history (glassez)
|
||||||
|
- SEARCH: Migrate socks.py from SocksiPy to PySocks 1.7.1 (FredBill1)
|
||||||
|
- SEARCH: Bump Python version minimum requirement (Chocobo1)
|
||||||
|
- WINDOWS: Opt into Windows SegmentHeap (Andarwinux)
|
||||||
|
- WINDOWS: Allow to choose color scheme on Windows (glassez)
|
||||||
|
- WINDOWS: Verify hash of Python installer (Chocobo1)
|
||||||
|
- LINUX: Add support for Thunar file manager (algebnaly)
|
||||||
|
- MACOS: Fix shift-click selection on macOS (Luke Memet)
|
||||||
|
|
||||||
Mon Oct 28th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.1
|
Mon Oct 28th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.1
|
||||||
- FEATURE: Add "Simple pread/pwrite" disk IO type (Hanabishi)
|
- FEATURE: Add "Simple pread/pwrite" disk IO type (Hanabishi)
|
||||||
|
|
|
@ -47,6 +47,9 @@ find_package(Boost ${minBoostVersion} REQUIRED)
|
||||||
find_package(OpenSSL ${minOpenSSLVersion} REQUIRED)
|
find_package(OpenSSL ${minOpenSSLVersion} REQUIRED)
|
||||||
find_package(ZLIB ${minZlibVersion} REQUIRED)
|
find_package(ZLIB ${minZlibVersion} REQUIRED)
|
||||||
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS Core Network Sql Xml LinguistTools)
|
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS Core Network Sql Xml LinguistTools)
|
||||||
|
if (Qt6_FOUND AND (Qt6_VERSION VERSION_GREATER_EQUAL 6.10))
|
||||||
|
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS CorePrivate)
|
||||||
|
endif()
|
||||||
if (DBUS)
|
if (DBUS)
|
||||||
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS DBus)
|
find_package(Qt6 ${minQt6Version} REQUIRED COMPONENTS DBus)
|
||||||
set_package_properties(Qt6DBus PROPERTIES
|
set_package_properties(Qt6DBus PROPERTIES
|
||||||
|
|
4
dist/mac/Info.plist
vendored
4
dist/mac/Info.plist
vendored
|
@ -55,7 +55,7 @@
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>5.1.0</string>
|
<string>5.1.2</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>${EXECUTABLE_NAME}</string>
|
<string>${EXECUTABLE_NAME}</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
<key>NSAppleScriptEnabled</key>
|
<key>NSAppleScriptEnabled</key>
|
||||||
<string>YES</string>
|
<string>YES</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>Copyright © 2006-2024 The qBittorrent project</string>
|
<string>Copyright © 2006-2025 The qBittorrent project</string>
|
||||||
<key>UTExportedTypeDeclarations</key>
|
<key>UTExportedTypeDeclarations</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
|
|
|
@ -105,7 +105,7 @@ GenericName[ka]=BitTorrent კლიენტი
|
||||||
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
|
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
|
||||||
Name[ka]=qBittorrent
|
Name[ka]=qBittorrent
|
||||||
GenericName[ko]=BitTorrent 클라이언트
|
GenericName[ko]=BitTorrent 클라이언트
|
||||||
Comment[ko]=BitTorrent를 통한 파일 다운로드 및 공유
|
Comment[ko]=BitTorrent를 통해 파일 다운로드 및 공유
|
||||||
Name[ko]=qBittorrent
|
Name[ko]=qBittorrent
|
||||||
GenericName[lt]=BitTorrent klientas
|
GenericName[lt]=BitTorrent klientas
|
||||||
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
|
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
|
||||||
|
|
|
@ -62,6 +62,6 @@
|
||||||
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
|
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
|
||||||
<content_rating type="oars-1.1"/>
|
<content_rating type="oars-1.1"/>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="5.1.0~beta1" date="2024-12-16"/>
|
<release version="5.1.2" date="2025-07-02"/>
|
||||||
</releases>
|
</releases>
|
||||||
</component>
|
</component>
|
||||||
|
|
7
dist/windows/config.nsh
vendored
7
dist/windows/config.nsh
vendored
|
@ -14,7 +14,7 @@
|
||||||
; 4.5.1.3 -> good
|
; 4.5.1.3 -> good
|
||||||
; 4.5.1.3.2 -> bad
|
; 4.5.1.3.2 -> bad
|
||||||
; 4.5.0beta -> bad
|
; 4.5.0beta -> bad
|
||||||
!define /ifndef QBT_VERSION "5.1.0"
|
!define /ifndef QBT_VERSION "5.1.2"
|
||||||
|
|
||||||
; Option that controls the installer's window name
|
; Option that controls the installer's window name
|
||||||
; If set, its value will be used like this:
|
; If set, its value will be used like this:
|
||||||
|
@ -86,7 +86,7 @@ OutFile "qbittorrent_${QBT_INSTALLER_FILENAME}_setup.exe"
|
||||||
;Installer Version Information
|
;Installer Version Information
|
||||||
VIAddVersionKey "ProductName" "qBittorrent"
|
VIAddVersionKey "ProductName" "qBittorrent"
|
||||||
VIAddVersionKey "CompanyName" "The qBittorrent project"
|
VIAddVersionKey "CompanyName" "The qBittorrent project"
|
||||||
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2024 The qBittorrent project"
|
VIAddVersionKey "LegalCopyright" "Copyright ©2006-2025 The qBittorrent project"
|
||||||
VIAddVersionKey "FileDescription" "qBittorrent - A Bittorrent Client"
|
VIAddVersionKey "FileDescription" "qBittorrent - A Bittorrent Client"
|
||||||
VIAddVersionKey "FileVersion" "${QBT_VERSION}"
|
VIAddVersionKey "FileVersion" "${QBT_VERSION}"
|
||||||
|
|
||||||
|
@ -111,7 +111,8 @@ RequestExecutionLevel user
|
||||||
!define MUI_HEADERIMAGE
|
!define MUI_HEADERIMAGE
|
||||||
!define MUI_COMPONENTSPAGE_NODESC
|
!define MUI_COMPONENTSPAGE_NODESC
|
||||||
;!define MUI_ICON "qbittorrent.ico"
|
;!define MUI_ICON "qbittorrent.ico"
|
||||||
!define MUI_LICENSEPAGE_CHECKBOX
|
!define MUI_LICENSEPAGE_BUTTON $(^NextBtn)
|
||||||
|
!define MUI_LICENSEPAGE_TEXT_BOTTOM "$_CLICK"
|
||||||
!define MUI_LANGDLL_ALLLANGUAGES
|
!define MUI_LANGDLL_ALLLANGUAGES
|
||||||
|
|
||||||
;--------------------------------
|
;--------------------------------
|
||||||
|
|
12
dist/windows/installer-translations/swedish.nsh
vendored
12
dist/windows/installer-translations/swedish.nsh
vendored
|
@ -7,21 +7,21 @@ LangString inst_desktop ${LANG_SWEDISH} "Skapa skrivbordsgenväg"
|
||||||
;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut"
|
;LangString inst_startmenu ${LANG_ENGLISH} "Create Start Menu Shortcut"
|
||||||
LangString inst_startmenu ${LANG_SWEDISH} "Skapa startmenygenväg"
|
LangString inst_startmenu ${LANG_SWEDISH} "Skapa startmenygenväg"
|
||||||
;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up"
|
;LangString inst_startup ${LANG_ENGLISH} "Start qBittorrent on Windows start up"
|
||||||
LangString inst_startup ${LANG_SWEDISH} "Starta qBittorrent vid Windows start"
|
LangString inst_startup ${LANG_SWEDISH} "Starta qBittorrent vid Windows-uppstart"
|
||||||
;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent"
|
;LangString inst_torrent ${LANG_ENGLISH} "Open .torrent files with qBittorrent"
|
||||||
LangString inst_torrent ${LANG_SWEDISH} "Öppna .torrent-filer med qBittorrent"
|
LangString inst_torrent ${LANG_SWEDISH} "Öppna .torrent-filer med qBittorrent"
|
||||||
;LangString inst_magnet ${LANG_ENGLISH} "Open magnet links with qBittorrent"
|
;LangString inst_magnet ${LANG_ENGLISH} "Open magnet links with qBittorrent"
|
||||||
LangString inst_magnet ${LANG_SWEDISH} "Öppna magnetlänkar med qBittorrent"
|
LangString inst_magnet ${LANG_SWEDISH} "Öppna magnetlänkar med qBittorrent"
|
||||||
;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule"
|
;LangString inst_firewall ${LANG_ENGLISH} "Add Windows Firewall rule"
|
||||||
LangString inst_firewall ${LANG_SWEDISH} "Lägg till Windows-brandväggregel"
|
LangString inst_firewall ${LANG_SWEDISH} "Lägg till Windows-brandväggsregel"
|
||||||
;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)"
|
;LangString inst_pathlimit ${LANG_ENGLISH} "Disable Windows path length limit (260 character MAX_PATH limitation, requires Windows 10 1607 or later)"
|
||||||
LangString inst_pathlimit ${LANG_SWEDISH} "Inaktivera gränsen för Windows-sökvägslängd (260 tecken MAX_PATH-begränsning, kräver Windows 10 1607 eller senare)"
|
LangString inst_pathlimit ${LANG_SWEDISH} "Inaktivera gränsen för Windows-sökvägslängd (260 tecken MAX_PATH-begränsning, kräver Windows 10 1607 eller senare)"
|
||||||
;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule"
|
;LangString inst_firewallinfo ${LANG_ENGLISH} "Adding Windows Firewall rule"
|
||||||
LangString inst_firewallinfo ${LANG_SWEDISH} "Lägger till Windows-brandväggregel"
|
LangString inst_firewallinfo ${LANG_SWEDISH} "Lägger till Windows-brandväggsregel"
|
||||||
;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing."
|
;LangString inst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before installing."
|
||||||
LangString inst_warning ${LANG_SWEDISH} "qBittorrent körs. Vänligen stäng programmet innan du installerar."
|
LangString inst_warning ${LANG_SWEDISH} "qBittorrent körs. Stäng programmet innan du installerar."
|
||||||
;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact."
|
;LangString inst_uninstall_question ${LANG_ENGLISH} "Current version will be uninstalled. User settings and torrents will remain intact."
|
||||||
LangString inst_uninstall_question ${LANG_SWEDISH} "Nuvarande version avinstalleras. Användarinställningar och torrenter kommer att förbli intakta."
|
LangString inst_uninstall_question ${LANG_SWEDISH} "Aktuell version avinstalleras. Användarinställningar och torrenter kommer att förbli intakta."
|
||||||
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
|
;LangString inst_unist ${LANG_ENGLISH} "Uninstalling previous version."
|
||||||
LangString inst_unist ${LANG_SWEDISH} "Avinstallerar tidigare version."
|
LangString inst_unist ${LANG_SWEDISH} "Avinstallerar tidigare version."
|
||||||
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
|
;LangString launch_qbt ${LANG_ENGLISH} "Launch qBittorrent."
|
||||||
|
@ -53,7 +53,7 @@ LangString remove_firewallinfo ${LANG_SWEDISH} "Tar bort Windows-brandväggsrege
|
||||||
;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data"
|
;LangString remove_cache ${LANG_ENGLISH} "Remove torrents and cached data"
|
||||||
LangString remove_cache ${LANG_SWEDISH} "Ta bort torrenter och cachade data"
|
LangString remove_cache ${LANG_SWEDISH} "Ta bort torrenter och cachade data"
|
||||||
;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling."
|
;LangString uninst_warning ${LANG_ENGLISH} "qBittorrent is running. Please close the application before uninstalling."
|
||||||
LangString uninst_warning ${LANG_SWEDISH} "qBittorrent körs. Vänligen stäng programmet innan du avinstallerar."
|
LangString uninst_warning ${LANG_SWEDISH} "qBittorrent körs. Stäng programmet innan du avinstallerar."
|
||||||
;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:"
|
;LangString uninst_tor_warn ${LANG_ENGLISH} "Not removing .torrent association. It is associated with:"
|
||||||
LangString uninst_tor_warn ${LANG_SWEDISH} "Tar inte bort .torrent-association. Den är associerad med:"
|
LangString uninst_tor_warn ${LANG_SWEDISH} "Tar inte bort .torrent-association. Den är associerad med:"
|
||||||
;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:"
|
;LangString uninst_mag_warn ${LANG_ENGLISH} "Not removing magnet association. It is associated with:"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
|
* Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
* Copyright (C) 2006 Christophe Dumez
|
* Copyright (C) 2006 Christophe Dumez
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
|
@ -124,6 +124,28 @@ namespace
|
||||||
const int PIXMAP_CACHE_SIZE = 64 * 1024 * 1024; // 64MiB
|
const int PIXMAP_CACHE_SIZE = 64 * 1024 * 1024; // 64MiB
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
const QString PARAM_ADDSTOPPED = u"@addStopped"_s;
|
||||||
|
const QString PARAM_CATEGORY = u"@category"_s;
|
||||||
|
const QString PARAM_FIRSTLASTPIECEPRIORITY = u"@firstLastPiecePriority"_s;
|
||||||
|
const QString PARAM_SAVEPATH = u"@savePath"_s;
|
||||||
|
const QString PARAM_SEQUENTIAL = u"@sequential"_s;
|
||||||
|
const QString PARAM_SKIPCHECKING = u"@skipChecking"_s;
|
||||||
|
const QString PARAM_SKIPDIALOG = u"@skipDialog"_s;
|
||||||
|
|
||||||
|
QString bindParamValue(const QStringView paramName, const QStringView paramValue)
|
||||||
|
{
|
||||||
|
return paramName + u'=' + paramValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::pair<QStringView, QStringView> parseParam(const QStringView param)
|
||||||
|
{
|
||||||
|
const qsizetype sepIndex = param.indexOf(u'=');
|
||||||
|
if (sepIndex >= 0)
|
||||||
|
return {param.first(sepIndex), param.sliced(sepIndex + 1)};
|
||||||
|
|
||||||
|
return {param, {}};
|
||||||
|
}
|
||||||
|
|
||||||
QString serializeParams(const QBtCommandLineParameters ¶ms)
|
QString serializeParams(const QBtCommandLineParameters ¶ms)
|
||||||
{
|
{
|
||||||
QStringList result;
|
QStringList result;
|
||||||
|
@ -138,85 +160,86 @@ namespace
|
||||||
const BitTorrent::AddTorrentParams &addTorrentParams = params.addTorrentParams;
|
const BitTorrent::AddTorrentParams &addTorrentParams = params.addTorrentParams;
|
||||||
|
|
||||||
if (!addTorrentParams.savePath.isEmpty())
|
if (!addTorrentParams.savePath.isEmpty())
|
||||||
result.append(u"@savePath=" + addTorrentParams.savePath.data());
|
result.append(bindParamValue(PARAM_SAVEPATH, addTorrentParams.savePath.data()));
|
||||||
|
|
||||||
if (addTorrentParams.addStopped.has_value())
|
if (addTorrentParams.addStopped.has_value())
|
||||||
result.append(*addTorrentParams.addStopped ? u"@addStopped=1"_s : u"@addStopped=0"_s);
|
result.append(bindParamValue(PARAM_ADDSTOPPED, (*addTorrentParams.addStopped ? u"1" : u"0")));
|
||||||
|
|
||||||
if (addTorrentParams.skipChecking)
|
if (addTorrentParams.skipChecking)
|
||||||
result.append(u"@skipChecking"_s);
|
result.append(PARAM_SKIPCHECKING);
|
||||||
|
|
||||||
if (!addTorrentParams.category.isEmpty())
|
if (!addTorrentParams.category.isEmpty())
|
||||||
result.append(u"@category=" + addTorrentParams.category);
|
result.append(bindParamValue(PARAM_CATEGORY, addTorrentParams.category));
|
||||||
|
|
||||||
if (addTorrentParams.sequential)
|
if (addTorrentParams.sequential)
|
||||||
result.append(u"@sequential"_s);
|
result.append(PARAM_SEQUENTIAL);
|
||||||
|
|
||||||
if (addTorrentParams.firstLastPiecePriority)
|
if (addTorrentParams.firstLastPiecePriority)
|
||||||
result.append(u"@firstLastPiecePriority"_s);
|
result.append(PARAM_FIRSTLASTPIECEPRIORITY);
|
||||||
|
|
||||||
if (params.skipDialog.has_value())
|
if (params.skipDialog.has_value())
|
||||||
result.append(*params.skipDialog ? u"@skipDialog=1"_s : u"@skipDialog=0"_s);
|
result.append(bindParamValue(PARAM_SKIPDIALOG, (*params.skipDialog ? u"1" : u"0")));
|
||||||
|
|
||||||
result += params.torrentSources;
|
result += params.torrentSources;
|
||||||
|
|
||||||
return result.join(PARAMS_SEPARATOR);
|
return result.join(PARAMS_SEPARATOR);
|
||||||
}
|
}
|
||||||
|
|
||||||
QBtCommandLineParameters parseParams(const QString &str)
|
QBtCommandLineParameters parseParams(const QStringView str)
|
||||||
{
|
{
|
||||||
QBtCommandLineParameters parsedParams;
|
QBtCommandLineParameters parsedParams;
|
||||||
BitTorrent::AddTorrentParams &addTorrentParams = parsedParams.addTorrentParams;
|
BitTorrent::AddTorrentParams &addTorrentParams = parsedParams.addTorrentParams;
|
||||||
|
|
||||||
for (QString param : asConst(str.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts)))
|
for (QStringView param : asConst(str.split(PARAMS_SEPARATOR, Qt::SkipEmptyParts)))
|
||||||
{
|
{
|
||||||
param = param.trimmed();
|
param = param.trimmed();
|
||||||
|
const auto [paramName, paramValue] = parseParam(param);
|
||||||
|
|
||||||
// Process strings indicating options specified by the user.
|
// Process strings indicating options specified by the user.
|
||||||
|
|
||||||
if (param.startsWith(u"@savePath="))
|
if (paramName == PARAM_SAVEPATH)
|
||||||
{
|
{
|
||||||
addTorrentParams.savePath = Path(param.mid(10));
|
addTorrentParams.savePath = Path(paramValue.toString());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param.startsWith(u"@addStopped="))
|
if (paramName == PARAM_ADDSTOPPED)
|
||||||
{
|
{
|
||||||
addTorrentParams.addStopped = (QStringView(param).mid(11).toInt() != 0);
|
addTorrentParams.addStopped = (paramValue.toInt() != 0);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param == u"@skipChecking")
|
if (paramName == PARAM_SKIPCHECKING)
|
||||||
{
|
{
|
||||||
addTorrentParams.skipChecking = true;
|
addTorrentParams.skipChecking = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param.startsWith(u"@category="))
|
if (paramName == PARAM_CATEGORY)
|
||||||
{
|
{
|
||||||
addTorrentParams.category = param.mid(10);
|
addTorrentParams.category = paramValue.toString();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param == u"@sequential")
|
if (paramName == PARAM_SEQUENTIAL)
|
||||||
{
|
{
|
||||||
addTorrentParams.sequential = true;
|
addTorrentParams.sequential = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param == u"@firstLastPiecePriority")
|
if (paramName == PARAM_FIRSTLASTPIECEPRIORITY)
|
||||||
{
|
{
|
||||||
addTorrentParams.firstLastPiecePriority = true;
|
addTorrentParams.firstLastPiecePriority = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (param.startsWith(u"@skipDialog="))
|
if (paramName == PARAM_SKIPDIALOG)
|
||||||
{
|
{
|
||||||
parsedParams.skipDialog = (QStringView(param).mid(12).toInt() != 0);
|
parsedParams.skipDialog = (paramValue.toInt() != 0);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
parsedParams.torrentSources.append(param);
|
parsedParams.torrentSources.append(param.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedParams;
|
return parsedParams;
|
||||||
|
@ -897,10 +920,10 @@ int Application::exec()
|
||||||
m_desktopIntegration->showNotification(tr("Torrent added"), tr("'%1' was added.", "e.g: xxx.avi was added.").arg(torrent->name()));
|
m_desktopIntegration->showNotification(tr("Torrent added"), tr("'%1' was added.", "e.g: xxx.avi was added.").arg(torrent->name()));
|
||||||
});
|
});
|
||||||
connect(m_addTorrentManager, &AddTorrentManager::addTorrentFailed, this
|
connect(m_addTorrentManager, &AddTorrentManager::addTorrentFailed, this
|
||||||
, [this](const QString &source, const QString &reason)
|
, [this](const QString &source, const BitTorrent::AddTorrentError &reason)
|
||||||
{
|
{
|
||||||
m_desktopIntegration->showNotification(tr("Add torrent failed")
|
m_desktopIntegration->showNotification(tr("Add torrent failed")
|
||||||
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason));
|
, tr("Couldn't add torrent '%1', reason: %2.").arg(source, reason.message));
|
||||||
});
|
});
|
||||||
|
|
||||||
disconnect(m_desktopIntegration, &DesktopIntegration::activationRequested, this, &Application::createStartupProgressDialog);
|
disconnect(m_desktopIntegration, &DesktopIntegration::activationRequested, this, &Application::createStartupProgressDialog);
|
||||||
|
|
|
@ -491,6 +491,12 @@ QString makeUsage(const QString &prgName)
|
||||||
{
|
{
|
||||||
const QString indentation {USAGE_INDENTATION, u' '};
|
const QString indentation {USAGE_INDENTATION, u' '};
|
||||||
|
|
||||||
|
#if defined(Q_OS_WIN)
|
||||||
|
const QString noSplashCommand = u"set QBT_NO_SPLASH=1 && " + prgName;
|
||||||
|
#else
|
||||||
|
const QString noSplashCommand = u"QBT_NO_SPLASH=1 " + prgName;
|
||||||
|
#endif
|
||||||
|
|
||||||
const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n'
|
const QString text = QCoreApplication::translate("CMD Options", "Usage:") + u'\n'
|
||||||
+ indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [(<filename> | <url>)...]") + u'\n'
|
+ indentation + prgName + u' ' + QCoreApplication::translate("CMD Options", "[options] [(<filename> | <url>)...]") + u'\n'
|
||||||
|
|
||||||
|
@ -542,7 +548,7 @@ QString makeUsage(const QString &prgName)
|
||||||
"'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper "
|
"'parameter-name', environment variable name is 'QBT_PARAMETER_NAME' (in upper "
|
||||||
"case, '-' replaced with '_'). To pass flag values, set the variable to '1' or "
|
"case, '-' replaced with '_'). To pass flag values, set the variable to '1' or "
|
||||||
"'TRUE'. For example, to disable the splash screen: "), 0) + u'\n'
|
"'TRUE'. For example, to disable the splash screen: "), 0) + u'\n'
|
||||||
+ u"QBT_NO_SPLASH=1 " + prgName + u'\n'
|
+ noSplashCommand + u'\n'
|
||||||
+ wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n';
|
+ wrapText(QCoreApplication::translate("CMD Options", "Command line parameters take precedence over environment variables"), 0) + u'\n';
|
||||||
|
|
||||||
return text;
|
return text;
|
||||||
|
|
|
@ -6,6 +6,7 @@ add_library(qbt_base STATIC
|
||||||
applicationcomponent.h
|
applicationcomponent.h
|
||||||
asyncfilestorage.h
|
asyncfilestorage.h
|
||||||
bittorrent/abstractfilestorage.h
|
bittorrent/abstractfilestorage.h
|
||||||
|
bittorrent/addtorrenterror.h
|
||||||
bittorrent/addtorrentparams.h
|
bittorrent/addtorrentparams.h
|
||||||
bittorrent/announcetimepoint.h
|
bittorrent/announcetimepoint.h
|
||||||
bittorrent/bandwidthscheduler.h
|
bittorrent/bandwidthscheduler.h
|
||||||
|
|
|
@ -140,7 +140,7 @@ void AddTorrentManager::onSessionTorrentAdded(BitTorrent::Torrent *torrent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason)
|
void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason)
|
||||||
{
|
{
|
||||||
if (const QString source = m_sourcesByInfoHash.take(infoHash); !source.isEmpty())
|
if (const QString source = m_sourcesByInfoHash.take(infoHash); !source.isEmpty())
|
||||||
{
|
{
|
||||||
|
@ -154,7 +154,7 @@ void AddTorrentManager::onSessionAddTorrentFailed(const BitTorrent::InfoHash &in
|
||||||
void AddTorrentManager::handleAddTorrentFailed(const QString &source, const QString &reason)
|
void AddTorrentManager::handleAddTorrentFailed(const QString &source, const QString &reason)
|
||||||
{
|
{
|
||||||
LogMsg(tr("Failed to add torrent. Source: \"%1\". Reason: \"%2\"").arg(source, reason), Log::WARNING);
|
LogMsg(tr("Failed to add torrent. Source: \"%1\". Reason: \"%2\"").arg(source, reason), Log::WARNING);
|
||||||
emit addTorrentFailed(source, reason);
|
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::Other, reason});
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
||||||
|
@ -187,7 +187,7 @@ void AddTorrentManager::handleDuplicateTorrent(const QString &source
|
||||||
|
|
||||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: %2. Result: %3")
|
LogMsg(tr("Detected an attempt to add a duplicate torrent. Source: %1. Existing torrent: %2. Result: %3")
|
||||||
.arg(source, existingTorrent->name(), message));
|
.arg(source, existingTorrent->name(), message));
|
||||||
emit addTorrentFailed(source, message);
|
emit addTorrentFailed(source, {BitTorrent::AddTorrentError::DuplicateTorrent, message});
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddTorrentManager::setTorrentFileGuard(const QString &source, std::shared_ptr<TorrentFileGuard> torrentFileGuard)
|
void AddTorrentManager::setTorrentFileGuard(const QString &source, std::shared_ptr<TorrentFileGuard> torrentFileGuard)
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
|
|
||||||
#include "base/applicationcomponent.h"
|
#include "base/applicationcomponent.h"
|
||||||
|
#include "base/bittorrent/addtorrenterror.h"
|
||||||
#include "base/bittorrent/addtorrentparams.h"
|
#include "base/bittorrent/addtorrentparams.h"
|
||||||
#include "base/torrentfileguard.h"
|
#include "base/torrentfileguard.h"
|
||||||
|
|
||||||
|
@ -66,7 +67,7 @@ public:
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void torrentAdded(const QString &source, BitTorrent::Torrent *torrent);
|
void torrentAdded(const QString &source, BitTorrent::Torrent *torrent);
|
||||||
void addTorrentFailed(const QString &source, const QString &reason);
|
void addTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &reason);
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
bool addTorrentToSession(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
bool addTorrentToSession(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||||
|
@ -79,7 +80,7 @@ protected:
|
||||||
private:
|
private:
|
||||||
void onDownloadFinished(const Net::DownloadResult &result);
|
void onDownloadFinished(const Net::DownloadResult &result);
|
||||||
void onSessionTorrentAdded(BitTorrent::Torrent *torrent);
|
void onSessionTorrentAdded(BitTorrent::Torrent *torrent);
|
||||||
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const QString &reason);
|
void onSessionAddTorrentFailed(const BitTorrent::InfoHash &infoHash, const BitTorrent::AddTorrentError &reason);
|
||||||
bool processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
bool processTorrent(const QString &source, const BitTorrent::TorrentDescriptor &torrentDescr
|
||||||
, const BitTorrent::AddTorrentParams &addTorrentParams);
|
, const BitTorrent::AddTorrentParams &addTorrentParams);
|
||||||
|
|
||||||
|
|
49
src/base/bittorrent/addtorrenterror.h
Normal file
49
src/base/bittorrent/addtorrenterror.h
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or
|
||||||
|
* modify it under the terms of the GNU General Public License
|
||||||
|
* as published by the Free Software Foundation; either version 2
|
||||||
|
* of the License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program; if not, write to the Free Software
|
||||||
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
*
|
||||||
|
* In addition, as a special exception, the copyright holders give permission to
|
||||||
|
* link this program with the OpenSSL project's "OpenSSL" library (or with
|
||||||
|
* modified versions of it that use the same license as the "OpenSSL" library),
|
||||||
|
* and distribute the linked executables. You must obey the GNU General Public
|
||||||
|
* License in all respects for all of the code used other than "OpenSSL". If you
|
||||||
|
* modify file(s), you may extend this exception to your version of the file(s),
|
||||||
|
* but you are not obligated to do so. If you do not wish to do so, delete this
|
||||||
|
* exception statement from your version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QMetaType>
|
||||||
|
#include <QString>
|
||||||
|
|
||||||
|
namespace BitTorrent
|
||||||
|
{
|
||||||
|
struct AddTorrentError
|
||||||
|
{
|
||||||
|
enum Kind
|
||||||
|
{
|
||||||
|
DuplicateTorrent,
|
||||||
|
Other
|
||||||
|
};
|
||||||
|
|
||||||
|
Kind kind = Other;
|
||||||
|
QString message;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(BitTorrent::AddTorrentError)
|
|
@ -147,7 +147,7 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::load(cons
|
||||||
const Path torrentFilePath = path() / Path(idString + u".torrent");
|
const Path torrentFilePath = path() / Path(idString + u".torrent");
|
||||||
const qint64 torrentSizeLimit = Preferences::instance()->getTorrentFileSizeLimit();
|
const qint64 torrentSizeLimit = Preferences::instance()->getTorrentFileSizeLimit();
|
||||||
|
|
||||||
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, torrentSizeLimit);
|
const auto resumeDataReadResult = Utils::IO::readFile(fastresumePath, -1);
|
||||||
if (!resumeDataReadResult)
|
if (!resumeDataReadResult)
|
||||||
return nonstd::make_unexpected(resumeDataReadResult.error().message);
|
return nonstd::make_unexpected(resumeDataReadResult.error().message);
|
||||||
|
|
||||||
|
@ -290,6 +290,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
|
||||||
lt::add_torrent_params &p = torrentParams.ltAddTorrentParams;
|
lt::add_torrent_params &p = torrentParams.ltAddTorrentParams;
|
||||||
|
|
||||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||||
|
if (ec)
|
||||||
|
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||||
|
|
||||||
if (!metadata.isEmpty())
|
if (!metadata.isEmpty())
|
||||||
{
|
{
|
||||||
|
@ -320,6 +322,8 @@ BitTorrent::LoadResumeDataResult BitTorrent::BencodeResumeDataStorage::loadTorre
|
||||||
|
|
||||||
p.save_path = Profile::instance()->fromPortablePath(
|
p.save_path = Profile::instance()->fromPortablePath(
|
||||||
Path(fromLTString(p.save_path))).toString().toStdString();
|
Path(fromLTString(p.save_path))).toString().toStdString();
|
||||||
|
if (p.save_path.empty())
|
||||||
|
return nonstd::make_unexpected(tr("Corrupted resume data: %1").arg(tr("save_path is invalid")));
|
||||||
|
|
||||||
torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed);
|
torrentParams.stopped = (p.flags & lt::torrent_flags::paused) && !(p.flags & lt::torrent_flags::auto_managed);
|
||||||
torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
|
torrentParams.operatingMode = (p.flags & lt::torrent_flags::paused) || (p.flags & lt::torrent_flags::auto_managed)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2021-2023 Vladimir Golovnev <glassez@yandex.ru>
|
* Copyright (C) 2021-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -217,80 +217,6 @@ namespace
|
||||||
{
|
{
|
||||||
return u"%1 %2"_s.arg(quoted(column.name), definition);
|
return u"%1 %2"_s.arg(quoted(column.name), definition);
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadTorrentParams parseQueryResultRow(const QSqlQuery &query)
|
|
||||||
{
|
|
||||||
LoadTorrentParams resumeData;
|
|
||||||
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
|
|
||||||
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
|
|
||||||
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
|
|
||||||
if (!tagsData.isEmpty())
|
|
||||||
{
|
|
||||||
const QStringList tagList = tagsData.split(u',');
|
|
||||||
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
|
|
||||||
}
|
|
||||||
resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
|
|
||||||
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
|
|
||||||
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
|
|
||||||
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
|
|
||||||
resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
|
|
||||||
resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
|
|
||||||
query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
|
|
||||||
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
|
|
||||||
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
|
|
||||||
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
|
|
||||||
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
|
|
||||||
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
|
|
||||||
resumeData.stopCondition = Utils::String::toEnum(
|
|
||||||
query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
|
|
||||||
resumeData.sslParameters =
|
|
||||||
{
|
|
||||||
.certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
|
|
||||||
.privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
|
|
||||||
.dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
|
|
||||||
};
|
|
||||||
|
|
||||||
resumeData.savePath = Profile::instance()->fromPortablePath(
|
|
||||||
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
|
||||||
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
|
|
||||||
if (!resumeData.useAutoTMM)
|
|
||||||
{
|
|
||||||
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
|
||||||
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
|
|
||||||
const auto *pref = Preferences::instance();
|
|
||||||
const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
|
|
||||||
const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
|
|
||||||
|
|
||||||
lt::error_code ec;
|
|
||||||
const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec
|
|
||||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
|
||||||
|
|
||||||
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
|
|
||||||
|
|
||||||
p = lt::read_resume_data(resumeDataRoot, ec);
|
|
||||||
|
|
||||||
if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
|
|
||||||
; !bencodedMetadata.isEmpty())
|
|
||||||
{
|
|
||||||
const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
|
|
||||||
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
|
||||||
p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
|
|
||||||
}
|
|
||||||
|
|
||||||
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
|
|
||||||
.toString().toStdString();
|
|
||||||
|
|
||||||
if (p.flags & lt::torrent_flags::stop_when_ready)
|
|
||||||
{
|
|
||||||
p.flags &= ~lt::torrent_flags::stop_when_ready;
|
|
||||||
resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
|
|
||||||
}
|
|
||||||
|
|
||||||
return resumeData;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace BitTorrent
|
namespace BitTorrent
|
||||||
|
@ -688,6 +614,90 @@ void BitTorrent::DBResumeDataStorage::enableWALMode() const
|
||||||
throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
|
throw RuntimeError(tr("WAL mode is probably unsupported due to filesystem limitations."));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LoadResumeDataResult DBResumeDataStorage::parseQueryResultRow(const QSqlQuery &query) const
|
||||||
|
{
|
||||||
|
LoadTorrentParams resumeData;
|
||||||
|
resumeData.name = query.value(DB_COLUMN_NAME.name).toString();
|
||||||
|
resumeData.category = query.value(DB_COLUMN_CATEGORY.name).toString();
|
||||||
|
const QString tagsData = query.value(DB_COLUMN_TAGS.name).toString();
|
||||||
|
if (!tagsData.isEmpty())
|
||||||
|
{
|
||||||
|
const QStringList tagList = tagsData.split(u',');
|
||||||
|
resumeData.tags.insert(tagList.cbegin(), tagList.cend());
|
||||||
|
}
|
||||||
|
resumeData.hasFinishedStatus = query.value(DB_COLUMN_HAS_SEED_STATUS.name).toBool();
|
||||||
|
resumeData.firstLastPiecePriority = query.value(DB_COLUMN_HAS_OUTER_PIECES_PRIORITY.name).toBool();
|
||||||
|
resumeData.ratioLimit = query.value(DB_COLUMN_RATIO_LIMIT.name).toInt() / 1000.0;
|
||||||
|
resumeData.seedingTimeLimit = query.value(DB_COLUMN_SEEDING_TIME_LIMIT.name).toInt();
|
||||||
|
resumeData.inactiveSeedingTimeLimit = query.value(DB_COLUMN_INACTIVE_SEEDING_TIME_LIMIT.name).toInt();
|
||||||
|
resumeData.shareLimitAction = Utils::String::toEnum<ShareLimitAction>(
|
||||||
|
query.value(DB_COLUMN_SHARE_LIMIT_ACTION.name).toString(), ShareLimitAction::Default);
|
||||||
|
resumeData.contentLayout = Utils::String::toEnum<TorrentContentLayout>(
|
||||||
|
query.value(DB_COLUMN_CONTENT_LAYOUT.name).toString(), TorrentContentLayout::Original);
|
||||||
|
resumeData.operatingMode = Utils::String::toEnum<TorrentOperatingMode>(
|
||||||
|
query.value(DB_COLUMN_OPERATING_MODE.name).toString(), TorrentOperatingMode::AutoManaged);
|
||||||
|
resumeData.stopped = query.value(DB_COLUMN_STOPPED.name).toBool();
|
||||||
|
resumeData.stopCondition = Utils::String::toEnum(
|
||||||
|
query.value(DB_COLUMN_STOP_CONDITION.name).toString(), Torrent::StopCondition::None);
|
||||||
|
resumeData.sslParameters =
|
||||||
|
{
|
||||||
|
.certificate = QSslCertificate(query.value(DB_COLUMN_SSL_CERTIFICATE.name).toByteArray()),
|
||||||
|
.privateKey = Utils::SSLKey::load(query.value(DB_COLUMN_SSL_PRIVATE_KEY.name).toByteArray()),
|
||||||
|
.dhParams = query.value(DB_COLUMN_SSL_DH_PARAMS.name).toByteArray()
|
||||||
|
};
|
||||||
|
|
||||||
|
resumeData.savePath = Profile::instance()->fromPortablePath(
|
||||||
|
Path(query.value(DB_COLUMN_TARGET_SAVE_PATH.name).toString()));
|
||||||
|
resumeData.useAutoTMM = resumeData.savePath.isEmpty();
|
||||||
|
if (!resumeData.useAutoTMM)
|
||||||
|
{
|
||||||
|
resumeData.downloadPath = Profile::instance()->fromPortablePath(
|
||||||
|
Path(query.value(DB_COLUMN_DOWNLOAD_PATH.name).toString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QByteArray bencodedResumeData = query.value(DB_COLUMN_RESUMEDATA.name).toByteArray();
|
||||||
|
const auto *pref = Preferences::instance();
|
||||||
|
const int bdecodeDepthLimit = pref->getBdecodeDepthLimit();
|
||||||
|
const int bdecodeTokenLimit = pref->getBdecodeTokenLimit();
|
||||||
|
|
||||||
|
lt::error_code ec;
|
||||||
|
const lt::bdecode_node resumeDataRoot = lt::bdecode(bencodedResumeData, ec, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||||
|
if (ec)
|
||||||
|
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||||
|
|
||||||
|
lt::add_torrent_params &p = resumeData.ltAddTorrentParams;
|
||||||
|
|
||||||
|
p = lt::read_resume_data(resumeDataRoot, ec);
|
||||||
|
if (ec)
|
||||||
|
return nonstd::make_unexpected(tr("Cannot parse resume data: %1").arg(QString::fromStdString(ec.message())));
|
||||||
|
|
||||||
|
if (const QByteArray bencodedMetadata = query.value(DB_COLUMN_METADATA.name).toByteArray()
|
||||||
|
; !bencodedMetadata.isEmpty())
|
||||||
|
{
|
||||||
|
const lt::bdecode_node torentInfoRoot = lt::bdecode(bencodedMetadata, ec
|
||||||
|
, nullptr, bdecodeDepthLimit, bdecodeTokenLimit);
|
||||||
|
if (ec)
|
||||||
|
return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
|
||||||
|
|
||||||
|
p.ti = std::make_shared<lt::torrent_info>(torentInfoRoot, ec);
|
||||||
|
if (ec)
|
||||||
|
return nonstd::make_unexpected(tr("Cannot parse torrent info: %1").arg(QString::fromStdString(ec.message())));
|
||||||
|
}
|
||||||
|
|
||||||
|
p.save_path = Profile::instance()->fromPortablePath(Path(fromLTString(p.save_path)))
|
||||||
|
.toString().toStdString();
|
||||||
|
if (p.save_path.empty())
|
||||||
|
return nonstd::make_unexpected(tr("Corrupted resume data: %1").arg(tr("save_path is invalid")));
|
||||||
|
|
||||||
|
if (p.flags & lt::torrent_flags::stop_when_ready)
|
||||||
|
{
|
||||||
|
p.flags &= ~lt::torrent_flags::stop_when_ready;
|
||||||
|
resumeData.stopCondition = Torrent::StopCondition::FilesChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resumeData;
|
||||||
|
}
|
||||||
|
|
||||||
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent)
|
BitTorrent::DBResumeDataStorage::Worker::Worker(const Path &dbPath, QReadWriteLock &dbLock, QObject *parent)
|
||||||
: QThread(parent)
|
: QThread(parent)
|
||||||
, m_path {dbPath}
|
, m_path {dbPath}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2021-2022 Vladimir Golovnev <glassez@yandex.ru>
|
* Copyright (C) 2021-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
* modify it under the terms of the GNU General Public License
|
* modify it under the terms of the GNU General Public License
|
||||||
|
@ -31,9 +31,10 @@
|
||||||
#include <QReadWriteLock>
|
#include <QReadWriteLock>
|
||||||
|
|
||||||
#include "base/pathfwd.h"
|
#include "base/pathfwd.h"
|
||||||
#include "base/utils/thread.h"
|
|
||||||
#include "resumedatastorage.h"
|
#include "resumedatastorage.h"
|
||||||
|
|
||||||
|
class QSqlQuery;
|
||||||
|
|
||||||
namespace BitTorrent
|
namespace BitTorrent
|
||||||
{
|
{
|
||||||
class DBResumeDataStorage final : public ResumeDataStorage
|
class DBResumeDataStorage final : public ResumeDataStorage
|
||||||
|
@ -58,6 +59,7 @@ namespace BitTorrent
|
||||||
void createDB() const;
|
void createDB() const;
|
||||||
void updateDB(int fromVersion) const;
|
void updateDB(int fromVersion) const;
|
||||||
void enableWALMode() const;
|
void enableWALMode() const;
|
||||||
|
LoadResumeDataResult parseQueryResultRow(const QSqlQuery &query) const;
|
||||||
|
|
||||||
class Worker;
|
class Worker;
|
||||||
Worker *m_asyncWorker = nullptr;
|
Worker *m_asyncWorker = nullptr;
|
||||||
|
|
|
@ -34,6 +34,7 @@
|
||||||
|
|
||||||
#include "base/pathfwd.h"
|
#include "base/pathfwd.h"
|
||||||
#include "base/tagset.h"
|
#include "base/tagset.h"
|
||||||
|
#include "addtorrenterror.h"
|
||||||
#include "addtorrentparams.h"
|
#include "addtorrentparams.h"
|
||||||
#include "categoryoptions.h"
|
#include "categoryoptions.h"
|
||||||
#include "sharelimitaction.h"
|
#include "sharelimitaction.h"
|
||||||
|
@ -481,7 +482,7 @@ namespace BitTorrent
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void startupProgressUpdated(int progress);
|
void startupProgressUpdated(int progress);
|
||||||
void addTorrentFailed(const InfoHash &infoHash, const QString &reason);
|
void addTorrentFailed(const InfoHash &infoHash, const AddTorrentError &reason);
|
||||||
void allTorrentsFinished();
|
void allTorrentsFinished();
|
||||||
void categoryAdded(const QString &categoryName);
|
void categoryAdded(const QString &categoryName);
|
||||||
void categoryRemoved(const QString &categoryName);
|
void categoryRemoved(const QString &categoryName);
|
||||||
|
|
|
@ -467,9 +467,11 @@ SessionImpl::SessionImpl(QObject *parent)
|
||||||
, m_additionalTrackers(BITTORRENT_SESSION_KEY(u"AdditionalTrackers"_s))
|
, m_additionalTrackers(BITTORRENT_SESSION_KEY(u"AdditionalTrackers"_s))
|
||||||
, m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false)
|
, m_isAddTrackersFromURLEnabled(BITTORRENT_SESSION_KEY(u"AddTrackersFromURLEnabled"_s), false)
|
||||||
, m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s))
|
, m_additionalTrackersURL(BITTORRENT_SESSION_KEY(u"AdditionalTrackersURL"_s))
|
||||||
, m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r;})
|
, m_globalMaxRatio(BITTORRENT_SESSION_KEY(u"GlobalMaxRatio"_s), -1, [](qreal r) { return r < 0 ? -1. : r; })
|
||||||
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s), -1, lowerLimited(-1))
|
, m_globalMaxSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxSeedingMinutes"_s)
|
||||||
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s), -1, lowerLimited(-1))
|
, Torrent::NO_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_SEEDING_TIME_LIMIT))
|
||||||
|
, m_globalMaxInactiveSeedingMinutes(BITTORRENT_SESSION_KEY(u"GlobalMaxInactiveSeedingMinutes"_s)
|
||||||
|
, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT, lowerLimited(Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT))
|
||||||
, m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false)
|
, m_isAddTorrentToQueueTop(BITTORRENT_SESSION_KEY(u"AddTorrentToTopOfQueue"_s), false)
|
||||||
, m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
|
, m_isAddTorrentStopped(BITTORRENT_SESSION_KEY(u"AddTorrentStopped"_s), false)
|
||||||
, m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
|
, m_torrentStopCondition(BITTORRENT_SESSION_KEY(u"TorrentStopCondition"_s), Torrent::StopCondition::None)
|
||||||
|
@ -974,23 +976,25 @@ bool SessionImpl::editCategory(const QString &name, const CategoryOptions &optio
|
||||||
if (options == currentOptions)
|
if (options == currentOptions)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
currentOptions = options;
|
|
||||||
storeCategories();
|
|
||||||
if (isDisableAutoTMMWhenCategorySavePathChanged())
|
if (isDisableAutoTMMWhenCategorySavePathChanged())
|
||||||
{
|
{
|
||||||
|
// This should be done before changing the category options
|
||||||
|
// to prevent the torrent from being moved at the new save path.
|
||||||
|
|
||||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||||
{
|
{
|
||||||
if (torrent->category() == name)
|
if (torrent->category() == name)
|
||||||
torrent->setAutoTMMEnabled(false);
|
torrent->setAutoTMMEnabled(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
currentOptions = options;
|
||||||
|
storeCategories();
|
||||||
|
|
||||||
|
for (TorrentImpl *const torrent : asConst(m_torrents))
|
||||||
{
|
{
|
||||||
for (TorrentImpl *const torrent : asConst(m_torrents))
|
if (torrent->category() == name)
|
||||||
{
|
torrent->handleCategoryOptionsChanged();
|
||||||
if (torrent->category() == name)
|
|
||||||
torrent->handleCategoryOptionsChanged();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emit categoryOptionsChanged(name);
|
emit categoryOptionsChanged(name);
|
||||||
|
@ -1218,7 +1222,7 @@ qreal SessionImpl::globalMaxRatio() const
|
||||||
void SessionImpl::setGlobalMaxRatio(qreal ratio)
|
void SessionImpl::setGlobalMaxRatio(qreal ratio)
|
||||||
{
|
{
|
||||||
if (ratio < 0)
|
if (ratio < 0)
|
||||||
ratio = -1.;
|
ratio = Torrent::NO_RATIO_LIMIT;
|
||||||
|
|
||||||
if (ratio != globalMaxRatio())
|
if (ratio != globalMaxRatio())
|
||||||
{
|
{
|
||||||
|
@ -1234,8 +1238,7 @@ int SessionImpl::globalMaxSeedingMinutes() const
|
||||||
|
|
||||||
void SessionImpl::setGlobalMaxSeedingMinutes(int minutes)
|
void SessionImpl::setGlobalMaxSeedingMinutes(int minutes)
|
||||||
{
|
{
|
||||||
if (minutes < 0)
|
minutes = std::max(minutes, Torrent::NO_SEEDING_TIME_LIMIT);
|
||||||
minutes = -1;
|
|
||||||
|
|
||||||
if (minutes != globalMaxSeedingMinutes())
|
if (minutes != globalMaxSeedingMinutes())
|
||||||
{
|
{
|
||||||
|
@ -1251,7 +1254,7 @@ int SessionImpl::globalMaxInactiveSeedingMinutes() const
|
||||||
|
|
||||||
void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes)
|
void SessionImpl::setGlobalMaxInactiveSeedingMinutes(int minutes)
|
||||||
{
|
{
|
||||||
minutes = std::max(minutes, -1);
|
minutes = std::max(minutes, Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT);
|
||||||
|
|
||||||
if (minutes != globalMaxInactiveSeedingMinutes())
|
if (minutes != globalMaxInactiveSeedingMinutes())
|
||||||
{
|
{
|
||||||
|
@ -2310,19 +2313,19 @@ void SessionImpl::processTorrentShareLimits(TorrentImpl *torrent)
|
||||||
QString description;
|
QString description;
|
||||||
|
|
||||||
if (const qreal ratio = torrent->realRatio();
|
if (const qreal ratio = torrent->realRatio();
|
||||||
(ratioLimit >= 0) && (ratio <= Torrent::MAX_RATIO) && (ratio >= ratioLimit))
|
(ratioLimit >= 0) && (ratio >= ratioLimit))
|
||||||
{
|
{
|
||||||
reached = true;
|
reached = true;
|
||||||
description = tr("Torrent reached the share ratio limit.");
|
description = tr("Torrent reached the share ratio limit.");
|
||||||
}
|
}
|
||||||
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
else if (const qlonglong seedingTimeInMinutes = torrent->finishedTime() / 60;
|
||||||
(seedingTimeLimit >= 0) && (seedingTimeInMinutes <= Torrent::MAX_SEEDING_TIME) && (seedingTimeInMinutes >= seedingTimeLimit))
|
(seedingTimeLimit >= 0) && (seedingTimeInMinutes >= seedingTimeLimit))
|
||||||
{
|
{
|
||||||
reached = true;
|
reached = true;
|
||||||
description = tr("Torrent reached the seeding time limit.");
|
description = tr("Torrent reached the seeding time limit.");
|
||||||
}
|
}
|
||||||
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
else if (const qlonglong inactiveSeedingTimeInMinutes = torrent->timeSinceActivity() / 60;
|
||||||
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes <= Torrent::MAX_INACTIVE_SEEDING_TIME) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
(inactiveSeedingTimeLimit >= 0) && (inactiveSeedingTimeInMinutes >= inactiveSeedingTimeLimit))
|
||||||
{
|
{
|
||||||
reached = true;
|
reached = true;
|
||||||
description = tr("Torrent reached the inactive seeding time limit.");
|
description = tr("Torrent reached the inactive seeding time limit.");
|
||||||
|
@ -2751,7 +2754,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
||||||
// We should not add the torrent if it is already
|
// We should not add the torrent if it is already
|
||||||
// processed or is pending to add to session
|
// processed or is pending to add to session
|
||||||
if (m_loadingTorrents.contains(id) || (infoHash.isHybrid() && m_loadingTorrents.contains(altID)))
|
if (m_loadingTorrents.contains(id) || (infoHash.isHybrid() && m_loadingTorrents.contains(altID)))
|
||||||
|
{
|
||||||
|
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, tr("Duplicate torrent")});
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (Torrent *torrent = findTorrent(infoHash))
|
if (Torrent *torrent = findTorrent(infoHash))
|
||||||
{
|
{
|
||||||
|
@ -2765,16 +2771,20 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
||||||
|
|
||||||
if (!isMergeTrackersEnabled())
|
if (!isMergeTrackersEnabled())
|
||||||
{
|
{
|
||||||
|
const QString message = tr("Merging of trackers is disabled");
|
||||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
||||||
.arg(torrent->name(), tr("Merging of trackers is disabled")));
|
.arg(torrent->name(), message));
|
||||||
|
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate());
|
const bool isPrivate = torrent->isPrivate() || (hasMetadata && source.info()->isPrivate());
|
||||||
if (isPrivate)
|
if (isPrivate)
|
||||||
{
|
{
|
||||||
|
const QString message = tr("Trackers cannot be merged because it is a private torrent");
|
||||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
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")));
|
.arg(torrent->name(), message));
|
||||||
|
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2782,8 +2792,10 @@ bool SessionImpl::addTorrent_impl(const TorrentDescriptor &source, const AddTorr
|
||||||
torrent->addTrackers(source.trackers());
|
torrent->addTrackers(source.trackers());
|
||||||
torrent->addUrlSeeds(source.urlSeeds());
|
torrent->addUrlSeeds(source.urlSeeds());
|
||||||
|
|
||||||
|
const QString message = tr("Trackers are merged from new source");
|
||||||
LogMsg(tr("Detected an attempt to add a duplicate torrent. Existing torrent: %1. Result: %2")
|
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")));
|
.arg(torrent->name(), message));
|
||||||
|
emit addTorrentFailed(infoHash, {AddTorrentError::DuplicateTorrent, message});
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3247,6 +3259,9 @@ void SessionImpl::setSavePath(const Path &path)
|
||||||
|
|
||||||
if (isDisableAutoTMMWhenDefaultSavePathChanged())
|
if (isDisableAutoTMMWhenDefaultSavePathChanged())
|
||||||
{
|
{
|
||||||
|
// This should be done before changing the save path
|
||||||
|
// to prevent the torrent from being moved at the new save path.
|
||||||
|
|
||||||
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
|
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
|
||||||
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
|
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
|
||||||
{
|
{
|
||||||
|
@ -3276,6 +3291,9 @@ void SessionImpl::setDownloadPath(const Path &path)
|
||||||
|
|
||||||
if (isDisableAutoTMMWhenDefaultSavePathChanged())
|
if (isDisableAutoTMMWhenDefaultSavePathChanged())
|
||||||
{
|
{
|
||||||
|
// This should be done before changing the save path
|
||||||
|
// to prevent the torrent from being moved at the new save path.
|
||||||
|
|
||||||
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
|
QSet<QString> affectedCatogories {{}}; // includes default (unnamed) category
|
||||||
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
|
for (auto it = m_categories.cbegin(); it != m_categories.cend(); ++it)
|
||||||
{
|
{
|
||||||
|
@ -5699,7 +5717,9 @@ void SessionImpl::handleAddTorrentAlert(const lt::add_torrent_alert *alert)
|
||||||
if (const auto loadingTorrentsIter = m_loadingTorrents.find(TorrentID::fromInfoHash(infoHash))
|
if (const auto loadingTorrentsIter = m_loadingTorrents.find(TorrentID::fromInfoHash(infoHash))
|
||||||
; loadingTorrentsIter != m_loadingTorrents.end())
|
; loadingTorrentsIter != m_loadingTorrents.end())
|
||||||
{
|
{
|
||||||
emit addTorrentFailed(infoHash, msg);
|
const AddTorrentError::Kind errorKind = (alert->error == lt::errors::duplicate_torrent)
|
||||||
|
? AddTorrentError::DuplicateTorrent : AddTorrentError::Other;
|
||||||
|
emit addTorrentFailed(infoHash, {errorKind, msg});
|
||||||
m_loadingTorrents.erase(loadingTorrentsIter);
|
m_loadingTorrents.erase(loadingTorrentsIter);
|
||||||
}
|
}
|
||||||
else if (const auto downloadedMetadataIter = m_downloadedMetadata.find(TorrentID::fromInfoHash(infoHash))
|
else if (const auto downloadedMetadataIter = m_downloadedMetadata.find(TorrentID::fromInfoHash(infoHash))
|
||||||
|
|
|
@ -29,6 +29,8 @@
|
||||||
|
|
||||||
#include "torrent.h"
|
#include "torrent.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
#include <QHash>
|
#include <QHash>
|
||||||
|
|
||||||
#include "infohash.h"
|
#include "infohash.h"
|
||||||
|
@ -51,9 +53,7 @@ namespace BitTorrent
|
||||||
const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2;
|
const int Torrent::USE_GLOBAL_INACTIVE_SEEDING_TIME = -2;
|
||||||
const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1;
|
const int Torrent::NO_INACTIVE_SEEDING_TIME_LIMIT = -1;
|
||||||
|
|
||||||
const qreal Torrent::MAX_RATIO = 9999;
|
const qreal Torrent::MAX_RATIO = std::numeric_limits<qreal>::infinity();
|
||||||
const int Torrent::MAX_SEEDING_TIME = 525600;
|
|
||||||
const int Torrent::MAX_INACTIVE_SEEDING_TIME = 525600;
|
|
||||||
|
|
||||||
TorrentID Torrent::id() const
|
TorrentID Torrent::id() const
|
||||||
{
|
{
|
||||||
|
|
|
@ -132,8 +132,6 @@ namespace BitTorrent
|
||||||
static const int NO_INACTIVE_SEEDING_TIME_LIMIT;
|
static const int NO_INACTIVE_SEEDING_TIME_LIMIT;
|
||||||
|
|
||||||
static const qreal MAX_RATIO;
|
static const qreal MAX_RATIO;
|
||||||
static const int MAX_SEEDING_TIME;
|
|
||||||
static const int MAX_INACTIVE_SEEDING_TIME;
|
|
||||||
|
|
||||||
using TorrentContentHandler::TorrentContentHandler;
|
using TorrentContentHandler::TorrentContentHandler;
|
||||||
|
|
||||||
|
|
|
@ -1549,7 +1549,8 @@ qreal TorrentImpl::realRatio() const
|
||||||
|
|
||||||
const qreal ratio = upload / static_cast<qreal>(download);
|
const qreal ratio = upload / static_cast<qreal>(download);
|
||||||
Q_ASSERT(ratio >= 0);
|
Q_ASSERT(ratio >= 0);
|
||||||
return (ratio > MAX_RATIO) ? MAX_RATIO : ratio;
|
|
||||||
|
return ratio;
|
||||||
}
|
}
|
||||||
|
|
||||||
int TorrentImpl::uploadPayloadRate() const
|
int TorrentImpl::uploadPayloadRate() const
|
||||||
|
@ -1615,18 +1616,20 @@ bool TorrentImpl::setCategory(const QString &category)
|
||||||
if (!category.isEmpty() && !m_session->categories().contains(category))
|
if (!category.isEmpty() && !m_session->categories().contains(category))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (m_session->isDisableAutoTMMWhenCategoryChanged())
|
||||||
|
{
|
||||||
|
// This should be done before changing the category name
|
||||||
|
// to prevent the torrent from being moved at the path of new category.
|
||||||
|
setAutoTMMEnabled(false);
|
||||||
|
}
|
||||||
|
|
||||||
const QString oldCategory = m_category;
|
const QString oldCategory = m_category;
|
||||||
m_category = category;
|
m_category = category;
|
||||||
deferredRequestResumeData();
|
deferredRequestResumeData();
|
||||||
m_session->handleTorrentCategoryChanged(this, oldCategory);
|
m_session->handleTorrentCategoryChanged(this, oldCategory);
|
||||||
|
|
||||||
if (m_useAutoTMM)
|
if (m_useAutoTMM)
|
||||||
{
|
adjustStorageLocation();
|
||||||
if (!m_session->isDisableAutoTMMWhenCategoryChanged())
|
|
||||||
adjustStorageLocation();
|
|
||||||
else
|
|
||||||
setAutoTMMEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -2710,8 +2713,6 @@ void TorrentImpl::setRatioLimit(qreal limit)
|
||||||
{
|
{
|
||||||
if (limit < USE_GLOBAL_RATIO)
|
if (limit < USE_GLOBAL_RATIO)
|
||||||
limit = NO_RATIO_LIMIT;
|
limit = NO_RATIO_LIMIT;
|
||||||
else if (limit > MAX_RATIO)
|
|
||||||
limit = MAX_RATIO;
|
|
||||||
|
|
||||||
if (m_ratioLimit != limit)
|
if (m_ratioLimit != limit)
|
||||||
{
|
{
|
||||||
|
@ -2725,8 +2726,6 @@ void TorrentImpl::setSeedingTimeLimit(int limit)
|
||||||
{
|
{
|
||||||
if (limit < USE_GLOBAL_SEEDING_TIME)
|
if (limit < USE_GLOBAL_SEEDING_TIME)
|
||||||
limit = NO_SEEDING_TIME_LIMIT;
|
limit = NO_SEEDING_TIME_LIMIT;
|
||||||
else if (limit > MAX_SEEDING_TIME)
|
|
||||||
limit = MAX_SEEDING_TIME;
|
|
||||||
|
|
||||||
if (m_seedingTimeLimit != limit)
|
if (m_seedingTimeLimit != limit)
|
||||||
{
|
{
|
||||||
|
@ -2740,8 +2739,6 @@ void TorrentImpl::setInactiveSeedingTimeLimit(int limit)
|
||||||
{
|
{
|
||||||
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
|
if (limit < USE_GLOBAL_INACTIVE_SEEDING_TIME)
|
||||||
limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
|
limit = NO_INACTIVE_SEEDING_TIME_LIMIT;
|
||||||
else if (limit > MAX_INACTIVE_SEEDING_TIME)
|
|
||||||
limit = MAX_SEEDING_TIME;
|
|
||||||
|
|
||||||
if (m_inactiveSeedingTimeLimit != limit)
|
if (m_inactiveSeedingTimeLimit != limit)
|
||||||
{
|
{
|
||||||
|
|
|
@ -30,8 +30,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QByteArray>
|
#include <QByteArray>
|
||||||
|
#include <QHash>
|
||||||
#include <QHostAddress>
|
#include <QHostAddress>
|
||||||
#include <QList>
|
#include <QList>
|
||||||
|
#include <QMap>
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "base/global.h"
|
#include "base/global.h"
|
||||||
|
|
|
@ -2054,6 +2054,19 @@ void Preferences::setAddNewTorrentDialogSavePathHistoryLength(const int value)
|
||||||
setValue(u"AddNewTorrentDialog/SavePathHistoryLength"_s, clampedValue);
|
setValue(u"AddNewTorrentDialog/SavePathHistoryLength"_s, clampedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Preferences::isAddNewTorrentDialogAttached() const
|
||||||
|
{
|
||||||
|
return value(u"AddNewTorrentDialog/Attached"_s, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Preferences::setAddNewTorrentDialogAttached(const bool attached)
|
||||||
|
{
|
||||||
|
if (attached == isAddNewTorrentDialogAttached())
|
||||||
|
return;
|
||||||
|
|
||||||
|
setValue(u"AddNewTorrentDialog/Attached"_s, attached);
|
||||||
|
}
|
||||||
|
|
||||||
void Preferences::apply()
|
void Preferences::apply()
|
||||||
{
|
{
|
||||||
if (SettingsStorage::instance()->save())
|
if (SettingsStorage::instance()->save())
|
||||||
|
|
|
@ -433,6 +433,8 @@ public:
|
||||||
void setAddNewTorrentDialogTopLevel(bool value);
|
void setAddNewTorrentDialogTopLevel(bool value);
|
||||||
int addNewTorrentDialogSavePathHistoryLength() const;
|
int addNewTorrentDialogSavePathHistoryLength() const;
|
||||||
void setAddNewTorrentDialogSavePathHistoryLength(int value);
|
void setAddNewTorrentDialogSavePathHistoryLength(int value);
|
||||||
|
bool isAddNewTorrentDialogAttached() const;
|
||||||
|
void setAddNewTorrentDialogAttached(bool attached);
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void setStatusFilterState(bool checked);
|
void setStatusFilterState(bool checked);
|
||||||
|
|
|
@ -48,16 +48,16 @@ const QString Article::KeyIsRead = u"isRead"_s;
|
||||||
|
|
||||||
Article::Article(Feed *feed, const QVariantHash &varHash)
|
Article::Article(Feed *feed, const QVariantHash &varHash)
|
||||||
: QObject(feed)
|
: QObject(feed)
|
||||||
, m_feed(feed)
|
, m_feed {feed}
|
||||||
, m_guid(varHash.value(KeyId).toString())
|
, m_guid {varHash.value(KeyId).toString()}
|
||||||
, m_date(varHash.value(KeyDate).toDateTime())
|
, m_date {varHash.value(KeyDate).toDateTime()}
|
||||||
, m_title(varHash.value(KeyTitle).toString())
|
, m_title {varHash.value(KeyTitle).toString()}
|
||||||
, m_author(varHash.value(KeyAuthor).toString())
|
, m_author {varHash.value(KeyAuthor).toString()}
|
||||||
, m_description(varHash.value(KeyDescription).toString())
|
, m_description {varHash.value(KeyDescription).toString()}
|
||||||
, m_torrentURL(varHash.value(KeyTorrentURL).toString())
|
, m_torrentURL {varHash.value(KeyTorrentURL).toString()}
|
||||||
, m_link(varHash.value(KeyLink).toString())
|
, m_link {varHash.value(KeyLink).toString()}
|
||||||
, m_isRead(varHash.value(KeyIsRead, false).toBool())
|
, m_isRead {varHash.value(KeyIsRead, false).toBool()}
|
||||||
, m_data(varHash)
|
, m_data {varHash}
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -375,10 +375,24 @@ void AutoDownloader::handleTorrentAdded(const QString &source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutoDownloader::handleAddTorrentFailed(const QString &source)
|
void AutoDownloader::handleAddTorrentFailed(const QString &source, const BitTorrent::AddTorrentError &error)
|
||||||
{
|
{
|
||||||
m_waitingJobs.remove(source);
|
const auto job = m_waitingJobs.take(source);
|
||||||
// TODO: Re-schedule job here.
|
if (!job)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (error.kind == BitTorrent::AddTorrentError::DuplicateTorrent)
|
||||||
|
{
|
||||||
|
if (Feed *feed = Session::instance()->feedByURL(job->feedURL))
|
||||||
|
{
|
||||||
|
if (Article *article = feed->articleByGUID(job->articleData.value(Article::KeyId).toString()))
|
||||||
|
article->markAsRead();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// TODO: Re-schedule job here.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void AutoDownloader::handleNewArticle(const Article *article)
|
void AutoDownloader::handleNewArticle(const Article *article)
|
||||||
|
|
|
@ -37,6 +37,7 @@
|
||||||
#include <QSharedPointer>
|
#include <QSharedPointer>
|
||||||
|
|
||||||
#include "base/applicationcomponent.h"
|
#include "base/applicationcomponent.h"
|
||||||
|
#include "base/bittorrent/addtorrenterror.h"
|
||||||
#include "base/exceptions.h"
|
#include "base/exceptions.h"
|
||||||
#include "base/settingvalue.h"
|
#include "base/settingvalue.h"
|
||||||
#include "base/utils/thread.h"
|
#include "base/utils/thread.h"
|
||||||
|
@ -111,7 +112,7 @@ namespace RSS
|
||||||
private slots:
|
private slots:
|
||||||
void process();
|
void process();
|
||||||
void handleTorrentAdded(const QString &source);
|
void handleTorrentAdded(const QString &source);
|
||||||
void handleAddTorrentFailed(const QString &url);
|
void handleAddTorrentFailed(const QString &url, const BitTorrent::AddTorrentError &error);
|
||||||
void handleNewArticle(const Article *article);
|
void handleNewArticle(const Article *article);
|
||||||
void handleFeedURLChanged(Feed *feed, const QString &oldURL);
|
void handleFeedURLChanged(Feed *feed, const QString &oldURL);
|
||||||
|
|
||||||
|
|
|
@ -487,14 +487,14 @@ void SearchPluginManager::updateNova()
|
||||||
const Path enginePath = engineLocation();
|
const Path enginePath = engineLocation();
|
||||||
|
|
||||||
QFile packageFile {(enginePath / Path(u"__init__.py"_s)).data()};
|
QFile packageFile {(enginePath / Path(u"__init__.py"_s)).data()};
|
||||||
packageFile.open(QIODevice::WriteOnly);
|
if (packageFile.open(QIODevice::WriteOnly))
|
||||||
packageFile.close();
|
packageFile.close();
|
||||||
|
|
||||||
Utils::Fs::mkdir(enginePath / Path(u"engines"_s));
|
Utils::Fs::mkdir(enginePath / Path(u"engines"_s));
|
||||||
|
|
||||||
QFile packageFile2 {(enginePath / Path(u"engines/__init__.py"_s)).data()};
|
QFile packageFile2 {(enginePath / Path(u"engines/__init__.py"_s)).data()};
|
||||||
packageFile2.open(QIODevice::WriteOnly);
|
if (packageFile2.open(QIODevice::WriteOnly))
|
||||||
packageFile2.close();
|
packageFile2.close();
|
||||||
|
|
||||||
// Copy search plugin files (if necessary)
|
// Copy search plugin files (if necessary)
|
||||||
const auto updateFile = [&enginePath](const Path &filename, const bool compareVersion)
|
const auto updateFile = [&enginePath](const Path &filename, const bool compareVersion)
|
||||||
|
|
|
@ -42,7 +42,7 @@
|
||||||
|
|
||||||
uint32_t Utils::Random::rand(const uint32_t min, const uint32_t max)
|
uint32_t Utils::Random::rand(const uint32_t min, const uint32_t max)
|
||||||
{
|
{
|
||||||
static RandomLayer layer;
|
static const RandomLayer layer;
|
||||||
|
|
||||||
// new distribution is cheap: https://stackoverflow.com/a/19036349
|
// new distribution is cheap: https://stackoverflow.com/a/19036349
|
||||||
std::uniform_int_distribution<uint32_t> uniform(min, max);
|
std::uniform_int_distribution<uint32_t> uniform(min, max);
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <cerrno>
|
#include <cerrno>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
|
@ -44,6 +45,27 @@ namespace
|
||||||
|
|
||||||
RandomLayer()
|
RandomLayer()
|
||||||
{
|
{
|
||||||
|
if (::getrandom(nullptr, 0, 0) < 0)
|
||||||
|
{
|
||||||
|
if (errno == ENOSYS)
|
||||||
|
{
|
||||||
|
// underlying kernel does not implement this system call
|
||||||
|
// fallback to `urandom`
|
||||||
|
m_randDev = fopen("/dev/urandom", "rb");
|
||||||
|
if (!m_randDev)
|
||||||
|
qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~RandomLayer()
|
||||||
|
{
|
||||||
|
if (m_randDev)
|
||||||
|
fclose(m_randDev);
|
||||||
}
|
}
|
||||||
|
|
||||||
static constexpr result_type min()
|
static constexpr result_type min()
|
||||||
|
@ -56,7 +78,15 @@ namespace
|
||||||
return std::numeric_limits<result_type>::max();
|
return std::numeric_limits<result_type>::max();
|
||||||
}
|
}
|
||||||
|
|
||||||
result_type operator()()
|
result_type operator()() const
|
||||||
|
{
|
||||||
|
if (!m_randDev)
|
||||||
|
return getRandomViaAPI();
|
||||||
|
return getRandomViaFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
result_type getRandomViaAPI() const
|
||||||
{
|
{
|
||||||
const int RETRY_MAX = 3;
|
const int RETRY_MAX = 3;
|
||||||
|
|
||||||
|
@ -68,10 +98,21 @@ namespace
|
||||||
return buf;
|
return buf;
|
||||||
|
|
||||||
if (result < 0)
|
if (result < 0)
|
||||||
qFatal("getrandom() error. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
qFatal("getrandom() error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||||
}
|
}
|
||||||
|
|
||||||
qFatal("getrandom() failed. Reason: too many retries.");
|
qFatal("getrandom() failed. Reason: too many retries.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result_type getRandomViaFile() const
|
||||||
|
{
|
||||||
|
result_type buf = 0;
|
||||||
|
if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
|
||||||
|
return buf;
|
||||||
|
|
||||||
|
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *m_randDev = nullptr;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,7 +46,7 @@ namespace
|
||||||
: m_randDev {fopen("/dev/urandom", "rb")}
|
: m_randDev {fopen("/dev/urandom", "rb")}
|
||||||
{
|
{
|
||||||
if (!m_randDev)
|
if (!m_randDev)
|
||||||
qFatal("Failed to open /dev/urandom. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
qFatal("Failed to open /dev/urandom. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||||
}
|
}
|
||||||
|
|
||||||
~RandomLayer()
|
~RandomLayer()
|
||||||
|
@ -67,10 +67,10 @@ namespace
|
||||||
result_type operator()() const
|
result_type operator()() const
|
||||||
{
|
{
|
||||||
result_type buf = 0;
|
result_type buf = 0;
|
||||||
if (fread(&buf, sizeof(buf), 1, m_randDev) != 1)
|
if (fread(&buf, sizeof(buf), 1, m_randDev) == 1)
|
||||||
qFatal("Read /dev/urandom error. Reason: %s. Error code: %d.", std::strerror(errno), errno);
|
return buf;
|
||||||
|
|
||||||
return buf;
|
qFatal("Read /dev/urandom error. Reason: \"%s\". Error code: %d.", std::strerror(errno), errno);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -60,7 +60,7 @@ namespace
|
||||||
return std::numeric_limits<result_type>::max();
|
return std::numeric_limits<result_type>::max();
|
||||||
}
|
}
|
||||||
|
|
||||||
result_type operator()()
|
result_type operator()() const
|
||||||
{
|
{
|
||||||
result_type buf = 0;
|
result_type buf = 0;
|
||||||
const bool result = m_processPrng(reinterpret_cast<PBYTE>(&buf), sizeof(buf));
|
const bool result = m_processPrng(reinterpret_cast<PBYTE>(&buf), sizeof(buf));
|
||||||
|
|
|
@ -61,7 +61,12 @@ QString Utils::String::fromLocal8Bit(const std::string_view string)
|
||||||
|
|
||||||
QString Utils::String::wildcardToRegexPattern(const QString &pattern)
|
QString Utils::String::wildcardToRegexPattern(const QString &pattern)
|
||||||
{
|
{
|
||||||
|
#if (QT_VERSION >= QT_VERSION_CHECK(6, 6, 1))
|
||||||
|
return QRegularExpression::wildcardToRegularExpression(pattern
|
||||||
|
, (QRegularExpression::UnanchoredWildcardConversion | QRegularExpression::NonPathWildcardConversion));
|
||||||
|
#else
|
||||||
return QRegularExpression::wildcardToRegularExpression(pattern, QRegularExpression::UnanchoredWildcardConversion);
|
return QRegularExpression::wildcardToRegularExpression(pattern, QRegularExpression::UnanchoredWildcardConversion);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
QStringList Utils::String::splitCommand(const QString &command)
|
QStringList Utils::String::splitCommand(const QString &command)
|
||||||
|
|
|
@ -30,9 +30,9 @@
|
||||||
|
|
||||||
#define QBT_VERSION_MAJOR 5
|
#define QBT_VERSION_MAJOR 5
|
||||||
#define QBT_VERSION_MINOR 1
|
#define QBT_VERSION_MINOR 1
|
||||||
#define QBT_VERSION_BUGFIX 0
|
#define QBT_VERSION_BUGFIX 2
|
||||||
#define QBT_VERSION_BUILD 0
|
#define QBT_VERSION_BUILD 0
|
||||||
#define QBT_VERSION_STATUS "beta1" // Should be empty for stable releases!
|
#define QBT_VERSION_STATUS "" // Should be empty for stable releases!
|
||||||
|
|
||||||
#define QBT__STRINGIFY(x) #x
|
#define QBT__STRINGIFY(x) #x
|
||||||
#define QBT_STRINGIFY(x) QBT__STRINGIFY(x)
|
#define QBT_STRINGIFY(x) QBT__STRINGIFY(x)
|
||||||
|
|
|
@ -287,6 +287,8 @@ if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
|
||||||
macosdockbadge/badger.mm
|
macosdockbadge/badger.mm
|
||||||
macosdockbadge/badgeview.h
|
macosdockbadge/badgeview.h
|
||||||
macosdockbadge/badgeview.mm
|
macosdockbadge/badgeview.mm
|
||||||
|
macosshiftclickhandler.h
|
||||||
|
macosshiftclickhandler.cpp
|
||||||
macutilities.h
|
macutilities.h
|
||||||
macutilities.mm
|
macutilities.mm
|
||||||
)
|
)
|
||||||
|
|
|
@ -67,7 +67,7 @@ AboutDialog::AboutDialog(QWidget *parent)
|
||||||
u"</p>"_s
|
u"</p>"_s
|
||||||
.arg(tr("An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.")
|
.arg(tr("An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.")
|
||||||
.replace(u"C++"_s, u"C\u2060+\u2060+"_s) // make C++ non-breaking
|
.replace(u"C++"_s, u"C\u2060+\u2060+"_s) // make C++ non-breaking
|
||||||
, tr("Copyright %1 2006-2024 The qBittorrent project").arg(C_COPYRIGHT)
|
, tr("Copyright %1 2006-2025 The qBittorrent project").arg(C_COPYRIGHT)
|
||||||
, tr("Home Page:")
|
, tr("Home Page:")
|
||||||
, tr("Forum:")
|
, tr("Forum:")
|
||||||
, tr("Bug Tracker:"));
|
, tr("Bug Tracker:"));
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
* Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
|
@ -384,7 +384,6 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
|
||||||
|
|
||||||
AddNewTorrentDialog::~AddNewTorrentDialog()
|
AddNewTorrentDialog::~AddNewTorrentDialog()
|
||||||
{
|
{
|
||||||
saveState();
|
|
||||||
delete m_ui;
|
delete m_ui;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -398,7 +397,7 @@ void AddNewTorrentDialog::loadState()
|
||||||
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
|
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
|
||||||
resize(dialogSize);
|
resize(dialogSize);
|
||||||
|
|
||||||
m_ui->splitter->restoreState(m_storeSplitterState);;
|
m_ui->splitter->restoreState(m_storeSplitterState);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AddNewTorrentDialog::saveState()
|
void AddNewTorrentDialog::saveState()
|
||||||
|
@ -834,6 +833,12 @@ void AddNewTorrentDialog::reject()
|
||||||
QDialog::reject();
|
QDialog::reject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AddNewTorrentDialog::done(const int result)
|
||||||
|
{
|
||||||
|
saveState();
|
||||||
|
QDialog::done(result);
|
||||||
|
}
|
||||||
|
|
||||||
void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata)
|
void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata)
|
||||||
{
|
{
|
||||||
Q_ASSERT(m_currentContext);
|
Q_ASSERT(m_currentContext);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Bittorrent Client using Qt and libtorrent.
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
|
* Copyright (C) 2022-2025 Vladimir Golovnev <glassez@yandex.ru>
|
||||||
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
|
||||||
*
|
*
|
||||||
* This program is free software; you can redistribute it and/or
|
* This program is free software; you can redistribute it and/or
|
||||||
|
@ -68,6 +68,11 @@ signals:
|
||||||
void torrentAccepted(const BitTorrent::TorrentDescriptor &torrentDescriptor, const BitTorrent::AddTorrentParams &addTorrentParams);
|
void torrentAccepted(const BitTorrent::TorrentDescriptor &torrentDescriptor, const BitTorrent::AddTorrentParams &addTorrentParams);
|
||||||
void torrentRejected(const BitTorrent::TorrentDescriptor &torrentDescriptor);
|
void torrentRejected(const BitTorrent::TorrentDescriptor &torrentDescriptor);
|
||||||
|
|
||||||
|
public slots:
|
||||||
|
void accept() override;
|
||||||
|
void reject() override;
|
||||||
|
void done(int result) override;
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void updateDiskSpaceLabel();
|
void updateDiskSpaceLabel();
|
||||||
void onSavePathChanged(const Path &newPath);
|
void onSavePathChanged(const Path &newPath);
|
||||||
|
@ -77,9 +82,6 @@ private slots:
|
||||||
void categoryChanged(int index);
|
void categoryChanged(int index);
|
||||||
void contentLayoutChanged();
|
void contentLayoutChanged();
|
||||||
|
|
||||||
void accept() override;
|
|
||||||
void reject() override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class TorrentContentAdaptor;
|
class TorrentContentAdaptor;
|
||||||
struct Context;
|
struct Context;
|
||||||
|
|
|
@ -99,6 +99,7 @@ namespace
|
||||||
ENABLE_SPEED_WIDGET,
|
ENABLE_SPEED_WIDGET,
|
||||||
#ifndef Q_OS_MACOS
|
#ifndef Q_OS_MACOS
|
||||||
ENABLE_ICONS_IN_MENUS,
|
ENABLE_ICONS_IN_MENUS,
|
||||||
|
USE_ATTACHED_ADD_NEW_TORRENT_DIALOG,
|
||||||
#endif
|
#endif
|
||||||
// embedded tracker
|
// embedded tracker
|
||||||
TRACKER_STATUS,
|
TRACKER_STATUS,
|
||||||
|
@ -330,6 +331,7 @@ void AdvancedSettings::saveAdvancedSettings() const
|
||||||
pref->setSpeedWidgetEnabled(m_checkBoxSpeedWidgetEnabled.isChecked());
|
pref->setSpeedWidgetEnabled(m_checkBoxSpeedWidgetEnabled.isChecked());
|
||||||
#ifndef Q_OS_MACOS
|
#ifndef Q_OS_MACOS
|
||||||
pref->setIconsInMenusEnabled(m_checkBoxIconsInMenusEnabled.isChecked());
|
pref->setIconsInMenusEnabled(m_checkBoxIconsInMenusEnabled.isChecked());
|
||||||
|
pref->setAddNewTorrentDialogAttached(m_checkBoxAttachedAddNewTorrentDialog.isChecked());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Tracker
|
// Tracker
|
||||||
|
@ -856,6 +858,9 @@ void AdvancedSettings::loadAdvancedSettings()
|
||||||
// Enable icons in menus
|
// Enable icons in menus
|
||||||
m_checkBoxIconsInMenusEnabled.setChecked(pref->iconsInMenusEnabled());
|
m_checkBoxIconsInMenusEnabled.setChecked(pref->iconsInMenusEnabled());
|
||||||
addRow(ENABLE_ICONS_IN_MENUS, tr("Enable icons in menus"), &m_checkBoxIconsInMenusEnabled);
|
addRow(ENABLE_ICONS_IN_MENUS, tr("Enable icons in menus"), &m_checkBoxIconsInMenusEnabled);
|
||||||
|
|
||||||
|
m_checkBoxAttachedAddNewTorrentDialog.setChecked(pref->isAddNewTorrentDialogAttached());
|
||||||
|
addRow(USE_ATTACHED_ADD_NEW_TORRENT_DIALOG, tr("Attach \"Add new torrent\" dialog to main window"), &m_checkBoxAttachedAddNewTorrentDialog);
|
||||||
#endif
|
#endif
|
||||||
// Tracker State
|
// Tracker State
|
||||||
m_checkBoxTrackerStatus.setChecked(session->isTrackerEnabled());
|
m_checkBoxTrackerStatus.setChecked(session->isTrackerEnabled());
|
||||||
|
|
|
@ -108,6 +108,7 @@ private:
|
||||||
|
|
||||||
#ifndef Q_OS_MACOS
|
#ifndef Q_OS_MACOS
|
||||||
QCheckBox m_checkBoxIconsInMenusEnabled;
|
QCheckBox m_checkBoxIconsInMenusEnabled;
|
||||||
|
QCheckBox m_checkBoxAttachedAddNewTorrentDialog;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
|
||||||
|
|
|
@ -82,6 +82,15 @@ GUIAddTorrentManager::GUIAddTorrentManager(IGUIApplication *app, BitTorrent::Ses
|
||||||
connect(btSession(), &BitTorrent::Session::metadataDownloaded, this, &GUIAddTorrentManager::onMetadataDownloaded);
|
connect(btSession(), &BitTorrent::Session::metadataDownloaded, this, &GUIAddTorrentManager::onMetadataDownloaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GUIAddTorrentManager::~GUIAddTorrentManager()
|
||||||
|
{
|
||||||
|
for (AddNewTorrentDialog *dialog : asConst(m_dialogs))
|
||||||
|
{
|
||||||
|
dialog->disconnect(this);
|
||||||
|
dialog->reject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool GUIAddTorrentManager::addTorrent(const QString &source, const BitTorrent::AddTorrentParams ¶ms, const AddTorrentOption option)
|
bool GUIAddTorrentManager::addTorrent(const QString &source, const BitTorrent::AddTorrentParams ¶ms, const AddTorrentOption option)
|
||||||
{
|
{
|
||||||
// `source`: .torrent file path, magnet URI or URL
|
// `source`: .torrent file path, magnet URI or URL
|
||||||
|
@ -225,12 +234,19 @@ bool GUIAddTorrentManager::processTorrent(const QString &source
|
||||||
if (!hasMetadata)
|
if (!hasMetadata)
|
||||||
btSession()->downloadMetadata(torrentDescr);
|
btSession()->downloadMetadata(torrentDescr);
|
||||||
|
|
||||||
|
#ifdef Q_OS_MACOS
|
||||||
|
const bool attached = false;
|
||||||
|
#else
|
||||||
|
const bool attached = Preferences::instance()->isAddNewTorrentDialogAttached();
|
||||||
|
#endif
|
||||||
|
|
||||||
// By not setting a parent to the "AddNewTorrentDialog", all those dialogs
|
// By not setting a parent to the "AddNewTorrentDialog", all those dialogs
|
||||||
// will be displayed on top and will not overlap with the main window.
|
// will be displayed on top and will not overlap with the main window.
|
||||||
auto *dlg = new AddNewTorrentDialog(torrentDescr, params, nullptr);
|
auto *dlg = new AddNewTorrentDialog(torrentDescr, params, (attached ? app()->mainWindow() : nullptr));
|
||||||
// Qt::Window is required to avoid showing only two dialog on top (see #12852).
|
// Qt::Window is required to avoid showing only two dialog on top (see #12852).
|
||||||
// Also improves the general convenience of adding multiple torrents.
|
// Also improves the general convenience of adding multiple torrents.
|
||||||
dlg->setWindowFlags(Qt::Window);
|
if (!attached)
|
||||||
|
dlg->setWindowFlags(Qt::Window);
|
||||||
|
|
||||||
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
dlg->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
m_dialogs[infoHash] = dlg;
|
m_dialogs[infoHash] = dlg;
|
||||||
|
|
|
@ -61,6 +61,7 @@ class GUIAddTorrentManager : public GUIApplicationComponent<AddTorrentManager>
|
||||||
|
|
||||||
public:
|
public:
|
||||||
GUIAddTorrentManager(IGUIApplication *app, BitTorrent::Session *session, QObject *parent = nullptr);
|
GUIAddTorrentManager(IGUIApplication *app, BitTorrent::Session *session, QObject *parent = nullptr);
|
||||||
|
~GUIAddTorrentManager() override;
|
||||||
|
|
||||||
bool addTorrent(const QString &source, const BitTorrent::AddTorrentParams ¶ms = {}, AddTorrentOption option = AddTorrentOption::Default);
|
bool addTorrent(const QString &source, const BitTorrent::AddTorrentParams ¶ms = {}, AddTorrentOption option = AddTorrentOption::Default);
|
||||||
|
|
||||||
|
|
73
src/gui/macosshiftclickhandler.cpp
Normal file
73
src/gui/macosshiftclickhandler.cpp
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2025 Luke Memet (lukemmtt)
|
||||||
|
*
|
||||||
|
* 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 "macosshiftclickhandler.h"
|
||||||
|
|
||||||
|
#include <QMouseEvent>
|
||||||
|
#include <QTreeView>
|
||||||
|
|
||||||
|
MacOSShiftClickHandler::MacOSShiftClickHandler(QTreeView *treeView)
|
||||||
|
: QObject(treeView)
|
||||||
|
, m_treeView {treeView}
|
||||||
|
{
|
||||||
|
treeView->installEventFilter(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MacOSShiftClickHandler::eventFilter(QObject *watched, QEvent *event)
|
||||||
|
{
|
||||||
|
if ((watched == m_treeView) && (event->type() == QEvent::MouseButtonPress))
|
||||||
|
{
|
||||||
|
const auto *mouseEvent = static_cast<QMouseEvent *>(event);
|
||||||
|
if (mouseEvent->button() != Qt::LeftButton)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const QModelIndex clickedIndex = m_treeView->indexAt(mouseEvent->position().toPoint());
|
||||||
|
if (!clickedIndex.isValid())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const Qt::KeyboardModifiers modifiers = mouseEvent->modifiers();
|
||||||
|
const bool shiftPressed = modifiers.testFlag(Qt::ShiftModifier);
|
||||||
|
|
||||||
|
if (shiftPressed && m_lastClickedIndex.isValid())
|
||||||
|
{
|
||||||
|
const QItemSelection selection(m_lastClickedIndex, clickedIndex);
|
||||||
|
const bool commandPressed = modifiers.testFlag(Qt::ControlModifier);
|
||||||
|
if (commandPressed)
|
||||||
|
m_treeView->selectionModel()->select(selection, (QItemSelectionModel::Select | QItemSelectionModel::Rows));
|
||||||
|
else
|
||||||
|
m_treeView->selectionModel()->select(selection, (QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows));
|
||||||
|
m_treeView->selectionModel()->setCurrentIndex(clickedIndex, QItemSelectionModel::NoUpdate);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!modifiers.testFlags(Qt::AltModifier | Qt::MetaModifier))
|
||||||
|
m_lastClickedIndex = clickedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return QObject::eventFilter(watched, event);
|
||||||
|
}
|
50
src/gui/macosshiftclickhandler.h
Normal file
50
src/gui/macosshiftclickhandler.h
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
/*
|
||||||
|
* Bittorrent Client using Qt and libtorrent.
|
||||||
|
* Copyright (C) 2025 Luke Memet (lukemmtt)
|
||||||
|
*
|
||||||
|
* 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 <QPersistentModelIndex>
|
||||||
|
|
||||||
|
class QTreeView;
|
||||||
|
|
||||||
|
// Workaround for QTBUG-115838: Shift-click range selection not working properly on macOS
|
||||||
|
class MacOSShiftClickHandler final : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
Q_DISABLE_COPY_MOVE(MacOSShiftClickHandler)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit MacOSShiftClickHandler(QTreeView *treeView);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||||
|
|
||||||
|
QTreeView *m_treeView = nullptr;
|
||||||
|
QPersistentModelIndex m_lastClickedIndex;
|
||||||
|
};
|
|
@ -1161,7 +1161,7 @@ void MainWindow::closeEvent(QCloseEvent *e)
|
||||||
if (!m_forceExit)
|
if (!m_forceExit)
|
||||||
{
|
{
|
||||||
hide();
|
hide();
|
||||||
e->accept();
|
e->ignore();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
|
@ -1660,11 +1660,11 @@ void MainWindow::handleUpdateCheckFinished(ProgramUpdater *updater, const bool i
|
||||||
updater->deleteLater();
|
updater->deleteLater();
|
||||||
};
|
};
|
||||||
|
|
||||||
const QString newVersion = updater->getNewVersion();
|
const auto newVersion = updater->getNewVersion();
|
||||||
if (!newVersion.isEmpty())
|
if (newVersion.isValid())
|
||||||
{
|
{
|
||||||
const QString msg {tr("A new version is available.") + u"<br/>"
|
const QString msg {tr("A new version is available.") + u"<br/>"
|
||||||
+ tr("Do you want to download %1?").arg(newVersion) + u"<br/><br/>"
|
+ tr("Do you want to download %1?").arg(newVersion.toString()) + u"<br/><br/>"
|
||||||
+ u"<a href=\"https://www.qbittorrent.org/news\">%1</a>"_s.arg(tr("Open changelog..."))};
|
+ u"<a href=\"https://www.qbittorrent.org/news\">%1</a>"_s.arg(tr("Open changelog..."))};
|
||||||
auto *msgBox = new QMessageBox {QMessageBox::Question, tr("qBittorrent Update Available"), msg
|
auto *msgBox = new QMessageBox {QMessageBox::Question, tr("qBittorrent Update Available"), msg
|
||||||
, (QMessageBox::Yes | QMessageBox::No), this};
|
, (QMessageBox::Yes | QMessageBox::No), this};
|
||||||
|
|
|
@ -3021,9 +3021,6 @@ Disable encryption: Only connect to peers without protocol encryption</string>
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
|
||||||
<double>9998.000000000000000</double>
|
|
||||||
</property>
|
|
||||||
<property name="singleStep">
|
<property name="singleStep">
|
||||||
<double>0.050000000000000</double>
|
<double>0.050000000000000</double>
|
||||||
</property>
|
</property>
|
||||||
|
@ -3283,15 +3280,9 @@ Disable encryption: Only connect to peers without protocol encryption</string>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QSpinBox" name="searchHistoryLengthSpinBox">
|
<widget class="QSpinBox" name="searchHistoryLengthSpinBox">
|
||||||
<property name="buttonSymbols">
|
|
||||||
<enum>QAbstractSpinBox::ButtonSymbols::PlusMinus</enum>
|
|
||||||
</property>
|
|
||||||
<property name="maximum">
|
<property name="maximum">
|
||||||
<number>99</number>
|
<number>99</number>
|
||||||
</property>
|
</property>
|
||||||
<property name="stepType">
|
|
||||||
<enum>QAbstractSpinBox::StepType::DefaultStepType</enum>
|
|
||||||
</property>
|
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
|
|
@ -35,10 +35,13 @@
|
||||||
#include <QtSystemDetection>
|
#include <QtSystemDetection>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QDesktopServices>
|
#include <QDesktopServices>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonValue>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QXmlStreamReader>
|
#include <QXmlStreamReader>
|
||||||
|
|
||||||
#include "base/global.h"
|
#include "base/global.h"
|
||||||
|
#include "base/logger.h"
|
||||||
#include "base/net/downloadmanager.h"
|
#include "base/net/downloadmanager.h"
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
#include "base/utils/version.h"
|
#include "base/utils/version.h"
|
||||||
|
@ -46,23 +49,20 @@
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
bool isVersionMoreRecent(const QString &remoteVersion)
|
bool isVersionMoreRecent(const ProgramUpdater::Version &remoteVersion)
|
||||||
{
|
{
|
||||||
using Version = Utils::Version<4, 3>;
|
if (!remoteVersion.isValid())
|
||||||
|
|
||||||
const auto newVersion = Version::fromString(remoteVersion);
|
|
||||||
if (!newVersion.isValid())
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const Version currentVersion {QBT_VERSION_MAJOR, QBT_VERSION_MINOR, QBT_VERSION_BUGFIX, QBT_VERSION_BUILD};
|
const ProgramUpdater::Version currentVersion {QBT_VERSION_MAJOR, QBT_VERSION_MINOR, QBT_VERSION_BUGFIX, QBT_VERSION_BUILD};
|
||||||
if (newVersion == currentVersion)
|
if (remoteVersion == currentVersion)
|
||||||
{
|
{
|
||||||
const bool isDevVersion = QStringLiteral(QBT_VERSION_STATUS).contains(
|
const bool isDevVersion = QStringLiteral(QBT_VERSION_STATUS).contains(
|
||||||
QRegularExpression(u"(alpha|beta|rc)"_s));
|
QRegularExpression(u"(alpha|beta|rc)"_s));
|
||||||
if (isDevVersion)
|
if (isDevVersion)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return (newVersion > currentVersion);
|
return (remoteVersion > currentVersion);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString buildVariant()
|
QString buildVariant()
|
||||||
|
@ -82,30 +82,34 @@ namespace
|
||||||
|
|
||||||
void ProgramUpdater::checkForUpdates() const
|
void ProgramUpdater::checkForUpdates() const
|
||||||
{
|
{
|
||||||
|
const auto USER_AGENT = QStringLiteral("qBittorrent/" QBT_VERSION_2 " ProgramUpdater (www.qbittorrent.org)");
|
||||||
const auto RSS_URL = u"https://www.fosshub.com/feed/5b8793a7f9ee5a5c3e97a3b2.xml"_s;
|
const auto RSS_URL = u"https://www.fosshub.com/feed/5b8793a7f9ee5a5c3e97a3b2.xml"_s;
|
||||||
|
const auto FALLBACK_URL = u"https://www.qbittorrent.org/versions.json"_s;
|
||||||
|
|
||||||
// Don't change this User-Agent. In case our updater goes haywire,
|
// Don't change this User-Agent. In case our updater goes haywire,
|
||||||
// the filehost can identify it and contact us.
|
// the filehost can identify it and contact us.
|
||||||
Net::DownloadManager::instance()->download(
|
Net::DownloadManager::instance()->download(Net::DownloadRequest(RSS_URL).userAgent(USER_AGENT)
|
||||||
Net::DownloadRequest(RSS_URL).userAgent(QStringLiteral("qBittorrent/" QBT_VERSION_2 " ProgramUpdater (www.qbittorrent.org)"))
|
|
||||||
, Preferences::instance()->useProxyForGeneralPurposes(), this, &ProgramUpdater::rssDownloadFinished);
|
, Preferences::instance()->useProxyForGeneralPurposes(), this, &ProgramUpdater::rssDownloadFinished);
|
||||||
|
Net::DownloadManager::instance()->download(Net::DownloadRequest(FALLBACK_URL).userAgent(USER_AGENT)
|
||||||
|
, Preferences::instance()->useProxyForGeneralPurposes(), this, &ProgramUpdater::fallbackDownloadFinished);
|
||||||
|
|
||||||
|
m_hasCompletedOneReq = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
QString ProgramUpdater::getNewVersion() const
|
ProgramUpdater::Version ProgramUpdater::getNewVersion() const
|
||||||
{
|
{
|
||||||
return m_newVersion;
|
return shouldUseFallback() ? m_fallbackRemoteVersion : m_remoteVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
|
void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
|
||||||
{
|
{
|
||||||
if (result.status != Net::DownloadStatus::Success)
|
if (result.status != Net::DownloadStatus::Success)
|
||||||
{
|
{
|
||||||
qDebug() << "Downloading the new qBittorrent updates RSS failed:" << result.errorString;
|
LogMsg(tr("Failed to download the update info. URL: %1. Error: %2").arg(result.url, result.errorString) , Log::WARNING);
|
||||||
emit updateCheckFinished();
|
handleFinishedRequest();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
qDebug("Finished downloading the new qBittorrent updates RSS");
|
|
||||||
|
|
||||||
const auto getStringValue = [](QXmlStreamReader &xml) -> QString
|
const auto getStringValue = [](QXmlStreamReader &xml) -> QString
|
||||||
{
|
{
|
||||||
xml.readNext();
|
xml.readNext();
|
||||||
|
@ -146,9 +150,10 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
|
||||||
if (!version.isEmpty())
|
if (!version.isEmpty())
|
||||||
{
|
{
|
||||||
qDebug("Detected version is %s", qUtf8Printable(version));
|
qDebug("Detected version is %s", qUtf8Printable(version));
|
||||||
if (isVersionMoreRecent(version))
|
const ProgramUpdater::Version tmpVer {version};
|
||||||
|
if (isVersionMoreRecent(tmpVer))
|
||||||
{
|
{
|
||||||
m_newVersion = version;
|
m_remoteVersion = tmpVer;
|
||||||
m_updateURL = updateLink;
|
m_updateURL = updateLink;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,10 +168,50 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emit updateCheckFinished();
|
handleFinishedRequest();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUpdater::fallbackDownloadFinished(const Net::DownloadResult &result)
|
||||||
|
{
|
||||||
|
if (result.status != Net::DownloadStatus::Success)
|
||||||
|
{
|
||||||
|
LogMsg(tr("Failed to download the update info. URL: %1. Error: %2").arg(result.url, result.errorString) , Log::WARNING);
|
||||||
|
handleFinishedRequest();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto json = QJsonDocument::fromJson(result.data);
|
||||||
|
|
||||||
|
#if defined(Q_OS_MACOS)
|
||||||
|
const QString platformKey = u"macos"_s;
|
||||||
|
#elif defined(Q_OS_WIN)
|
||||||
|
const QString platformKey = u"win"_s;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (const QJsonValue verJSON = json[platformKey][u"version"_s]; verJSON.isString())
|
||||||
|
{
|
||||||
|
const ProgramUpdater::Version tmpVer {verJSON.toString()};
|
||||||
|
if (isVersionMoreRecent(tmpVer))
|
||||||
|
m_fallbackRemoteVersion = tmpVer;
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFinishedRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ProgramUpdater::updateProgram() const
|
bool ProgramUpdater::updateProgram() const
|
||||||
{
|
{
|
||||||
return QDesktopServices::openUrl(m_updateURL);
|
return QDesktopServices::openUrl(shouldUseFallback() ? u"https://www.qbittorrent.org/download"_s : m_updateURL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramUpdater::handleFinishedRequest()
|
||||||
|
{
|
||||||
|
if (m_hasCompletedOneReq)
|
||||||
|
emit updateCheckFinished();
|
||||||
|
else
|
||||||
|
m_hasCompletedOneReq = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProgramUpdater::shouldUseFallback() const
|
||||||
|
{
|
||||||
|
return m_fallbackRemoteVersion > m_remoteVersion;
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +30,10 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QString>
|
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "base/utils/version.h"
|
||||||
|
|
||||||
namespace Net
|
namespace Net
|
||||||
{
|
{
|
||||||
struct DownloadResult;
|
struct DownloadResult;
|
||||||
|
@ -45,9 +46,10 @@ class ProgramUpdater final : public QObject
|
||||||
|
|
||||||
public:
|
public:
|
||||||
using QObject::QObject;
|
using QObject::QObject;
|
||||||
|
using Version = Utils::Version<4, 3>;
|
||||||
|
|
||||||
void checkForUpdates() const;
|
void checkForUpdates() const;
|
||||||
QString getNewVersion() const;
|
Version getNewVersion() const;
|
||||||
bool updateProgram() const;
|
bool updateProgram() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
|
@ -55,8 +57,14 @@ signals:
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void rssDownloadFinished(const Net::DownloadResult &result);
|
void rssDownloadFinished(const Net::DownloadResult &result);
|
||||||
|
void fallbackDownloadFinished(const Net::DownloadResult &result);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_newVersion;
|
void handleFinishedRequest();
|
||||||
|
bool shouldUseFallback() const;
|
||||||
|
|
||||||
|
mutable bool m_hasCompletedOneReq = false;
|
||||||
|
Version m_remoteVersion;
|
||||||
|
Version m_fallbackRemoteVersion;
|
||||||
QUrl m_updateURL;
|
QUrl m_updateURL;
|
||||||
};
|
};
|
||||||
|
|
|
@ -439,10 +439,10 @@ void PropertiesWidget::loadDynamicData()
|
||||||
|
|
||||||
// Update ratio info
|
// Update ratio info
|
||||||
const qreal ratio = m_torrent->realRatio();
|
const qreal ratio = m_torrent->realRatio();
|
||||||
m_ui->labelShareRatioVal->setText(ratio > BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(ratio, 2));
|
m_ui->labelShareRatioVal->setText(ratio >= BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(ratio, 2));
|
||||||
|
|
||||||
const qreal popularity = m_torrent->popularity();
|
const qreal popularity = m_torrent->popularity();
|
||||||
m_ui->labelPopularityVal->setText(popularity > BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(popularity, 2));
|
m_ui->labelPopularityVal->setText(popularity >= BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(popularity, 2));
|
||||||
|
|
||||||
m_ui->labelSeedsVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)")
|
m_ui->labelSeedsVal->setText(tr("%1 (%2 total)", "%1 and %2 are numbers, e.g. 3 (10 total)")
|
||||||
.arg(QString::number(m_torrent->seedsCount())
|
.arg(QString::number(m_torrent->seedsCount())
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
#include <QString>
|
#include <QString>
|
||||||
|
|
||||||
#include "base/global.h"
|
#include "base/global.h"
|
||||||
|
#include "base/logger.h"
|
||||||
#include "base/net/downloadmanager.h"
|
#include "base/net/downloadmanager.h"
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
#include "base/rss/rss_article.h"
|
#include "base/rss/rss_article.h"
|
||||||
|
@ -415,16 +416,52 @@ void RSSWidget::downloadSelectedTorrents()
|
||||||
// open the url of the selected RSS articles in the Web browser
|
// open the url of the selected RSS articles in the Web browser
|
||||||
void RSSWidget::openSelectedArticlesUrls()
|
void RSSWidget::openSelectedArticlesUrls()
|
||||||
{
|
{
|
||||||
|
qsizetype emptyLinkCount = 0;
|
||||||
|
qsizetype badLinkCount = 0;
|
||||||
|
QString articleTitle;
|
||||||
for (QListWidgetItem *item : asConst(m_ui->articleListWidget->selectedItems()))
|
for (QListWidgetItem *item : asConst(m_ui->articleListWidget->selectedItems()))
|
||||||
{
|
{
|
||||||
auto *article = item->data(Qt::UserRole).value<RSS::Article *>();
|
auto *article = item->data(Qt::UserRole).value<RSS::Article *>();
|
||||||
Q_ASSERT(article);
|
Q_ASSERT(article);
|
||||||
|
|
||||||
// Mark as read
|
|
||||||
article->markAsRead();
|
article->markAsRead();
|
||||||
|
|
||||||
if (!article->link().isEmpty())
|
const QString articleLink = article->link();
|
||||||
QDesktopServices::openUrl(QUrl(article->link()));
|
const QUrl articleLinkURL {articleLink};
|
||||||
|
if (articleLinkURL.isEmpty()) [[unlikely]]
|
||||||
|
{
|
||||||
|
if (articleTitle.isEmpty())
|
||||||
|
articleTitle = article->title();
|
||||||
|
++emptyLinkCount;
|
||||||
|
}
|
||||||
|
else if (articleLinkURL.isLocalFile()) [[unlikely]]
|
||||||
|
{
|
||||||
|
if (badLinkCount == 0)
|
||||||
|
articleTitle = article->title();
|
||||||
|
++badLinkCount;
|
||||||
|
|
||||||
|
LogMsg(tr("Blocked opening RSS article URL. URL pointing to local file might be malicious behaviour. Article: \"%1\". URL: \"%2\".")
|
||||||
|
.arg(article->title(), articleLink), Log::WARNING);
|
||||||
|
}
|
||||||
|
else [[likely]]
|
||||||
|
{
|
||||||
|
QDesktopServices::openUrl(articleLinkURL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (badLinkCount > 0)
|
||||||
|
{
|
||||||
|
QString message = tr("Blocked opening RSS article URL. The following article URL is pointing to local file and it may be malicious behaviour:\n%1").arg(articleTitle);
|
||||||
|
if (badLinkCount > 1)
|
||||||
|
message.append(u"\n" + tr("There are %1 more articles with the same issue.").arg(badLinkCount - 1));
|
||||||
|
QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
|
||||||
|
}
|
||||||
|
else if (emptyLinkCount > 0)
|
||||||
|
{
|
||||||
|
QString message = tr("The following article has no news URL provided:\n%1").arg(articleTitle);
|
||||||
|
if (emptyLinkCount > 1)
|
||||||
|
message.append(u"\n" + tr("There are %1 more articles with the same issue.").arg(emptyLinkCount - 1));
|
||||||
|
QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -35,10 +35,12 @@
|
||||||
#include <QHeaderView>
|
#include <QHeaderView>
|
||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QMenu>
|
#include <QMenu>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
#include <QStandardItemModel>
|
#include <QStandardItemModel>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
|
#include "base/logger.h"
|
||||||
#include "base/preferences.h"
|
#include "base/preferences.h"
|
||||||
#include "base/search/searchdownloadhandler.h"
|
#include "base/search/searchdownloadhandler.h"
|
||||||
#include "base/search/searchhandler.h"
|
#include "base/search/searchhandler.h"
|
||||||
|
@ -319,15 +321,52 @@ void SearchJobWidget::downloadTorrents(const AddTorrentOption option)
|
||||||
downloadTorrent(rowIndex, option);
|
downloadTorrent(rowIndex, option);
|
||||||
}
|
}
|
||||||
|
|
||||||
void SearchJobWidget::openTorrentPages() const
|
void SearchJobWidget::openTorrentPages()
|
||||||
{
|
{
|
||||||
const QModelIndexList rows {m_ui->resultsBrowser->selectionModel()->selectedRows()};
|
const QModelIndexList rows = m_ui->resultsBrowser->selectionModel()->selectedRows();
|
||||||
|
qsizetype emptyLinkCount = 0;
|
||||||
|
qsizetype badLinkCount = 0;
|
||||||
|
QString warningEntryName;
|
||||||
for (const QModelIndex &rowIndex : rows)
|
for (const QModelIndex &rowIndex : rows)
|
||||||
{
|
{
|
||||||
const QString descrLink = m_proxyModel->data(
|
const QString entryName = m_proxyModel->index(rowIndex.row(), SearchSortModel::NAME).data().toString();
|
||||||
m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK)).toString();
|
const QString descrLink = m_proxyModel->index(rowIndex.row(), SearchSortModel::DESC_LINK).data().toString();
|
||||||
if (!descrLink.isEmpty())
|
|
||||||
QDesktopServices::openUrl(QUrl::fromEncoded(descrLink.toUtf8()));
|
const QUrl descrLinkURL {descrLink};
|
||||||
|
if (descrLinkURL.isEmpty()) [[unlikely]]
|
||||||
|
{
|
||||||
|
if (warningEntryName.isEmpty())
|
||||||
|
warningEntryName = entryName;
|
||||||
|
++emptyLinkCount;
|
||||||
|
}
|
||||||
|
else if (descrLinkURL.isLocalFile()) [[unlikely]]
|
||||||
|
{
|
||||||
|
if (badLinkCount == 0)
|
||||||
|
warningEntryName = entryName;
|
||||||
|
++badLinkCount;
|
||||||
|
|
||||||
|
LogMsg(tr("Blocked opening search result description page URL. URL pointing to local file might be malicious behaviour. Name: \"%1\". URL: \"%2\".")
|
||||||
|
.arg(entryName, descrLink), Log::WARNING);
|
||||||
|
}
|
||||||
|
else [[likely]]
|
||||||
|
{
|
||||||
|
QDesktopServices::openUrl(descrLinkURL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (badLinkCount > 0)
|
||||||
|
{
|
||||||
|
QString message = tr("Blocked opening search result description page URL. The following result URL is pointing to local file and it may be malicious behaviour:\n%1").arg(warningEntryName);
|
||||||
|
if (badLinkCount > 1)
|
||||||
|
message.append(u"\n" + tr("There are %1 more results with the same issue.").arg(badLinkCount - 1));
|
||||||
|
QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
|
||||||
|
}
|
||||||
|
else if (emptyLinkCount > 0)
|
||||||
|
{
|
||||||
|
QString message = tr("Entry \"%1\" has no description page URL provided.").arg(warningEntryName);
|
||||||
|
if (emptyLinkCount > 1)
|
||||||
|
message.append(u"\n" + tr("There are %1 more entries with the same issue.").arg(emptyLinkCount - 1));
|
||||||
|
QMessageBox::warning(this, u"qBittorrent"_s, message, QMessageBox::Ok);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -127,7 +127,7 @@ private:
|
||||||
void onUIThemeChanged();
|
void onUIThemeChanged();
|
||||||
|
|
||||||
void downloadTorrents(AddTorrentOption option = AddTorrentOption::Default);
|
void downloadTorrents(AddTorrentOption option = AddTorrentOption::Default);
|
||||||
void openTorrentPages() const;
|
void openTorrentPages();
|
||||||
void copyTorrentURLs() const;
|
void copyTorrentURLs() const;
|
||||||
void copyTorrentDownloadLinks() const;
|
void copyTorrentDownloadLinks() const;
|
||||||
void copyTorrentNames() const;
|
void copyTorrentNames() const;
|
||||||
|
|
|
@ -47,9 +47,6 @@
|
||||||
<property name="enabled">
|
<property name="enabled">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
|
||||||
<double>9998.000000000000000</double>
|
|
||||||
</property>
|
|
||||||
<property name="singleStep">
|
<property name="singleStep">
|
||||||
<double>0.050000000000000</double>
|
<double>0.050000000000000</double>
|
||||||
</property>
|
</property>
|
||||||
|
|
|
@ -293,7 +293,7 @@ QString TransferListModel::displayValue(const BitTorrent::Torrent *torrent, cons
|
||||||
if (hideValues && (value <= 0))
|
if (hideValues && (value <= 0))
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
return ((static_cast<int>(value) == -1) || (value > BitTorrent::Torrent::MAX_RATIO))
|
return ((static_cast<int>(value) == -1) || (value >= BitTorrent::Torrent::MAX_RATIO))
|
||||||
? C_INFINITY : Utils::String::fromDouble(value, 2);
|
? C_INFINITY : Utils::String::fromDouble(value, 2);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -74,6 +74,7 @@
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
|
||||||
#ifdef Q_OS_MACOS
|
#ifdef Q_OS_MACOS
|
||||||
|
#include "macosshiftclickhandler.h"
|
||||||
#include "macutilities.h"
|
#include "macutilities.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -158,6 +159,7 @@ TransferListWidget::TransferListWidget(IGUIApplication *app, QWidget *parent)
|
||||||
setDropIndicatorShown(true);
|
setDropIndicatorShown(true);
|
||||||
#if defined(Q_OS_MACOS)
|
#if defined(Q_OS_MACOS)
|
||||||
setAttribute(Qt::WA_MacShowFocusRect, false);
|
setAttribute(Qt::WA_MacShowFocusRect, false);
|
||||||
|
new MacOSShiftClickHandler(this);
|
||||||
#endif
|
#endif
|
||||||
header()->setFirstSectionMovable(true);
|
header()->setFirstSectionMovable(true);
|
||||||
header()->setStretchLastSection(false);
|
header()->setStretchLastSection(false);
|
||||||
|
@ -309,10 +311,7 @@ void TransferListWidget::torrentDoubleClicked()
|
||||||
case PREVIEW_FILE:
|
case PREVIEW_FILE:
|
||||||
if (torrentContainsPreviewableFiles(torrent))
|
if (torrentContainsPreviewableFiles(torrent))
|
||||||
{
|
{
|
||||||
auto *dialog = new PreviewSelectDialog(this, torrent);
|
openPreviewSelectDialog(torrent);
|
||||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
|
|
||||||
dialog->show();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -614,10 +613,7 @@ void TransferListWidget::previewSelectedTorrents()
|
||||||
{
|
{
|
||||||
if (torrentContainsPreviewableFiles(torrent))
|
if (torrentContainsPreviewableFiles(torrent))
|
||||||
{
|
{
|
||||||
auto *dialog = new PreviewSelectDialog(this, torrent);
|
openPreviewSelectDialog(torrent);
|
||||||
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
||||||
connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile);
|
|
||||||
dialog->show();
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -1446,3 +1442,13 @@ void TransferListWidget::wheelEvent(QWheelEvent *event)
|
||||||
|
|
||||||
QTreeView::wheelEvent(event); // event delegated to base class
|
QTreeView::wheelEvent(event); // event delegated to base class
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void TransferListWidget::openPreviewSelectDialog(const BitTorrent::Torrent *torrent)
|
||||||
|
{
|
||||||
|
auto *dialog = new PreviewSelectDialog(this, torrent);
|
||||||
|
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
||||||
|
// Qt::QueuedConnection is required to prevent a bug on wayland compositors where the preview won't open.
|
||||||
|
// It occurs when the window focus shifts immediately after TransferListWidget::previewFile has been called.
|
||||||
|
connect(dialog, &PreviewSelectDialog::readyToPreviewFile, this, &TransferListWidget::previewFile, Qt::QueuedConnection);
|
||||||
|
dialog->show();
|
||||||
|
}
|
||||||
|
|
|
@ -123,6 +123,7 @@ private:
|
||||||
void dragMoveEvent(QDragMoveEvent *event) override;
|
void dragMoveEvent(QDragMoveEvent *event) override;
|
||||||
void dropEvent(QDropEvent *event) override;
|
void dropEvent(QDropEvent *event) override;
|
||||||
void wheelEvent(QWheelEvent *event) override;
|
void wheelEvent(QWheelEvent *event) override;
|
||||||
|
void openPreviewSelectDialog(const BitTorrent::Torrent *torrent);
|
||||||
QModelIndex mapToSource(const QModelIndex &index) const;
|
QModelIndex mapToSource(const QModelIndex &index) const;
|
||||||
QModelIndexList mapToSource(const QModelIndexList &indexes) const;
|
QModelIndexList mapToSource(const QModelIndexList &indexes) const;
|
||||||
QModelIndex mapFromSource(const QModelIndex &index) const;
|
QModelIndex mapFromSource(const QModelIndex &index) const;
|
||||||
|
|
|
@ -84,6 +84,38 @@ inline QHash<QString, UIThemeColor> defaultUIThemeColors()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Palette isn't customizable in default theme
|
||||||
|
inline QHash<QString, UIThemeColor> defaultPaletteColors()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
{u"Palette.Window"_s, {{}, {}}},
|
||||||
|
{u"Palette.WindowText"_s, {{}, {}}},
|
||||||
|
{u"Palette.Base"_s, {{}, {}}},
|
||||||
|
{u"Palette.AlternateBase"_s, {{}, {}}},
|
||||||
|
{u"Palette.Text"_s, {{}, {}}},
|
||||||
|
{u"Palette.ToolTipBase"_s, {{}, {}}},
|
||||||
|
{u"Palette.ToolTipText"_s, {{}, {}}},
|
||||||
|
{u"Palette.BrightText"_s, {{}, {}}},
|
||||||
|
{u"Palette.Highlight"_s, {{}, {}}},
|
||||||
|
{u"Palette.HighlightedText"_s, {{}, {}}},
|
||||||
|
{u"Palette.Button"_s, {{}, {}}},
|
||||||
|
{u"Palette.ButtonText"_s, {{}, {}}},
|
||||||
|
{u"Palette.Link"_s, {{}, {}}},
|
||||||
|
{u"Palette.LinkVisited"_s, {{}, {}}},
|
||||||
|
{u"Palette.Light"_s, {{}, {}}},
|
||||||
|
{u"Palette.Midlight"_s, {{}, {}}},
|
||||||
|
{u"Palette.Mid"_s, {{}, {}}},
|
||||||
|
{u"Palette.Dark"_s, {{}, {}}},
|
||||||
|
{u"Palette.Shadow"_s, {{}, {}}},
|
||||||
|
{u"Palette.WindowTextDisabled"_s, {{}, {}}},
|
||||||
|
{u"Palette.TextDisabled"_s, {{}, {}}},
|
||||||
|
{u"Palette.ToolTipTextDisabled"_s, {{}, {}}},
|
||||||
|
{u"Palette.BrightTextDisabled"_s, {{}, {}}},
|
||||||
|
{u"Palette.HighlightedTextDisabled"_s, {{}, {}}},
|
||||||
|
{u"Palette.ButtonTextDisabled"_s, {{}, {}}}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
inline QSet<QString> defaultUIThemeIcons()
|
inline QSet<QString> defaultUIThemeIcons()
|
||||||
{
|
{
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -273,8 +273,6 @@ void UIThemeDialog::loadColors()
|
||||||
int row = 2;
|
int row = 2;
|
||||||
for (const QString &id : colorIDs)
|
for (const QString &id : colorIDs)
|
||||||
{
|
{
|
||||||
if (id == u"Log.Normal")
|
|
||||||
qDebug() << "!!!!!!!";
|
|
||||||
m_ui->colorsLayout->addWidget(new QLabel(id), row, 0);
|
m_ui->colorsLayout->addWidget(new QLabel(id), row, 0);
|
||||||
|
|
||||||
const UIThemeColor &defaultColor = defaultColors.value(id);
|
const UIThemeColor &defaultColor = defaultColors.value(id);
|
||||||
|
|
|
@ -105,6 +105,8 @@ DefaultThemeSource::DefaultThemeSource()
|
||||||
, m_colors {defaultUIThemeColors()}
|
, m_colors {defaultUIThemeColors()}
|
||||||
{
|
{
|
||||||
loadColors();
|
loadColors();
|
||||||
|
// Palette isn't customizable in default theme
|
||||||
|
m_colors.insert(defaultPaletteColors());
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray DefaultThemeSource::readStyleSheet()
|
QByteArray DefaultThemeSource::readStyleSheet()
|
||||||
|
|
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
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