Compare commits

...

31 commits

Author SHA1 Message Date
sledgehammer999
da87be2b12
Bump to 5.1.0 2025-04-27 11:53:39 +03:00
sledgehammer999
891265b390
Update Changelog 2025-04-27 11:53:25 +03:00
sledgehammer999
f46e44d3ed
Sync translations from Transifex and run lupdate 2025-04-27 11:52:43 +03:00
sledgehammer999
a4094a440d
Bump copyright year 2025-04-20 23:26:52 +03:00
sledgehammer999
46c3da21e1
Sync translations from Transifex and run lupdate 2025-04-20 23:23:23 +03:00
Vladimir Golovnev
2f06ea2587
Backport changes to v5.1.x branch
PR #22490.
2025-04-17 20:54:03 +03:00
Vladimir Golovnev
cfbf6b73ff
Prevent crash due to corrupted resume data
PR #22569.
Closes #22540.
2025-04-17 11:17:19 +03:00
Vladimir Golovnev
c687a7d0d3
Fix the torrent relocates files when switching to "manual" mode
PR #22564.
Closes #22283.
Closes #22546.
2025-04-16 10:24:34 +03:00
Vladimir Golovnev
009cc71f9b
Explicitly reject opened Add torrent dialogs when exiting app
PR #22535.
Closes #19933.
Supercedes #22533.
2025-04-14 09:53:07 +03:00
Chocobo1
de1cf208ce
WebUI: avoid saving invalid size
Don't save the wrong size when the tab is collapsed.
Reported in: https://github.com/qbittorrent/qBittorrent/pull/21215/files#r1966052959

PR #22537.
2025-04-12 15:13:47 +03:00
skomerko
5f49472fa4
WebUI: Set status filter to 'All' if selected filter is no longer visible
Fixup for #21145

To reproduce:
1. Select status filter with 0 torrents
2. Enable 'Auto hide zero status filters' and save settings. Hidden filter is still selected:

PR #22487.
2025-04-12 07:12:23 +03:00
skomerko
2076302170
WebUI: Show 'Edit tracker URL...' only when one tracker is selected
We can only edit one URL through the dialog, so there's no point in showing this context option when more than one tracker is selected in trackers table.

PR #22311.
2025-04-12 07:11:41 +03:00
skomerko
2a33e187eb
WebUI: Update sort icon after changing column order
This PR fixes a bug where the sort icon did not update correctly after reordering columns.

Steps to reproduce:
1. Sort a column
2. Move it to a different position
3. The sort icon remains in its original location

PR #22299.
2025-04-12 07:10:59 +03:00
FredBill1
00149e03c0
Migrate socks.py from SocksiPy to PySocks 1.7.1
Migrate `socks.py` from SocksiPy 1.01 to [PySocks 1.7.1](c2fa43cbe1/socks.py), allowing python 3+ compatibility, [details](https://github.com/qbittorrent/qBittorrent/issues/16447#issuecomment-2776894026).

The content of the `socks.py` is entirely copied from the [PySocks repository](c2fa43cbe1/socks.py), the only modification is the license header at the top of the file and trimming trail whitespaces.

Closes #16447.
PR #22507.
2025-04-09 12:57:05 +03:00
Chocobo1
57d529c17a
WebUI: fix preferences not applied in magnet handler
Thanks for the diagnosis in this [post](https://github.com/qbittorrent/qBittorrent/issues/22495#issue-2958553624).

Closes #21486.
Closes #22495.
PR #22504.
2025-04-05 08:58:38 +03:00
Vladimir Golovnev
d492fcf29a
Add option to enable previous Add new torrent dialog behavior
Some people are still unhappy with "standalone window mode" of "Add new torrent dialog" so just provide them with an option to use old "modal dialog mode" in all the current qBittorrent branches.

PR #22492 (based on original PR #19874).
2025-03-31 09:19:03 +03:00
Chocobo1
d0caa35b39
WebUI: fix Tag counter counting wrong
Related: https://github.com/qbittorrent/qBittorrent/pull/22103/files/73e9116d21015542caeb9a3cfd56bfb256ebed9d#r2014898781

PR #22480.
2025-03-29 16:02:47 +03:00
Vladimir Golovnev
ec7a00af92
Restore ability to use server-side translation by custom WebUI
PR #20968.
2025-03-28 09:08:56 +03:00
Vladimir Golovnev
76a3aba7e0
Backport changes to v5.1.x branch
PR #22268.
2025-03-16 10:39:31 +03:00
Chocobo1
7003ac3f4d
WebUI v5.1 fixes
PR #22282.
2025-03-15 14:52:48 +03:00
skomerko
964be0fa1c
WebUI: Maintain row highlight after rearranging table columns
This PR fixes a bug where row highlight effect would be lost after reordering columns.

PR #22339.
2025-03-15 12:47:11 +03:00
skomerko
c1defceccf
WebUI: Fix bug where the 'Tracker editing' dialog displays incorrect data
In Trackers table, moving the 'URL' column from its default (2) position caused the 'Tracker editing' dialog to display incorrect data.
Steps to reproduce:
1. Move 'URL' column in Trackers table to any position from default
2. Choose tracker URL and click 'Edit tracker URL'

PR #22338.
2025-03-15 12:46:17 +03:00
Vladimir Golovnev
260394623d
Add missing includes
PR #22362.
2025-03-05 09:07:47 +03:00
Vladimir Golovnev
478c2d5b12
Don't miss to declare some of the color IDs
PR #22330.
Closes #22326.
2025-02-25 18:57:22 +03:00
Vladimir Golovnev
49cfbd9a49
Improve command line parameters serialization
PR #22319.
Closes #22306.
2025-02-25 09:12:26 +03:00
Luke Memet
d028f46fab
Fix shift-click selection on macOS
PR #22284.
Closes #16818.
2025-02-19 13:53:49 +03:00
Daniel Nylander
57b24a200e
NSIS: Update Swedish translation
PR #22046.
2025-02-19 13:49:25 +03:00
Chocobo1
269dfe87e0
GHA CI: fix AppImage building
Upstream now defaults to static runtime and the previous URL is invalid now.
Upstream commits:
* c28054bab6
* ce5291e259

Also fuse2 is not needed now as stated on:
https://github.com/AppImage/type2-runtime?tab=readme-ov-file#type2-runtime-

PR #22286.
2025-02-16 08:22:26 +03:00
Vladimir Golovnev
6a1c465d85
WebAPI: Don't trim string parameters
PR #22266.
Closes #19485.
Closes #22254.
2025-02-12 09:35:29 +03:00
sledgehammer999
bc7d5c1f8f
Bump to 5.1.0rc1 2025-02-11 02:01:34 +02:00
sledgehammer999
8aabef423c
Create new resources for this branch for Transifex 2025-02-11 01:59:07 +02:00
158 changed files with 46244 additions and 66358 deletions

View file

@ -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

View file

@ -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

View file

@ -1,4 +1,95 @@
Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.1.0 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)

2
dist/mac/Info.plist vendored
View file

@ -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>

View file

@ -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

View file

@ -62,6 +62,6 @@
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url> <url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>
<releases> <releases>
<release version="5.1.0~beta1" date="2024-12-16"/> <release version="5.1.0" date="2025-04-27"/>
</releases> </releases>
</component> </component>

View file

@ -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}"

View file

@ -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:"

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2015-2025 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez * 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 &params) QString serializeParams(const QBtCommandLineParameters &params)
{ {
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;

View file

@ -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)

View file

@ -1,6 +1,6 @@
/* /*
* Bittorrent Client using Qt and libtorrent. * Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2021-2023 Vladimir Golovnev <glassez@yandex.ru> * Copyright (C) 2021-2025 Vladimir Golovnev <glassez@yandex.ru>
* *
* This program is free software; you can redistribute it and/or * This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License * modify it under the terms of the GNU General Public License
@ -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}

View file

@ -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;

View file

@ -974,23 +974,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);
@ -3247,6 +3249,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 +3281,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)
{ {

View file

@ -1615,18 +1615,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;

View file

@ -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"

View file

@ -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())

View file

@ -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);

View file

@ -32,7 +32,7 @@
#define QBT_VERSION_MINOR 1 #define QBT_VERSION_MINOR 1
#define QBT_VERSION_BUGFIX 0 #define QBT_VERSION_BUGFIX 0
#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)

View file

@ -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
) )

View file

@ -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:"));

View file

@ -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);

View file

@ -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;

View file

@ -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());

View file

@ -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)

View file

@ -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 &params, const AddTorrentOption option) bool GUIAddTorrentManager::addTorrent(const QString &source, const BitTorrent::AddTorrentParams &params, 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;

View file

@ -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 &params = {}, AddTorrentOption option = AddTorrentOption::Default); bool addTorrent(const QString &source, const BitTorrent::AddTorrentParams &params = {}, AddTorrentOption option = AddTorrentOption::Default);

View 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);
}

View 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;
};

View file

@ -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);

View file

@ -80,7 +80,33 @@ inline QHash<QString, UIThemeColor> defaultUIThemeColors()
{u"TransferList.StoppedUploading"_s, {Color::Primer::Light::doneFg, Color::Primer::Dark::doneFg}}, {u"TransferList.StoppedUploading"_s, {Color::Primer::Light::doneFg, Color::Primer::Dark::doneFg}},
{u"TransferList.Moving"_s, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}}, {u"TransferList.Moving"_s, {Color::Primer::Light::successFg, Color::Primer::Dark::successFg}},
{u"TransferList.MissingFiles"_s, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}}, {u"TransferList.MissingFiles"_s, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}},
{u"TransferList.Error"_s, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}} {u"TransferList.Error"_s, {Color::Primer::Light::dangerFg, Color::Primer::Dark::dangerFg}},
{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, {{}, {}}}
}; };
} }

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

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

View file

@ -35,7 +35,7 @@ BEGIN
VALUE "FileDescription", "qBittorrent - A Bittorrent Client" VALUE "FileDescription", "qBittorrent - A Bittorrent Client"
VALUE "FileVersion", VER_FILEVERSION_STR VALUE "FileVersion", VER_FILEVERSION_STR
VALUE "InternalName", "qbittorrent" VALUE "InternalName", "qbittorrent"
VALUE "LegalCopyright", "Copyright ©2006-2024 The qBittorrent Project" VALUE "LegalCopyright", "Copyright ©2006-2025 The qBittorrent Project"
VALUE "OriginalFilename", "qbittorrent.exe" VALUE "OriginalFilename", "qbittorrent.exe"
VALUE "ProductName", "qBittorrent" VALUE "ProductName", "qBittorrent"
VALUE "ProductVersion", VER_PRODUCTVERSION_STR VALUE "ProductVersion", VER_PRODUCTVERSION_STR

File diff suppressed because it is too large Load diff

View file

@ -49,7 +49,7 @@ void RSSController::addFolderAction()
{ {
requireParams({u"path"_s}); requireParams({u"path"_s});
const QString path = params()[u"path"_s].trimmed(); const QString path = params()[u"path"_s];
const nonstd::expected<void, QString> result = RSS::Session::instance()->addFolder(path); const nonstd::expected<void, QString> result = RSS::Session::instance()->addFolder(path);
if (!result) if (!result)
throw APIError(APIErrorType::Conflict, result.error()); throw APIError(APIErrorType::Conflict, result.error());
@ -59,8 +59,8 @@ void RSSController::addFeedAction()
{ {
requireParams({u"url"_s, u"path"_s}); requireParams({u"url"_s, u"path"_s});
const QString url = params()[u"url"_s].trimmed(); const QString url = params()[u"url"_s];
const QString path = params()[u"path"_s].trimmed(); const QString path = params()[u"path"_s];
const nonstd::expected<void, QString> result = RSS::Session::instance()->addFeed(url, (path.isEmpty() ? url : path)); const nonstd::expected<void, QString> result = RSS::Session::instance()->addFeed(url, (path.isEmpty() ? url : path));
if (!result) if (!result)
throw APIError(APIErrorType::Conflict, result.error()); throw APIError(APIErrorType::Conflict, result.error());
@ -70,8 +70,8 @@ void RSSController::setFeedURLAction()
{ {
requireParams({u"path"_s, u"url"_s}); requireParams({u"path"_s, u"url"_s});
const QString path = params()[u"path"_s].trimmed(); const QString path = params()[u"path"_s];
const QString url = params()[u"url"_s].trimmed(); const QString url = params()[u"url"_s];
const nonstd::expected<void, QString> result = RSS::Session::instance()->setFeedURL(path, url); const nonstd::expected<void, QString> result = RSS::Session::instance()->setFeedURL(path, url);
if (!result) if (!result)
throw APIError(APIErrorType::Conflict, result.error()); throw APIError(APIErrorType::Conflict, result.error());
@ -81,7 +81,7 @@ void RSSController::removeItemAction()
{ {
requireParams({u"path"_s}); requireParams({u"path"_s});
const QString path = params()[u"path"_s].trimmed(); const QString path = params()[u"path"_s];
const nonstd::expected<void, QString> result = RSS::Session::instance()->removeItem(path); const nonstd::expected<void, QString> result = RSS::Session::instance()->removeItem(path);
if (!result) if (!result)
throw APIError(APIErrorType::Conflict, result.error()); throw APIError(APIErrorType::Conflict, result.error());
@ -91,8 +91,8 @@ void RSSController::moveItemAction()
{ {
requireParams({u"itemPath"_s, u"destPath"_s}); requireParams({u"itemPath"_s, u"destPath"_s});
const QString itemPath = params()[u"itemPath"_s].trimmed(); const QString itemPath = params()[u"itemPath"_s];
const QString destPath = params()[u"destPath"_s].trimmed(); const QString destPath = params()[u"destPath"_s];
const nonstd::expected<void, QString> result = RSS::Session::instance()->moveItem(itemPath, destPath); const nonstd::expected<void, QString> result = RSS::Session::instance()->moveItem(itemPath, destPath);
if (!result) if (!result)
throw APIError(APIErrorType::Conflict, result.error()); throw APIError(APIErrorType::Conflict, result.error());
@ -146,8 +146,8 @@ void RSSController::setRuleAction()
{ {
requireParams({u"ruleName"_s, u"ruleDef"_s}); requireParams({u"ruleName"_s, u"ruleDef"_s});
const QString ruleName {params()[u"ruleName"_s].trimmed()}; const QString ruleName {params()[u"ruleName"_s]};
const QByteArray ruleDef {params()[u"ruleDef"_s].trimmed().toUtf8()}; const QByteArray ruleDef {params()[u"ruleDef"_s].toUtf8()};
const auto jsonObj = QJsonDocument::fromJson(ruleDef).object(); const auto jsonObj = QJsonDocument::fromJson(ruleDef).object();
RSS::AutoDownloader::instance()->setRule(RSS::AutoDownloadRule::fromJsonObject(jsonObj, ruleName)); RSS::AutoDownloader::instance()->setRule(RSS::AutoDownloadRule::fromJsonObject(jsonObj, ruleName));
@ -157,8 +157,8 @@ void RSSController::renameRuleAction()
{ {
requireParams({u"ruleName"_s, u"newRuleName"_s}); requireParams({u"ruleName"_s, u"newRuleName"_s});
const QString ruleName {params()[u"ruleName"_s].trimmed()}; const QString ruleName {params()[u"ruleName"_s]};
const QString newRuleName {params()[u"newRuleName"_s].trimmed()}; const QString newRuleName {params()[u"newRuleName"_s]};
RSS::AutoDownloader::instance()->renameRule(ruleName, newRuleName); RSS::AutoDownloader::instance()->renameRule(ruleName, newRuleName);
} }
@ -167,7 +167,7 @@ void RSSController::removeRuleAction()
{ {
requireParams({u"ruleName"_s}); requireParams({u"ruleName"_s});
const QString ruleName {params()[u"ruleName"_s].trimmed()}; const QString ruleName {params()[u"ruleName"_s]};
RSS::AutoDownloader::instance()->removeRule(ruleName); RSS::AutoDownloader::instance()->removeRule(ruleName);
} }

View file

@ -537,15 +537,12 @@ void WebApplication::sendFile(const Path &path)
const QDateTime lastModified = Utils::Fs::lastModified(path); const QDateTime lastModified = Utils::Fs::lastModified(path);
// find translated file in cache // find translated file in cache
if (!m_isAltUIUsed) if (const auto it = m_translatedFiles.constFind(path);
(it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified))
{ {
if (const auto it = m_translatedFiles.constFind(path); print(it->data, it->mimeType);
(it != m_translatedFiles.constEnd()) && (lastModified <= it->lastModified)) setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(it->mimeType)});
{ return;
print(it->data, it->mimeType);
setHeader({Http::HEADER_CACHE_CONTROL, getCachingInterval(it->mimeType)});
return;
}
} }
const auto readResult = Utils::IO::readFile(path, MAX_ALLOWED_FILESIZE); const auto readResult = Utils::IO::readFile(path, MAX_ALLOWED_FILESIZE);
@ -576,7 +573,7 @@ void WebApplication::sendFile(const Path &path)
QByteArray data = readResult.value(); QByteArray data = readResult.value();
const QMimeType mimeType = QMimeDatabase().mimeTypeForFileNameAndData(path.data(), data); const QMimeType mimeType = QMimeDatabase().mimeTypeForFileNameAndData(path.data(), data);
const bool isTranslatable = !m_isAltUIUsed && mimeType.inherits(u"text/plain"_s); const bool isTranslatable = mimeType.inherits(u"text/plain"_s);
if (isTranslatable) if (isTranslatable)
{ {

View file

@ -100,6 +100,7 @@ ol {
.dynamicTableDiv, .dynamicTableDiv,
.mochaContentWrapper, .mochaContentWrapper,
.panel, .panel,
.scrollableMenu,
#rssDetailsView { #rssDetailsView {
scrollbar-width: thin; scrollbar-width: thin;
} }

View file

@ -21,7 +21,7 @@
} = window.MUI.Windows.instances["multiRenamePage"]; } = window.MUI.Windows.instances["multiRenamePage"];
const bulkRenameFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const bulkRenameFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: "#bulkRenameFilesTableDiv tr", targets: "#bulkRenameFilesTableDiv tbody tr",
menu: "multiRenameFilesMenu", menu: "multiRenameFilesMenu",
actions: { actions: {
ToggleSelection: (element, ref) => { ToggleSelection: (element, ref) => {

View file

@ -381,8 +381,10 @@ window.addEventListener("DOMContentLoaded", () => {
return false; return false;
let removed = false; let removed = false;
for (const data of categoryMap.values()) for (const data of categoryMap.values()) {
removed ||= data.torrents.delete(hash); const deleteResult = data.torrents.delete(hash);
removed ||= deleteResult;
}
return removed; return removed;
}; };
@ -418,8 +420,10 @@ window.addEventListener("DOMContentLoaded", () => {
return false; return false;
let removed = false; let removed = false;
for (const torrents of tagMap.values()) for (const torrents of tagMap.values()) {
removed ||= torrents.delete(hash); const deleteResult = torrents.delete(hash);
removed ||= deleteResult;
}
return removed; return removed;
}; };
@ -477,6 +481,8 @@ window.addEventListener("DOMContentLoaded", () => {
updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]"); updateFilter("checking", "QBT_TR(Checking (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]"); updateFilter("moving", "QBT_TR(Moving (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]"); updateFilter("errored", "QBT_TR(Errored (%1))QBT_TR[CONTEXT=StatusFilterWidget]");
if (useAutoHideZeroStatusFilters && document.getElementById(`${selectedStatus}_filter`).classList.contains("invisible"))
setStatusFilter("all");
}; };
const highlightSelectedStatus = () => { const highlightSelectedStatus = () => {
@ -1509,7 +1515,9 @@ window.addEventListener("DOMContentLoaded", () => {
}, },
column: "mainColumn", column: "mainColumn",
onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => { onResize: window.qBittorrent.Misc.createDebounceHandler(500, (e) => {
saveColumnSizes(); const isHidden = (parseInt(document.getElementById("propertiesPanel").style.height, 10) === 0);
if (!isHidden)
saveColumnSizes();
}), }),
height: null height: null
}); });

View file

@ -131,7 +131,11 @@ window.qBittorrent.Download ??= (() => {
} }
}; };
$(window).addEventListener("load", () => { $(window).addEventListener("load", async () => {
// user might load this page directly (via browser magnet handler)
// so wait for crucial initialization to complete
await window.parent.qBittorrent.Client.initializeCaches();
getPreferences(); getPreferences();
getCategories(); getCategories();
}); });

View file

@ -89,7 +89,6 @@ window.qBittorrent.DynamicTable ??= (() => {
this.setupCommonEvents(); this.setupCommonEvents();
this.setupHeaderEvents(); this.setupHeaderEvents();
this.setupHeaderMenu(); this.setupHeaderMenu();
this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === "1"));
this.setupAltRow(); this.setupAltRow();
}, },
@ -294,6 +293,7 @@ window.qBittorrent.DynamicTable ??= (() => {
this.updateTableHeaders(); this.updateTableHeaders();
this.tableBody.replaceChildren(); this.tableBody.replaceChildren();
this.updateTable(true); this.updateTable(true);
this.reselectRows(this.selectedRowsIds());
} }
if (this.currentHeaderAction === "drag") { if (this.currentHeaderAction === "drag") {
resetElementBorderStyle(el); resetElementBorderStyle(el);
@ -600,19 +600,21 @@ window.qBittorrent.DynamicTable ??= (() => {
updateTableHeaders: function() { updateTableHeaders: function() {
this.updateHeader(this.hiddenTableHeader); this.updateHeader(this.hiddenTableHeader);
this.updateHeader(this.fixedTableHeader); this.updateHeader(this.fixedTableHeader);
this.setSortedColumnIcon(this.sortedColumn, null, (this.reverseSort === "1"));
}, },
updateHeader: function(header) { updateHeader: function(header) {
const ths = this.getRowCells(header); const ths = this.getRowCells(header);
for (let i = 0; i < ths.length; ++i) { for (let i = 0; i < ths.length; ++i) {
const th = ths[i]; const th = ths[i];
th._this = this; if (th.columnName !== this.columns[i].name) {
th.title = this.columns[i].caption; th.title = this.columns[i].caption;
th.textContent = this.columns[i].caption; th.textContent = this.columns[i].caption;
th.style.cssText = `width: ${this.columns[i].width}px; ${this.columns[i].style}`; th.style.cssText = `width: ${this.columns[i].width}px; ${this.columns[i].style}`;
th.columnName = this.columns[i].name; th.columnName = this.columns[i].name;
th.classList.add(`column_${th.columnName}`); th.className = `column_${th.columnName}`;
th.classList.toggle("invisible", ((this.columns[i].visible === "0") || this.columns[i].force_hide)); th.classList.toggle("invisible", ((this.columns[i].visible === "0") || this.columns[i].force_hide));
}
} }
}, },
@ -750,10 +752,7 @@ window.qBittorrent.DynamicTable ??= (() => {
reselectRows: function(rowIds) { reselectRows: function(rowIds) {
this.deselectAll(); this.deselectAll();
this.selectedRows = rowIds.slice(); this.selectedRows = rowIds.slice();
for (const tr of this.getTrs()) { this.setRowClass();
if (rowIds.includes(tr.rowId))
tr.classList.add("selected");
}
}, },
setRowClass: function() { setRowClass: function() {
@ -1752,7 +1751,7 @@ window.qBittorrent.DynamicTable ??= (() => {
td.append(span); td.append(span);
} }
span.style.backgroundImage = `url('images/flags/${country_code ?? "xx"}.svg')`; span.style.backgroundImage = `url('images/flags/${country_code || "xx"}.svg')`;
span.textContent = country; span.textContent = country;
td.title = country; td.title = country;
}; };
@ -2058,7 +2057,11 @@ window.qBittorrent.DynamicTable ??= (() => {
break; break;
} }
td.className = statusClass; for (const c of [...td.classList]) {
if (c.startsWith("tracker"))
td.classList.remove(c);
}
td.classList.add(statusClass);
td.textContent = status; td.textContent = status;
td.title = status; td.title = status;
}; };

View file

@ -575,7 +575,7 @@ window.qBittorrent.PropFiles ??= (() => {
}; };
const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const torrentFilesContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: "#torrentFilesTableDiv tr", targets: "#torrentFilesTableDiv tbody tr",
menu: "torrentFilesMenu", menu: "torrentFilesMenu",
actions: { actions: {
Rename: (element, ref) => { Rename: (element, ref) => {

View file

@ -138,8 +138,6 @@ window.qBittorrent.PropTrackers ??= (() => {
addTrackerFN(); addTrackerFN();
}, },
EditTracker: (element, ref) => { EditTracker: (element, ref) => {
// only allow editing of one row
element.firstElementChild.click();
editTrackerFN(element); editTrackerFN(element);
}, },
RemoveTracker: (element, ref) => { RemoveTracker: (element, ref) => {
@ -162,7 +160,11 @@ window.qBittorrent.PropTrackers ??= (() => {
this.hideItem("CopyTrackerUrl"); this.hideItem("CopyTrackerUrl");
} }
else { else {
this.showItem("EditTracker"); if (selectedTrackers.length === 1)
this.showItem("EditTracker");
else
this.hideItem("EditTracker");
this.showItem("RemoveTracker"); this.showItem("RemoveTracker");
this.showItem("CopyTrackerUrl"); this.showItem("CopyTrackerUrl");
} }
@ -196,7 +198,7 @@ window.qBittorrent.PropTrackers ??= (() => {
if (current_hash.length === 0) if (current_hash.length === 0)
return; return;
const trackerUrl = encodeURIComponent(element.childNodes[1].textContent); const trackerUrl = encodeURIComponent(torrentTrackersTable.selectedRowsIds()[0]);
new MochaUI.Window({ new MochaUI.Window({
id: "trackersPage", id: "trackersPage",
icon: "images/qbittorrent-tray.svg", icon: "images/qbittorrent-tray.svg",

View file

@ -97,8 +97,8 @@ window.qBittorrent.Search ??= (() => {
} }
}, },
offsets: { offsets: {
x: -15, x: 2,
y: -53 y: -60
}, },
onShow: function() { onShow: function() {
setActiveTab(this.options.element); setActiveTab(this.options.element);
@ -109,7 +109,7 @@ window.qBittorrent.Search ??= (() => {
// load "Search in" preference from local storage // load "Search in" preference from local storage
$("searchInTorrentName").value = (LocalPreferences.get("search_in_filter") === "names") ? "names" : "everywhere"; $("searchInTorrentName").value = (LocalPreferences.get("search_in_filter") === "names") ? "names" : "everywhere";
const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({ const searchResultsTableContextMenu = new window.qBittorrent.ContextMenu.ContextMenu({
targets: "#searchResultsTableDiv tr", targets: "#searchResultsTableDiv tbody tr",
menu: "searchResultsTableMenu", menu: "searchResultsTableMenu",
actions: { actions: {
Download: downloadSearchTorrent, Download: downloadSearchTorrent,
@ -184,7 +184,10 @@ window.qBittorrent.Search ??= (() => {
closeTabElem.src = "images/application-exit.svg"; closeTabElem.src = "images/application-exit.svg";
closeTabElem.width = "10"; closeTabElem.width = "10";
closeTabElem.height = "10"; closeTabElem.height = "10";
closeTabElem.addEventListener("click", function(e) { qBittorrent.Search.closeSearchTab(this); }); closeTabElem.addEventListener("click", function(e) {
e.stopPropagation();
closeSearchTab(this);
});
tabElem.prepend(closeTabElem); tabElem.prepend(closeTabElem);
tabElem.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg")); tabElem.appendChild(getStatusIconElement("QBT_TR(Searching...)QBT_TR[CONTEXT=SearchJobWidget]", "images/queued.svg"));

View file

@ -5,7 +5,7 @@
<h3 id="qbittorrentVersion">qBittorrent</h3> <h3 id="qbittorrentVersion">qBittorrent</h3>
</div> </div>
<p>QBT_TR(An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.)QBT_TR[CONTEXT=AboutDialog]</p> <p>QBT_TR(An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.)QBT_TR[CONTEXT=AboutDialog]</p>
<p>Copyright © 2006-2024 The qBittorrent project</p> <p>Copyright © 2006-2025 The qBittorrent project</p>
<table> <table>
<tbody> <tbody>
<tr> <tr>

View file

@ -98,7 +98,7 @@
} }
}, },
offsets: { offsets: {
x: -15, x: 0,
y: 2 y: 2
}, },
onShow: function() { onShow: function() {

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