Compare commits

...

141 Commits

Author SHA1 Message Date
sledgehammer999
cce295faeb Bump to 5.0.0 2024-09-29 20:53:45 +03:00
sledgehammer999
db5479ea01 Update Changelog 2024-09-29 20:49:43 +03:00
sledgehammer999
e1216c4c9a Sync translations from Transifex and run lupdate 2024-09-29 20:36:58 +03:00
sledgehammer999
f4a0868426 Make Program Updater choose the same build for download
We're probably stuck offering the duo of RC_1_2 and RC_2_0 for some
time in the future. So hardcode the choices and make the Program Updater
choose the variant the user currently uses.
2024-09-29 20:28:10 +03:00
sledgehammer999
59a5fcf7d0 Sync translations from Transifex and run lupdate 2024-09-13 11:10:38 +03:00
Vladimir Golovnev
f9a2b02a8d Backport changes to v5.0.x branch
PR #21241.
2024-09-12 08:42:52 +03:00
skomerko
04f6a565f3 WebUI: Provide 'Merge trackers to existing torrent' option
PR #21302.
2024-09-11 19:18:17 +03:00
Vladimir Golovnev
3e96048ee4 Apply "merge trackers" logic regardless of way the torrent is added
PR #21299.
2024-09-07 09:13:19 +04:00
Prince Gupta
d4ccf3001c Fix highlighted piece color
PR #20971.
2024-09-02 16:17:57 +03:00
skomerko
64506f16bd WebUI: Provide 'Use Category paths in Manual Mode' option
PR #21223.
2024-08-26 13:10:05 +03:00
sledgehammer999
24a7a835af Create new resources for this branch for Transifex 2024-08-26 01:11:05 +03:00
sledgehammer999
93b9bf9552 Sync translations from Transifex and run lupdate 2024-08-26 01:05:21 +03:00
Vladimir Golovnev
f4125601de Refresh search results colors once color scheme is changed
* Refresh search results colors once color scheme is changed
* Improve color of visited search result items

PR #21189.
Closes #21187.
2024-08-21 15:12:07 +03:00
sledgehammer999
2d67729617 Bump to v5.0.0rc1 2024-08-18 23:21:21 +03:00
sledgehammer999
878ebbed41 Update Changelog 2024-08-18 23:17:25 +03:00
Vladimir Golovnev
c61c3d7cd8 Backport changes to v5.0.x branch
PR #21164.
2024-08-16 07:17:21 +03:00
skomerko
978fbbdc0d WebUI: Always create generic filter items
PR #21188.
2024-08-15 20:37:19 +03:00
stalkerok
63689cf763 Add a flag about the connection peers are using NAT hole punching
PR #21052.
2024-08-15 20:33:45 +03:00
thalieht
cebc72d3cf WebUI: Add missing columns in transfer list
* Incomplete Save Path
* Info Hash v1
* Info Hash v2

PR #21158.
2024-08-15 20:32:40 +03:00
Vladimir Golovnev
a67bd271c6 Refresh pieces bar colors once color scheme is changed
PR #21183.
Closes #21155.
2024-08-13 09:37:47 +03:00
skomerko
a8cffbb205 WebUI: Clear trackerList on full update
Like other similar data structures, trackerList also need to be cleared in the event of a full sync update.

PR #21148.
2024-08-11 14:20:45 +03:00
Vladimir Golovnev
7dfb0110d4 Fix Incomplete Save Path cannot be changed for torrents without metadata
PR #21152.
Closes #21140.
2024-08-08 08:22:54 +03:00
Vladimir Golovnev
3ad8fcbdd2 Hide zero status filters when torrents removed
PR #21150.
Closes #21146.
2024-08-08 08:22:51 +03:00
Vladimir Golovnev
195eae5f3d Backport changes to v5.0.x branch
PR #20996.
2024-08-02 21:22:49 +03:00
Hanabishi
920ae26f7b WebUI: Fix Torrent Management Mode selector
PR #21053.
2024-07-15 17:40:17 +03:00
David Newhall
09ed0d6b66 WebAPI: Add root_path to torrent/info result
PR #21066.
Closes #21057.
2024-07-15 08:52:52 +03:00
Vladimir Golovnev
4f0cc8aa11 Fix incorrect sorting by "private" column
PR #21041.
2024-07-15 08:52:42 +03:00
ManiMatter
4d490c84e7 Add ability to display torrent "privateness" in UI
PR #20951.

---------

Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
Co-authored-by: Vladimir Golovnev <glassez@yandex.ru>
Co-authored-by: thalieht <thalieht@users.noreply.github.com>
2024-07-15 08:52:23 +03:00
Vladimir Golovnev
96607ce874 Prevent incorrect size from being used for creating array
PR #21050.
2024-07-12 08:51:08 +03:00
Vladimir Golovnev
418edc7471 Apply bulk changes to correct content widget items
PR #21006.
Closes #21001.
2024-07-08 16:51:33 +03:00
Vladimir Golovnev
bd01b7c4df WebUI: Correctly apply changed "save path" of RSS rules
PR #21030.
Closes #20141.
2024-07-08 10:18:02 +03:00
Vladimir Golovnev
b0ac763048 Show scroll bar in Torrent Tags dialog
PR #21026.
Closes #21022.
2024-07-07 16:10:07 +03:00
Vladimir Golovnev
127d2d6f0b Fix handling of tags containing '&' character
PR #21024.
Closes #20773.
2024-07-07 16:10:05 +03:00
Vladimir Golovnev
4149609e78 Allow to move content files to Trash instead of deleting them
PR #20252.
2024-07-07 16:09:48 +03:00
Vladimir Golovnev
78c549f83e Use custom storage when reloading torrent
PR #20998.
2024-07-07 16:07:22 +03:00
Thomas Piccirello
a3a53e2e0e WebUI: Fix preference name conflict
PR #20990.
2024-07-07 16:06:55 +03:00
Vladimir Golovnev
5aaa43e01d Restore ability to use server-side translation by custom WebUI
PR #20968.
2024-06-29 21:59:22 +03:00
Chocobo1
86745d7b07 GHA CI: use static versions of AppImage builder
It does not affect the produced artifacts. The only difference is the
tool itself won't depend on some specific OS image or library version.

PR #20983.
2024-06-25 21:13:20 +03:00
Thomas Piccirello
210650a5ee Use enabled search plugins by default in WebUI
PR #20969.
Closes #20558.
2024-06-25 21:13:20 +03:00
Chocobo1
fe93b6d0d8 Use proper casting
Previously `m_shutdownTimeout * 1000` was calculated in `int` and now it
is `qint64`.

PR #20982.
2024-06-25 21:13:19 +03:00
Chocobo1
e8b585acd8 Allow numeric types
The canonical type for `size_string` is `str`. However numeric types are also accepted in order
to accommodate poorly written plugins.

PR #20976.
2024-06-25 21:13:19 +03:00
vikas_c
cea20141a9 Show download progress for folders with zero byte size as 100 instead of 0
Fixes the download progress calculation for folders with zero size.
Previously, the progress would be Zero. Now, folders with zero size
show 100% progress.

PR #20567.
2024-06-25 21:13:19 +03:00
Chocobo1
0f5a27ed50 Improve connection handling
1. Previously unhandled connections will stay in pending state. It won't
be closed until timeout happened. This may lead to wasting system
resources. Now the (over-limit) connection is actively rejected.

2. When out-of-memory occurs here, reject the new connection instead of
throwing exception and crash.

3. Also clean up some unused bits.

PR #20961.
2024-06-25 21:13:18 +03:00
Vladimir Golovnev
c2cf898ccd Allow to use regular expression to filter torrent content
PR #20944.
Closes #19934.
2024-06-25 21:13:18 +03:00
Chocobo1
5e5aa8a563 Add type annotations
A few code are revised because the type checker (mypy) doesn't allow
changing types on a variable.

PR #20935.
2024-06-25 21:13:18 +03:00
ManiMatter
12a4c3fda2 WebAPI: Add "private" filter for 'info' endpoint
PR #20833.

---------

Co-authored-by: Vladimir Golovnev <glassez@yandex.ru>
Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
2024-06-25 21:13:17 +03:00
Vladimir Golovnev
5f50b701d2 Don't use custom "file icon provider" on Windows
PR #20936.
Closes #20908.
2024-06-25 21:13:17 +03:00
Chocobo1
9f20d9c3aa Revise Protocol column
Add "BT" (BitTorrent) to avoid confusion about which protocol it is referring to.
Also its value doesn't need to be translated.

PR #20897.
2024-06-25 21:13:17 +03:00
Vladimir Golovnev
05e3130baa Apply share limits when torrent downloading is finished
PR #20917.
Closes #20874.
2024-06-25 21:13:17 +03:00
Vladimir Golovnev
683492648f Apply filename filter to subfolder names as well
PR #20902.
Closes #14480.
2024-06-25 21:13:17 +03:00
Chocobo1
2f2e158877 WebUI: unify comment format 2024-06-25 21:13:16 +03:00
BurningMop
e60e96cb0e Add optional headers to search request
PR #20923.
2024-06-25 21:13:16 +03:00
Chocobo1
5f31208bf1 Add required manifest field
https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests#assemblyidentity

PR #20907.
2024-06-25 21:13:16 +03:00
Chocobo1
fa58e58e70 WebUI: unify curly bracket usage 2024-06-25 21:13:16 +03:00
dependabot[bot]
671943a9a6 GHA CI: Bump Github Actions versions
PR #20913.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Chocobo1 <Chocobo1@users.noreply.github.com>
2024-06-25 21:13:16 +03:00
Chocobo1
8bad80bcdd Avoid redundant lookup
PR #20890.
2024-06-25 21:13:15 +03:00
thalieht
c44e300507 Increase default height of 'Share ratio limit' dialog in WebUI
PR #20866.
2024-06-25 21:13:15 +03:00
Chocobo1
318a677e8f Avoid creating redundant temporary file list
PR #20863.
2024-06-25 21:13:15 +03:00
Chocobo1
0246df790a Use Qt built-in methods 2024-06-25 21:13:15 +03:00
Chocobo1
782fbc1425 Use simpler conversion
The cookie value can only contain ASCII characters.
2024-06-25 21:13:15 +03:00
Chocobo1
7deccd5592 WebUI: add missing break 2024-06-25 21:13:14 +03:00
Chocobo1
4a36fe7278 WebUI: don't auto infer radix parameter 2024-06-25 21:13:14 +03:00
Chocobo1
1c5af96ad8 WebUI: simplify code 2024-06-25 21:13:14 +03:00
Chocobo1
3bb47a5410 WebUI: iterate over own properties only 2024-06-25 21:13:14 +03:00
Chocobo1
d7abeb4bf0 WebUI: use assignment operator shorthand 2024-06-25 21:13:14 +03:00
Chocobo1
a19d623ead WebUI: prefer arrow function in callbacks 2024-06-25 21:13:13 +03:00
Chocobo1
1ef21bc2b7 WebUI: enforce usage of const whenever possible 2024-06-25 21:13:13 +03:00
Chocobo1
4687b4e8e4 WebUI: enforce string quotes coding style 2024-06-25 19:33:20 +03:00
Thomas Piccirello
d2e5163861 WebUI: Restore previously used tab on load
This PR restores the users previously used tab (Transfer, Search, RSS, etc.) when the WebUI is reloaded.

PR #20705.
2024-06-25 19:30:10 +03:00
sledgehammer999
8a15ea8026 Merge pull request #20963 from sledgehammer999/revert_webui_i18n
Revert i18next
2024-06-25 03:02:24 +03:00
sledgehammer999
2b99554813 Update WebUI translation files 2024-06-17 02:07:15 +03:00
sledgehammer999
e6638f9c19 Revert "Use client side translation for public login page"
This reverts commit ac91c1348b.
2024-06-16 23:31:19 +03:00
sledgehammer999
ec6eac2ba1 Revert "Avoid leaking user locale preference to the web"
This reverts commit 66c34ddb6e.
2024-06-16 23:14:21 +03:00
Chocobo1
a126a7b493 Adjust user agent version automatically
The version calculation is an estimation and it will drift off after some time. Hopefully the
drift offset won't be noticeable within a few years.

Also switched the user agent to Windows 10 which has the largest portion of users to avoid
standing out from the crowd.

PR #20864.
2024-05-20 13:50:18 +08:00
Vladimir Golovnev
b8a774f1fb Improve structure of AddNewTorrentDialog code
Restructures the code to separate the basic logic from the logic that depends on the parameters and properties of the torrent being added.
Also fixes "Never show again" checkbox functionality.

PR #20848.
2024-05-18 10:38:49 +03:00
Vladimir Golovnev
e09a871ca3 Revamp alerts handling
PR #20854.
2024-05-18 10:36:08 +03:00
Chocobo1
04154ebb76 GHA CI: don't use hardcoded path
PR #20763.
2024-05-17 01:44:43 +08:00
Vladimir Golovnev
fb796ec595 Don't hide member variables when storing current speeds
PR #20847.
Closes #20843.
2024-05-16 08:17:51 +03:00
Vladimir Golovnev
00ca209ab9 Allow to rearrange search tabs
PR #20842.
Closes #20841.
2024-05-16 08:16:54 +03:00
Vladimir Golovnev
4d8713ce11 WebAPI: Add a way to download .torrent file using search plugin
* Simplify nova2dl script
* Use search engine name instead of site URL (like nova2 does)
* Add a way to download torrent using search plugin

PR #20824.
2024-05-15 08:47:40 +03:00
Hanabishi
2c47f09d7a Sanitize peer client names
PR #20788.
Closes #20010.
2024-05-11 18:46:11 +03:00
Chocobo1
a19ef58400 Merge pull request #20800 from Chocobo1/eslint_v9
WebUI: migrate to ESLint v9
2024-05-11 13:56:08 +08:00
cayenne17
21a4ab6bac Update User-Agent
PR #20801.
2024-05-10 21:47:55 +03:00
Vladimir Golovnev
2b728b3bc0 Add an option to set BitTorrent session shutdown timeout
PR #20797.
2024-05-07 13:15:39 +03:00
Chocobo1
6231208ddf WebUI: add linting for regular expressions
And applies to following suggestions:
* Use case-insensitive flag `i`
* Use `\w` for matching characters
* Sort the regex flags
2024-05-04 15:01:03 +08:00
Chocobo1
e2d6cd31b2 WebUI: migrate to ESLint v9 2024-05-04 14:59:45 +08:00
Chocobo1
79eb7b8e38 WebUI: migrate ESLint rules
https://eslint.style/guide/migration

PR #20727.
2024-05-03 21:03:08 +08:00
Vladimir Golovnev
8ef7d3ec9a Add ability to pause/resume entire BitTorrent session
PR #20757.
Closes #18993.
2024-05-03 09:02:50 +03:00
ManiMatter
05416458db WebAPI: Provide "isPrivate" flag via "torrents/info" endpoint
PR #20686.
2024-05-02 13:04:03 +03:00
Vladislav Grechannik
cd3982cf3c Include missing header
PR #20776.
Closes #20774.
2024-05-01 12:37:22 +03:00
Paweł Kotiuk
a1af077889 Add API for listing directory content
PR #20314.
2024-04-29 21:13:32 +03:00
ducalex
42b87963fd Add date column to the built-in search engine
Adds a date column to the built-in search engine to show when a torrent was published/uploaded on the engine site.
When a plugin wants to show a date, it can now add a `pub_date` entry to its result dict. The value format is a unix timestamp (an integer representing seconds since epoch).
Plugins with no date support will keep working.

PR #20703.
2024-04-29 21:10:24 +03:00
Chocobo1
775b38079f Avoid repetitive function calls
PR #20764.
2024-04-29 13:05:05 +08:00
Chocobo1
d65d8558d6 Merge pull request #20728 from Chocobo1/webui_state
WebUI: clean up code
2024-04-29 12:58:14 +08:00
Chocobo1
b1175b60e1 Use proper comparison for None
PR #20762.
2024-04-29 12:47:27 +08:00
luzpaz
d3315f7cc7 WebUI: Fix inconsistent naming between (Done/Progress) column
Closes #20602.
PR #20700.
2024-04-27 14:43:47 +08:00
Chocobo1
321d7e5b17 Adjust tracker tier when adding additional trackers
Closes #20102.
PR #20729.
2024-04-25 12:18:30 +08:00
milahu
4ac586c896 Lazy load search plugins
PR #20553.
2024-04-24 22:15:22 +03:00
Vladimir Golovnev
ca71c186e0 Don't forget to resume "missing files" torrent when rechecking
PR #20747.
2024-04-24 09:15:19 +03:00
xavier2k6
ddb0ff29e2 GHA CI: Use Qt 6.7.0 on Windows and macOS
PR #20431.
2024-04-22 15:21:16 +03:00
xavier2k6
6c57fad0cd GHA CI: Bump Boost version to 1.85.0
PR #20723.
2024-04-22 13:39:58 +08:00
Chocobo1
1c7ecb7371 WebUI: migrate away from MooTools deprecated functions
https://mootools.net/core/docs/1.6.0/Core/Core#Deprecated-Functions
2024-04-21 16:55:30 +08:00
Chocobo1
4945ed576a WebUI: enforce strict comparison operators 2024-04-21 16:44:15 +08:00
xavier2k6
c6f4e95b7d Raise minimum libtorrent 2 version to 2.0.10
PR #20447.
2024-04-21 09:52:51 +03:00
Vladimir Golovnev
fc3953dbaa Don't overwrite stored layout of main window with incorrect one
Prevents overwriting of the stored layout in case the main window was hidden at startup and
has not been shown since, because incorrect dimensions can be provided by it in this case.

PR #20725.
Closes #20720.
2024-04-20 11:10:31 +03:00
Chocobo1
75e2ae2fa0 WebUI: clean up code
Use proper function for finding match.
Use strict comparison operators.
2024-04-19 16:13:27 +08:00
Vladimir Golovnev
7310eec74e Focus on Download button if torrent link retrieved from the clipboard
PR #20716.
Closes #20682.
2024-04-19 09:21:52 +03:00
Vladimir Golovnev
3e0fd01604 Add extra offset for dialog frame
PR #20715.
Closes #20609.
2024-04-18 09:04:06 +03:00
Vladimir Golovnev
ace5286402 Prevent invalid status filter index from being used
PR #20714.
Closes #20701.
2024-04-18 07:59:24 +03:00
Chocobo1
d7cded54e4 WebUI: enforce parentheses around operators
PR #20696.
2024-04-15 12:50:07 +08:00
Chocobo1
6c82d5e305 WebUI: fix wrong peer number
PR #20695.
2024-04-15 12:42:47 +08:00
Chocobo1
c036313adf Update screenshot URL in appstream metadata
Those URL are pointing to our git repo:
723c0df824/src/img/screenshots/linux

PR #20694.
2024-04-15 12:17:40 +08:00
Thomas Piccirello
29f0adf215 WebUI: Restore search tabs on load
This PR restores searches previously performed in the same browser (via local storage).

PR #20637.
2024-04-15 12:07:15 +08:00
Thomas Piccirello
e697d40382 WebUI: Improve table scrolling and selection on mobile
This PR improves touch interaction with table rows that have a context menu. Previously, those rows couldn't be selected or scrolled on mobile. Additionally, this PR modifies the context menu to appear when the user removes their finger/touch, rather than the current behavior of appearing mid-touch. This allows us to only display the context menu if the user's finger remains on the same element, which should significantly reduce erroneous context menu triggering.

Closes #19819.
Closes #19820,
Closes #19823.
PR #20639.
2024-04-09 14:33:10 +08:00
Chocobo1
01cc4ea90b GHA CI: revise packaging failure detection
Fix up 1d221c22e4.
PR #20664.
2024-04-09 14:22:05 +08:00
Chocobo1
d407e954d1 GHA CI: lock to ESLint v8
For unknown reasons, ESLint v9 doesn't work correctly. Migration to ESLint v9 will be done
later when it is stable enough.

PR #20665.
2024-04-09 14:12:30 +08:00
Vladimir Golovnev
f085f8c076 Fix Enter key behavior in Add new torrent dialog
Prevent inappropriate default completer from being used by path edit.

PR #20670.
Closes #20663.
2024-04-08 16:02:26 +03:00
Chocobo1
92ce507151 WebUI: Allow to specify login page language via query parameter
There were a few reports that the user has messed up their browser's language and this PR gives an escape hatch in case the user is unable to configure the browser's language for various reasons.
Example for choosing French: http://127.0.0.1:8080/?lang=fr

PR #20591.
2024-04-06 15:13:58 +08:00
Vladimir Golovnev
67dfce7437 WebAPI: return correct value for queued uploading state
PR #20651.
Closes #20648.
2024-04-04 08:41:25 +03:00
dependabot[bot]
e4aad461c7 GHA CI: Bump Github Actions versions
PR #20652.

---

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-03 13:00:50 +08:00
Aliaksei Urbanski
f37d0c486c Add the Popularity metric
PR #20180.
2024-04-01 21:23:08 +03:00
Thomas Piccirello
8e6515be2c WebUI: Fix error when category doesn't exist
This prevents hitting a TypeError when the category stored in localstorage does not exist. The behavior for a nonexistent category now mirrors that of a nonexistent tag or filter - no option is selected and no torrents are shown.

Closes #20623.
PR #20638.
2024-04-01 20:15:30 +08:00
Chocobo1
1d221c22e4 GHA CI: retry action on failure
PR #20641.
2024-04-01 19:59:26 +08:00
Chocobo1
2fe91a6c8f GHA CI: only store cache for master branch
Also set a lower cache limit for macOS to prevent cache thrashing. Previously the default was 5G.

PR #20640.
2024-04-01 19:36:45 +08:00
Chocobo1
90383567b2 Revise Tracker related classes
And also rename them.

PR #20489.
2024-04-01 19:17:35 +08:00
Thomas Piccirello
4967f977c5 WebUI: Improve accuracy of trackers list
This PR fixes various accounting issues with the trackers list. Removing a torrent would not update the trackers list, nor would removing a tracker from a torrent. And removing a tracker with a shared host but unique url (e.g. example.com/1 and example.com/2) would erroneously remove the tracker's host from the list.

Closes #20053.
Closes #20054.
PR  #20601.
2024-03-29 15:43:49 +08:00
Thomas Piccirello
eb9e98a4b3 WebUI: Add support for running concurrent searches
This PR adds support for running multiple concurrent searches in the Web UI. This is already supported in the GUI as well as by the Web API. Behavior mimics the GUI as closely as possible.

All filters and sorting are preserved per-tab, allowing you to apply unique filters and sorts to each of your searches. Row selection is also preserved across tab navigation.

Closes #12840.
PR #20593.
2024-03-29 15:05:43 +08:00
Vladimir Golovnev
f5cac13979 Prevent app from being closed when disabling system tray icon
PR #20627.
Closes #20604.
2024-03-29 09:38:54 +03:00
Chocobo1
f20467889d Improve AppStream metadata
PR #20606.
2024-03-26 22:06:57 +08:00
Vladimir Golovnev
5e8b9df859 Revamp system tray icon menu handling
Update system tray icon menu without re-create it.

PR #20597.
Closes #20516.
2024-03-26 15:24:43 +03:00
Thomas Piccirello
489bacd766 WebUI: Conditionally show filters sidebar
This fixes a bug where the filters sidebar would always be displayed when switching back to the Transfers tab.

Closes #19257.
PR #20600.
2024-03-26 12:41:47 +08:00
thalieht
5d1c249606 Use Start/Stop instead of Resume/Pause
PR #20532.

---------

Co-authored-by: Vladimir Golovnev (Glassez) <glassez@yandex.ru>
2024-03-25 19:11:04 +03:00
Chocobo1
f2d6129db3 Merge pull request #20590 from Chocobo1/py
GHA CI: check python scripts
2024-03-25 13:42:09 +08:00
HamletDuFromage
5c67c5a77d Add regex toggle for WebUI torrent filtering
PR #20566.
2024-03-24 13:44:57 +08:00
Vladimir Golovnev
ce013f132f Refresh custom colors once color scheme is changed
PR #20588.
2024-03-23 11:32:07 +03:00
Chocobo1
abcf1e076e Remove unused script 2024-03-23 13:55:58 +08:00
Chocobo1
47c38e8d91 Apply formatting 2024-03-23 13:55:58 +08:00
Chocobo1
34d19e5714 GHA CI: check python scripts 2024-03-23 13:54:57 +08:00
Vladimir Golovnev
25b7972f88 Initialize completer for file system path widget on demand
PR #20586.
2024-03-23 08:18:36 +03:00
Vladimir Golovnev
845f9a821e Use better icons for RSS articles (#20587) 2024-03-22 18:46:25 +03:00
Vladimir Golovnev
b489262f51 Add workaround to refresh styled controls once color scheme is changed
PR #20569.
2024-03-21 11:14:41 +03:00
408 changed files with 111083 additions and 96025 deletions

View File

@@ -19,11 +19,10 @@ jobs:
matrix:
libt_version: ["2.0.10", "1.2.19"]
qbt_gui: ["GUI=ON", "GUI=OFF"]
qt_version: ["6.5.2"]
qt_version: ["6.7.0"]
env:
boost_path: "${{ github.workspace }}/../boost"
openssl_root: /usr/local/opt/openssl@3
libtorrent_path: "${{ github.workspace }}/../libtorrent"
steps:
@@ -31,7 +30,7 @@ jobs:
uses: actions/checkout@v4
- name: Install dependencies
uses: Wandalen/wretry.action@v1
uses: Wandalen/wretry.action@v3
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
HOMEBREW_NO_INSTALL_CLEANUP: 1
@@ -47,13 +46,15 @@ jobs:
- name: Setup ccache
uses: Chocobo1/setup-ccache-action@v1
with:
store_cache: ${{ startsWith(github.ref, 'refs/heads/') }}
store_cache: ${{ github.ref == 'refs/heads/master' }}
update_packager_index: false
ccache_options: |
max_size=2G
- name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "84"
BOOST_MINOR_VERSION: "85"
BOOST_PATCH_VERSION: "0"
run: |
boost_url="https://boostorg.jfrog.io/artifactory/main/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz"
@@ -68,7 +69,7 @@ jobs:
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
- name: Install Qt
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
version: ${{ matrix.qt_version }}
archives: qtbase qtdeclarative qtsvg qttools
@@ -92,8 +93,7 @@ jobs:
-DCMAKE_CXX_STANDARD=17 \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \
-Ddeprecated-functions=OFF \
-DOPENSSL_ROOT_DIR="${{ env.openssl_root }}"
-Ddeprecated-functions=OFF
cmake --build build
sudo cmake --install build
@@ -107,7 +107,6 @@ jobs:
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DBOOST_ROOT="${{ env.boost_path }}" \
-DOPENSSL_ROOT_DIR="${{ env.openssl_root }}" \
-DTESTING=ON \
-DVERBOSE_CONFIGURE=ON \
-D${{ matrix.qbt_gui }}
@@ -122,8 +121,18 @@ jobs:
if [ "${{ matrix.qbt_gui }}" = "GUI=OFF" ]; then
appName="qbittorrent-nox"
fi
# package
pushd build
macdeployqt "$appName.app" -dmg -no-strip
PACKAGE_RETRY=0
while [ "$PACKAGE_RETRY" -lt "3" ]; do
macdeployqt "$appName.app" -dmg -no-strip
if [ -f "$appName.dmg" ]; then
break
fi
sleep 5
PACKAGE_RETRY=$((PACKAGE_RETRY + 1))
echo "Retry $PACKAGE_RETRY..."
done
popd
# prepare upload folder
mkdir upload

89
.github/workflows/ci_python.yaml vendored Normal file
View File

@@ -0,0 +1,89 @@
name: CI - Python
on: [pull_request, push]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: ${{ github.head_ref != '' }}
jobs:
ci:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup python (auxiliary scripts)
uses: actions/setup-python@v5
with:
python-version: '3' # use default version
- name: Install tools (auxiliary scripts)
run: pip install bandit pycodestyle pyflakes
- name: Gather files (auxiliary scripts)
run: |
export "PY_FILES=$(find . -type f -name '*.py' ! -path '*searchengine*' -printf '%p ')"
echo $PY_FILES
echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV"
- name: Lint code (auxiliary scripts)
run: |
pyflakes $PY_FILES
bandit --skip B314,B405 $PY_FILES
- name: Format code (auxiliary scripts)
run: |
pycodestyle \
--max-line-length=1000 \
--statistics \
$PY_FILES
- name: Build code (auxiliary scripts)
run: |
python -m compileall $PY_FILES
- name: Setup python (search engine)
uses: actions/setup-python@v5
with:
python-version: '3.7'
- name: Install tools (search engine)
run: pip install bandit mypy pycodestyle pyflakes pyright
- name: Gather files (search engine)
run: |
export "PY_FILES=$(find . -type f -name '*.py' -path '*searchengine*' ! -name 'socks.py' -printf '%p ')"
echo $PY_FILES
echo "PY_FILES=$PY_FILES" >> "$GITHUB_ENV"
- name: Check typings (search engine)
run: |
MYPYPATH="src/searchengine/nova3" \
mypy \
--follow-imports skip \
--strict \
$PY_FILES
pyright \
$PY_FILES
- name: Lint code (search engine)
run: |
pyflakes $PY_FILES
bandit --skip B110,B310,B314,B405 $PY_FILES
- name: Format code (search engine)
run: |
pycodestyle \
--ignore=E265,E402 \
--max-line-length=1000 \
--statistics \
$PY_FILES
- name: Build code (search engine)
run: |
python -m compileall $PY_FILES

View File

@@ -41,7 +41,7 @@ jobs:
- name: Setup ccache
uses: Chocobo1/setup-ccache-action@v1
with:
store_cache: ${{ startsWith(github.ref, 'refs/heads/') }}
store_cache: ${{ github.ref == 'refs/heads/master' }}
update_packager_index: false
ccache_options: |
max_size=2G
@@ -64,7 +64,7 @@ jobs:
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
- name: Install Qt
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
version: ${{ matrix.qt_version }}
archives: icu qtbase qtdeclarative qtsvg qttools
@@ -138,12 +138,12 @@ jobs:
curl \
-L \
-Z \
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage \
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-x86_64.AppImage \
-O https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-static-x86_64.AppImage \
-O https://github.com/linuxdeploy/linuxdeploy-plugin-qt/releases/download/continuous/linuxdeploy-plugin-qt-static-x86_64.AppImage \
-O https://github.com/linuxdeploy/linuxdeploy-plugin-appimage/releases/download/continuous/linuxdeploy-plugin-appimage-x86_64.AppImage
chmod +x \
linuxdeploy-x86_64.AppImage \
linuxdeploy-plugin-qt-x86_64.AppImage \
linuxdeploy-static-x86_64.AppImage \
linuxdeploy-plugin-qt-static-x86_64.AppImage \
linuxdeploy-plugin-appimage-x86_64.AppImage
- name: Prepare files for AppImage
@@ -156,12 +156,12 @@ jobs:
- name: Package AppImage
run: |
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --plugin qt
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --plugin qt
rm qbittorrent/apprun-hooks/*
cp .github/workflows/helper/appimage/export_vars.sh qbittorrent/apprun-hooks/export_vars.sh
NO_APPSTREAM=1 \
OUTPUT=upload/qbittorrent-CI_Ubuntu_x86_64.AppImage \
./linuxdeploy-x86_64.AppImage --appdir qbittorrent --output appimage
./linuxdeploy-static-x86_64.AppImage --appdir qbittorrent --output appimage
- name: Upload build artifacts
uses: actions/upload-artifact@v4

View File

@@ -41,7 +41,7 @@ jobs:
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
config-file: ./.github/workflows/helper/codeql/js.yaml
config-file: .github/workflows/helper/codeql/js.yaml
languages: javascript
- name: Run CodeQL analysis

View File

@@ -78,7 +78,7 @@ jobs:
- name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "84"
BOOST_MINOR_VERSION: "85"
BOOST_PATCH_VERSION: "0"
run: |
$boost_url="https://boostorg.jfrog.io/artifactory/main/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz"
@@ -93,9 +93,9 @@ jobs:
move "${{ github.workspace }}/../boost_*" "${{ env.boost_path }}"
- name: Install Qt
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
version: "6.5.2"
version: "6.7.0"
archives: qtbase qtsvg qttools
cache: true
@@ -153,26 +153,26 @@ jobs:
copy build/qbittorrent.pdb upload/qBittorrent
copy dist/windows/qt.conf upload/qBittorrent
# runtimes
copy "${{ env.Qt6_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
copy "${{ env.Qt6_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Core.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Gui.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Network.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Sql.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Svg.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Widgets.dll" upload/qBittorrent
copy "${{ env.Qt_ROOT_DIR }}/bin/Qt6Xml.dll" upload/qBittorrent
mkdir upload/qBittorrent/plugins/iconengines
copy "${{ env.Qt6_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
copy "${{ env.Qt_ROOT_DIR }}/plugins/iconengines/qsvgicon.dll" upload/qBittorrent/plugins/iconengines
mkdir upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt6_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qico.dll" upload/qBittorrent/plugins/imageformats
copy "${{ env.Qt_ROOT_DIR }}/plugins/imageformats/qsvg.dll" upload/qBittorrent/plugins/imageformats
mkdir upload/qBittorrent/plugins/platforms
copy "${{ env.Qt6_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
copy "${{ env.Qt_ROOT_DIR }}/plugins/platforms/qwindows.dll" upload/qBittorrent/plugins/platforms
mkdir upload/qBittorrent/plugins/sqldrivers
copy "${{ env.Qt6_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers
copy "${{ env.Qt_ROOT_DIR }}/plugins/sqldrivers/qsqlite.dll" upload/qBittorrent/plugins/sqldrivers
mkdir upload/qBittorrent/plugins/styles
copy "${{ env.Qt6_DIR }}/plugins/styles/qwindowsvistastyle.dll" upload/qBittorrent/plugins/styles
copy "${{ env.Qt_ROOT_DIR }}/plugins/styles/qmodernwindowsstyle.dll" upload/qBittorrent/plugins/styles
mkdir upload/qBittorrent/plugins/tls
copy "${{ env.Qt6_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls
copy "${{ env.Qt_ROOT_DIR }}/plugins/tls/qschannelbackend.dll" upload/qBittorrent/plugins/tls
# cmake additionals
mkdir upload/cmake
copy build/compile_commands.json upload/cmake

View File

@@ -37,7 +37,7 @@ jobs:
- name: Install boost
env:
BOOST_MAJOR_VERSION: "1"
BOOST_MINOR_VERSION: "84"
BOOST_MINOR_VERSION: "85"
BOOST_PATCH_VERSION: "0"
run: |
boost_url="https://boostorg.jfrog.io/artifactory/main/release/${{ env.BOOST_MAJOR_VERSION }}.${{ env.BOOST_MINOR_VERSION }}.${{ env.BOOST_PATCH_VERSION }}/source/boost_${{ env.BOOST_MAJOR_VERSION }}_${{ env.BOOST_MINOR_VERSION }}_${{ env.BOOST_PATCH_VERSION }}.tar.gz"
@@ -52,7 +52,7 @@ jobs:
mv "${{ github.workspace }}/.."/boost_* "${{ env.boost_path }}"
- name: Install Qt
uses: jurplel/install-qt-action@v3
uses: jurplel/install-qt-action@v4
with:
version: ${{ matrix.qt_version }}
archives: icu qtbase qtdeclarative qtsvg qttools

View File

@@ -30,6 +30,7 @@ from typing import Optional, Sequence
import argparse
import re
def main(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument('filenames', nargs='*', help='Filenames to check')
@@ -47,12 +48,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
for line in file:
if (match := regex.match(line)) is not None:
error_buffer += str(f"Defect file: \"{filename}\"\n"
f"Line: {line_counter}\n"
f"Column span: {match.span()}\n"
f"Part: \"{match.group()}\"\n\n")
f"Line: {line_counter}\n"
f"Column span: {match.span()}\n"
f"Part: \"{match.group()}\"\n\n")
line_counter += 1
except UnicodeDecodeError as error:
except UnicodeDecodeError:
# not a text file, skip
continue
@@ -64,5 +65,6 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
return 0
if __name__ == '__main__':
exit(main())

View File

@@ -67,7 +67,7 @@ repos:
hooks:
- id: codespell
name: Check spelling (codespell)
args: ["--ignore-words-list", "additionals,curren,fo,ist,ket,superseeding,te,ths"]
args: ["--ignore-words-list", "additionals,curren,fo,ist,ket,searchin,superseeding,te,ths"]
exclude: |
(?x)^(
.*\.desktop |
@@ -78,11 +78,7 @@ repos:
m4/.* |
src/base/3rdparty/.* |
src/searchengine/nova3/socks.py |
src/webui/www/private/lang/.* |
src/webui/www/private/scripts/lib/.* |
src/webui/www/public/lang/.* |
src/webui/www/public/scripts/lib/.* |
src/webui/www/transifex/.*
src/webui/www/private/scripts/lib/.*
)$
exclude_types:
- ts
@@ -106,11 +102,7 @@ repos:
m4/.* |
src/base/3rdparty/.* |
src/searchengine/nova3/socks.py |
src/webui/www/private/lang/.* |
src/webui/www/private/scripts/lib/.* |
src/webui/www/public/lang/.* |
src/webui/www/public/scripts/lib/.* |
src/webui/www/transifex/.*
src/webui/www/private/scripts/lib/.*
)$
exclude_types:
- svg

View File

@@ -1,7 +1,7 @@
[main]
host = https://www.transifex.com
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_master]
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_v50x]
file_filter = src/lang/qbittorrent_<lang>.ts
source_file = src/lang/qbittorrent_en.ts
source_lang = en
@@ -9,7 +9,7 @@ type = QT
minimum_perc = 23
lang_map = pt: pt_PT, zh: zh_CN
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui]
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui_v50x]
file_filter = src/webui/www/translations/webui_<lang>.ts
source_file = src/webui/www/translations/webui_en.ts
source_lang = en
@@ -17,14 +17,6 @@ type = QT
minimum_perc = 23
lang_map = pt: pt_PT, zh: zh_CN
[o:sledgehammer999:p:qbittorrent:r:qbittorrent_webui_json]
file_filter = src/webui/www/transifex/<lang>.json
source_file = src/webui/www/transifex/en.json
source_lang = en
type = KEYVALUEJSON
minimum_perc = 23
lang_map = pt: pt_PT, zh: zh_CN
[o:sledgehammer999:p:qbittorrent:r:qbittorrentdesktop_master]
source_file = dist/unix/org.qbittorrent.qBittorrent.desktop
source_lang = en

View File

@@ -11,7 +11,7 @@ set(minBoostVersion 1.76)
set(minQt6Version 6.5.0)
set(minOpenSSLVersion 3.0.2)
set(minLibtorrent1Version 1.2.19)
set(minLibtorrentVersion 2.0.9)
set(minLibtorrentVersion 2.0.10)
set(minZlibVersion 1.2.11)
include(GNUInstallDirs)

View File

@@ -1,4 +1,4 @@
Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
Sun Sep 29th 2024 - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
- FEATURE: Support creating .torrent with larger piece size (Chocobo1)
- FEATURE: Improve tracker entries handling (glassez)
- FEATURE: Add separate filter item for tracker errors (glassez)
@@ -12,14 +12,30 @@ Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
- FEATURE: Enable Ctrl+F hotkey for more inputs (thalieht)
- FEATURE: Add seeding limits to RSS and Watched folders options UI (glassez)
- FEATURE: Subcategories implicitly follow the parent category options (glassez)
- FEATURE: Add support for SSL torrents (Chocobo1, Radu Carpa)
- FEATURE: Add option to name each qbittorrent instance (Chocobo1)
- FEATURE: Add button for sending test email (Thomas Piccirello)
- FEATURE: Allow torrents to override default share limit action (glassez)
- FEATURE: Use Start/Stop instead of Resume/Pause (thalieht)
- FEATURE: Add the Popularity metric (Aliaksei Urbanski)
- FEATURE: Focus on Download button if torrent link retrieved from the clipboard (glassez)
- FEATURE: Add ability to pause/resume entire BitTorrent session (glassez)
- FEATURE: Add an option to set BitTorrent session shutdown timeout (glassez)
- FEATURE: Apply "Excluded file names" to folder names as well (glassez)
- FEATURE: Allow to use regular expression to filter torrent content (glassez)
- FEATURE: Allow to move content files to Trash instead of deleting them (glassez)
- FEATURE: Add ability to display torrent "privateness" in UI (ManiMatter)
- FEATURE: Add a flag in `Peers` tab denoting a connection using NAT hole punching (stalkerok)
- BUGFIX: Display error message when unrecoverable error occurred (glassez)
- BUGFIX: Update size of selected files when selection is changed (glassez)
- BUGFIX: Normalize tags by trimming leading/trailing whitespace (glassez)
- BUGFIX: Correctly handle share limits in torrent options dialog (glassez)
- BUGFIX: Adjust tracker tier when adding additional trackers (Chocobo1)
- BUGFIX: Fix inconsistent naming between `Done/Progress` column (luzpaz)
- BUGFIX: Sanitize peer client names (Hanabishi)
- BUGFIX: Apply share limits immediately when torrent downloading is finished (glassez)
- BUGFIX: Show download progress for folders with zero byte size as 100 instead of 0 (vikas_c)
- BUGFIX: Fix highlighted piece color (Prince Gupta)
- BUGFIX: Apply "merge trackers" logic regardless of way the torrent is added (glassez)
- WEBUI: Improve WebUI responsiveness (Chocobo1)
- WEBUI: Do not exit the app when WebUI has failed to start (Hanabishi)
- WEBUI: Add `Moving` filter to side panel (xavier2k6)
@@ -28,14 +44,37 @@ Unreleased - sledgehammer999 <sledgehammer999@qbittorrent.org> - v5.0.0
- WEBUI: Leave the fields empty when value is invalid (Chocobo1)
- WEBUI: Use natural sorting (Chocobo1)
- WEBUI: Improve WebUI login behavior (JayRet)
- WEBUI: Conditionally show filters sidebar (Thomas Piccirello)
- WEBUI: Add support for running concurrent searches (Thomas Piccirello)
- WEBUI: Improve accuracy of trackers list (Thomas Piccirello)
- WEBUI: Fix error when category doesn't exist (Thomas Piccirello)
- WEBUI: Improve table scrolling and selection on mobile (Thomas Piccirello)
- WEBUI: Restore search tabs on load (Thomas Piccirello)
- WEBUI: Restore previously used tab on load (Thomas Piccirello)
- WEBUI: Increase default height of `Share ratio limit` dialog (thalieht)
- WEBUI: Use enabled search plugins by default (Thomas Piccirello)
- WEBUI: Add columns `Incomplete Save Path`, `Info Hash v1`, `Info Hash v2` (thalieht)
- WEBUI: Always create generic filter items (skomerko)
- WEBUI: Provide `Use Category paths in Manual Mode` option (skomerko)
- WEBUI: Provide `Merge trackers to existing torrent` option (skomerko)
- WEBAPI: Fix wrong timestamp values (Chocobo1)
- WEBAPI: Send binary data with filename and mime type specified (glassez)
- WEBAPI: Expose API for the torrent creator (glassez, Radu Carpa)
- WEBAPI: Add support for SSL torrents (Chocobo1, Radu Carpa)
- WEBAPI: Provide endpoint for listing directory content (Paweł Kotiuk)
- WEBAPI: Provide "private" flag via "torrents/info" endpoint (ManiMatter)
- WEBAPI: Add a way to download .torrent file using search plugin (glassez)
- WEBAPI: Add "private" filter for "torrents/info" endpoint (ManiMatter)
- WEBAPI: Add root_path to "torrents/info" result (David Newhall)
- RSS: Show RSS feed title in HTML browser (Jay)
- RSS: Allow to set delay between requests to the same host (jNullj)
- SEARCH: Allow users to specify Python executable path (Chocobo1)
- SEARCH: Lazy load search plugins (milahu)
- SEARCH: Add date column to the built-in search engine (ducalex)
- SEARCH: Allow to rearrange search tabs (glassez)
- WINDOWS: Use Fusion style on Windows 10+. It has better compatibility with dark mode (glassez)
- WINDOWS: Allow to set qBittorrent as default program (glassez)
- WINDOWS: Don't access "Favorites" folder unexpectedly (glassez)
- LINUX: Add support for systemd power management (Chocobo1)
- LINUX: Add support for localized man pages (Victor Chernyakin)
- LINUX: Specify a locale if none is set (Chocobo1)

View File

@@ -5,7 +5,7 @@ qBittorrent - A BitTorrent client in C++ / Qt
- Boost >= 1.76
- libtorrent-rasterbar 1.2.19 - 1.2.x || 2.0.9 - 2.0.x
- libtorrent-rasterbar 1.2.19 - 1.2.x || 2.0.10 - 2.0.x
* By Arvid Norberg, https://www.libtorrent.org/
* Be careful: another library (the one used by rTorrent) uses a similar name

View File

@@ -38,7 +38,7 @@ if (GUI)
COMPONENT data
)
install(FILES org.qbittorrent.qBittorrent.appdata.xml
install(FILES org.qbittorrent.qBittorrent.metainfo.xml
DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/metainfo/
COMPONENT data
)

View File

@@ -14,216 +14,220 @@ Keywords=bittorrent;torrent;magnet;download;p2p;
SingleMainWindow=true
# Translations
Comment[af]=Aflaai en deel lêers oor BitTorrent
GenericName[af]=BitTorrent kliënt
Comment[af]=Aflaai en deel lêers oor BitTorrent
Name[af]=qBittorrent
Comment[ar]=نزّل وشارك الملفات عبر كيوبت‎تورنت
GenericName[ar]=عميل بت‎تورنت
Comment[ar]=نزّل وشارك الملفات عبر كيوبت‎تورنت
Name[ar]=qBittorrent
Comment[be]=Спампоўванне і раздача файлаў праз пратакол BitTorrent
GenericName[be]=Кліент BitTorrent
Comment[be]=Спампоўванне і раздача файлаў праз пратакол BitTorrent
Name[be]=qBittorrent
Comment[bg]=Сваляне и споделяне на файлове чрез BitTorrent
GenericName[bg]=BitTorrent клиент
Comment[bg]=Сваляне и споделяне на файлове чрез BitTorrent
Name[bg]=qBittorrent
Comment[bn]=বিটটরেন্টে ফাইল ডাউনলোড এবং শেয়ার করুন
GenericName[bn]=বিটটরেন্ট ক্লায়েন্ট
Comment[bn]=বিটটরেন্টে ফাইল ডাউনলোড এবং শেয়ার করুন
Name[bn]=qBittorrent
Comment[zh]=通过 BitTorrent 下载和分享文件
GenericName[zh]=BitTorrent 客户端
Comment[zh]=通过 BitTorrent 下载和分享文件
Name[zh]=qBittorrent
Comment[bs]=Preuzmi i dijeli datoteke preko BitTorrent-a
GenericName[bs]=BitTorrent klijent
Comment[bs]=Preuzmi i dijeli datoteke preko BitTorrent-a
Name[bs]=qBittorrent
Comment[ca]=Baixeu i compartiu fitxers amb el BitTorrent
GenericName[ca]=Client de BitTorrent
Comment[ca]=Baixeu i compartiu fitxers amb el BitTorrent
Name[ca]=qBittorrent
Comment[cs]=Stahování a sdílení souborů přes síť BitTorrent
GenericName[cs]=BitTorrent klient
Comment[cs]=Stahování a sdílení souborů přes síť BitTorrent
Name[cs]=qBittorrent
Comment[da]=Download og del filer over BitTorrent
GenericName[da]=BitTorrent-klient
Comment[da]=Download og del filer over BitTorrent
Name[da]=qBittorrent
Comment[de]=Über BitTorrent Dateien herunterladen und teilen
GenericName[de]=BitTorrent Client
Comment[de]=Über BitTorrent Dateien herunterladen und teilen
Name[de]=qBittorrent
Comment[el]=Κάντε λήψη και μοιραστείτε αρχεία μέσω BitTorrent
GenericName[el]=BitTorrent client
Comment[el]=Κάντε λήψη και μοιραστείτε αρχεία μέσω BitTorrent
Name[el]=qBittorrent
Comment[en_GB]=Download and share files over BitTorrent
GenericName[en_GB]=BitTorrent client
Comment[en_GB]=Download and share files over BitTorrent
Name[en_GB]=qBittorrent
Comment[es]=Descargue y comparta archivos por BitTorrent
GenericName[es]=Cliente BitTorrent
Comment[es]=Descargue y comparta archivos por BitTorrent
Name[es]=qBittorrent
Comment[et]=Lae alla ja jaga faile üle BitTorrenti
GenericName[et]=BitTorrent klient
Comment[et]=Lae alla ja jaga faile üle BitTorrenti
Name[et]=qBittorrent
Comment[eu]=Jeitsi eta elkarbanatu agiriak BitTorrent bidez
GenericName[eu]=BitTorrent bezeroa
Comment[eu]=Jeitsi eta elkarbanatu agiriak BitTorrent bidez
Name[eu]=qBittorrent
Comment[fa]=دانلود و به اشتراک گذاری فایل های بوسیله بیت تورنت
GenericName[fa]=بیت تورنت نسخه کلاینت
Comment[fa]=دانلود و به اشتراک گذاری فایل های بوسیله بیت تورنت
Name[fa]=qBittorrent
Comment[fi]=Lataa ja jaa tiedostoja BitTorrentia käyttäen
GenericName[fi]=BitTorrent-asiakasohjelma
Comment[fi]=Lataa ja jaa tiedostoja BitTorrentia käyttäen
Name[fi]=qBittorrent
Comment[fr]=Télécharger et partager des fichiers sur BitTorrent
GenericName[fr]=Client BitTorrent
Comment[fr]=Télécharger et partager des fichiers sur BitTorrent
Name[fr]=qBittorrent
Comment[gl]=Descargar e compartir ficheiros co protocolo BitTorrent
GenericName[gl]=Cliente BitTorrent
Comment[gl]=Descargar e compartir ficheiros co protocolo BitTorrent
Name[gl]=qBittorrent
Comment[gu]=બિટ્ટોરેંટ પર ફાઈલો ડાઉનલોડ અને શેર કરો
GenericName[gu]=બિટ્ટોરેંટ ક્લાયન્ટ
Comment[gu]=બિટ્ટોરેંટ પર ફાઈલો ડાઉનલોડ અને શેર કરો
Name[gu]=qBittorrent
Comment[he]=הורד ושתף קבצים על גבי ביטורנט
GenericName[he]=לקוח ביטורנט
Comment[he]=הורד ושתף קבצים על גבי ביטורנט
Name[he]=qBittorrent
Comment[hr]=Preuzmite i dijelite datoteke putem BitTorrenta
GenericName[hr]=BitTorrent klijent
Comment[hr]=Preuzmite i dijelite datoteke putem BitTorrenta
Name[hr]=qBittorrent
Comment[hu]=Fájlok letöltése és megosztása a BitTorrent hálózaton keresztül
GenericName[hu]=BitTorrent kliens
Comment[hu]=Fájlok letöltése és megosztása a BitTorrent hálózaton keresztül
Name[hu]=qBittorrent
Comment[hy]=Նիշքերի փոխանցում BitTorrent-ի միջոցով
GenericName[hy]=BitTorrent սպասառու
Comment[hy]=Նիշքերի փոխանցում BitTorrent-ի միջոցով
Name[hy]=qBittorrent
Comment[id]=Unduh dan berbagi berkas melalui BitTorrent
GenericName[id]=Klien BitTorrent
Comment[id]=Unduh dan berbagi berkas melalui BitTorrent
Name[id]=qBittorrent
Comment[is]=Sækja og deila skrám yfir BitTorrent
GenericName[is]=BitTorrent biðlarar
Comment[is]=Sækja og deila skrám yfir BitTorrent
Name[is]=qBittorrent
Comment[it]=Scarica e condividi file tramite BitTorrent
GenericName[it]=Client BitTorrent
Comment[it]=Scarica e condividi file tramite BitTorrent
Name[it]=qBittorrent
Comment[ja]=BitTorrentでファイルのダウンロードと共有
GenericName[ja]=BitTorrentクライアント
Comment[ja]=BitTorrentでファイルのダウンロードと共有
Name[ja]=qBittorrent
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
GenericName[ka]=BitTorrent კლიენტი
Comment[ka]=გადმოტვირთეთ და გააზიარეთ ფაილები BitTorrent-ის საშუალებით
Name[ka]=qBittorrent
Comment[ko]=BitTorrent를 통한 파일 내려받기 및 공유
GenericName[ko]=BitTorrent 클라이언트
Comment[ko]=BitTorrent를 통한 파일 다운로드 및 공유
Name[ko]=qBittorrent
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
GenericName[lt]=BitTorrent klientas
Comment[lt]=Atsisiųskite bei dalinkitės failais BitTorrent tinkle
Name[lt]=qBittorrent
Comment[mk]=Превземајте и споделувајте фајлови преку BitTorrent
GenericName[mk]=BitTorrent клиент
Comment[mk]=Превземајте и споделувајте фајлови преку BitTorrent
Name[mk]=qBittorrent
Comment[my]=တောရန့်ဖြင့်ဖိုင်များဒေါင်းလုဒ်ဆွဲရန်နှင့်မျှဝေရန်
GenericName[my]=တောရန့်စီမံခန့်ခွဲသည့်အရာ
Comment[my]=တောရန့်ဖြင့်ဖိုင်များဒေါင်းလုဒ်ဆွဲရန်နှင့်မျှဝေရန်
Name[my]=qBittorrent
Comment[nb]=Last ned og del filer over BitTorrent
GenericName[nb]=BitTorrent-klient
Comment[nb]=Last ned og del filer over BitTorrent
Name[nb]=qBittorrent
Comment[nl]=Bestanden downloaden en delen via BitTorrent
GenericName[nl]=BitTorrent-client
Comment[nl]=Bestanden downloaden en delen via BitTorrent
Name[nl]=qBittorrent
Comment[pl]=Pobieraj i dziel się plikami przez BitTorrent
GenericName[pl]=Klient BitTorrent
Comment[pl]=Pobieraj i dziel się plikami przez BitTorrent
Name[pl]=qBittorrent
Comment[pt]=Transferir e partilhar ficheiros por BitTorrent
GenericName[pt]=Cliente BitTorrent
Comment[pt]=Transferir e partilhar ficheiros por BitTorrent
Name[pt]=qBittorrent
Comment[pt_BR]=Baixe e compartilhe arquivos pelo BitTorrent
GenericName[pt_BR]=Cliente BitTorrent
Comment[pt_BR]=Baixe e compartilhe arquivos pelo BitTorrent
Name[pt_BR]=qBittorrent
Comment[ro]=Descărcați și partajați fișiere prin BitTorrent
GenericName[ro]=Client BitTorrent
Comment[ro]=Descărcați și partajați fișiere prin BitTorrent
Name[ro]=qBittorrent
Comment[ru]=Обмен файлами по сети БитТоррент
GenericName[ru]=Клиент сети БитТоррент
Comment[ru]=Обмен файлами по сети БитТоррент
Name[ru]=qBittorrent
Comment[sk]=Sťahovanie a zdieľanie súborov prostredníctvom siete BitTorrent
GenericName[sk]=Klient siete BitTorrent
Comment[sk]=Sťahovanie a zdieľanie súborov prostredníctvom siete BitTorrent
Name[sk]=qBittorrent
Comment[sl]=Prenesite in delite datoteke preko BitTorrenta
GenericName[sl]=BitTorrent odjemalec
Comment[sl]=Prenesite in delite datoteke preko BitTorrenta
Name[sl]=qBittorrent
GenericName[sq]=Klienti BitTorrent
Comment[sq]=Shkarko dhe shpërndaj skedarë në BitTorrent
Name[sq]=qBittorrent
Comment[sr]=Преузимајте и делите фајлове преко BitTorrent протокола
GenericName[sr]=BitTorrent-клијент
GenericName[sr]=BitTorrent клијент
Comment[sr]=Преузимајте и делите фајлове преко BitTorrent-а
Name[sr]=qBittorrent
Comment[sr@latin]=Preuzimanje i deljenje fajlova preko BitTorrent-a
GenericName[sr@latin]=BitTorrent klijent
Comment[sr@latin]=Preuzimanje i deljenje fajlova preko BitTorrent-a
Name[sr@latin]=qBittorrent
Comment[sv]=Hämta och dela filer över BitTorrent
GenericName[sv]=BitTorrent-klient
Comment[sv]=Hämta och dela filer över BitTorrent
Name[sv]=qBittorrent
Comment[ta]=BitTorrent வழியாக கோப்புகளை பதிவிறக்க மற்றும் பகிர
GenericName[ta]=BitTorrent வாடிக்கையாளர்
Comment[ta]=BitTorrent வழியாக கோப்புகளை பதிவிறக்க மற்றும் பகிர
Name[ta]=qBittorrent
Comment[te]=క్యు బిట్ టొరెంట్ తో ఫైల్స్ దిగుమతి చేసుకోండి , పంచుకోండి
GenericName[te]=క్యు బిట్ టొరెంట్ క్లయింట్
Comment[te]=క్యు బిట్ టొరెంట్ తో ఫైల్స్ దిగుమతి చేసుకోండి , పంచుకోండి
Name[te]=qBittorrent
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่าน BitTorrent
GenericName[th]=โปรแกรมบิททอเร้นท์
Comment[th]=ดาวน์โหลดและแชร์ไฟล์ผ่าน BitTorrent
Name[th]=qBittorrent
Comment[tr]=Dosyaları BitTorrent üzerinden indirin ve paylaşın
GenericName[tr]=BitTorrent istemcisi
Comment[tr]=Dosyaları BitTorrent üzerinden indirin ve paylaşın
Name[tr]=qBittorrent
Comment[ur]=BitTorrent پر فائلوں کو ڈاؤن لوڈ کریں اور اشتراک کریں
GenericName[ur]=قیو بٹ ٹورنٹ کلائنٹ
Comment[ur]=BitTorrent پر فائلوں کو ڈاؤن لوڈ کریں اور اشتراک کریں
Name[ur]=qBittorrent
Comment[uk]=Завантажуйте та поширюйте файли через BitTorrent
GenericName[uk]=BitTorrent-клієнт
Comment[uk]=Завантажуйте та поширюйте файли через BitTorrent
Name[uk]=qBittorrent
Comment[vi]=Tải xuống và chia sẻ tệp qua BitTorrent
GenericName[vi]=Máy khách BitTorrent
Comment[vi]=Tải xuống và chia sẻ tệp qua BitTorrent
Name[vi]=qBittorrent
Comment[zh_HK]=經由BitTorrent下載並分享檔案
GenericName[zh_HK]=BitTorrent用戶端
Comment[zh_HK]=經由BitTorrent下載並分享檔案
Name[zh_HK]=qBittorrent
Comment[zh_TW]=經由 BitTorrent 下載並分享檔案
GenericName[zh_TW]=BitTorrent 用戶端
Comment[zh_TW]=使用 BitTorrent 下載並分享檔案
Name[zh_TW]=qBittorrent
Comment[eo]=Elŝutu kaj kunhavigu dosierojn per BitTorrent
GenericName[eo]=BitTorrent-kliento
Comment[eo]=Elŝutu kaj kunhavigu dosierojn per BitTorrent
Name[eo]=qBittorrent
Comment[kk]=BitTorrent арқылы файл жүктеу және бөлісу
GenericName[kk]=BitTorrent клиенті
Comment[kk]=BitTorrent арқылы файл жүктеу және бөлісу
Name[kk]=qBittorrent
Comment[en_AU]=Download and share files over BitTorrent
GenericName[en_AU]=BitTorrent client
Comment[en_AU]=Download and share files over BitTorrent
Name[en_AU]=qBittorrent
Name[rm]=qBittorrent
Name[jv]=qBittorrent
Comment[oc]=Telecargar e partejar de fichièrs amb BitTorrent
GenericName[oc]=Client BitTorrent
Comment[oc]=Telecargar e partejar de fichièrs amb BitTorrent
Name[oc]=qBittorrent
Name[ug]=qBittorrent
Name[yi]=qBittorrent
Comment[nqo]=ߞߐߕߐ߯ߘߐ ߟߎ߬ ߟߊߖߌ߰ ߞߊ߬ ߓߊ߲߫ ߞߵߊ߬ߟߎ߬ ߘߐߕߟߊ߫ ߓߌߙߏߙߍ߲ߕ ߞߊ߲߬
GenericName[nqo]=ߓߌߙߏߙߍ߲ߕ ߕߣߐ߬ߓߐ߬ߟߊ
Comment[nqo]=ߞߐߕߐ߯ߘߐ ߟߎ߬ ߟߊߖߌ߰ ߞߊ߬ ߓߊ߲߫ ߞߵߊ߬ߟߎ߬ ߘߐߕߟߊ߫ ߓߌߙߏߙߍ߲ߕ ߞߊ߲߬
Name[nqo]=qBittorrent
Comment[uz@Latn]=BitTorrent orqali fayllarni yuklab olish va baham korish
GenericName[uz@Latn]=BitTorrent mijozi
Comment[uz@Latn]=BitTorrent orqali fayllarni yuklab olish va baham korish
Name[uz@Latn]=qBittorrent
Comment[ltg]=Atsasyuteit i daleit failus ar BitTorrent
GenericName[ltg]=BitTorrent klients
Comment[ltg]=Atsasyuteit i daleit failus ar BitTorrent
Name[ltg]=qBittorrent
Comment[hi_IN]=BitTorrent द्वारा फाइल डाउनलोड व सहभाजन
GenericName[hi_IN]=Bittorrent साधन
Comment[hi_IN]=BitTorrent द्वारा फाइल डाउनलोड व सहभाजन
Name[hi_IN]=qBittorrent
Comment[az@latin]=Faylları BitTorrent vasitəsilə endirin və paylaşın
GenericName[az@latin]=BitTorrent client
Comment[az@latin]=Faylları BitTorrent vasitəsilə endirin və paylaşın
Name[az@latin]=qBittorrent
Comment[lv_LV]=Lejupielādēt un koplietot failus ar BitTorrent
GenericName[lv_LV]=BitTorrent klients
Comment[lv_LV]=Lejupielādēt un koplietot failus ar BitTorrent
Name[lv_LV]=qBittorrent
Comment[ms_MY]=Muat turun dan kongsi fail melalui BitTorrent
GenericName[ms_MY]=Klien BitTorrent
Comment[ms_MY]=Muat turun dan kongsi fail melalui BitTorrent
Name[ms_MY]=qBittorrent
Comment[mn_MN]=BitTorrent-оор файлуудаа тат, түгээ
GenericName[mn_MN]=BitTorrent татагч
Comment[mn_MN]=BitTorrent-оор файлуудаа тат, түгээ
Name[mn_MN]=qBittorrent
Comment[ne_NP]=फाइलहरू डाउनलोड गर्नुहोस् र BitTorrent मा साझा गर्नुहोस्
GenericName[ne_NP]=BitTorrent क्लाइन्ट
Comment[ne_NP]=फाइलहरू डाउनलोड गर्नुहोस् र BitTorrent मा साझा गर्नुहोस्
Name[ne_NP]=qBittorrent
Comment[pt_PT]=Transferir e partilhar ficheiros por BitTorrent
GenericName[pt_PT]=Cliente BitTorrent
Comment[pt_PT]=Transferir e partilhar ficheiros por BitTorrent
Name[pt_PT]=qBittorrent
GenericName[si_LK]=BitTorrent සේවාදායකයා
Comment[si_LK]=BitTorrent හරහා ගොනු බාගත කර බෙදාගන්න.
Name[si_LK]=qBittorrent

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2014 sledgehammer999 <sledgehammer999@qbittorrent.org> -->
<component type="desktop">
<component type="desktop-application">
<id>org.qbittorrent.qBittorrent</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>GPL-3.0-or-later and OpenSSL</project_license>
@@ -14,33 +14,12 @@
</p>
<ul>
<li>Polished µTorrent-like User Interface</li>
<li>
Well-integrated and extensible Search Engine
<ul>
<li>Simultaneous search in many Torrent search sites</li>
<li>Category-specific search requests (e.g. Books, Music, Software)</li>
</ul>
</li>
<li>Well-integrated and extensible Search Engine</li>
<li>RSS feed support with advanced download filters (incl. regex)</li>
<li>
Many Bittorrent extensions supported:
<ul>
<li>Magnet links</li>
<li>Distributed hash table (DHT), peer exchange protocol (PEX), local peer discovery (LSD)</li>
<li>Private torrents</li>
<li>Encrypted connections</li>
<li>and many more...</li>
</ul>
</li>
<li>Many Bittorrent extensions supported</li>
<li>Remote control through Web user interface, written with AJAX</li>
<li>Sequential downloading (Download in order)</li>
<li>
Advanced control over torrents, trackers and peers
<ul>
<li>Torrents queueing and prioritizing</li>
<li>Torrent content selection and prioritizing</li>
</ul>
</li>
<li>Advanced control over torrents, trackers and peers</li>
<li>Bandwidth scheduler</li>
<li>Torrent creation tool</li>
<li>IP Filtering (eMule &amp; PeerGuardian format compatible)</li>
@@ -53,27 +32,36 @@
<launchable type="desktop-id">org.qbittorrent.qBittorrent.desktop</launchable>
<screenshots>
<screenshot type="default">
<image height="675" width="1200">https://alexpl.fedorapeople.org/AppData/qbittorrent/screens/qbittorrent_01.png</image>
<caption>Main window (General tab collapsed)</caption>
<image>https://raw.githubusercontent.com/qbittorrent/qBittorrent-website/2741f2a90854604e268c6bba9e6859aad0103583/src/img/screenshots/linux/1.webp</image>
</screenshot>
<screenshot>
<image height="675" width="1200">https://alexpl.fedorapeople.org/AppData/qbittorrent/screens/qbittorrent_02.png</image>
<caption>Main window (General tab expanded)</caption>
<image>https://raw.githubusercontent.com/qbittorrent/qBittorrent-website/2741f2a90854604e268c6bba9e6859aad0103583/src/img/screenshots/linux/2.webp</image>
</screenshot>
<screenshot>
<image height="675" width="1200">https://alexpl.fedorapeople.org/AppData/qbittorrent/screens/qbittorrent_03.png</image>
<caption>Options dialog</caption>
<image>https://raw.githubusercontent.com/qbittorrent/qBittorrent-website/2741f2a90854604e268c6bba9e6859aad0103583/src/img/screenshots/linux/3.webp</image>
</screenshot>
<screenshot>
<image height="675" width="1200">https://alexpl.fedorapeople.org/AppData/qbittorrent/screens/qbittorrent_04.png</image>
<caption>Search engine</caption>
<image>https://raw.githubusercontent.com/qbittorrent/qBittorrent-website/2741f2a90854604e268c6bba9e6859aad0103583/src/img/screenshots/linux/4.webp</image>
</screenshot>
</screenshots>
<update_contact>sledgehammer999@qbittorrent.org</update_contact>
<developer_name>The qBittorrent Project</developer_name>
<developer id="org.qbittorrent">
<name>The qBittorrent Project</name>
</developer>
<url type="homepage">https://www.qbittorrent.org/</url>
<url type="bugtracker">https://bugs.qbittorrent.org/</url>
<url type="donation">https://www.qbittorrent.org/donate</url>
<url type="faq">https://wiki.qbittorrent.org/Frequently-Asked-Questions</url>
<url type="help">https://forum.qbittorrent.org/</url>
<url type="translate">https://github.com/qbittorrent/qBittorrent/wiki/How-to-translate-qBittorrent</url>
<url type="donation">https://www.qbittorrent.org/donate</url>
<url type="translate">https://wiki.qbittorrent.org/How-to-translate-qBittorrent</url>
<url type="vcs-browser">https://github.com/qbittorrent/qBittorrent</url>
<url type="contribute">https://github.com/qbittorrent/qBittorrent/blob/master/CONTRIBUTING.md</url>
<content_rating type="oars-1.1"/>
<releases>
<release version="5.0.0~beta1" date="2024-03-19"/>
<release version="5.0.0" date="2024-09-29"/>
</releases>
</component>

View File

@@ -7,9 +7,11 @@ import shutil
import sys
from typing import List
def isNotStub(path: str) -> bool:
return (os.path.getsize(path) >= (10 * 1024))
def main() -> int:
parser = argparse.ArgumentParser(description='Gather valid Qt translations for NSIS packaging.')
parser.add_argument("qt_translations_folder", help="Qt's translations folder")
@@ -27,5 +29,6 @@ def main() -> int:
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez
*
* This program is free software; you can redistribute it and/or
@@ -140,8 +140,8 @@ namespace
if (!addTorrentParams.savePath.isEmpty())
result.append(u"@savePath=" + addTorrentParams.savePath.data());
if (addTorrentParams.addPaused.has_value())
result.append(*addTorrentParams.addPaused ? u"@addPaused=1"_s : u"@addPaused=0"_s);
if (addTorrentParams.addStopped.has_value())
result.append(*addTorrentParams.addStopped ? u"@addStopped=1"_s : u"@addStopped=0"_s);
if (addTorrentParams.skipChecking)
result.append(u"@skipChecking"_s);
@@ -180,9 +180,9 @@ namespace
continue;
}
if (param.startsWith(u"@addPaused="))
if (param.startsWith(u"@addStopped="))
{
addTorrentParams.addPaused = (QStringView(param).mid(11).toInt() != 0);
addTorrentParams.addStopped = (QStringView(param).mid(11).toInt() != 0);
continue;
}
@@ -251,6 +251,7 @@ Application::Application(int &argc, char **argv)
#if !defined(DISABLE_GUI)
setDesktopFileName(u"org.qbittorrent.qBittorrent"_s);
setQuitOnLastWindowClosed(false);
setQuitLockEnabled(false);
QPixmapCache::setCacheLimit(PIXMAP_CACHE_SIZE);
#endif
@@ -832,7 +833,7 @@ int Application::exec()
m_desktopIntegration = new DesktopIntegration;
m_desktopIntegration->setToolTip(tr("Loading torrents..."));
#ifndef Q_OS_MACOS
auto *desktopIntegrationMenu = new QMenu;
auto *desktopIntegrationMenu = m_desktopIntegration->menu();
auto *actionExit = new QAction(tr("E&xit"), desktopIntegrationMenu);
actionExit->setIcon(UIThemeManager::instance()->getIcon(u"application-exit"_s));
actionExit->setMenuRole(QAction::QuitRole);
@@ -843,8 +844,6 @@ int Application::exec()
});
desktopIntegrationMenu->addAction(actionExit);
m_desktopIntegration->setMenu(desktopIntegrationMenu);
const bool isHidden = m_desktopIntegration->isActive() && (startUpWindowState() == WindowState::Hidden);
#else
const bool isHidden = false;

View File

@@ -263,8 +263,8 @@ namespace
}
throw CommandLineParameterError(QCoreApplication::translate("CMD Options", "Parameter '%1' must follow syntax '%1=%2'",
"e.g. Parameter '--add-paused' must follow syntax "
"'--add-paused=<true|false>'")
"e.g. Parameter '--add-stopped' must follow syntax "
"'--add-stopped=<true|false>'")
.arg(fullParameter(), u"<true|false>"_s));
}
@@ -318,7 +318,7 @@ namespace
constexpr const StringOption CONFIGURATION_OPTION {"configuration"};
constexpr const BoolOption RELATIVE_FASTRESUME {"relative-fastresume"};
constexpr const StringOption SAVE_PATH_OPTION {"save-path"};
constexpr const TriStateBoolOption PAUSED_OPTION {"add-paused", true};
constexpr const TriStateBoolOption STOPPED_OPTION {"add-stopped", true};
constexpr const BoolOption SKIP_HASH_CHECK_OPTION {"skip-hash-check"};
constexpr const StringOption CATEGORY_OPTION {"category"};
constexpr const BoolOption SEQUENTIAL_OPTION {"sequential"};
@@ -345,7 +345,7 @@ QBtCommandLineParameters::QBtCommandLineParameters(const QProcessEnvironment &en
addTorrentParams.skipChecking = SKIP_HASH_CHECK_OPTION.value(env);
addTorrentParams.sequential = SEQUENTIAL_OPTION.value(env);
addTorrentParams.firstLastPiecePriority = FIRST_AND_LAST_OPTION.value(env);
addTorrentParams.addPaused = PAUSED_OPTION.value(env);
addTorrentParams.addStopped = STOPPED_OPTION.value(env);
}
QBtCommandLineParameters parseCommandLine(const QStringList &args)
@@ -417,9 +417,9 @@ QBtCommandLineParameters parseCommandLine(const QStringList &args)
{
result.addTorrentParams.savePath = Path(SAVE_PATH_OPTION.value(arg));
}
else if (arg == PAUSED_OPTION)
else if (arg == STOPPED_OPTION)
{
result.addTorrentParams.addPaused = PAUSED_OPTION.value(arg);
result.addTorrentParams.addStopped = STOPPED_OPTION.value(arg);
}
else if (arg == SKIP_HASH_CHECK_OPTION)
{
@@ -523,7 +523,7 @@ QString makeUsage(const QString &prgName)
+ wrapText(QCoreApplication::translate("CMD Options", "Options when adding new torrents:"), 0) + u'\n'
+ SAVE_PATH_OPTION.usage(QCoreApplication::translate("CMD Options", "path")) + wrapText(QCoreApplication::translate("CMD Options", "Torrent save path")) + u'\n'
+ PAUSED_OPTION.usage() + wrapText(QCoreApplication::translate("CMD Options", "Add torrents as started or paused")) + u'\n'
+ STOPPED_OPTION.usage() + wrapText(QCoreApplication::translate("CMD Options", "Add torrents as running or stopped")) + u'\n'
+ SKIP_HASH_CHECK_OPTION.usage() + wrapText(QCoreApplication::translate("CMD Options", "Skip hash check")) + u'\n'
+ CATEGORY_OPTION.usage(QCoreApplication::translate("CMD Options", "name"))
+ wrapText(QCoreApplication::translate("CMD Options", "Assign torrents to category. If the category doesn't exist, it will be "

View File

@@ -46,7 +46,7 @@
namespace
{
const int MIGRATION_VERSION = 7;
const int MIGRATION_VERSION = 8;
const QString MIGRATION_VERSION_KEY = u"Meta/MigrationVersion"_s;
void exportWebUIHttpsFiles()
@@ -468,6 +468,16 @@ namespace
settingsStorage->removeValue(oldKey);
}
void migrateAddPausedSetting()
{
auto *settingsStorage = SettingsStorage::instance();
const auto oldKey = u"BitTorrent/Session/AddTorrentPaused"_s;
const auto newKey = u"BitTorrent/Session/AddTorrentStopped"_s;
settingsStorage->storeValue(newKey, settingsStorage->loadValue<bool>(oldKey));
settingsStorage->removeValue(oldKey);
}
}
bool upgrade()
@@ -509,6 +519,9 @@ bool upgrade()
if (version < 7)
migrateShareLimitActionSettings();
if (version < 8)
migrateAddPausedSetting();
version = MIGRATION_VERSION;
}

View File

@@ -38,6 +38,8 @@ add_library(qbt_base STATIC
bittorrent/torrent.h
bittorrent/torrentcontenthandler.h
bittorrent/torrentcontentlayout.h
bittorrent/torrentcontentremoveoption.h
bittorrent/torrentcontentremover.h
bittorrent/torrentcreationmanager.h
bittorrent/torrentcreationtask.h
bittorrent/torrentcreator.h
@@ -46,6 +48,7 @@ add_library(qbt_base STATIC
bittorrent/torrentinfo.h
bittorrent/tracker.h
bittorrent/trackerentry.h
bittorrent/trackerentrystatus.h
concepts/explicitlyconvertibleto.h
concepts/stringable.h
digest32.h
@@ -106,6 +109,7 @@ add_library(qbt_base STATIC
utils/io.h
utils/misc.h
utils/net.h
utils/number.h
utils/os.h
utils/password.h
utils/random.h
@@ -143,6 +147,7 @@ add_library(qbt_base STATIC
bittorrent/sslparameters.cpp
bittorrent/torrent.cpp
bittorrent/torrentcontenthandler.cpp
bittorrent/torrentcontentremover.cpp
bittorrent/torrentcreationmanager.cpp
bittorrent/torrentcreationtask.cpp
bittorrent/torrentcreator.cpp
@@ -151,6 +156,7 @@ add_library(qbt_base STATIC
bittorrent/torrentinfo.cpp
bittorrent/tracker.cpp
bittorrent/trackerentry.cpp
bittorrent/trackerentrystatus.cpp
exceptions.cpp
http/connection.cpp
http/httperror.cpp
@@ -199,6 +205,7 @@ add_library(qbt_base STATIC
utils/io.cpp
utils/misc.cpp
utils/net.cpp
utils/number.cpp
utils/os.cpp
utils/password.cpp
utils/random.cpp

View File

@@ -116,7 +116,7 @@ BitTorrent::AddTorrentParams BitTorrent::parseAddTorrentParams(const QJsonObject
.downloadPath = Path(jsonObj.value(PARAM_DOWNLOADPATH).toString()),
.addForced = (getEnum<TorrentOperatingMode>(jsonObj, PARAM_OPERATINGMODE) == TorrentOperatingMode::Forced),
.addToQueueTop = getOptionalBool(jsonObj, PARAM_QUEUETOP),
.addPaused = getOptionalBool(jsonObj, PARAM_STOPPED),
.addStopped = getOptionalBool(jsonObj, PARAM_STOPPED),
.stopCondition = getOptionalEnum<Torrent::StopCondition>(jsonObj, PARAM_STOPCONDITION),
.filePaths = {},
.filePriorities = {},
@@ -163,8 +163,8 @@ QJsonObject BitTorrent::serializeAddTorrentParams(const AddTorrentParams &params
if (params.addToQueueTop)
jsonObj[PARAM_QUEUETOP] = *params.addToQueueTop;
if (params.addPaused)
jsonObj[PARAM_STOPPED] = *params.addPaused;
if (params.addStopped)
jsonObj[PARAM_STOPPED] = *params.addStopped;
if (params.stopCondition)
jsonObj[PARAM_STOPCONDITION] = Utils::String::fromEnum(*params.stopCondition);
if (params.contentLayout)

View File

@@ -59,7 +59,7 @@ namespace BitTorrent
bool firstLastPiecePriority = false;
bool addForced = false;
std::optional<bool> addToQueueTop;
std::optional<bool> addPaused;
std::optional<bool> addStopped;
std::optional<Torrent::StopCondition> stopCondition;
PathList filePaths; // used if TorrentInfo is set
QVector<DownloadPriority> filePriorities; // used if TorrentInfo is set

View File

@@ -40,7 +40,6 @@
#include <QRegularExpression>
#include <QThread>
#include "base/algorithm.h"
#include "base/exceptions.h"
#include "base/global.h"
#include "base/logger.h"

View File

@@ -240,11 +240,11 @@ void CustomDiskIOThread::handleCompleteFiles(lt::storage_index_t storage, const
lt::storage_interface *customStorageConstructor(const lt::storage_params &params, lt::file_pool &pool)
{
return new CustomStorage {params, pool};
return new CustomStorage(params, pool);
}
CustomStorage::CustomStorage(const lt::storage_params &params, lt::file_pool &filePool)
: lt::default_storage {params, filePool}
: lt::default_storage(params, filePool)
, m_savePath {params.path}
{
}

View File

@@ -198,7 +198,12 @@ QString PeerInfo::I2PAddress() const
QString PeerInfo::client() const
{
return QString::fromStdString(m_nativeInfo.client);
auto client = QString::fromStdString(m_nativeInfo.client).simplified();
// remove non-printable characters
erase_if(client, [](const QChar &c) { return !c.isPrint(); });
return client;
}
QString PeerInfo::peerIdClient() const
@@ -362,6 +367,10 @@ void PeerInfo::determineFlags()
if (useUTPSocket())
updateFlags(u'P', C_UTP);
// h = Peer is using NAT hole punching
if (isHolepunched())
updateFlags(u'h', tr("Peer is using NAT hole punching"));
m_flags.chop(1);
m_flagsDescription.chop(1);
}

View File

@@ -37,16 +37,12 @@
#include "addtorrentparams.h"
#include "categoryoptions.h"
#include "sharelimitaction.h"
#include "torrentcontentremoveoption.h"
#include "trackerentry.h"
#include "trackerentrystatus.h"
class QString;
enum DeleteOption
{
DeleteTorrent,
DeleteTorrentAndFiles
};
namespace BitTorrent
{
class InfoHash;
@@ -57,6 +53,12 @@ namespace BitTorrent
struct CacheStatus;
struct SessionStatus;
enum class TorrentRemoveOption
{
KeepContent,
RemoveContent
};
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
@@ -213,8 +215,8 @@ namespace BitTorrent
virtual void setPeXEnabled(bool enabled) = 0;
virtual bool isAddTorrentToQueueTop() const = 0;
virtual void setAddTorrentToQueueTop(bool value) = 0;
virtual bool isAddTorrentPaused() const = 0;
virtual void setAddTorrentPaused(bool value) = 0;
virtual bool isAddTorrentStopped() const = 0;
virtual void setAddTorrentStopped(bool value) = 0;
virtual Torrent::StopCondition torrentStopCondition() const = 0;
virtual void setTorrentStopCondition(Torrent::StopCondition stopCondition) = 0;
virtual TorrentContentLayout torrentContentLayout() const = 0;
@@ -255,6 +257,8 @@ namespace BitTorrent
virtual void setPerformanceWarningEnabled(bool enable) = 0;
virtual int saveResumeDataInterval() const = 0;
virtual void setSaveResumeDataInterval(int value) = 0;
virtual int shutdownTimeout() const = 0;
virtual void setShutdownTimeout(int value) = 0;
virtual int port() const = 0;
virtual void setPort(int port) = 0;
virtual bool isSSLEnabled() const = 0;
@@ -422,16 +426,24 @@ namespace BitTorrent
virtual void setExcludedFileNamesEnabled(bool enabled) = 0;
virtual QStringList excludedFileNames() const = 0;
virtual void setExcludedFileNames(const QStringList &newList) = 0;
virtual bool isFilenameExcluded(const QString &fileName) const = 0;
virtual void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) = 0;
virtual QStringList bannedIPs() const = 0;
virtual void setBannedIPs(const QStringList &newList) = 0;
virtual ResumeDataStorageType resumeDataStorageType() const = 0;
virtual void setResumeDataStorageType(ResumeDataStorageType type) = 0;
virtual bool isMergeTrackersEnabled() const = 0;
virtual void setMergeTrackersEnabled(bool enabled) = 0;
virtual bool isStartPaused() const = 0;
virtual void setStartPaused(bool value) = 0;
virtual TorrentContentRemoveOption torrentContentRemoveOption() const = 0;
virtual void setTorrentContentRemoveOption(TorrentContentRemoveOption option) = 0;
virtual bool isRestored() const = 0;
virtual bool isPaused() const = 0;
virtual void pause() = 0;
virtual void resume() = 0;
virtual Torrent *getTorrent(const TorrentID &id) const = 0;
virtual Torrent *findTorrent(const InfoHash &infoHash) const = 0;
virtual QVector<Torrent *> torrents() const = 0;
@@ -444,7 +456,7 @@ namespace BitTorrent
virtual bool isKnownTorrent(const InfoHash &infoHash) const = 0;
virtual bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams &params = {}) = 0;
virtual bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteOption::DeleteTorrent) = 0;
virtual bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) = 0;
virtual bool downloadMetadata(const TorrentDescriptor &torrentDescr) = 0;
virtual bool cancelDownloadMetadata(const TorrentID &id) = 0;
@@ -465,6 +477,8 @@ namespace BitTorrent
void loadTorrentFailed(const QString &error);
void metadataDownloaded(const TorrentInfo &info);
void restored();
void paused();
void resumed();
void speedLimitModeChanged(bool alternative);
void statsUpdated();
void subcategoriesSupportChanged();
@@ -476,8 +490,8 @@ namespace BitTorrent
void torrentFinished(Torrent *torrent);
void torrentFinishedChecking(Torrent *torrent);
void torrentMetadataReceived(Torrent *torrent);
void torrentPaused(Torrent *torrent);
void torrentResumed(Torrent *torrent);
void torrentStopped(Torrent *torrent);
void torrentStarted(Torrent *torrent);
void torrentSavePathChanged(Torrent *torrent);
void torrentSavingModeChanged(Torrent *torrent);
void torrentsLoaded(const QVector<Torrent *> &torrents);
@@ -490,6 +504,6 @@ namespace BitTorrent
void trackersRemoved(Torrent *torrent, const QStringList &trackers);
void trackerSuccess(Torrent *torrent, const QString &tracker);
void trackerWarning(Torrent *torrent, const QString &tracker);
void trackerEntriesUpdated(Torrent *torrent, const QHash<QString, TrackerEntry> &updatedTrackerEntries);
void trackerEntryStatusesUpdated(Torrent *torrent, const QHash<QString, TrackerEntryStatus> &updatedTrackers);
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -54,7 +54,7 @@
#include "session.h"
#include "sessionstatus.h"
#include "torrentinfo.h"
#include "trackerentry.h"
#include "trackerentrystatus.h"
class QString;
class QThread;
@@ -69,16 +69,19 @@ class NativeSessionExtension;
namespace BitTorrent
{
enum class MoveStorageMode;
enum class MoveStorageContext;
class InfoHash;
class ResumeDataStorage;
class Torrent;
class TorrentContentRemover;
class TorrentDescriptor;
class TorrentImpl;
class Tracker;
struct LoadTorrentParams;
enum class MoveStorageMode;
enum class MoveStorageContext;
struct LoadTorrentParams;
struct TrackerEntry;
struct SessionMetricIndices
{
@@ -189,8 +192,8 @@ namespace BitTorrent
void setPeXEnabled(bool enabled) override;
bool isAddTorrentToQueueTop() const override;
void setAddTorrentToQueueTop(bool value) override;
bool isAddTorrentPaused() const override;
void setAddTorrentPaused(bool value) override;
bool isAddTorrentStopped() const override;
void setAddTorrentStopped(bool value) override;
Torrent::StopCondition torrentStopCondition() const override;
void setTorrentStopCondition(Torrent::StopCondition stopCondition) override;
TorrentContentLayout torrentContentLayout() const override;
@@ -231,6 +234,8 @@ namespace BitTorrent
void setPerformanceWarningEnabled(bool enable) override;
int saveResumeDataInterval() const override;
void setSaveResumeDataInterval(int value) override;
int shutdownTimeout() const override;
void setShutdownTimeout(int value) override;
int port() const override;
void setPort(int port) override;
bool isSSLEnabled() const override;
@@ -398,16 +403,24 @@ namespace BitTorrent
void setExcludedFileNamesEnabled(bool enabled) override;
QStringList excludedFileNames() const override;
void setExcludedFileNames(const QStringList &excludedFileNames) override;
bool isFilenameExcluded(const QString &fileName) const override;
void applyFilenameFilter(const PathList &files, QList<BitTorrent::DownloadPriority> &priorities) override;
QStringList bannedIPs() const override;
void setBannedIPs(const QStringList &newList) override;
ResumeDataStorageType resumeDataStorageType() const override;
void setResumeDataStorageType(ResumeDataStorageType type) override;
bool isMergeTrackersEnabled() const override;
void setMergeTrackersEnabled(bool enabled) override;
bool isStartPaused() const override;
void setStartPaused(bool value) override;
TorrentContentRemoveOption torrentContentRemoveOption() const override;
void setTorrentContentRemoveOption(TorrentContentRemoveOption option) override;
bool isRestored() const override;
bool isPaused() const override;
void pause() override;
void resume() override;
Torrent *getTorrent(const TorrentID &id) const override;
Torrent *findTorrent(const InfoHash &infoHash) const override;
QVector<Torrent *> torrents() const override;
@@ -420,7 +433,7 @@ namespace BitTorrent
bool isKnownTorrent(const InfoHash &infoHash) const override;
bool addTorrent(const TorrentDescriptor &torrentDescr, const AddTorrentParams &params = {}) override;
bool deleteTorrent(const TorrentID &id, DeleteOption deleteOption = DeleteTorrent) override;
bool removeTorrent(const TorrentID &id, TorrentRemoveOption deleteOption = TorrentRemoveOption::KeepContent) override;
bool downloadMetadata(const TorrentDescriptor &torrentDescr) override;
bool cancelDownloadMetadata(const TorrentID &id) override;
@@ -439,8 +452,8 @@ namespace BitTorrent
void handleTorrentTagRemoved(TorrentImpl *torrent, const Tag &tag);
void handleTorrentSavingModeChanged(TorrentImpl *torrent);
void handleTorrentMetadataReceived(TorrentImpl *torrent);
void handleTorrentPaused(TorrentImpl *torrent);
void handleTorrentResumed(TorrentImpl *torrent);
void handleTorrentStopped(TorrentImpl *torrent);
void handleTorrentStarted(TorrentImpl *torrent);
void handleTorrentChecked(TorrentImpl *torrent);
void handleTorrentFinished(TorrentImpl *torrent);
void handleTorrentTrackersAdded(TorrentImpl *torrent, const QVector<TrackerEntry> &newTrackers);
@@ -477,11 +490,11 @@ namespace BitTorrent
void configureDeferred();
void readAlerts();
void enqueueRefresh();
void processShareLimits();
void generateResumeData();
void handleIPFilterParsed(int ruleCount);
void handleIPFilterError();
void fileSearchFinished(const TorrentID &id, const Path &savePath, const PathList &fileNames);
void torrentContentRemovingFinished(const QString &torrentName, const QString &errorMessage);
private:
struct ResumeSessionContext;
@@ -497,8 +510,9 @@ namespace BitTorrent
struct RemovingTorrentData
{
QString name;
Path pathToRemove;
DeleteOption deleteOption {};
Path contentStoragePath;
PathList fileNames;
TorrentRemoveOption removeOption {};
};
explicit SessionImpl(QObject *parent = nullptr);
@@ -526,6 +540,7 @@ namespace BitTorrent
void enableIPFilter();
void disableIPFilter();
void processTrackerStatuses();
void processTorrentShareLimits(TorrentImpl *torrent);
void populateExcludedFileNamesRegExpList();
void prepareStartup();
void handleLoadedResumeData(ResumeSessionContext *context);
@@ -538,34 +553,34 @@ namespace BitTorrent
void updateSeedingLimitTimer();
void exportTorrentFile(const Torrent *torrent, const Path &folderPath);
void handleAlert(const lt::alert *a);
void handleAddTorrentAlerts(const std::vector<lt::alert *> &alerts);
void dispatchTorrentAlert(const lt::torrent_alert *a);
void handleStateUpdateAlert(const lt::state_update_alert *p);
void handleMetadataReceivedAlert(const lt::metadata_received_alert *p);
void handleFileErrorAlert(const lt::file_error_alert *p);
void handleTorrentRemovedAlert(const lt::torrent_removed_alert *p);
void handleTorrentDeletedAlert(const lt::torrent_deleted_alert *p);
void handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *p);
void handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *a);
void handlePortmapWarningAlert(const lt::portmap_error_alert *p);
void handlePortmapAlert(const lt::portmap_alert *p);
void handlePeerBlockedAlert(const lt::peer_blocked_alert *p);
void handlePeerBanAlert(const lt::peer_ban_alert *p);
void handleUrlSeedAlert(const lt::url_seed_alert *p);
void handleListenSucceededAlert(const lt::listen_succeeded_alert *p);
void handleListenFailedAlert(const lt::listen_failed_alert *p);
void handleExternalIPAlert(const lt::external_ip_alert *p);
void handleSessionErrorAlert(const lt::session_error_alert *p) const;
void handleSessionStatsAlert(const lt::session_stats_alert *p);
void handleAlertsDroppedAlert(const lt::alerts_dropped_alert *p) const;
void handleStorageMovedAlert(const lt::storage_moved_alert *p);
void handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *p);
void handleSocks5Alert(const lt::socks5_alert *p) const;
void handleI2PAlert(const lt::i2p_alert *p) const;
void handleTrackerAlert(const lt::tracker_alert *a);
void handleAlert(const lt::alert *alert);
void dispatchTorrentAlert(const lt::torrent_alert *alert);
void handleAddTorrentAlert(const lt::add_torrent_alert *alert);
void handleStateUpdateAlert(const lt::state_update_alert *alert);
void handleMetadataReceivedAlert(const lt::metadata_received_alert *alert);
void handleFileErrorAlert(const lt::file_error_alert *alert);
void handleTorrentRemovedAlert(const lt::torrent_removed_alert *alert);
void handleTorrentDeletedAlert(const lt::torrent_deleted_alert *alert);
void handleTorrentDeleteFailedAlert(const lt::torrent_delete_failed_alert *alert);
void handleTorrentNeedCertAlert(const lt::torrent_need_cert_alert *alert);
void handlePortmapWarningAlert(const lt::portmap_error_alert *alert);
void handlePortmapAlert(const lt::portmap_alert *alert);
void handlePeerBlockedAlert(const lt::peer_blocked_alert *alert);
void handlePeerBanAlert(const lt::peer_ban_alert *alert);
void handleUrlSeedAlert(const lt::url_seed_alert *alert);
void handleListenSucceededAlert(const lt::listen_succeeded_alert *alert);
void handleListenFailedAlert(const lt::listen_failed_alert *alert);
void handleExternalIPAlert(const lt::external_ip_alert *alert);
void handleSessionErrorAlert(const lt::session_error_alert *alert) const;
void handleSessionStatsAlert(const lt::session_stats_alert *alert);
void handleAlertsDroppedAlert(const lt::alerts_dropped_alert *alert) const;
void handleStorageMovedAlert(const lt::storage_moved_alert *alert);
void handleStorageMovedFailedAlert(const lt::storage_moved_failed_alert *alert);
void handleSocks5Alert(const lt::socks5_alert *alert) const;
void handleI2PAlert(const lt::i2p_alert *alert) const;
void handleTrackerAlert(const lt::tracker_alert *alert);
#ifdef QBT_USES_LIBTORRENT2
void handleTorrentConflictAlert(const lt::torrent_conflict_alert *a);
void handleTorrentConflictAlert(const lt::torrent_conflict_alert *alert);
#endif
TorrentImpl *createTorrent(const lt::torrent_handle &nativeHandle, const LoadTorrentParams &params);
@@ -587,15 +602,9 @@ namespace BitTorrent
void saveStatistics() const;
void loadStatistics();
void updateTrackerEntries(lt::torrent_handle torrentHandle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers);
void updateTrackerEntryStatuses(lt::torrent_handle torrentHandle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>> updatedTrackers);
// BitTorrent
lt::session *m_nativeSession = nullptr;
NativeSessionExtension *m_nativeSessionExtension = nullptr;
bool m_deferredConfigureScheduled = false;
bool m_IPFilteringConfigured = false;
mutable bool m_listenInterfaceConfigured = false;
void handleRemovedTorrent(const TorrentID &torrentID, const QString &partfileRemoveError = {});
CachedSettingValue<QString> m_DHTBootstrapNodes;
CachedSettingValue<bool> m_isDHTEnabled;
@@ -663,7 +672,7 @@ namespace BitTorrent
CachedSettingValue<int> m_globalMaxSeedingMinutes;
CachedSettingValue<int> m_globalMaxInactiveSeedingMinutes;
CachedSettingValue<bool> m_isAddTorrentToQueueTop;
CachedSettingValue<bool> m_isAddTorrentPaused;
CachedSettingValue<bool> m_isAddTorrentStopped;
CachedSettingValue<Torrent::StopCondition> m_torrentStopCondition;
CachedSettingValue<TorrentContentLayout> m_torrentContentLayout;
CachedSettingValue<bool> m_isAppendExtensionEnabled;
@@ -680,6 +689,7 @@ namespace BitTorrent
CachedSettingValue<bool> m_isBandwidthSchedulerEnabled;
CachedSettingValue<bool> m_isPerformanceWarningEnabled;
CachedSettingValue<int> m_saveResumeDataInterval;
CachedSettingValue<int> m_shutdownTimeout;
CachedSettingValue<int> m_port;
CachedSettingValue<bool> m_sslEnabled;
CachedSettingValue<int> m_sslPort;
@@ -720,8 +730,18 @@ namespace BitTorrent
CachedSettingValue<int> m_I2POutboundQuantity;
CachedSettingValue<int> m_I2PInboundLength;
CachedSettingValue<int> m_I2POutboundLength;
CachedSettingValue<TorrentContentRemoveOption> m_torrentContentRemoveOption;
SettingValue<bool> m_startPaused;
lt::session *m_nativeSession = nullptr;
NativeSessionExtension *m_nativeSessionExtension = nullptr;
bool m_deferredConfigureScheduled = false;
bool m_IPFilteringConfigured = false;
mutable bool m_listenInterfaceConfigured = false;
bool m_isRestored = false;
bool m_isPaused = isStartPaused();
// Order is important. This needs to be declared after its CachedSettingsValue
// counterpart, because it uses it for initialization in the constructor
@@ -729,7 +749,7 @@ namespace BitTorrent
const bool m_wasPexEnabled = m_isPeXEnabled;
int m_numResumeData = 0;
QVector<TrackerEntry> m_additionalTrackerList;
QVector<TrackerEntry> m_additionalTrackerEntries;
QVector<QRegularExpression> m_excludedFileNamesRegExpList;
// Statistics
@@ -753,6 +773,7 @@ namespace BitTorrent
QThreadPool *m_asyncWorker = nullptr;
ResumeDataStorage *m_resumeDataStorage = nullptr;
FileSearcher *m_fileSearcher = nullptr;
TorrentContentRemover *m_torrentContentRemover = nullptr;
QHash<TorrentID, lt::torrent_handle> m_downloadedMetadata;
@@ -764,9 +785,12 @@ namespace BitTorrent
QMap<QString, CategoryOptions> m_categories;
TagSet m_tags;
qsizetype m_receivedAddTorrentAlertsCount = 0;
QList<Torrent *> m_loadedTorrents;
// This field holds amounts of peers reported by trackers in their responses to announces
// (torrent.tracker_name.tracker_local_endpoint.protocol_version.num_peers)
QHash<lt::torrent_handle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>>> m_updatedTrackerEntries;
QHash<lt::torrent_handle, QHash<std::string, QHash<lt::tcp::endpoint, QMap<int, int>>>> m_updatedTrackerStatuses;
// I/O errored torrents
QSet<TorrentID> m_recentErroredTorrents;
@@ -793,6 +817,8 @@ namespace BitTorrent
QTimer *m_wakeupCheckTimer = nullptr;
QDateTime m_wakeupCheckTimestamp;
QList<TorrentImpl *> m_pendingFinishedTorrents;
friend void Session::initInstance();
friend void Session::freeInstance();
friend Session *Session::instance();

View File

@@ -60,9 +60,9 @@ namespace BitTorrent
return infoHash().toTorrentID();
}
bool Torrent::isResumed() const
bool Torrent::isRunning() const
{
return !isPaused();
return !isStopped();
}
qlonglong Torrent::remainingSize() const

View File

@@ -48,14 +48,17 @@ class QUrl;
namespace BitTorrent
{
enum class DownloadPriority;
class InfoHash;
class PeerInfo;
class Session;
class TorrentID;
class TorrentInfo;
struct PeerAddress;
struct SSLParameters;
struct TrackerEntry;
struct TrackerEntryStatus;
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
@@ -94,8 +97,8 @@ namespace BitTorrent
CheckingUploading,
CheckingDownloading,
PausedDownloading,
PausedUploading,
StoppedDownloading,
StoppedUploading,
Moving,
@@ -225,10 +228,11 @@ namespace BitTorrent
virtual void setShareLimitAction(ShareLimitAction action) = 0;
virtual PathList filePaths() const = 0;
virtual PathList actualFilePaths() const = 0;
virtual TorrentInfo info() const = 0;
virtual bool isFinished() const = 0;
virtual bool isPaused() const = 0;
virtual bool isStopped() const = 0;
virtual bool isQueued() const = 0;
virtual bool isForced() const = 0;
virtual bool isChecking() const = 0;
@@ -245,7 +249,7 @@ namespace BitTorrent
virtual bool hasMissingFiles() const = 0;
virtual bool hasError() const = 0;
virtual int queuePosition() const = 0;
virtual QVector<TrackerEntry> trackers() const = 0;
virtual QVector<TrackerEntryStatus> trackers() const = 0;
virtual QVector<QUrl> urlSeeds() const = 0;
virtual QString error() const = 0;
virtual qlonglong totalDownload() const = 0;
@@ -279,6 +283,7 @@ namespace BitTorrent
virtual int maxSeedingTime() const = 0;
virtual int maxInactiveSeedingTime() const = 0;
virtual qreal realRatio() const = 0;
virtual qreal popularity() const = 0;
virtual int uploadPayloadRate() const = 0;
virtual int downloadPayloadRate() const = 0;
virtual qlonglong totalPayloadUpload() const = 0;
@@ -290,8 +295,8 @@ namespace BitTorrent
virtual void setName(const QString &name) = 0;
virtual void setSequentialDownload(bool enable) = 0;
virtual void setFirstLastPiecePriority(bool enabled) = 0;
virtual void pause() = 0;
virtual void resume(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) = 0;
virtual void stop() = 0;
virtual void start(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) = 0;
virtual void forceReannounce(int index = -1) = 0;
virtual void forceDHTAnnounce() = 0;
virtual void forceRecheck() = 0;
@@ -325,7 +330,7 @@ namespace BitTorrent
virtual void fetchDownloadingPieces(std::function<void (QBitArray)> resultHandler) const = 0;
TorrentID id() const;
bool isResumed() const;
bool isRunning() const;
qlonglong remainingSize() const;
void toggleSequentialDownload();

View File

@@ -0,0 +1,50 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QMetaEnum>
namespace BitTorrent
{
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
inline namespace TorrentContentRemoveOptionNS
{
Q_NAMESPACE
enum class TorrentContentRemoveOption
{
Delete,
MoveToTrash
};
Q_ENUM_NS(TorrentContentRemoveOption)
}
}

View File

@@ -0,0 +1,61 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "torrentcontentremover.h"
#include "base/utils/fs.h"
void BitTorrent::TorrentContentRemover::performJob(const QString &torrentName, const Path &basePath
, const PathList &fileNames, const TorrentContentRemoveOption option)
{
QString errorMessage;
if (!fileNames.isEmpty())
{
const auto removeFileFn = [&option](const Path &filePath)
{
return ((option == TorrentContentRemoveOption::MoveToTrash)
? Utils::Fs::moveFileToTrash : Utils::Fs::removeFile)(filePath);
};
for (const Path &fileName : fileNames)
{
if (const auto result = removeFileFn(basePath / fileName)
; !result && errorMessage.isEmpty())
{
errorMessage = result.error();
}
}
const Path rootPath = Path::findRootFolder(fileNames);
if (!rootPath.isEmpty())
Utils::Fs::smartRemoveEmptyFolderTree(basePath / rootPath);
}
emit jobFinished(torrentName, errorMessage);
}

View File

@@ -0,0 +1,53 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QObject>
#include "base/path.h"
#include "torrentcontentremoveoption.h"
namespace BitTorrent
{
class TorrentContentRemover final : public QObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(TorrentContentRemover)
public:
using QObject::QObject;
public slots:
void performJob(const QString &torrentName, const Path &basePath
, const PathList &fileNames, TorrentContentRemoveOption option);
signals:
void jobFinished(const QString &torrentName, const QString &errorMessage);
};
}

View File

@@ -71,11 +71,16 @@
#include "peeraddress.h"
#include "peerinfo.h"
#include "sessionimpl.h"
#include "trackerentry.h"
#if defined(Q_OS_MACOS) || defined(Q_OS_WIN)
#include "base/utils/os.h"
#endif // Q_OS_MACOS || Q_OS_WIN
#ifndef QBT_USES_LIBTORRENT2
#include "customstorage.h"
#endif
using namespace BitTorrent;
namespace
@@ -101,15 +106,15 @@ namespace
return QString::fromStdString((std::stringstream() << ltTCPEndpoint).str());
}
void updateTrackerEntry(TrackerEntry &trackerEntry, const lt::announce_entry &nativeEntry
void updateTrackerEntryStatus(TrackerEntryStatus &trackerEntryStatus, const lt::announce_entry &nativeEntry
, const QSet<int> &btProtocols, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
{
Q_ASSERT(trackerEntry.url == QString::fromStdString(nativeEntry.url));
Q_ASSERT(trackerEntryStatus.url == QString::fromStdString(nativeEntry.url));
trackerEntry.tier = nativeEntry.tier;
trackerEntryStatus.tier = nativeEntry.tier;
// remove outdated endpoints
trackerEntry.endpointEntries.removeIf([&nativeEntry](const QHash<std::pair<QString, int>, TrackerEndpointEntry>::iterator &iter)
trackerEntryStatus.endpoints.removeIf([&nativeEntry](const QHash<std::pair<QString, int>, TrackerEndpointStatus>::iterator &iter)
{
return std::none_of(nativeEntry.endpoints.cbegin(), nativeEntry.endpoints.cend()
, [&endpointName = std::get<0>(iter.key())](const auto &existingEndpoint)
@@ -141,61 +146,61 @@ namespace
const lt::announce_endpoint &ltAnnounceInfo = ltAnnounceEndpoint;
#endif
const QMap<int, int> &endpointUpdateInfo = updateInfo[ltAnnounceEndpoint.local_endpoint];
TrackerEndpointEntry &trackerEndpointEntry = trackerEntry.endpointEntries[std::make_pair(endpointName, protocolVersion)];
TrackerEndpointStatus &trackerEndpointStatus = trackerEntryStatus.endpoints[std::make_pair(endpointName, protocolVersion)];
trackerEndpointEntry.name = endpointName;
trackerEndpointEntry.btVersion = protocolVersion;
trackerEndpointEntry.numPeers = endpointUpdateInfo.value(protocolVersion, trackerEndpointEntry.numPeers);
trackerEndpointEntry.numSeeds = ltAnnounceInfo.scrape_complete;
trackerEndpointEntry.numLeeches = ltAnnounceInfo.scrape_incomplete;
trackerEndpointEntry.numDownloaded = ltAnnounceInfo.scrape_downloaded;
trackerEndpointEntry.nextAnnounceTime = fromLTTimePoint32(ltAnnounceInfo.next_announce);
trackerEndpointEntry.minAnnounceTime = fromLTTimePoint32(ltAnnounceInfo.min_announce);
trackerEndpointStatus.name = endpointName;
trackerEndpointStatus.btVersion = protocolVersion;
trackerEndpointStatus.numPeers = endpointUpdateInfo.value(protocolVersion, trackerEndpointStatus.numPeers);
trackerEndpointStatus.numSeeds = ltAnnounceInfo.scrape_complete;
trackerEndpointStatus.numLeeches = ltAnnounceInfo.scrape_incomplete;
trackerEndpointStatus.numDownloaded = ltAnnounceInfo.scrape_downloaded;
trackerEndpointStatus.nextAnnounceTime = fromLTTimePoint32(ltAnnounceInfo.next_announce);
trackerEndpointStatus.minAnnounceTime = fromLTTimePoint32(ltAnnounceInfo.min_announce);
if (ltAnnounceInfo.updating)
{
trackerEndpointEntry.status = TrackerEntryStatus::Updating;
trackerEndpointStatus.state = TrackerEndpointState::Updating;
++numUpdating;
}
else if (ltAnnounceInfo.fails > 0)
{
if (ltAnnounceInfo.last_error == lt::errors::tracker_failure)
{
trackerEndpointEntry.status = TrackerEntryStatus::TrackerError;
trackerEndpointStatus.state = TrackerEndpointState::TrackerError;
++numTrackerError;
}
else if (ltAnnounceInfo.last_error == lt::errors::announce_skipped)
{
trackerEndpointEntry.status = TrackerEntryStatus::Unreachable;
trackerEndpointStatus.state = TrackerEndpointState::Unreachable;
++numUnreachable;
}
else
{
trackerEndpointEntry.status = TrackerEntryStatus::NotWorking;
trackerEndpointStatus.state = TrackerEndpointState::NotWorking;
++numNotWorking;
}
}
else if (nativeEntry.verified)
{
trackerEndpointEntry.status = TrackerEntryStatus::Working;
trackerEndpointStatus.state = TrackerEndpointState::Working;
++numWorking;
}
else
{
trackerEndpointEntry.status = TrackerEntryStatus::NotContacted;
trackerEndpointStatus.state = TrackerEndpointState::NotContacted;
}
if (!ltAnnounceInfo.message.empty())
{
trackerEndpointEntry.message = QString::fromStdString(ltAnnounceInfo.message);
trackerEndpointStatus.message = QString::fromStdString(ltAnnounceInfo.message);
}
else if (ltAnnounceInfo.last_error)
{
trackerEndpointEntry.message = QString::fromLocal8Bit(ltAnnounceInfo.last_error.message());
trackerEndpointStatus.message = QString::fromLocal8Bit(ltAnnounceInfo.last_error.message());
}
else
{
trackerEndpointEntry.message.clear();
trackerEndpointStatus.message.clear();
}
}
}
@@ -204,58 +209,58 @@ namespace
{
if (numUpdating > 0)
{
trackerEntry.status = TrackerEntryStatus::Updating;
trackerEntryStatus.state = TrackerEndpointState::Updating;
}
else if (numWorking > 0)
{
trackerEntry.status = TrackerEntryStatus::Working;
trackerEntryStatus.state = TrackerEndpointState::Working;
}
else if (numTrackerError > 0)
{
trackerEntry.status = TrackerEntryStatus::TrackerError;
trackerEntryStatus.state = TrackerEndpointState::TrackerError;
}
else if (numUnreachable == numEndpoints)
{
trackerEntry.status = TrackerEntryStatus::Unreachable;
trackerEntryStatus.state = TrackerEndpointState::Unreachable;
}
else if ((numUnreachable + numNotWorking) == numEndpoints)
{
trackerEntry.status = TrackerEntryStatus::NotWorking;
trackerEntryStatus.state = TrackerEndpointState::NotWorking;
}
}
trackerEntry.numPeers = -1;
trackerEntry.numSeeds = -1;
trackerEntry.numLeeches = -1;
trackerEntry.numDownloaded = -1;
trackerEntry.nextAnnounceTime = QDateTime();
trackerEntry.minAnnounceTime = QDateTime();
trackerEntry.message.clear();
trackerEntryStatus.numPeers = -1;
trackerEntryStatus.numSeeds = -1;
trackerEntryStatus.numLeeches = -1;
trackerEntryStatus.numDownloaded = -1;
trackerEntryStatus.nextAnnounceTime = QDateTime();
trackerEntryStatus.minAnnounceTime = QDateTime();
trackerEntryStatus.message.clear();
for (const TrackerEndpointEntry &endpointEntry : asConst(trackerEntry.endpointEntries))
for (const TrackerEndpointStatus &endpointStatus : asConst(trackerEntryStatus.endpoints))
{
trackerEntry.numPeers = std::max(trackerEntry.numPeers, endpointEntry.numPeers);
trackerEntry.numSeeds = std::max(trackerEntry.numSeeds, endpointEntry.numSeeds);
trackerEntry.numLeeches = std::max(trackerEntry.numLeeches, endpointEntry.numLeeches);
trackerEntry.numDownloaded = std::max(trackerEntry.numDownloaded, endpointEntry.numDownloaded);
trackerEntryStatus.numPeers = std::max(trackerEntryStatus.numPeers, endpointStatus.numPeers);
trackerEntryStatus.numSeeds = std::max(trackerEntryStatus.numSeeds, endpointStatus.numSeeds);
trackerEntryStatus.numLeeches = std::max(trackerEntryStatus.numLeeches, endpointStatus.numLeeches);
trackerEntryStatus.numDownloaded = std::max(trackerEntryStatus.numDownloaded, endpointStatus.numDownloaded);
if (endpointEntry.status == trackerEntry.status)
if (endpointStatus.state == trackerEntryStatus.state)
{
if (!trackerEntry.nextAnnounceTime.isValid() || (trackerEntry.nextAnnounceTime > endpointEntry.nextAnnounceTime))
if (!trackerEntryStatus.nextAnnounceTime.isValid() || (trackerEntryStatus.nextAnnounceTime > endpointStatus.nextAnnounceTime))
{
trackerEntry.nextAnnounceTime = endpointEntry.nextAnnounceTime;
trackerEntry.minAnnounceTime = endpointEntry.minAnnounceTime;
if ((endpointEntry.status != TrackerEntryStatus::Working)
|| !endpointEntry.message.isEmpty())
trackerEntryStatus.nextAnnounceTime = endpointStatus.nextAnnounceTime;
trackerEntryStatus.minAnnounceTime = endpointStatus.minAnnounceTime;
if ((endpointStatus.state != TrackerEndpointState::Working)
|| !endpointStatus.message.isEmpty())
{
trackerEntry.message = endpointEntry.message;
trackerEntryStatus.message = endpointStatus.message;
}
}
if (endpointEntry.status == TrackerEntryStatus::Working)
if (endpointStatus.state == TrackerEndpointState::Working)
{
if (trackerEntry.message.isEmpty())
trackerEntry.message = endpointEntry.message;
if (trackerEntryStatus.message.isEmpty())
trackerEntryStatus.message = endpointStatus.message;
}
}
}
@@ -347,9 +352,9 @@ TorrentImpl::TorrentImpl(SessionImpl *session, lt::session *nativeSession
setStopCondition(params.stopCondition);
const auto *extensionData = static_cast<ExtensionData *>(m_ltAddTorrentParams.userdata);
m_trackerEntries.reserve(static_cast<decltype(m_trackerEntries)::size_type>(extensionData->trackers.size()));
m_trackerEntryStatuses.reserve(static_cast<decltype(m_trackerEntryStatuses)::size_type>(extensionData->trackers.size()));
for (const lt::announce_entry &announceEntry : extensionData->trackers)
m_trackerEntries.append({QString::fromStdString(announceEntry.url), announceEntry.tier});
m_trackerEntryStatuses.append({QString::fromStdString(announceEntry.url), announceEntry.tier});
m_urlSeeds.reserve(static_cast<decltype(m_urlSeeds)::size_type>(extensionData->urlSeeds.size()));
for (const std::string &urlSeed : extensionData->urlSeeds)
m_urlSeeds.append(QString::fromStdString(urlSeed));
@@ -455,6 +460,8 @@ Path TorrentImpl::savePath() const
void TorrentImpl::setSavePath(const Path &path)
{
Q_ASSERT(!isAutoTMMEnabled());
if (isAutoTMMEnabled()) [[unlikely]]
return;
const Path basePath = m_session->useCategoryPathsInManualMode()
? m_session->categorySavePath(category()) : m_session->savePath();
@@ -482,6 +489,8 @@ Path TorrentImpl::downloadPath() const
void TorrentImpl::setDownloadPath(const Path &path)
{
Q_ASSERT(!isAutoTMMEnabled());
if (isAutoTMMEnabled()) [[unlikely]]
return;
const Path basePath = m_session->useCategoryPathsInManualMode()
? m_session->categoryDownloadPath(category()) : m_session->downloadPath();
@@ -601,27 +610,32 @@ Path TorrentImpl::makeUserPath(const Path &path) const
return userPath;
}
QVector<TrackerEntry> TorrentImpl::trackers() const
QVector<TrackerEntryStatus> TorrentImpl::trackers() const
{
return m_trackerEntries;
return m_trackerEntryStatuses;
}
void TorrentImpl::addTrackers(QVector<TrackerEntry> trackers)
{
trackers.removeIf([](const TrackerEntry &entry) { return entry.url.isEmpty(); });
trackers.removeIf([](const TrackerEntry &trackerEntry) { return trackerEntry.url.isEmpty(); });
const auto newTrackers = QSet<TrackerEntry>(trackers.cbegin(), trackers.cend())
- QSet<TrackerEntry>(m_trackerEntries.cbegin(), m_trackerEntries.cend());
if (newTrackers.isEmpty())
QSet<TrackerEntry> currentTrackerSet;
currentTrackerSet.reserve(m_trackerEntryStatuses.size());
for (const TrackerEntryStatus &status : asConst(m_trackerEntryStatuses))
currentTrackerSet.insert({.url = status.url, .tier = status.tier});
const auto newTrackerSet = QSet<TrackerEntry>(trackers.cbegin(), trackers.cend()) - currentTrackerSet;
if (newTrackerSet.isEmpty())
return;
trackers = QVector<TrackerEntry>(newTrackers.cbegin(), newTrackers.cend());
for (const TrackerEntry &tracker : trackers)
trackers = QVector<TrackerEntry>(newTrackerSet.cbegin(), newTrackerSet.cend());
for (const TrackerEntry &tracker : asConst(trackers))
{
m_nativeHandle.add_tracker(makeNativeAnnounceEntry(tracker.url, tracker.tier));
m_trackerEntries.append(trackers);
std::sort(m_trackerEntries.begin(), m_trackerEntries.end()
, [](const TrackerEntry &lhs, const TrackerEntry &rhs) { return lhs.tier < rhs.tier; });
m_trackerEntryStatuses.append({tracker.url, tracker.tier});
}
std::sort(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
, [](const TrackerEntryStatus &left, const TrackerEntryStatus &right) { return left.tier < right.tier; });
deferredRequestResumeData();
m_session->handleTorrentTrackersAdded(this, trackers);
@@ -632,13 +646,13 @@ void TorrentImpl::removeTrackers(const QStringList &trackers)
QStringList removedTrackers = trackers;
for (const QString &tracker : trackers)
{
if (!m_trackerEntries.removeOne({tracker}))
if (!m_trackerEntryStatuses.removeOne({tracker}))
removedTrackers.removeOne(tracker);
}
std::vector<lt::announce_entry> nativeTrackers;
nativeTrackers.reserve(m_trackerEntries.size());
for (const TrackerEntry &tracker : asConst(m_trackerEntries))
nativeTrackers.reserve(m_trackerEntryStatuses.size());
for (const TrackerEntryStatus &tracker : asConst(m_trackerEntryStatuses))
nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));
if (!removedTrackers.isEmpty())
@@ -652,20 +666,25 @@ void TorrentImpl::removeTrackers(const QStringList &trackers)
void TorrentImpl::replaceTrackers(QVector<TrackerEntry> trackers)
{
trackers.removeIf([](const TrackerEntry &entry) { return entry.url.isEmpty(); });
trackers.removeIf([](const TrackerEntry &trackerEntry) { return trackerEntry.url.isEmpty(); });
// Filter out duplicate trackers
const auto uniqueTrackers = QSet<TrackerEntry>(trackers.cbegin(), trackers.cend());
trackers = QVector<TrackerEntry>(uniqueTrackers.cbegin(), uniqueTrackers.cend());
std::sort(trackers.begin(), trackers.end()
, [](const TrackerEntry &lhs, const TrackerEntry &rhs) { return lhs.tier < rhs.tier; });
, [](const TrackerEntry &left, const TrackerEntry &right) { return left.tier < right.tier; });
std::vector<lt::announce_entry> nativeTrackers;
nativeTrackers.reserve(trackers.size());
m_trackerEntryStatuses.clear();
for (const TrackerEntry &tracker : trackers)
{
nativeTrackers.emplace_back(makeNativeAnnounceEntry(tracker.url, tracker.tier));
m_trackerEntryStatuses.append({tracker.url, tracker.tier});
}
m_nativeHandle.replace_trackers(nativeTrackers);
m_trackerEntries = trackers;
// Clear the peer list if it's a private torrent since
// we do not want to keep connecting with peers from old tracker.
@@ -971,6 +990,21 @@ PathList TorrentImpl::filePaths() const
return m_filePaths;
}
PathList TorrentImpl::actualFilePaths() const
{
if (!hasMetadata())
return {};
PathList paths;
paths.reserve(filesCount());
const lt::file_storage files = nativeTorrentInfo()->files();
for (const lt::file_index_t &nativeIndex : asConst(m_torrentInfo.nativeIndexes()))
paths.emplaceBack(files.file_path(nativeIndex));
return paths;
}
QVector<DownloadPriority> TorrentImpl::filePriorities() const
{
return m_filePriorities;
@@ -981,15 +1015,18 @@ TorrentInfo TorrentImpl::info() const
return m_torrentInfo;
}
bool TorrentImpl::isPaused() const
bool TorrentImpl::isStopped() const
{
return m_isStopped;
}
bool TorrentImpl::isQueued() const
{
// Torrent is Queued if it isn't in Paused state but paused internally
return (!isPaused()
if (!m_session->isQueueingSystemEnabled())
return false;
// Torrent is Queued if it isn't in Stopped state but paused internally
return (!isStopped()
&& (m_nativeStatus.flags & lt::torrent_flags::auto_managed)
&& (m_nativeStatus.flags & lt::torrent_flags::paused));
}
@@ -1009,7 +1046,7 @@ bool TorrentImpl::isDownloading() const
case TorrentState::ForcedDownloadingMetadata:
case TorrentState::StalledDownloading:
case TorrentState::CheckingDownloading:
case TorrentState::PausedDownloading:
case TorrentState::StoppedDownloading:
case TorrentState::QueuedDownloading:
case TorrentState::ForcedDownloading:
return true;
@@ -1049,7 +1086,7 @@ bool TorrentImpl::isCompleted() const
case TorrentState::Uploading:
case TorrentState::StalledUploading:
case TorrentState::CheckingUploading:
case TorrentState::PausedUploading:
case TorrentState::StoppedUploading:
case TorrentState::QueuedUploading:
case TorrentState::ForcedUploading:
return true;
@@ -1102,7 +1139,7 @@ bool TorrentImpl::isFinished() const
bool TorrentImpl::isForced() const
{
return (!isPaused() && (m_operatingMode == TorrentOperatingMode::Forced));
return (!isStopped() && (m_operatingMode == TorrentOperatingMode::Forced));
}
bool TorrentImpl::isSequentialDownload() const
@@ -1140,23 +1177,23 @@ void TorrentImpl::updateState()
}
else if (!hasMetadata())
{
if (isPaused())
m_state = TorrentState::PausedDownloading;
else if (m_session->isQueueingSystemEnabled() && isQueued())
if (isStopped())
m_state = TorrentState::StoppedDownloading;
else if (isQueued())
m_state = TorrentState::QueuedDownloading;
else
m_state = isForced() ? TorrentState::ForcedDownloadingMetadata : TorrentState::DownloadingMetadata;
}
else if ((m_nativeStatus.state == lt::torrent_status::checking_files) && !isPaused())
else if ((m_nativeStatus.state == lt::torrent_status::checking_files) && !isStopped())
{
// If the torrent is not just in the "checking" state, but is being actually checked
m_state = m_hasFinishedStatus ? TorrentState::CheckingUploading : TorrentState::CheckingDownloading;
}
else if (isFinished())
{
if (isPaused())
m_state = TorrentState::PausedUploading;
else if (m_session->isQueueingSystemEnabled() && isQueued())
if (isStopped())
m_state = TorrentState::StoppedUploading;
else if (isQueued())
m_state = TorrentState::QueuedUploading;
else if (isForced())
m_state = TorrentState::ForcedUploading;
@@ -1167,9 +1204,9 @@ void TorrentImpl::updateState()
}
else
{
if (isPaused())
m_state = TorrentState::PausedDownloading;
else if (m_session->isQueueingSystemEnabled() && isQueued())
if (isStopped())
m_state = TorrentState::StoppedDownloading;
else if (isQueued())
m_state = TorrentState::QueuedDownloading;
else if (isForced())
m_state = TorrentState::ForcedDownloading;
@@ -1236,7 +1273,7 @@ qlonglong TorrentImpl::finishedTime() const
qlonglong TorrentImpl::eta() const
{
if (isPaused()) return MAX_ETA;
if (isStopped()) return MAX_ETA;
const SpeedSampleAvg speedAverage = m_payloadRateMonitor.average();
@@ -1433,11 +1470,13 @@ QBitArray TorrentImpl::pieces() const
QBitArray TorrentImpl::downloadingPieces() const
{
QBitArray result(piecesCount());
if (!hasMetadata())
return {};
std::vector<lt::partial_piece_info> queue;
m_nativeHandle.get_download_queue(queue);
QBitArray result {piecesCount()};
for (const lt::partial_piece_info &info : queue)
result.setBit(LT::toUnderlyingType(info.piece_index));
@@ -1499,14 +1538,14 @@ qreal TorrentImpl::realRatio() const
int TorrentImpl::uploadPayloadRate() const
{
// workaround: suppress the speed for paused state
return isPaused() ? 0 : m_nativeStatus.upload_payload_rate;
// workaround: suppress the speed for Stopped state
return isStopped() ? 0 : m_nativeStatus.upload_payload_rate;
}
int TorrentImpl::downloadPayloadRate() const
{
// workaround: suppress the speed for paused state
return isPaused() ? 0 : m_nativeStatus.download_payload_rate;
// workaround: suppress the speed for Stopped state
return isStopped() ? 0 : m_nativeStatus.download_payload_rate;
}
qlonglong TorrentImpl::totalPayloadUpload() const
@@ -1534,6 +1573,15 @@ qlonglong TorrentImpl::nextAnnounce() const
return lt::total_seconds(m_nativeStatus.next_announce);
}
qreal TorrentImpl::popularity() const
{
// in order to produce floating-point numbers using `std::chrono::duration_cast`,
// we should use `qreal` as `Rep` to define the `months` duration
using months = std::chrono::duration<qreal, std::chrono::months::period>;
const auto activeMonths = std::chrono::duration_cast<months>(m_nativeStatus.active_duration).count();
return (activeMonths > 0) ? (realRatio() / activeMonths) : 0;
}
void TorrentImpl::setName(const QString &name)
{
if (m_name != name)
@@ -1588,7 +1636,17 @@ void TorrentImpl::forceRecheck()
// an incorrect one during the interval until the cached state is updated in a regular way.
m_nativeStatus.state = lt::torrent_status::checking_resume_data;
m_hasMissingFiles = false;
if (m_hasMissingFiles)
{
m_hasMissingFiles = false;
if (!isStopped())
{
setAutoManaged(m_operatingMode == TorrentOperatingMode::AutoManaged);
if (m_operatingMode == TorrentOperatingMode::Forced)
m_nativeHandle.resume();
}
}
m_unchecked = false;
m_completedFiles.fill(false);
@@ -1597,10 +1655,10 @@ void TorrentImpl::forceRecheck()
m_nativeStatus.pieces.clear_all();
m_nativeStatus.num_pieces = 0;
if (isPaused())
if (isStopped())
{
// When "force recheck" is applied on paused torrent, we temporarily resume it
resume();
// When "force recheck" is applied on Stopped torrent, we start them to perform checking
start();
m_stopCondition = StopCondition::FilesChecked;
}
}
@@ -1679,16 +1737,16 @@ void TorrentImpl::fileSearchFinished(const Path &savePath, const PathList &fileN
endReceivedMetadataHandling(savePath, fileNames);
}
TrackerEntry TorrentImpl::updateTrackerEntry(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
TrackerEntryStatus TorrentImpl::updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo)
{
const auto it = std::find_if(m_trackerEntries.begin(), m_trackerEntries.end()
, [&announceEntry](const TrackerEntry &trackerEntry)
const auto it = std::find_if(m_trackerEntryStatuses.begin(), m_trackerEntryStatuses.end()
, [&announceEntry](const TrackerEntryStatus &trackerEntryStatus)
{
return (trackerEntry.url == QString::fromStdString(announceEntry.url));
return (trackerEntryStatus.url == QString::fromStdString(announceEntry.url));
});
Q_ASSERT(it != m_trackerEntries.end());
if (it == m_trackerEntries.end()) [[unlikely]]
Q_ASSERT(it != m_trackerEntryStatuses.end());
if (it == m_trackerEntryStatuses.end()) [[unlikely]]
return {};
#ifdef QBT_USES_LIBTORRENT2
@@ -1701,14 +1759,21 @@ TrackerEntry TorrentImpl::updateTrackerEntry(const lt::announce_entry &announceE
#else
const QSet<int> btProtocols {1};
#endif
::updateTrackerEntry(*it, announceEntry, btProtocols, updateInfo);
::updateTrackerEntryStatus(*it, announceEntry, btProtocols, updateInfo);
return *it;
}
void TorrentImpl::resetTrackerEntries()
void TorrentImpl::resetTrackerEntryStatuses()
{
for (auto &trackerEntry : m_trackerEntries)
trackerEntry = {trackerEntry.url, trackerEntry.tier};
for (TrackerEntryStatus &status : m_trackerEntryStatuses)
{
const QString tempUrl = status.url;
const int tempTier = status.tier;
status.clear();
status.url = tempUrl;
status.tier = tempTier;
}
}
std::shared_ptr<const libtorrent::torrent_info> TorrentImpl::nativeTorrentInfo() const
@@ -1751,12 +1816,13 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
const Path filePath = actualFilePath.removedExtension(QB_EXT);
m_filePaths.append(filePath);
lt::download_priority_t &nativePriority = p.file_priorities[LT::toUnderlyingType(nativeIndex)];
if ((nativePriority != lt::dont_download) && m_session->isFilenameExcluded(filePath.filename()))
nativePriority = lt::dont_download;
const auto priority = LT::fromNative(nativePriority);
m_filePriorities.append(priority);
m_filePriorities.append(LT::fromNative(p.file_priorities[LT::toUnderlyingType(nativeIndex)]));
}
m_session->applyFilenameFilter(fileNames, m_filePriorities);
for (int i = 0; i < m_filePriorities.size(); ++i)
p.file_priorities[LT::toUnderlyingType(nativeIndexes[i])] = LT::toNative(m_filePriorities[i]);
p.save_path = savePath.toString().toStdString();
p.ti = metadata;
@@ -1768,7 +1834,7 @@ void TorrentImpl::endReceivedMetadataHandling(const Path &savePath, const PathLi
p.flags |= lt::torrent_flags::paused;
p.flags &= ~lt::torrent_flags::auto_managed;
m_session->handleTorrentPaused(this);
m_session->handleTorrentStopped(this);
}
reload();
@@ -1819,6 +1885,9 @@ void TorrentImpl::reload()
auto *const extensionData = new ExtensionData;
p.userdata = LTClientData(extensionData);
#ifndef QBT_USES_LIBTORRENT2
p.storage = customStorageConstructor;
#endif
m_nativeHandle = m_nativeSession->add_torrent(p);
m_nativeStatus = extensionData->status;
@@ -1836,14 +1905,14 @@ void TorrentImpl::reload()
}
}
void TorrentImpl::pause()
void TorrentImpl::stop()
{
if (!m_isStopped)
{
m_stopCondition = StopCondition::None;
m_isStopped = true;
deferredRequestResumeData();
m_session->handleTorrentPaused(this);
m_session->handleTorrentStopped(this);
}
if (m_maintenanceJob == MaintenanceJob::None)
@@ -1855,7 +1924,7 @@ void TorrentImpl::pause()
}
}
void TorrentImpl::resume(const TorrentOperatingMode mode)
void TorrentImpl::start(const TorrentOperatingMode mode)
{
if (hasError())
{
@@ -1878,7 +1947,7 @@ void TorrentImpl::resume(const TorrentOperatingMode mode)
{
m_isStopped = false;
deferredRequestResumeData();
m_session->handleTorrentResumed(this);
m_session->handleTorrentStarted(this);
}
if (m_maintenanceJob == MaintenanceJob::None)
@@ -1893,8 +1962,17 @@ void TorrentImpl::moveStorage(const Path &newPath, const MoveStorageContext cont
{
if (!hasMetadata())
{
m_savePath = newPath;
m_session->handleTorrentSavePathChanged(this);
if (context == MoveStorageContext::ChangeSavePath)
{
m_savePath = newPath;
m_session->handleTorrentSavePathChanged(this);
}
else if (context == MoveStorageContext::ChangeDownloadPath)
{
m_downloadPath = newPath;
m_session->handleTorrentSavePathChanged(this);
}
return;
}
@@ -1926,6 +2004,11 @@ void TorrentImpl::handleStateUpdate(const lt::torrent_status &nativeStatus)
updateStatus(nativeStatus);
}
void TorrentImpl::handleQueueingModeChanged()
{
updateState();
}
void TorrentImpl::handleMoveStorageJobFinished(const Path &path, const MoveStorageContext context, const bool hasOutstandingJob)
{
if (context == MoveStorageContext::ChangeSavePath)
@@ -1967,7 +2050,7 @@ void TorrentImpl::handleTorrentCheckedAlert([[maybe_unused]] const lt::torrent_c
}
if (stopCondition() == StopCondition::FilesChecked)
pause();
stop();
m_statusUpdatedTriggers.enqueue([this]()
{
@@ -1983,7 +2066,7 @@ void TorrentImpl::handleTorrentCheckedAlert([[maybe_unused]] const lt::torrent_c
adjustStorageLocation();
manageActualFilePaths();
if (!isPaused())
if (!isStopped())
{
// torrent is internally paused using NativeTorrentExtension after files checked
// so we need to resume it if there is no corresponding "stop condition" set
@@ -2468,7 +2551,7 @@ void TorrentImpl::setStopCondition(const StopCondition stopCondition)
if (stopCondition == m_stopCondition)
return;
if (isPaused())
if (isStopped())
return;
if ((stopCondition == StopCondition::MetadataReceived) && hasMetadata())
@@ -2738,7 +2821,7 @@ QString TorrentImpl::createMagnetURI() const
ret += u"&dn=" + QString::fromLatin1(QUrl::toPercentEncoding(displayName));
}
for (const TrackerEntry &tracker : asConst(trackers()))
for (const TrackerEntryStatus &tracker : asConst(trackers()))
{
ret += u"&tr=" + QString::fromLatin1(QUrl::toPercentEncoding(tracker.url));
}
@@ -2766,8 +2849,8 @@ nonstd::expected<lt::entry, QString> TorrentImpl::exportTorrent() const
#endif
lt::create_torrent creator {*torrentInfo};
for (const TrackerEntry &entry : asConst(trackers()))
creator.add_tracker(entry.url.toStdString(), entry.tier);
for (const TrackerEntryStatus &status : asConst(trackers()))
creator.add_tracker(status.url.toStdString(), status.tier);
return creator.generate();
}

View File

@@ -55,7 +55,7 @@
#include "torrent.h"
#include "torrentcontentlayout.h"
#include "torrentinfo.h"
#include "trackerentry.h"
#include "trackerentrystatus.h"
namespace BitTorrent
{
@@ -153,11 +153,12 @@ namespace BitTorrent
Path actualFilePath(int index) const override;
qlonglong fileSize(int index) const override;
PathList filePaths() const override;
PathList actualFilePaths() const override;
QVector<DownloadPriority> filePriorities() const override;
TorrentInfo info() const override;
bool isFinished() const override;
bool isPaused() const override;
bool isStopped() const override;
bool isQueued() const override;
bool isForced() const override;
bool isChecking() const override;
@@ -175,7 +176,7 @@ namespace BitTorrent
bool hasMissingFiles() const override;
bool hasError() const override;
int queuePosition() const override;
QVector<TrackerEntry> trackers() const override;
QVector<TrackerEntryStatus> trackers() const override;
QVector<QUrl> urlSeeds() const override;
QString error() const override;
qlonglong totalDownload() const override;
@@ -210,6 +211,7 @@ namespace BitTorrent
int maxSeedingTime() const override;
int maxInactiveSeedingTime() const override;
qreal realRatio() const override;
qreal popularity() const override;
int uploadPayloadRate() const override;
int downloadPayloadRate() const override;
qlonglong totalPayloadUpload() const override;
@@ -222,8 +224,8 @@ namespace BitTorrent
void setName(const QString &name) override;
void setSequentialDownload(bool enable) override;
void setFirstLastPiecePriority(bool enabled) override;
void pause() override;
void resume(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) override;
void stop() override;
void start(TorrentOperatingMode mode = TorrentOperatingMode::AutoManaged) override;
void forceReannounce(int index = -1) override;
void forceDHTAnnounce() override;
void forceRecheck() override;
@@ -268,6 +270,7 @@ namespace BitTorrent
void handleAlert(const lt::alert *a);
void handleStateUpdate(const lt::torrent_status &nativeStatus);
void handleQueueingModeChanged();
void handleCategoryOptionsChanged();
void handleAppendExtensionToggled();
void handleUnwantedFolderToggled();
@@ -275,8 +278,8 @@ namespace BitTorrent
void deferredRequestResumeData();
void handleMoveStorageJobFinished(const Path &path, MoveStorageContext context, bool hasOutstandingJob);
void fileSearchFinished(const Path &savePath, const PathList &fileNames);
TrackerEntry updateTrackerEntry(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo);
void resetTrackerEntries();
TrackerEntryStatus updateTrackerEntryStatus(const lt::announce_entry &announceEntry, const QHash<lt::tcp::endpoint, QMap<int, int>> &updateInfo);
void resetTrackerEntryStatuses();
private:
using EventTrigger = std::function<void ()>;
@@ -349,7 +352,7 @@ namespace BitTorrent
MaintenanceJob m_maintenanceJob = MaintenanceJob::None;
QVector<TrackerEntry> m_trackerEntries;
QVector<TrackerEntryStatus> m_trackerEntryStatuses;
QVector<QUrl> m_urlSeeds;
FileErrorInfo m_lastFileError;

View File

@@ -279,7 +279,7 @@ QVector<TrackerEntry> TorrentInfo::trackers() const
QVector<TrackerEntry> ret;
ret.reserve(static_cast<decltype(ret)::size_type>(trackers.size()));
for (const lt::announce_entry &tracker : trackers)
ret.append({QString::fromStdString(tracker.url), tracker.tier});
ret.append({.url = QString::fromStdString(tracker.url), .tier = tracker.tier});
return ret;
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Mike Tzou (Chocobo1)
* Copyright (C) 2015-2023 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
@@ -28,7 +29,9 @@
#include "trackerentry.h"
#include <QHash>
#include <QList>
#include <QStringView>
QList<BitTorrent::TrackerEntry> BitTorrent::parseTrackerEntries(const QStringView str)
{

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Mike Tzou (Chocobo1)
* Copyright (C) 2015-2023 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
@@ -29,56 +30,16 @@
#pragma once
#include <QtContainerFwd>
#include <QDateTime>
#include <QHash>
#include <QString>
#include <QStringView>
class QStringView;
namespace BitTorrent
{
enum class TrackerEntryStatus
{
NotContacted = 1,
Working = 2,
Updating = 3,
NotWorking = 4,
TrackerError = 5,
Unreachable = 6
};
struct TrackerEndpointEntry
{
QString name {};
int btVersion = 1;
TrackerEntryStatus status = TrackerEntryStatus::NotContacted;
QString message {};
int numPeers = -1;
int numSeeds = -1;
int numLeeches = -1;
int numDownloaded = -1;
QDateTime nextAnnounceTime {};
QDateTime minAnnounceTime {};
};
struct TrackerEntry
{
QString url {};
int tier = 0;
TrackerEntryStatus status = TrackerEntryStatus::NotContacted;
QString message {};
int numPeers = -1;
int numSeeds = -1;
int numLeeches = -1;
int numDownloaded = -1;
QDateTime nextAnnounceTime {};
QDateTime minAnnounceTime {};
QHash<std::pair<QString, int>, TrackerEndpointEntry> endpointEntries {};
};
QList<TrackerEntry> parseTrackerEntries(QStringView str);

View File

@@ -0,0 +1,54 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2023 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "trackerentrystatus.h"
void BitTorrent::TrackerEntryStatus::clear()
{
url.clear();
tier = 0;
state = TrackerEndpointState::NotContacted;
message.clear();
numPeers = -1;
numSeeds = -1;
numLeeches = -1;
numDownloaded = -1;
nextAnnounceTime = {};
minAnnounceTime = {};
endpoints.clear();
}
bool BitTorrent::operator==(const TrackerEntryStatus &left, const TrackerEntryStatus &right)
{
return (left.url == right.url);
}
std::size_t BitTorrent::qHash(const TrackerEntryStatus &key, const std::size_t seed)
{
return ::qHash(key.url, seed);
}

View File

@@ -0,0 +1,89 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015-2023 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 <QDateTime>
#include <QHash>
#include <QString>
class QStringView;
namespace BitTorrent
{
enum class TrackerEndpointState
{
NotContacted = 1,
Working = 2,
Updating = 3,
NotWorking = 4,
TrackerError = 5,
Unreachable = 6
};
struct TrackerEndpointStatus
{
QString name {};
int btVersion = 1;
TrackerEndpointState state = TrackerEndpointState::NotContacted;
QString message {};
int numPeers = -1;
int numSeeds = -1;
int numLeeches = -1;
int numDownloaded = -1;
QDateTime nextAnnounceTime {};
QDateTime minAnnounceTime {};
};
struct TrackerEntryStatus
{
QString url {};
int tier = 0;
TrackerEndpointState state = TrackerEndpointState::NotContacted;
QString message {};
int numPeers = -1;
int numSeeds = -1;
int numLeeches = -1;
int numDownloaded = -1;
QDateTime nextAnnounceTime {};
QDateTime minAnnounceTime {};
QHash<std::pair<QString, int>, TrackerEndpointStatus> endpoints {};
void clear();
};
bool operator==(const TrackerEntryStatus &left, const TrackerEntryStatus &right);
std::size_t qHash(const TrackerEntryStatus &key, std::size_t seed = 0);
}

View File

@@ -44,6 +44,7 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
, m_requestHandler(requestHandler)
{
m_socket->setParent(this);
connect(m_socket, &QAbstractSocket::disconnected, this, &Connection::closed);
// reserve common size for requests, don't use the max allowed size which is too big for
// memory constrained platforms
@@ -62,11 +63,6 @@ Connection::Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObj
});
}
Connection::~Connection()
{
m_socket->close();
}
void Connection::read()
{
// reuse existing buffer and avoid unnecessary memory allocation/relocation
@@ -182,11 +178,6 @@ bool Connection::hasExpired(const qint64 timeout) const
&& m_idleTimer.hasExpired(timeout);
}
bool Connection::isClosed() const
{
return (m_socket->state() == QAbstractSocket::UnconnectedState);
}
bool Connection::acceptsGzipEncoding(QString codings)
{
// [rfc7231] 5.3.4. Accept-Encoding

View File

@@ -47,10 +47,11 @@ namespace Http
public:
Connection(QTcpSocket *socket, IRequestHandler *requestHandler, QObject *parent = nullptr);
~Connection();
bool hasExpired(qint64 timeout) const;
bool isClosed() const;
signals:
void closed();
private:
static bool acceptsGzipEncoding(QString codings);

View File

@@ -32,7 +32,10 @@
#include <algorithm>
#include <chrono>
#include <memory>
#include <new>
#include <QtLogging>
#include <QNetworkProxy>
#include <QSslCipher>
#include <QSslConfiguration>
@@ -40,7 +43,6 @@
#include <QStringList>
#include <QTimer>
#include "base/algorithm.h"
#include "base/global.h"
#include "base/utils/net.h"
#include "base/utils/sslkey.h"
@@ -113,32 +115,38 @@ Server::Server(IRequestHandler *requestHandler, QObject *parent)
void Server::incomingConnection(const qintptr socketDescriptor)
{
if (m_connections.size() >= CONNECTIONS_LIMIT) return;
QTcpSocket *serverSocket = nullptr;
if (m_https)
serverSocket = new QSslSocket(this);
else
serverSocket = new QTcpSocket(this);
std::unique_ptr<QTcpSocket> serverSocket = m_https ? std::make_unique<QSslSocket>(this) : std::make_unique<QTcpSocket>(this);
if (!serverSocket->setSocketDescriptor(socketDescriptor))
return;
if (m_connections.size() >= CONNECTIONS_LIMIT)
{
delete serverSocket;
qWarning("Too many connections. Exceeded CONNECTIONS_LIMIT (%d). Connection closed.", CONNECTIONS_LIMIT);
return;
}
if (m_https)
try
{
static_cast<QSslSocket *>(serverSocket)->setProtocol(QSsl::SecureProtocols);
static_cast<QSslSocket *>(serverSocket)->setPrivateKey(m_key);
static_cast<QSslSocket *>(serverSocket)->setLocalCertificateChain(m_certificates);
static_cast<QSslSocket *>(serverSocket)->setPeerVerifyMode(QSslSocket::VerifyNone);
static_cast<QSslSocket *>(serverSocket)->startServerEncryption();
}
if (m_https)
{
auto *sslSocket = static_cast<QSslSocket *>(serverSocket.get());
sslSocket->setProtocol(QSsl::SecureProtocols);
sslSocket->setPrivateKey(m_key);
sslSocket->setLocalCertificateChain(m_certificates);
sslSocket->setPeerVerifyMode(QSslSocket::VerifyNone);
sslSocket->startServerEncryption();
}
auto *c = new Connection(serverSocket, m_requestHandler, this);
m_connections.insert(c);
connect(serverSocket, &QAbstractSocket::disconnected, this, [c, this]() { removeConnection(c); });
auto *connection = new Connection(serverSocket.release(), m_requestHandler, this);
m_connections.insert(connection);
connect(connection, &Connection::closed, this, [this, connection] { removeConnection(connection); });
}
catch (const std::bad_alloc &exception)
{
// drop the connection instead of throwing exception and crash
qWarning("Failed to allocate memory for HTTP connection. Connection closed.");
return;
}
}
void Server::removeConnection(Connection *connection)

View File

@@ -32,6 +32,7 @@
#include <algorithm>
#include <QByteArray>
#include <QDateTime>
#include <QDebug>
#include <QNetworkAccessManager>
@@ -54,8 +55,27 @@ using namespace std::chrono_literals;
namespace
{
// Disguise as Firefox to avoid web server banning
const char DEFAULT_USER_AGENT[] = "Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0";
// Disguise as browser to circumvent website blocking
QByteArray getBrowserUserAgent()
{
// Firefox release calendar
// https://whattrainisitnow.com/calendar/
// https://wiki.mozilla.org/index.php?title=Release_Management/Calendar&redirect=no
static QByteArray ret;
if (ret.isEmpty())
{
const std::chrono::time_point baseDate = std::chrono::sys_days(2024y / 04 / 16);
const int baseVersion = 125;
const std::chrono::time_point nowDate = std::chrono::system_clock::now();
const int nowVersion = baseVersion + std::chrono::duration_cast<std::chrono::months>(nowDate - baseDate).count();
QByteArray userAgentTemplate = QByteArrayLiteral("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:%1.0) Gecko/20100101 Firefox/%1.0");
ret = userAgentTemplate.replace("%1", QByteArray::number(nowVersion));
}
return ret;
}
}
class Net::DownloadManager::NetworkCookieJar final : public QNetworkCookieJar
@@ -292,7 +312,7 @@ void Net::DownloadManager::processRequest(DownloadHandlerImpl *downloadHandler)
QNetworkRequest request {downloadRequest.url()};
if (downloadRequest.userAgent().isEmpty())
request.setRawHeader("User-Agent", DEFAULT_USER_AGENT);
request.setRawHeader("User-Agent", getBrowserUserAgent());
else
request.setRawHeader("User-Agent", downloadRequest.userAgent().toUtf8());

View File

@@ -43,7 +43,6 @@ class QSslSocket;
#else
class QTcpSocket;
#endif
class QTextCodec;
namespace Net
{

View File

@@ -134,17 +134,17 @@ void Preferences::setCustomUIThemePath(const Path &path)
setValue(u"Preferences/General/CustomUIThemePath"_s, path);
}
bool Preferences::deleteTorrentFilesAsDefault() const
bool Preferences::removeTorrentContent() const
{
return value(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, false);
}
void Preferences::setDeleteTorrentFilesAsDefault(const bool del)
void Preferences::setRemoveTorrentContent(const bool remove)
{
if (del == deleteTorrentFilesAsDefault())
if (remove == removeTorrentContent())
return;
setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, del);
setValue(u"Preferences/General/DeleteTorrentsFilesAsDefault"_s, remove);
}
bool Preferences::confirmOnExit() const
@@ -1397,19 +1397,6 @@ void Preferences::setConfirmRemoveAllTags(const bool enabled)
setValue(u"Preferences/Advanced/confirmRemoveAllTags"_s, enabled);
}
bool Preferences::confirmPauseAndResumeAll() const
{
return value(u"GUI/ConfirmActions/PauseAndResumeAllTorrents"_s, true);
}
void Preferences::setConfirmPauseAndResumeAll(const bool enabled)
{
if (enabled == confirmPauseAndResumeAll())
return;
setValue(u"GUI/ConfirmActions/PauseAndResumeAllTorrents"_s, enabled);
}
bool Preferences::confirmMergeTrackers() const
{
return value(u"GUI/ConfirmActions/MergeTrackers"_s, true);

View File

@@ -105,8 +105,8 @@ public:
void setUseCustomUITheme(bool use);
Path customUIThemePath() const;
void setCustomUIThemePath(const Path &path);
bool deleteTorrentFilesAsDefault() const;
void setDeleteTorrentFilesAsDefault(bool del);
bool removeTorrentContent() const;
void setRemoveTorrentContent(bool remove);
bool confirmOnExit() const;
void setConfirmOnExit(bool confirm);
bool speedInTitleBar() const;
@@ -305,8 +305,6 @@ public:
void setConfirmTorrentRecheck(bool enabled);
bool confirmRemoveAllTags() const;
void setConfirmRemoveAllTags(bool enabled);
bool confirmPauseAndResumeAll() const;
void setConfirmPauseAndResumeAll(bool enabled);
bool confirmMergeTrackers() const;
void setConfirmMergeTrackers(bool enabled);
bool confirmRemoveTrackerFromAllTorrents() const;

View File

@@ -468,7 +468,7 @@ QJsonObject AutoDownloadRule::toJsonObject() const
// TODO: The following code is deprecated. Replace with the commented one after several releases in 4.6.x.
// === BEGIN DEPRECATED CODE === //
, {S_ADD_PAUSED, toJsonValue(addTorrentParams.addPaused)}
, {S_ADD_PAUSED, toJsonValue(addTorrentParams.addStopped)}
, {S_CONTENT_LAYOUT, contentLayoutToJsonValue(addTorrentParams.contentLayout)}
, {S_SAVE_PATH, addTorrentParams.savePath.toString()}
, {S_ASSIGNED_CATEGORY, addTorrentParams.category}
@@ -525,7 +525,7 @@ AutoDownloadRule AutoDownloadRule::fromJsonObject(const QJsonObject &jsonObj, co
{
addTorrentParams.savePath = Path(jsonObj.value(S_SAVE_PATH).toString());
addTorrentParams.category = jsonObj.value(S_ASSIGNED_CATEGORY).toString();
addTorrentParams.addPaused = toOptionalBool(jsonObj.value(S_ADD_PAUSED));
addTorrentParams.addStopped = toOptionalBool(jsonObj.value(S_ADD_PAUSED));
if (!addTorrentParams.savePath.isEmpty())
addTorrentParams.useAutoTMM = false;
@@ -568,7 +568,7 @@ QVariantHash AutoDownloadRule::toLegacyDict() const
{u"enabled"_s, isEnabled()},
{u"category_assigned"_s, addTorrentParams.category},
{u"use_regex"_s, useRegex()},
{u"add_paused"_s, toAddPausedLegacy(addTorrentParams.addPaused)},
{u"add_paused"_s, toAddPausedLegacy(addTorrentParams.addStopped)},
{u"episode_filter"_s, episodeFilter()},
{u"last_match"_s, lastMatch()},
{u"ignore_days"_s, ignoreDays()}};
@@ -579,7 +579,7 @@ AutoDownloadRule AutoDownloadRule::fromLegacyDict(const QVariantHash &dict)
BitTorrent::AddTorrentParams addTorrentParams;
addTorrentParams.savePath = Path(dict.value(u"save_path"_s).toString());
addTorrentParams.category = dict.value(u"category_assigned"_s).toString();
addTorrentParams.addPaused = addPausedLegacyToOptionalBool(dict.value(u"add_paused"_s).toInt());
addTorrentParams.addStopped = addPausedLegacyToOptionalBool(dict.value(u"add_paused"_s).toInt());
if (!addTorrentParams.savePath.isEmpty())
addTorrentParams.useAutoTMM = false;

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018-2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -36,10 +36,10 @@
#include "base/utils/fs.h"
#include "searchpluginmanager.h"
SearchDownloadHandler::SearchDownloadHandler(const QString &siteUrl, const QString &url, SearchPluginManager *manager)
: QObject {manager}
SearchDownloadHandler::SearchDownloadHandler(const QString &pluginName, const QString &url, SearchPluginManager *manager)
: QObject(manager)
, m_manager {manager}
, m_downloadProcess {new QProcess {this}}
, m_downloadProcess {new QProcess(this)}
{
m_downloadProcess->setEnvironment(QProcess::systemEnvironment());
connect(m_downloadProcess, qOverload<int, QProcess::ExitStatus>(&QProcess::finished)
@@ -48,7 +48,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &siteUrl, const QStri
{
Utils::ForeignApps::PYTHON_ISOLATE_MODE_FLAG,
(SearchPluginManager::engineLocation() / Path(u"nova2dl.py"_s)).toString(),
siteUrl,
pluginName,
url
};
// Launch search

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2018-2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -41,7 +41,7 @@ class SearchDownloadHandler : public QObject
friend class SearchPluginManager;
SearchDownloadHandler(const QString &siteUrl, const QString &url, SearchPluginManager *manager);
SearchDownloadHandler(const QString &pluginName, const QString &url, SearchPluginManager *manager);
signals:
void downloadFinished(const QString &path);

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -55,18 +55,19 @@ namespace
PL_LEECHS,
PL_ENGINE_URL,
PL_DESC_LINK,
PL_PUB_DATE,
NB_PLUGIN_COLUMNS
};
}
SearchHandler::SearchHandler(const QString &pattern, const QString &category, const QStringList &usedPlugins, SearchPluginManager *manager)
: QObject {manager}
: QObject(manager)
, m_pattern {pattern}
, m_category {category}
, m_usedPlugins {usedPlugins}
, m_manager {manager}
, m_searchProcess {new QProcess {this}}
, m_searchTimeout {new QTimer {this}}
, m_searchProcess {new QProcess(this)}
, m_searchTimeout {new QTimer(this)}
{
// Load environment variables (proxy)
m_searchProcess->setEnvironment(QProcess::systemEnvironment());
@@ -176,7 +177,8 @@ bool SearchHandler::parseSearchResult(const QStringView line, SearchResult &sear
const QList<QStringView> parts = line.split(u'|');
const int nbFields = parts.size();
if (nbFields < (NB_PLUGIN_COLUMNS - 1)) return false; // -1 because desc_link is optional
if (nbFields <= PL_ENGINE_URL)
return false; // Anything after ENGINE_URL is optional
searchResult = SearchResult();
searchResult.fileUrl = parts.at(PL_DL_LINK).trimmed().toString(); // download URL
@@ -193,10 +195,19 @@ bool SearchHandler::parseSearchResult(const QStringView line, SearchResult &sear
if (!ok || (searchResult.nbLeechers < 0))
searchResult.nbLeechers = -1;
searchResult.siteUrl = parts.at(PL_ENGINE_URL).trimmed().toString(); // Search site URL
if (nbFields == NB_PLUGIN_COLUMNS)
searchResult.siteUrl = parts.at(PL_ENGINE_URL).trimmed().toString(); // Search engine site URL
searchResult.engineName = m_manager->pluginNameBySiteURL(searchResult.siteUrl); // Search engine name
if (nbFields > PL_DESC_LINK)
searchResult.descrLink = parts.at(PL_DESC_LINK).trimmed().toString(); // Description Link
if (nbFields > PL_PUB_DATE)
{
const qint64 secs = parts.at(PL_PUB_DATE).trimmed().toLongLong(&ok);
if (ok && (secs > 0))
searchResult.pubDate = QDateTime::fromSecsSinceEpoch(secs); // Date
}
return true;
}

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -30,6 +30,7 @@
#pragma once
#include <QByteArray>
#include <QDateTime>
#include <QList>
#include <QObject>
#include <QString>
@@ -45,8 +46,10 @@ struct SearchResult
qlonglong fileSize = 0;
qlonglong nbSeeders = 0;
qlonglong nbLeechers = 0;
QString engineName;
QString siteUrl;
QString descrLink;
QDateTime pubDate;
};
class SearchPluginManager;

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -181,6 +181,17 @@ PluginInfo *SearchPluginManager::pluginInfo(const QString &name) const
return m_plugins.value(name);
}
QString SearchPluginManager::pluginNameBySiteURL(const QString &siteURL) const
{
for (const PluginInfo *plugin : asConst(m_plugins))
{
if (plugin->url == siteURL)
return plugin->name;
}
return {};
}
void SearchPluginManager::enablePlugin(const QString &name, const bool enabled)
{
PluginInfo *plugin = m_plugins.value(name, nullptr);
@@ -338,9 +349,9 @@ void SearchPluginManager::checkForUpdates()
, this, &SearchPluginManager::versionInfoDownloadFinished);
}
SearchDownloadHandler *SearchPluginManager::downloadTorrent(const QString &siteUrl, const QString &url)
SearchDownloadHandler *SearchPluginManager::downloadTorrent(const QString &pluginName, const QString &url)
{
return new SearchDownloadHandler {siteUrl, url, this};
return new SearchDownloadHandler(pluginName, url, this);
}
SearchHandler *SearchPluginManager::startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins)

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015, 2018 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2015-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -75,6 +75,7 @@ public:
QStringList supportedCategories() const;
QStringList getPluginCategories(const QString &pluginName) const;
PluginInfo *pluginInfo(const QString &name) const;
QString pluginNameBySiteURL(const QString &siteURL) const;
void enablePlugin(const QString &name, bool enabled = true);
void updatePlugin(const QString &name);
@@ -84,7 +85,7 @@ public:
void checkForUpdates();
SearchHandler *startSearch(const QString &pattern, const QString &category, const QStringList &usedPlugins);
SearchDownloadHandler *downloadTorrent(const QString &siteUrl, const QString &url);
SearchDownloadHandler *downloadTorrent(const QString &pluginName, const QString &url);
static PluginVersion getPluginVersion(const Path &filePath);
static QString categoryFullName(const QString &categoryName);

View File

@@ -38,8 +38,8 @@ const std::optional<Tag> TorrentFilter::AnyTag;
const TorrentFilter TorrentFilter::DownloadingTorrent(TorrentFilter::Downloading);
const TorrentFilter TorrentFilter::SeedingTorrent(TorrentFilter::Seeding);
const TorrentFilter TorrentFilter::CompletedTorrent(TorrentFilter::Completed);
const TorrentFilter TorrentFilter::PausedTorrent(TorrentFilter::Paused);
const TorrentFilter TorrentFilter::ResumedTorrent(TorrentFilter::Resumed);
const TorrentFilter TorrentFilter::StoppedTorrent(TorrentFilter::Stopped);
const TorrentFilter TorrentFilter::RunningTorrent(TorrentFilter::Running);
const TorrentFilter TorrentFilter::ActiveTorrent(TorrentFilter::Active);
const TorrentFilter TorrentFilter::InactiveTorrent(TorrentFilter::Inactive);
const TorrentFilter TorrentFilter::StalledTorrent(TorrentFilter::Stalled);
@@ -52,19 +52,21 @@ const TorrentFilter TorrentFilter::ErroredTorrent(TorrentFilter::Errored);
using BitTorrent::Torrent;
TorrentFilter::TorrentFilter(const Type type, const std::optional<TorrentIDSet> &idSet
, const std::optional<QString> &category, const std::optional<Tag> &tag)
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
: m_type {type}
, m_category {category}
, m_tag {tag}
, m_idSet {idSet}
, m_private {isPrivate}
{
}
TorrentFilter::TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet
, const std::optional<QString> &category, const std::optional<Tag> &tag)
, const std::optional<QString> &category, const std::optional<Tag> &tag, const std::optional<bool> isPrivate)
: m_category {category}
, m_tag {tag}
, m_idSet {idSet}
, m_private {isPrivate}
{
setTypeByName(filter);
}
@@ -90,10 +92,10 @@ bool TorrentFilter::setTypeByName(const QString &filter)
type = Seeding;
else if (filter == u"completed")
type = Completed;
else if (filter == u"paused")
type = Paused;
else if (filter == u"resumed")
type = Resumed;
else if (filter == u"stopped")
type = Stopped;
else if (filter == u"running")
type = Running;
else if (filter == u"active")
type = Active;
else if (filter == u"inactive")
@@ -147,19 +149,31 @@ bool TorrentFilter::setTag(const std::optional<Tag> &tag)
return false;
}
bool TorrentFilter::setPrivate(const std::optional<bool> isPrivate)
{
if (m_private != isPrivate)
{
m_private = isPrivate;
return true;
}
return false;
}
bool TorrentFilter::match(const Torrent *const torrent) const
{
if (!torrent) return false;
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent));
return (matchState(torrent) && matchHash(torrent) && matchCategory(torrent) && matchTag(torrent) && matchPrivate(torrent));
}
bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
{
const BitTorrent::TorrentState state = torrent->state();
switch (m_type)
{
case All:
default:
return true;
case Downloading:
return torrent->isDownloading();
@@ -167,29 +181,32 @@ bool TorrentFilter::matchState(const BitTorrent::Torrent *const torrent) const
return torrent->isUploading();
case Completed:
return torrent->isCompleted();
case Paused:
return torrent->isPaused();
case Resumed:
return torrent->isResumed();
case Stopped:
return torrent->isStopped();
case Running:
return torrent->isRunning();
case Active:
return torrent->isActive();
case Inactive:
return torrent->isInactive();
case Stalled:
return (torrent->state() == BitTorrent::TorrentState::StalledUploading)
|| (torrent->state() == BitTorrent::TorrentState::StalledDownloading);
return (state == BitTorrent::TorrentState::StalledUploading)
|| (state == BitTorrent::TorrentState::StalledDownloading);
case StalledUploading:
return torrent->state() == BitTorrent::TorrentState::StalledUploading;
return state == BitTorrent::TorrentState::StalledUploading;
case StalledDownloading:
return torrent->state() == BitTorrent::TorrentState::StalledDownloading;
return state == BitTorrent::TorrentState::StalledDownloading;
case Checking:
return (torrent->state() == BitTorrent::TorrentState::CheckingUploading)
|| (torrent->state() == BitTorrent::TorrentState::CheckingDownloading)
|| (torrent->state() == BitTorrent::TorrentState::CheckingResumeData);
return (state == BitTorrent::TorrentState::CheckingUploading)
|| (state == BitTorrent::TorrentState::CheckingDownloading)
|| (state == BitTorrent::TorrentState::CheckingResumeData);
case Moving:
return torrent->isMoving();
case Errored:
return torrent->isErrored();
default:
Q_ASSERT(false);
return false;
}
}
@@ -220,3 +237,11 @@ bool TorrentFilter::matchTag(const BitTorrent::Torrent *const torrent) const
return torrent->hasTag(*m_tag);
}
bool TorrentFilter::matchPrivate(const BitTorrent::Torrent *const torrent) const
{
if (!m_private)
return true;
return m_private == torrent->isPrivate();
}

View File

@@ -52,8 +52,8 @@ public:
Downloading,
Seeding,
Completed,
Resumed,
Paused,
Running,
Stopped,
Active,
Inactive,
Stalled,
@@ -61,7 +61,9 @@ public:
StalledDownloading,
Checking,
Moving,
Errored
Errored,
_Count
};
// These mean any permutation, including no category / tag.
@@ -72,8 +74,8 @@ public:
static const TorrentFilter DownloadingTorrent;
static const TorrentFilter SeedingTorrent;
static const TorrentFilter CompletedTorrent;
static const TorrentFilter PausedTorrent;
static const TorrentFilter ResumedTorrent;
static const TorrentFilter StoppedTorrent;
static const TorrentFilter RunningTorrent;
static const TorrentFilter ActiveTorrent;
static const TorrentFilter InactiveTorrent;
static const TorrentFilter StalledTorrent;
@@ -85,16 +87,24 @@ public:
TorrentFilter() = default;
// category & tags: pass empty string for uncategorized / untagged torrents.
TorrentFilter(Type type, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tag = AnyTag);
TorrentFilter(const QString &filter, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory, const std::optional<Tag> &tags = AnyTag);
TorrentFilter(Type type
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tag = AnyTag
, std::optional<bool> isPrivate = {});
TorrentFilter(const QString &filter
, const std::optional<TorrentIDSet> &idSet = AnyID
, const std::optional<QString> &category = AnyCategory
, const std::optional<Tag> &tags = AnyTag
, std::optional<bool> isPrivate = {});
bool setType(Type type);
bool setTypeByName(const QString &filter);
bool setTorrentIDSet(const std::optional<TorrentIDSet> &idSet);
bool setCategory(const std::optional<QString> &category);
bool setTag(const std::optional<Tag> &tag);
bool setPrivate(std::optional<bool> isPrivate);
bool match(const BitTorrent::Torrent *torrent) const;
@@ -103,9 +113,11 @@ private:
bool matchHash(const BitTorrent::Torrent *torrent) const;
bool matchCategory(const BitTorrent::Torrent *torrent) const;
bool matchTag(const BitTorrent::Torrent *torrent) const;
bool matchPrivate(const BitTorrent::Torrent *torrent) const;
Type m_type {All};
std::optional<QString> m_category;
std::optional<Tag> m_tag;
std::optional<TorrentIDSet> m_idSet;
std::optional<bool> m_private;
};

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -29,8 +29,6 @@
#include "fs.h"
#include <cerrno>
#include <cstring>
#include <filesystem>
#if defined(Q_OS_WIN)
@@ -52,6 +50,7 @@
#include <unistd.h>
#endif
#include <QCoreApplication>
#include <QDateTime>
#include <QDebug>
#include <QDir>
@@ -311,20 +310,42 @@ bool Utils::Fs::renameFile(const Path &from, const Path &to)
*
* This function will try to fix the file permissions before removing it.
*/
bool Utils::Fs::removeFile(const Path &path)
nonstd::expected<void, QString> Utils::Fs::removeFile(const Path &path)
{
if (QFile::remove(path.data()))
return true;
QFile file {path.data()};
if (file.remove())
return {};
if (!file.exists())
return true;
return {};
// Make sure we have read/write permissions
file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
return file.remove();
if (file.remove())
return {};
return nonstd::make_unexpected(file.errorString());
}
nonstd::expected<void, QString> Utils::Fs::moveFileToTrash(const Path &path)
{
QFile file {path.data()};
if (file.moveToTrash())
return {};
if (!file.exists())
return {};
// Make sure we have read/write permissions
file.setPermissions(file.permissions() | QFile::ReadOwner | QFile::WriteOwner | QFile::ReadUser | QFile::WriteUser);
if (file.moveToTrash())
return {};
const QString errorMessage = file.errorString();
return nonstd::make_unexpected(!errorMessage.isEmpty() ? errorMessage : QCoreApplication::translate("fs", "Unknown error"));
}
bool Utils::Fs::isReadable(const Path &path)
{
return QFileInfo(path.data()).isReadable();

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -35,6 +35,7 @@
#include <QString>
#include "base/3rdparty/expected.hpp"
#include "base/global.h"
#include "base/pathfwd.h"
@@ -60,7 +61,8 @@ namespace Utils::Fs
bool copyFile(const Path &from, const Path &to);
bool renameFile(const Path &from, const Path &to);
bool removeFile(const Path &path);
nonstd::expected<void, QString> removeFile(const Path &path);
nonstd::expected<void, QString> moveFileToTrash(const Path &path);
bool mkdir(const Path &dirPath);
bool mkpath(const Path &dirPath);
bool rmdir(const Path &dirPath);

44
src/base/utils/number.cpp Normal file
View File

@@ -0,0 +1,44 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Mike Tzou (Chocobo1)
*
* 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 "number.h"
#include <algorithm>
#include <cstdint>
#include <limits>
int Utils::Number::clampingAdd(const int num1, const int num2)
{
static_assert(sizeof(int64_t) > sizeof(int));
const int64_t intMin = std::numeric_limits<int>::min();
const int64_t intMax = std::numeric_limits<int>::max();
const int64_t sumResult = static_cast<int64_t>(num1) + num2;
const int64_t clampedValue = std::clamp(sumResult, intMin, intMax);
return static_cast<int>(clampedValue);
}

35
src/base/utils/number.h Normal file
View File

@@ -0,0 +1,35 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Mike Tzou (Chocobo1)
*
* 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
namespace Utils::Number
{
// math addition that the result will never overflow/underflow
int clampingAdd(int num1, int num2);
}

View File

@@ -32,7 +32,7 @@
#define QBT_VERSION_MINOR 0
#define QBT_VERSION_BUGFIX 0
#define QBT_VERSION_BUILD 0
#define QBT_VERSION_STATUS "beta1" // Should be empty for stable releases!
#define QBT_VERSION_STATUS "" // Should be empty for stable releases!
#define QBT__STRINGIFY(x) #x
#define QBT_STRINGIFY(x) QBT__STRINGIFY(x)

View File

@@ -52,6 +52,8 @@ add_library(qbt_gui STATIC
desktopintegration.h
downloadfromurldialog.h
executionlogwidget.h
filterpatternformat.h
filterpatternformatmenu.h
flowlayout.h
fspathedit.h
fspathedit_p.h
@@ -151,6 +153,7 @@ add_library(qbt_gui STATIC
desktopintegration.cpp
downloadfromurldialog.cpp
executionlogwidget.cpp
filterpatternformatmenu.cpp
flowlayout.cpp
fspathedit.cpp
fspathedit_p.cpp

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -42,17 +42,20 @@
#include <QMessageBox>
#include <QPushButton>
#include <QShortcut>
#include <QSignalBlocker>
#include <QSize>
#include <QString>
#include <QUrl>
#include <QVector>
#include "base/bittorrent/addtorrentparams.h"
#include "base/bittorrent/downloadpriority.h"
#include "base/bittorrent/infohash.h"
#include "base/bittorrent/session.h"
#include "base/bittorrent/torrent.h"
#include "base/bittorrent/torrentcontenthandler.h"
#include "base/bittorrent/torrentcontentlayout.h"
#include "base/bittorrent/torrentdescriptor.h"
#include "base/global.h"
#include "base/preferences.h"
#include "base/settingsstorage.h"
@@ -61,6 +64,7 @@
#include "base/utils/fs.h"
#include "base/utils/misc.h"
#include "base/utils/string.h"
#include "filterpatternformatmenu.h"
#include "lineedit.h"
#include "torrenttagsdialog.h"
@@ -178,6 +182,11 @@ public:
return (m_filePaths.isEmpty() ? m_torrentInfo.filePath(index) : m_filePaths.at(index));
}
PathList filePaths() const
{
return (m_filePaths.isEmpty() ? m_torrentInfo.filePaths() : m_filePaths);
}
void renameFile(const int index, const Path &newFilePath) override
{
Q_ASSERT((index >= 0) && (index < filesCount()));
@@ -271,128 +280,51 @@ private:
BitTorrent::TorrentContentLayout m_currentContentLayout;
};
struct AddNewTorrentDialog::Context
{
BitTorrent::TorrentDescriptor torrentDescr;
BitTorrent::AddTorrentParams torrentParams;
};
AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &torrentDescr
, const BitTorrent::AddTorrentParams &inParams, QWidget *parent)
: QDialog(parent)
, m_ui {new Ui::AddNewTorrentDialog}
, m_torrentDescr {torrentDescr}
, m_torrentParams {inParams}
, m_filterLine {new LineEdit(this)}
, m_storeDialogSize {SETTINGS_KEY(u"DialogSize"_s)}
, m_storeDefaultCategory {SETTINGS_KEY(u"DefaultCategory"_s)}
, m_storeRememberLastSavePath {SETTINGS_KEY(u"RememberLastSavePath"_s)}
, m_storeTreeHeaderState {u"GUI/Qt6/" SETTINGS_KEY(u"TreeHeaderState"_s)}
, m_storeSplitterState {u"GUI/Qt6/" SETTINGS_KEY(u"SplitterState"_s)}
, m_storeFilterPatternFormat {u"GUI/" SETTINGS_KEY(u"FilterPatternFormat"_s)}
{
// TODO: set dialog file properties using m_torrentParams.filePriorities
m_ui->setupUi(this);
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
m_ui->lblMetaLoading->setVisible(false);
m_ui->progMetaLoading->setVisible(false);
m_ui->buttonSave->setVisible(false);
connect(m_ui->buttonSave, &QPushButton::clicked, this, &AddNewTorrentDialog::saveTorrentFile);
m_ui->savePath->setMode(FileSystemPathEdit::Mode::DirectorySave);
m_ui->savePath->setDialogCaption(tr("Choose save path"));
m_ui->savePath->setMaxVisibleItems(20);
const auto *session = BitTorrent::Session::instance();
m_ui->downloadPath->setMode(FileSystemPathEdit::Mode::DirectorySave);
m_ui->downloadPath->setDialogCaption(tr("Choose save path"));
m_ui->downloadPath->setMaxVisibleItems(20);
m_ui->addToQueueTopCheckBox->setChecked(m_torrentParams.addToQueueTop.value_or(session->isAddTorrentToQueueTop()));
m_ui->stopConditionComboBox->setToolTip(
u"<html><body><p><b>" + tr("None") + u"</b> - " + tr("No stop condition is set.") + u"</p><p><b>" +
tr("Metadata received") + u"</b> - " + tr("Torrent will stop after metadata is received.") +
u" <em>" + tr("Torrents that have metadata initially will be added as stopped.") + u"</em></p><p><b>" +
tr("Files checked") + u"</b> - " + tr("Torrent will stop after files are initially checked.") +
u" <em>" + tr("This will also download metadata if it wasn't there initially.") + u"</em></p></body></html>");
u"<html><body><p><b>" + tr("None") + u"</b> - " + tr("No stop condition is set.") + u"</p><p><b>"
+ tr("Metadata received") + u"</b> - " + tr("Torrent will stop after metadata is received.")
+ u" <em>" + tr("Torrents that have metadata initially will be added as stopped.") + u"</em></p><p><b>"
+ tr("Files checked") + u"</b> - " + tr("Torrent will stop after files are initially checked.")
+ u" <em>" + tr("This will also download metadata if it wasn't there initially.") + u"</em></p></body></html>");
m_ui->stopConditionComboBox->addItem(tr("None"), QVariant::fromValue(BitTorrent::Torrent::StopCondition::None));
if (!hasMetadata())
m_ui->stopConditionComboBox->addItem(tr("Metadata received"), QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived));
m_ui->stopConditionComboBox->addItem(tr("Files checked"), QVariant::fromValue(BitTorrent::Torrent::StopCondition::FilesChecked));
const auto stopCondition = m_torrentParams.stopCondition.value_or(session->torrentStopCondition());
if (hasMetadata() && (stopCondition == BitTorrent::Torrent::StopCondition::MetadataReceived))
{
m_ui->startTorrentCheckBox->setChecked(false);
m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(BitTorrent::Torrent::StopCondition::None)));
}
else
{
m_ui->startTorrentCheckBox->setChecked(!m_torrentParams.addPaused.value_or(session->isAddTorrentPaused()));
m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(stopCondition)));
}
m_ui->stopConditionLabel->setEnabled(m_ui->startTorrentCheckBox->isChecked());
m_ui->stopConditionComboBox->setEnabled(m_ui->startTorrentCheckBox->isChecked());
connect(m_ui->startTorrentCheckBox, &QCheckBox::toggled, this, [this](const bool checked)
{
m_ui->stopConditionLabel->setEnabled(checked);
m_ui->stopConditionComboBox->setEnabled(checked);
});
m_ui->comboTTM->blockSignals(true); // the TreeView size isn't correct if the slot does its job at this point
m_ui->comboTTM->setCurrentIndex(session->isAutoTMMDisabledByDefault() ? 0 : 1);
m_ui->comboTTM->blockSignals(false);
connect(m_ui->comboTTM, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::TMMChanged);
connect(m_ui->savePath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onSavePathChanged);
connect(m_ui->downloadPath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onDownloadPathChanged);
connect(m_ui->groupBoxDownloadPath, &QGroupBox::toggled, this, &AddNewTorrentDialog::onUseDownloadPathChanged);
m_ui->checkBoxRememberLastSavePath->setChecked(m_storeRememberLastSavePath);
m_ui->contentLayoutComboBox->setCurrentIndex(
static_cast<int>(m_torrentParams.contentLayout.value_or(session->torrentContentLayout())));
connect(m_ui->contentLayoutComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::contentLayoutChanged);
m_ui->sequentialCheckBox->setChecked(m_torrentParams.sequential);
m_ui->firstLastCheckBox->setChecked(m_torrentParams.firstLastPiecePriority);
m_ui->skipCheckingCheckBox->setChecked(m_torrentParams.skipChecking);
m_ui->doNotDeleteTorrentCheckBox->setVisible(TorrentFileGuard::autoDeleteMode() != TorrentFileGuard::Never);
// Load categories
QStringList categories = session->categories();
std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
const QString defaultCategory = m_storeDefaultCategory;
if (!m_torrentParams.category.isEmpty())
m_ui->categoryComboBox->addItem(m_torrentParams.category);
if (!defaultCategory.isEmpty())
m_ui->categoryComboBox->addItem(defaultCategory);
m_ui->categoryComboBox->addItem(u""_s);
for (const QString &category : asConst(categories))
{
if ((category != defaultCategory) && (category != m_torrentParams.category))
m_ui->categoryComboBox->addItem(category);
}
connect(m_ui->categoryComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::categoryChanged);
m_ui->tagsLineEdit->setText(Utils::String::joinIntoString(m_torrentParams.tags, u", "_s));
connect(m_ui->tagsEditButton, &QAbstractButton::clicked, this, [this]
{
auto *dlg = new TorrentTagsDialog(m_torrentParams.tags, this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
connect(dlg, &TorrentTagsDialog::accepted, this, [this, dlg]
{
m_torrentParams.tags = dlg->tags();
m_ui->tagsLineEdit->setText(Utils::String::joinIntoString(m_torrentParams.tags, u", "_s));
});
dlg->open();
});
// Torrent content filtering
m_filterLine->setPlaceholderText(tr("Filter files..."));
m_filterLine->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
connect(m_filterLine, &LineEdit::textChanged, m_ui->contentTreeView, &TorrentContentWidget::setFilterPattern);
m_filterLine->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_filterLine, &QWidget::customContextMenuRequested, this, &AddNewTorrentDialog::showContentFilterContextMenu);
m_ui->contentFilterLayout->insertWidget(3, m_filterLine);
const auto *focusSearchHotkey = new QShortcut(QKeySequence::Find, this);
connect(focusSearchHotkey, &QShortcut::activated, this, [this]()
@@ -403,9 +335,6 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
loadState();
connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll);
connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone);
if (const QByteArray state = m_storeTreeHeaderState; !state.isEmpty())
m_ui->contentTreeView->header()->restoreState(state);
// Hide useless columns after loading the header state
@@ -415,17 +344,34 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
m_ui->contentTreeView->setColumnsVisibilityMode(TorrentContentWidget::ColumnsVisibilityMode::Locked);
m_ui->contentTreeView->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Rename);
m_ui->labelCommentData->setText(tr("Not Available", "This comment is unavailable"));
m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable"));
m_filterLine->blockSignals(true);
// Default focus
if (m_ui->comboTTM->currentIndex() == 0) // 0 is Manual mode
m_ui->savePath->setFocus();
else
m_ui->categoryComboBox->setFocus();
connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
connect(m_ui->buttonSave, &QPushButton::clicked, this, &AddNewTorrentDialog::saveTorrentFile);
connect(m_ui->savePath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onSavePathChanged);
connect(m_ui->downloadPath, &FileSystemPathEdit::selectedPathChanged, this, &AddNewTorrentDialog::onDownloadPathChanged);
connect(m_ui->groupBoxDownloadPath, &QGroupBox::toggled, this, &AddNewTorrentDialog::onUseDownloadPathChanged);
connect(m_ui->comboTMM, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::TMMChanged);
connect(m_ui->startTorrentCheckBox, &QCheckBox::toggled, this, [this](const bool checked)
{
m_ui->stopConditionLabel->setEnabled(checked);
m_ui->stopConditionComboBox->setEnabled(checked);
});
connect(m_ui->contentLayoutComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::contentLayoutChanged);
connect(m_ui->categoryComboBox, &QComboBox::currentIndexChanged, this, &AddNewTorrentDialog::categoryChanged);
connect(m_ui->tagsEditButton, &QAbstractButton::clicked, this, [this]
{
auto *dlg = new TorrentTagsDialog(m_currentContext->torrentParams.tags, this);
dlg->setAttribute(Qt::WA_DeleteOnClose);
connect(dlg, &TorrentTagsDialog::accepted, this, [this, dlg]
{
m_currentContext->torrentParams.tags = dlg->tags();
m_ui->tagsLineEdit->setText(Utils::String::joinIntoString(m_currentContext->torrentParams.tags, u", "_s));
});
dlg->open();
});
connect(m_filterLine, &LineEdit::textChanged, this, &AddNewTorrentDialog::setContentFilterPattern);
connect(m_ui->buttonSelectAll, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkAll);
connect(m_ui->buttonSelectNone, &QPushButton::clicked, m_ui->contentTreeView, &TorrentContentWidget::checkNone);
connect(Preferences::instance(), &Preferences::changed, []
{
const int length = Preferences::instance()->addNewTorrentDialogSavePathHistoryLength();
@@ -433,25 +379,7 @@ AddNewTorrentDialog::AddNewTorrentDialog(const BitTorrent::TorrentDescriptor &to
, QStringList(settings()->loadValue<QStringList>(KEY_SAVEPATHHISTORY).mid(0, length)));
});
const BitTorrent::InfoHash infoHash = m_torrentDescr.infoHash();
m_ui->labelInfohash1Data->setText(infoHash.v1().isValid() ? infoHash.v1().toString() : tr("N/A"));
m_ui->labelInfohash2Data->setText(infoHash.v2().isValid() ? infoHash.v2().toString() : tr("N/A"));
if (hasMetadata())
{
setupTreeview();
}
else
{
// Set dialog title
const QString torrentName = m_torrentDescr.name();
setWindowTitle(torrentName.isEmpty() ? tr("Magnet link") : torrentName);
updateDiskSpaceLabel();
setMetadataProgressIndicator(true, tr("Retrieving metadata..."));
}
TMMChanged(m_ui->comboTTM->currentIndex());
setCurrentContext(std::make_shared<Context>(Context {torrentDescr, inParams}));
}
AddNewTorrentDialog::~AddNewTorrentDialog()
@@ -460,16 +388,6 @@ AddNewTorrentDialog::~AddNewTorrentDialog()
delete m_ui;
}
BitTorrent::TorrentDescriptor AddNewTorrentDialog::torrentDescriptor() const
{
return m_torrentDescr;
}
BitTorrent::AddTorrentParams AddNewTorrentDialog::addTorrentParams() const
{
return m_torrentParams;
}
bool AddNewTorrentDialog::isDoNotDeleteTorrentChecked() const
{
return m_ui->doNotDeleteTorrentCheckBox->isChecked();
@@ -485,9 +403,16 @@ void AddNewTorrentDialog::loadState()
void AddNewTorrentDialog::saveState()
{
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
const bool hasMetadata = torrentDescr.info().has_value();
m_storeDialogSize = size();
m_storeSplitterState = m_ui->splitter->saveState();
if (hasMetadata())
if (hasMetadata)
m_storeTreeHeaderState = m_ui->contentTreeView->header()->saveState();
}
@@ -501,14 +426,174 @@ void AddNewTorrentDialog::showEvent(QShowEvent *event)
raise();
}
void AddNewTorrentDialog::setCurrentContext(const std::shared_ptr<Context> context)
{
Q_ASSERT(context);
if (!context) [[unlikely]]
return;
m_currentContext = context;
const QSignalBlocker comboTMMSignalBlocker {m_ui->comboTMM};
const QSignalBlocker startTorrentCheckBoxSignalBlocker {m_ui->startTorrentCheckBox};
const QSignalBlocker contentLayoutComboBoxSignalBlocker {m_ui->contentLayoutComboBox};
const QSignalBlocker categoryComboBoxSignalBlocker {m_ui->categoryComboBox};
const BitTorrent::AddTorrentParams &addTorrentParams = m_currentContext->torrentParams;
const auto *session = BitTorrent::Session::instance();
// TODO: set dialog file properties using m_torrentParams.filePriorities
m_ui->comboTMM->setCurrentIndex(addTorrentParams.useAutoTMM.value_or(!session->isAutoTMMDisabledByDefault()) ? 1 : 0);
m_ui->addToQueueTopCheckBox->setChecked(addTorrentParams.addToQueueTop.value_or(session->isAddTorrentToQueueTop()));
m_ui->startTorrentCheckBox->setChecked(!addTorrentParams.addStopped.value_or(session->isAddTorrentStopped()));
m_ui->stopConditionLabel->setEnabled(m_ui->startTorrentCheckBox->isChecked());
m_ui->stopConditionComboBox->setEnabled(m_ui->startTorrentCheckBox->isChecked());
m_ui->contentLayoutComboBox->setCurrentIndex(
static_cast<int>(addTorrentParams.contentLayout.value_or(session->torrentContentLayout())));
m_ui->sequentialCheckBox->setChecked(addTorrentParams.sequential);
m_ui->firstLastCheckBox->setChecked(addTorrentParams.firstLastPiecePriority);
m_ui->skipCheckingCheckBox->setChecked(addTorrentParams.skipChecking);
m_ui->tagsLineEdit->setText(Utils::String::joinIntoString(addTorrentParams.tags, u", "_s));
// Load categories
QStringList categories = session->categories();
std::sort(categories.begin(), categories.end(), Utils::Compare::NaturalLessThan<Qt::CaseInsensitive>());
const QString defaultCategory = m_storeDefaultCategory;
if (!addTorrentParams.category.isEmpty())
m_ui->categoryComboBox->addItem(addTorrentParams.category);
if (!defaultCategory.isEmpty())
m_ui->categoryComboBox->addItem(defaultCategory);
m_ui->categoryComboBox->addItem(u""_s);
for (const QString &category : asConst(categories))
{
if ((category != defaultCategory) && (category != addTorrentParams.category))
m_ui->categoryComboBox->addItem(category);
}
m_filterLine->blockSignals(true);
m_filterLine->clear();
// Default focus
if (m_ui->comboTMM->currentIndex() == 0) // 0 is Manual mode
m_ui->savePath->setFocus();
else
m_ui->categoryComboBox->setFocus();
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
const BitTorrent::InfoHash infoHash = torrentDescr.infoHash();
const bool hasMetadata = torrentDescr.info().has_value();
if (hasMetadata)
m_ui->stopConditionComboBox->removeItem(m_ui->stopConditionComboBox->findData(QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived)));
else
m_ui->stopConditionComboBox->insertItem(1, tr("Metadata received"), QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived));
const auto stopCondition = addTorrentParams.stopCondition.value_or(session->torrentStopCondition());
if (hasMetadata && (stopCondition == BitTorrent::Torrent::StopCondition::MetadataReceived))
{
m_ui->startTorrentCheckBox->setChecked(false);
m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(BitTorrent::Torrent::StopCondition::None)));
}
else
{
m_ui->startTorrentCheckBox->setChecked(!addTorrentParams.addStopped.value_or(session->isAddTorrentStopped()));
m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(stopCondition)));
}
m_ui->labelInfohash1Data->setText(infoHash.v1().isValid() ? infoHash.v1().toString() : tr("N/A"));
m_ui->labelInfohash2Data->setText(infoHash.v2().isValid() ? infoHash.v2().toString() : tr("N/A"));
if (hasMetadata)
{
m_ui->lblMetaLoading->setVisible(false);
m_ui->progMetaLoading->setVisible(false);
m_ui->buttonSave->setVisible(false);
setupTreeview();
}
else
{
// Set dialog title
const QString torrentName = torrentDescr.name();
setWindowTitle(torrentName.isEmpty() ? tr("Magnet link") : torrentName);
m_ui->labelCommentData->setText(tr("Not Available", "This comment is unavailable"));
m_ui->labelDateData->setText(tr("Not Available", "This date is unavailable"));
updateDiskSpaceLabel();
setMetadataProgressIndicator(true, tr("Retrieving metadata..."));
}
TMMChanged(m_ui->comboTMM->currentIndex());
}
void AddNewTorrentDialog::updateCurrentContext()
{
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
BitTorrent::AddTorrentParams &addTorrentParams = m_currentContext->torrentParams;
addTorrentParams.skipChecking = m_ui->skipCheckingCheckBox->isChecked();
// Category
addTorrentParams.category = m_ui->categoryComboBox->currentText();
if (m_ui->defaultCategoryCheckbox->isChecked())
m_storeDefaultCategory = addTorrentParams.category;
m_storeRememberLastSavePath = m_ui->checkBoxRememberLastSavePath->isChecked();
addTorrentParams.addToQueueTop = m_ui->addToQueueTopCheckBox->isChecked();
addTorrentParams.addStopped = !m_ui->startTorrentCheckBox->isChecked();
addTorrentParams.stopCondition = m_ui->stopConditionComboBox->currentData().value<BitTorrent::Torrent::StopCondition>();
addTorrentParams.contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
addTorrentParams.sequential = m_ui->sequentialCheckBox->isChecked();
addTorrentParams.firstLastPiecePriority = m_ui->firstLastCheckBox->isChecked();
const bool useAutoTMM = (m_ui->comboTMM->currentIndex() == 1); // 1 is Automatic mode. Handle all non 1 values as manual mode.
addTorrentParams.useAutoTMM = useAutoTMM;
if (!useAutoTMM)
{
const int savePathHistoryLength = Preferences::instance()->addNewTorrentDialogSavePathHistoryLength();
const Path savePath = m_ui->savePath->selectedPath();
addTorrentParams.savePath = savePath;
updatePathHistory(KEY_SAVEPATHHISTORY, savePath, savePathHistoryLength);
addTorrentParams.useDownloadPath = m_ui->groupBoxDownloadPath->isChecked();
if (addTorrentParams.useDownloadPath)
{
const Path downloadPath = m_ui->downloadPath->selectedPath();
addTorrentParams.downloadPath = downloadPath;
updatePathHistory(KEY_DOWNLOADPATHHISTORY, downloadPath, savePathHistoryLength);
}
else
{
addTorrentParams.downloadPath = Path();
}
}
else
{
addTorrentParams.savePath = Path();
addTorrentParams.downloadPath = Path();
addTorrentParams.useDownloadPath = std::nullopt;
}
}
void AddNewTorrentDialog::updateDiskSpaceLabel()
{
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
const bool hasMetadata = torrentDescr.info().has_value();
// Determine torrent size
qlonglong torrentSize = 0;
if (hasMetadata())
if (hasMetadata)
{
const auto torrentInfo = *m_torrentDescr.info();
const auto torrentInfo = *torrentDescr.info();
const QVector<BitTorrent::DownloadPriority> &priorities = m_contentAdaptor->filePriorities();
Q_ASSERT(priorities.size() == torrentInfo.filesCount());
for (int i = 0; i < priorities.size(); ++i)
@@ -548,7 +633,7 @@ void AddNewTorrentDialog::onUseDownloadPathChanged(const bool checked)
void AddNewTorrentDialog::categoryChanged([[maybe_unused]] const int index)
{
if (m_ui->comboTTM->currentIndex() == 1)
if (m_ui->comboTMM->currentIndex() == 1)
{
const auto *btSession = BitTorrent::Session::instance();
const QString categoryName = m_ui->categoryComboBox->currentText();
@@ -567,7 +652,14 @@ void AddNewTorrentDialog::categoryChanged([[maybe_unused]] const int index)
void AddNewTorrentDialog::contentLayoutChanged()
{
if (!hasMetadata())
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
const bool hasMetadata = torrentDescr.info().has_value();
if (!hasMetadata)
return;
const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
@@ -577,37 +669,66 @@ void AddNewTorrentDialog::contentLayoutChanged()
void AddNewTorrentDialog::saveTorrentFile()
{
Q_ASSERT(hasMetadata());
if (!hasMetadata()) [[unlikely]]
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
const auto torrentInfo = *m_torrentDescr.info();
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
const bool hasMetadata = torrentDescr.info().has_value();
Q_ASSERT(hasMetadata);
if (!hasMetadata) [[unlikely]]
return;
const auto torrentInfo = *torrentDescr.info();
const QString filter {tr("Torrent file (*%1)").arg(TORRENT_FILE_EXTENSION)};
Path path {QFileDialog::getSaveFileName(this, tr("Save as torrent file")
, QDir::home().absoluteFilePath(torrentInfo.name() + TORRENT_FILE_EXTENSION)
, filter)};
if (path.isEmpty()) return;
if (path.isEmpty())
return;
if (!path.hasExtension(TORRENT_FILE_EXTENSION))
path += TORRENT_FILE_EXTENSION;
const auto result = m_torrentDescr.saveToFile(path);
if (!result)
if (const auto result = torrentDescr.saveToFile(path); !result)
{
QMessageBox::critical(this, tr("I/O Error")
, tr("Couldn't export torrent metadata file '%1'. Reason: %2.").arg(path.toString(), result.error()));
}
}
bool AddNewTorrentDialog::hasMetadata() const
void AddNewTorrentDialog::showContentFilterContextMenu()
{
return m_torrentDescr.info().has_value();
QMenu *menu = m_filterLine->createStandardContextMenu();
auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu);
connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format)
{
m_storeFilterPatternFormat = format;
setContentFilterPattern();
});
menu->addSeparator();
menu->addMenu(formatMenu);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(QCursor::pos());
}
void AddNewTorrentDialog::setContentFilterPattern()
{
m_ui->contentTreeView->setFilterPattern(m_filterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards));
}
void AddNewTorrentDialog::populateSavePaths()
{
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
const BitTorrent::AddTorrentParams &addTorrentParams = m_currentContext->torrentParams;
const auto *btSession = BitTorrent::Session::instance();
m_ui->savePath->blockSignals(true);
@@ -629,8 +750,8 @@ void AddNewTorrentDialog::populateSavePaths()
}
else
{
if (!m_torrentParams.savePath.isEmpty())
setPath(m_ui->savePath, m_torrentParams.savePath);
if (!addTorrentParams.savePath.isEmpty())
setPath(m_ui->savePath, addTorrentParams.savePath);
else if (!m_storeRememberLastSavePath)
setPath(m_ui->savePath, btSession->savePath());
else
@@ -661,11 +782,11 @@ void AddNewTorrentDialog::populateSavePaths()
}
else
{
const bool useDownloadPath = m_torrentParams.useDownloadPath.value_or(btSession->isDownloadPathEnabled());
const bool useDownloadPath = addTorrentParams.useDownloadPath.value_or(btSession->isDownloadPathEnabled());
m_ui->groupBoxDownloadPath->setChecked(useDownloadPath);
if (!m_torrentParams.downloadPath.isEmpty())
setPath(m_ui->downloadPath, m_torrentParams.downloadPath);
if (!addTorrentParams.downloadPath.isEmpty())
setPath(m_ui->downloadPath, addTorrentParams.downloadPath);
else if (!m_storeRememberLastSavePath)
setPath(m_ui->downloadPath, btSession->downloadPath());
else
@@ -682,63 +803,30 @@ void AddNewTorrentDialog::populateSavePaths()
void AddNewTorrentDialog::accept()
{
// TODO: Check if destination actually exists
m_torrentParams.skipChecking = m_ui->skipCheckingCheckBox->isChecked();
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
// Category
m_torrentParams.category = m_ui->categoryComboBox->currentText();
if (m_ui->defaultCategoryCheckbox->isChecked())
m_storeDefaultCategory = m_torrentParams.category;
updateCurrentContext();
emit torrentAccepted(m_currentContext->torrentDescr, m_currentContext->torrentParams);
m_storeRememberLastSavePath = m_ui->checkBoxRememberLastSavePath->isChecked();
m_torrentParams.addToQueueTop = m_ui->addToQueueTopCheckBox->isChecked();
m_torrentParams.addPaused = !m_ui->startTorrentCheckBox->isChecked();
m_torrentParams.stopCondition = m_ui->stopConditionComboBox->currentData().value<BitTorrent::Torrent::StopCondition>();
m_torrentParams.contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_torrentParams.sequential = m_ui->sequentialCheckBox->isChecked();
m_torrentParams.firstLastPiecePriority = m_ui->firstLastCheckBox->isChecked();
const bool useAutoTMM = (m_ui->comboTTM->currentIndex() == 1); // 1 is Automatic mode. Handle all non 1 values as manual mode.
m_torrentParams.useAutoTMM = useAutoTMM;
if (!useAutoTMM)
{
const int savePathHistoryLength = Preferences::instance()->addNewTorrentDialogSavePathHistoryLength();
const Path savePath = m_ui->savePath->selectedPath();
m_torrentParams.savePath = savePath;
updatePathHistory(KEY_SAVEPATHHISTORY, savePath, savePathHistoryLength);
m_torrentParams.useDownloadPath = m_ui->groupBoxDownloadPath->isChecked();
if (m_torrentParams.useDownloadPath)
{
const Path downloadPath = m_ui->downloadPath->selectedPath();
m_torrentParams.downloadPath = downloadPath;
updatePathHistory(KEY_DOWNLOADPATHHISTORY, downloadPath, savePathHistoryLength);
}
else
{
m_torrentParams.downloadPath = Path();
}
}
else
{
m_torrentParams.savePath = Path();
m_torrentParams.downloadPath = Path();
m_torrentParams.useDownloadPath = std::nullopt;
}
setEnabled(!m_ui->checkBoxNeverShow->isChecked());
Preferences::instance()->setAddNewTorrentDialogEnabled(!m_ui->checkBoxNeverShow->isChecked());
QDialog::accept();
}
void AddNewTorrentDialog::reject()
{
if (!hasMetadata())
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
const bool hasMetadata = torrentDescr.info().has_value();
if (!hasMetadata)
{
setMetadataProgressIndicator(false);
BitTorrent::Session::instance()->cancelDownloadMetadata(m_torrentDescr.infoHash().toTorrentID());
BitTorrent::Session::instance()->cancelDownloadMetadata(torrentDescr.infoHash().toTorrentID());
}
QDialog::reject();
@@ -746,15 +834,20 @@ void AddNewTorrentDialog::reject()
void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata)
{
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
Q_ASSERT(metadata.isValid());
if (!metadata.isValid()) [[unlikely]]
return;
Q_ASSERT(metadata.matchesInfoHash(m_torrentDescr.infoHash()));
if (!metadata.matchesInfoHash(m_torrentDescr.infoHash())) [[unlikely]]
BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
Q_ASSERT(metadata.matchesInfoHash(torrentDescr.infoHash()));
if (!metadata.matchesInfoHash(torrentDescr.infoHash())) [[unlikely]]
return;
m_torrentDescr.setTorrentInfo(metadata);
torrentDescr.setTorrentInfo(metadata);
setMetadataProgressIndicator(true, tr("Parsing metadata..."));
// Update UI
@@ -773,7 +866,7 @@ void AddNewTorrentDialog::updateMetadata(const BitTorrent::TorrentInfo &metadata
}
m_ui->buttonSave->setVisible(true);
if (m_torrentDescr.infoHash().v2().isValid())
if (torrentDescr.infoHash().v2().isValid())
{
m_ui->buttonSave->setEnabled(false);
m_ui->buttonSave->setToolTip(tr("Cannot create v2 torrent until its data is fully downloaded."));
@@ -790,24 +883,32 @@ void AddNewTorrentDialog::setMetadataProgressIndicator(bool visibleIndicator, co
void AddNewTorrentDialog::setupTreeview()
{
Q_ASSERT(hasMetadata());
if (!hasMetadata()) [[unlikely]]
Q_ASSERT(m_currentContext);
if (!m_currentContext) [[unlikely]]
return;
const BitTorrent::TorrentDescriptor &torrentDescr = m_currentContext->torrentDescr;
const bool hasMetadata = torrentDescr.info().has_value();
Q_ASSERT(hasMetadata);
if (!hasMetadata) [[unlikely]]
return;
// Set dialog title
setWindowTitle(m_torrentDescr.name());
setWindowTitle(torrentDescr.name());
const auto &torrentInfo = *m_torrentDescr.info();
const auto &torrentInfo = *torrentDescr.info();
// Set torrent information
m_ui->labelCommentData->setText(Utils::Misc::parseHtmlLinks(torrentInfo.comment().toHtmlEscaped()));
m_ui->labelDateData->setText(!torrentInfo.creationDate().isNull() ? QLocale().toString(torrentInfo.creationDate(), QLocale::ShortFormat) : tr("Not available"));
if (m_torrentParams.filePaths.isEmpty())
m_torrentParams.filePaths = torrentInfo.filePaths();
BitTorrent::AddTorrentParams &addTorrentParams = m_currentContext->torrentParams;
if (addTorrentParams.filePaths.isEmpty())
addTorrentParams.filePaths = torrentInfo.filePaths();
m_contentAdaptor = std::make_unique<TorrentContentAdaptor>(torrentInfo, m_torrentParams.filePaths
, m_torrentParams.filePriorities, [this] { updateDiskSpaceLabel(); });
m_contentAdaptor = std::make_unique<TorrentContentAdaptor>(torrentInfo, addTorrentParams.filePaths
, addTorrentParams.filePriorities, [this] { updateDiskSpaceLabel(); });
const auto contentLayout = static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex());
m_contentAdaptor->applyContentLayout(contentLayout);
@@ -816,15 +917,7 @@ void AddNewTorrentDialog::setupTreeview()
{
// Check file name blacklist for torrents that are manually added
QVector<BitTorrent::DownloadPriority> priorities = m_contentAdaptor->filePriorities();
for (int i = 0; i < priorities.size(); ++i)
{
if (priorities[i] == BitTorrent::DownloadPriority::Ignored)
continue;
if (BitTorrent::Session::instance()->isFilenameExcluded(torrentInfo.filePath(i).filename()))
priorities[i] = BitTorrent::DownloadPriority::Ignored;
}
BitTorrent::Session::instance()->applyFilenameFilter(m_contentAdaptor->filePaths(), priorities);
m_contentAdaptor->prioritizeFiles(priorities);
}

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2012 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -33,13 +33,19 @@
#include <QDialog>
#include "base/bittorrent/addtorrentparams.h"
#include "base/bittorrent/torrentdescriptor.h"
#include "base/path.h"
#include "base/settingvalue.h"
#include "filterpatternformat.h"
class LineEdit;
namespace BitTorrent
{
class TorrentDescriptor;
class TorrentInfo;
struct AddTorrentParams;
}
namespace Ui
{
class AddNewTorrentDialog;
@@ -55,12 +61,12 @@ public:
, const BitTorrent::AddTorrentParams &inParams, QWidget *parent);
~AddNewTorrentDialog() override;
BitTorrent::TorrentDescriptor torrentDescriptor() const;
BitTorrent::AddTorrentParams addTorrentParams() const;
bool isDoNotDeleteTorrentChecked() const;
void updateMetadata(const BitTorrent::TorrentInfo &metadata);
signals:
void torrentAccepted(const BitTorrent::TorrentDescriptor &torrentDescriptor, const BitTorrent::AddTorrentParams &addTorrentParams);
private slots:
void updateDiskSpaceLabel();
void onSavePathChanged(const Path &newPath);
@@ -75,29 +81,34 @@ private slots:
private:
class TorrentContentAdaptor;
struct Context;
void showEvent(QShowEvent *event) override;
void setCurrentContext(std::shared_ptr<Context> context);
void updateCurrentContext();
void populateSavePaths();
void loadState();
void saveState();
void setMetadataProgressIndicator(bool visibleIndicator, const QString &labelText = {});
void setupTreeview();
void saveTorrentFile();
bool hasMetadata() const;
void showEvent(QShowEvent *event) override;
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::AddNewTorrentDialog *m_ui = nullptr;
std::unique_ptr<TorrentContentAdaptor> m_contentAdaptor;
BitTorrent::TorrentDescriptor m_torrentDescr;
BitTorrent::AddTorrentParams m_torrentParams;
int m_savePathIndex = -1;
int m_downloadPathIndex = -1;
bool m_useDownloadPath = false;
LineEdit *m_filterLine = nullptr;
std::shared_ptr<Context> m_currentContext;
SettingValue<QSize> m_storeDialogSize;
SettingValue<QString> m_storeDefaultCategory;
SettingValue<bool> m_storeRememberLastSavePath;
SettingValue<QByteArray> m_storeTreeHeaderState;
SettingValue<QByteArray> m_storeSplitterState;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
};

View File

@@ -76,7 +76,7 @@
</widget>
</item>
<item>
<widget class="QComboBox" name="comboTTM">
<widget class="QComboBox" name="comboTMM">
<property name="toolTip">
<string>Automatic mode means that various torrent properties(eg save path) will be decided by the associated category</string>
</property>

View File

@@ -240,15 +240,15 @@ void AddTorrentParamsWidget::populate()
m_ui->tagsLineEdit->setText(Utils::String::joinIntoString(m_addTorrentParams.tags, u", "_s));
m_ui->startTorrentComboBox->disconnect(this);
m_ui->startTorrentComboBox->setCurrentIndex(m_addTorrentParams.addPaused
? m_ui->startTorrentComboBox->findData(!*m_addTorrentParams.addPaused) : 0);
m_ui->startTorrentComboBox->setCurrentIndex(m_addTorrentParams.addStopped
? m_ui->startTorrentComboBox->findData(!*m_addTorrentParams.addStopped) : 0);
connect(m_ui->startTorrentComboBox, &QComboBox::currentIndexChanged, this, [this]
{
const QVariant data = m_ui->startTorrentComboBox->currentData();
if (!data.isValid())
m_addTorrentParams.addPaused = std::nullopt;
m_addTorrentParams.addStopped = std::nullopt;
else
m_addTorrentParams.addPaused = !data.toBool();
m_addTorrentParams.addStopped = !data.toBool();
});
m_ui->skipCheckingCheckBox->disconnect(this);

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2016 qBittorrent project
* Copyright (C) 2016-2024 qBittorrent project
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -63,6 +63,7 @@ namespace
// qBittorrent section
QBITTORRENT_HEADER,
RESUME_DATA_STORAGE,
TORRENT_CONTENT_REMOVE_OPTION,
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
MEMORY_WORKING_SET_LIMIT,
#endif
@@ -105,6 +106,8 @@ namespace
ENABLE_MARK_OF_THE_WEB,
#endif // Q_OS_MACOS || Q_OS_WIN
PYTHON_EXECUTABLE_PATH,
START_SESSION_PAUSED,
SESSION_SHUTDOWN_TIMEOUT,
// libtorrent section
LIBTORRENT_HEADER,
@@ -331,6 +334,10 @@ void AdvancedSettings::saveAdvancedSettings() const
#endif // Q_OS_MACOS || Q_OS_WIN
// Python executable path
pref->setPythonExecutablePath(Path(m_pythonExecutablePath.text().trimmed()));
// Start session paused
session->setStartPaused(m_checkBoxStartSessionPaused.isChecked());
// Session shutdown timeout
session->setShutdownTimeout(m_spinBoxSessionShutdownTimeout.value());
// Choking algorithm
session->setChokingAlgorithm(m_comboBoxChokingAlgorithm.currentData().value<BitTorrent::ChokingAlgorithm>());
// Seed choking algorithm
@@ -358,6 +365,8 @@ void AdvancedSettings::saveAdvancedSettings() const
session->setI2PInboundLength(m_spinBoxI2PInboundLength.value());
session->setI2POutboundLength(m_spinBoxI2POutboundLength.value());
#endif
session->setTorrentContentRemoveOption(m_comboBoxTorrentContentRemoveOption.currentData().value<BitTorrent::TorrentContentRemoveOption>());
}
#ifndef QBT_USES_LIBTORRENT2
@@ -466,6 +475,11 @@ void AdvancedSettings::loadAdvancedSettings()
m_comboBoxResumeDataStorage.setCurrentIndex(m_comboBoxResumeDataStorage.findData(QVariant::fromValue(session->resumeDataStorageType())));
addRow(RESUME_DATA_STORAGE, tr("Resume data storage type (requires restart)"), &m_comboBoxResumeDataStorage);
m_comboBoxTorrentContentRemoveOption.addItem(tr("Delete files permanently"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::Delete));
m_comboBoxTorrentContentRemoveOption.addItem(tr("Move files to trash (if possible)"), QVariant::fromValue(BitTorrent::TorrentContentRemoveOption::MoveToTrash));
m_comboBoxTorrentContentRemoveOption.setCurrentIndex(m_comboBoxTorrentContentRemoveOption.findData(QVariant::fromValue(session->torrentContentRemoveOption())));
addRow(TORRENT_CONTENT_REMOVE_OPTION, tr("Torrent content removing mode"), &m_comboBoxTorrentContentRemoveOption);
#if defined(QBT_USES_LIBTORRENT2) && !defined(Q_OS_MACOS)
// Physical memory (RAM) usage limit
m_spinBoxMemoryWorkingSetLimit.setMinimum(1);
@@ -843,6 +857,18 @@ void AdvancedSettings::loadAdvancedSettings()
m_pythonExecutablePath.setPlaceholderText(tr("(Auto detect if empty)"));
m_pythonExecutablePath.setText(pref->getPythonExecutablePath().toString());
addRow(PYTHON_EXECUTABLE_PATH, tr("Python executable path (may require restart)"), &m_pythonExecutablePath);
// Start session paused
m_checkBoxStartSessionPaused.setChecked(session->isStartPaused());
addRow(START_SESSION_PAUSED, tr("Start BitTorrent session in paused state"), &m_checkBoxStartSessionPaused);
// Session shutdown timeout
m_spinBoxSessionShutdownTimeout.setMinimum(-1);
m_spinBoxSessionShutdownTimeout.setMaximum(std::numeric_limits<int>::max());
m_spinBoxSessionShutdownTimeout.setValue(session->shutdownTimeout());
m_spinBoxSessionShutdownTimeout.setSuffix(tr(" sec", " seconds"));
m_spinBoxSessionShutdownTimeout.setSpecialValueText(tr("-1 (unlimited)"));
m_spinBoxSessionShutdownTimeout.setToolTip(u"Sets the timeout for the session to be shut down gracefully, at which point it will be forcibly terminated.<br>Note that this does not apply to the saving resume data time."_s);
addRow(SESSION_SHUTDOWN_TIMEOUT, tr("BitTorrent session shutdown timeout [-1: unlimited]"), &m_spinBoxSessionShutdownTimeout);
// Choking algorithm
m_comboBoxChokingAlgorithm.addItem(tr("Fixed slots"), QVariant::fromValue(BitTorrent::ChokingAlgorithm::FixedSlots));
m_comboBoxChokingAlgorithm.addItem(tr("Upload rate based"), QVariant::fromValue(BitTorrent::ChokingAlgorithm::RateBased));
@@ -940,6 +966,7 @@ void AdvancedSettings::addRow(const int row, const QString &text, T *widget)
{
auto *label = new QLabel(text);
label->setOpenExternalLinks(true);
label->setToolTip(widget->toolTip());
setCellWidget(row, PROPERTY, label);
setCellWidget(row, VALUE, widget);

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2015 qBittorrent project
* Copyright (C) 2015-2024 qBittorrent project
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -73,15 +73,15 @@ private:
m_spinBoxOutgoingPortsMin, m_spinBoxOutgoingPortsMax, m_spinBoxUPnPLeaseDuration, m_spinBoxPeerToS,
m_spinBoxListRefresh, m_spinBoxTrackerPort, m_spinBoxSendBufferWatermark, m_spinBoxSendBufferLowWatermark,
m_spinBoxSendBufferWatermarkFactor, m_spinBoxConnectionSpeed, m_spinBoxSocketSendBufferSize, m_spinBoxSocketReceiveBufferSize, m_spinBoxSocketBacklogSize,
m_spinBoxMaxConcurrentHTTPAnnounces, m_spinBoxStopTrackerTimeout,
m_spinBoxMaxConcurrentHTTPAnnounces, m_spinBoxStopTrackerTimeout, m_spinBoxSessionShutdownTimeout,
m_spinBoxSavePathHistoryLength, m_spinBoxPeerTurnover, m_spinBoxPeerTurnoverCutoff, m_spinBoxPeerTurnoverInterval, m_spinBoxRequestQueueSize;
QCheckBox m_checkBoxOsCache, m_checkBoxRecheckCompleted, m_checkBoxResolveCountries, m_checkBoxResolveHosts,
m_checkBoxProgramNotifications, m_checkBoxTorrentAddedNotifications, m_checkBoxReannounceWhenAddressChanged, m_checkBoxTrackerFavicon, m_checkBoxTrackerStatus,
m_checkBoxTrackerPortForwarding, m_checkBoxConfirmTorrentRecheck, m_checkBoxConfirmRemoveAllTags, m_checkBoxAnnounceAllTrackers, m_checkBoxAnnounceAllTiers,
m_checkBoxMultiConnectionsPerIp, m_checkBoxValidateHTTPSTrackerCertificate, m_checkBoxSSRFMitigation, m_checkBoxBlockPeersOnPrivilegedPorts, m_checkBoxPieceExtentAffinity,
m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents;
m_checkBoxSuggestMode, m_checkBoxSpeedWidgetEnabled, m_checkBoxIDNSupport, m_checkBoxConfirmRemoveTrackerFromAllTorrents, m_checkBoxStartSessionPaused;
QComboBox m_comboBoxInterface, m_comboBoxInterfaceAddress, m_comboBoxDiskIOReadMode, m_comboBoxDiskIOWriteMode, m_comboBoxUtpMixedMode, m_comboBoxChokingAlgorithm,
m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage;
m_comboBoxSeedChokingAlgorithm, m_comboBoxResumeDataStorage, m_comboBoxTorrentContentRemoveOption;
QLineEdit m_lineEditAppInstanceName, m_pythonExecutablePath, m_lineEditAnnounceIP, m_lineEditDHTBootstrapNodes;
#ifndef QBT_USES_LIBTORRENT2

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -30,6 +31,7 @@
#include <QPushButton>
#include "base/bittorrent/session.h"
#include "base/global.h"
#include "base/preferences.h"
#include "uithememanager.h"
@@ -53,8 +55,8 @@ DeletionConfirmationDialog::DeletionConfirmationDialog(QWidget *parent, const in
m_ui->rememberBtn->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s));
m_ui->rememberBtn->setIconSize(Utils::Gui::mediumIconSize());
m_ui->checkPermDelete->setChecked(defaultDeleteFiles || Preferences::instance()->deleteTorrentFilesAsDefault());
connect(m_ui->checkPermDelete, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState);
m_ui->checkRemoveContent->setChecked(defaultDeleteFiles || Preferences::instance()->removeTorrentContent());
connect(m_ui->checkRemoveContent, &QCheckBox::clicked, this, &DeletionConfirmationDialog::updateRememberButtonState);
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Remove"));
m_ui->buttonBox->button(QDialogButtonBox::Cancel)->setFocus();
@@ -67,18 +69,18 @@ DeletionConfirmationDialog::~DeletionConfirmationDialog()
delete m_ui;
}
bool DeletionConfirmationDialog::isDeleteFileSelected() const
bool DeletionConfirmationDialog::isRemoveContentSelected() const
{
return m_ui->checkPermDelete->isChecked();
return m_ui->checkRemoveContent->isChecked();
}
void DeletionConfirmationDialog::updateRememberButtonState()
{
m_ui->rememberBtn->setEnabled(m_ui->checkPermDelete->isChecked() != Preferences::instance()->deleteTorrentFilesAsDefault());
m_ui->rememberBtn->setEnabled(m_ui->checkRemoveContent->isChecked() != Preferences::instance()->removeTorrentContent());
}
void DeletionConfirmationDialog::on_rememberBtn_clicked()
{
Preferences::instance()->setDeleteTorrentFilesAsDefault(m_ui->checkPermDelete->isChecked());
Preferences::instance()->setRemoveTorrentContent(m_ui->checkRemoveContent->isChecked());
m_ui->rememberBtn->setEnabled(false);
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -37,16 +38,16 @@ namespace Ui
class DeletionConfirmationDialog;
}
class DeletionConfirmationDialog : public QDialog
class DeletionConfirmationDialog final : public QDialog
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(DeletionConfirmationDialog)
public:
DeletionConfirmationDialog(QWidget *parent, int size, const QString &name, bool defaultDeleteFiles);
~DeletionConfirmationDialog();
~DeletionConfirmationDialog() override;
bool isDeleteFileSelected() const;
bool isRemoveContentSelected() const;
private slots:
void updateRememberButtonState();

View File

@@ -75,7 +75,7 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkPermDelete">
<widget class="QCheckBox" name="checkRemoveContent">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Minimum">
<horstretch>0</horstretch>
@@ -88,7 +88,7 @@
</font>
</property>
<property name="text">
<string>Also permanently delete the files</string>
<string>Also remove the content files</string>
</property>
</widget>
</item>

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -73,6 +73,7 @@ using namespace std::chrono_literals;
DesktopIntegration::DesktopIntegration(QObject *parent)
: QObject(parent)
, m_storeNotificationEnabled {NOTIFICATIONS_SETTINGS_KEY(u"Enabled"_s), true}
, m_menu {new QMenu}
#ifdef QBT_USES_DBUS
, m_storeNotificationTimeOut {NOTIFICATIONS_SETTINGS_KEY(u"Timeout"_s), -1}
#endif
@@ -80,6 +81,7 @@ DesktopIntegration::DesktopIntegration(QObject *parent)
#ifdef Q_OS_MACOS
desktopIntegrationInstance = this;
MacUtils::overrideDockClickHandler(handleDockClicked);
m_menu->setAsDockMenu();
#else
if (Preferences::instance()->systemTrayEnabled())
createTrayIcon();
@@ -132,46 +134,6 @@ QMenu *DesktopIntegration::menu() const
return m_menu;
}
void DesktopIntegration::setMenu(QMenu *menu)
{
if (menu == m_menu)
return;
#if defined Q_OS_MACOS
if (m_menu)
delete m_menu;
m_menu = menu;
if (m_menu)
m_menu->setAsDockMenu();
#elif defined Q_OS_UNIX
const bool systemTrayEnabled = m_systrayIcon;
if (m_menu)
{
if (m_systrayIcon)
{
delete m_systrayIcon;
m_systrayIcon = nullptr;
}
delete m_menu;
}
m_menu = menu;
if (systemTrayEnabled && !m_systrayIcon)
createTrayIcon();
#else
if (m_menu)
delete m_menu;
m_menu = menu;
if (m_systrayIcon)
m_systrayIcon->setContextMenu(m_menu);
#endif
}
bool DesktopIntegration::isNotificationsEnabled() const
{
return m_storeNotificationEnabled;

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -56,7 +56,6 @@ public:
void setToolTip(const QString &toolTip);
QMenu *menu() const;
void setMenu(QMenu *menu);
bool isNotificationsEnabled() const;
void setNotificationsEnabled(bool value);

View File

@@ -90,11 +90,12 @@ DownloadFromURLDialog::DownloadFromURLDialog(QWidget *parent)
urls << urlString;
}
const QString text = urls.join(u'\n')
+ (!urls.isEmpty() ? u"\n" : u"");
m_ui->textUrls->setText(text);
m_ui->textUrls->moveCursor(QTextCursor::End);
if (!urls.isEmpty())
{
m_ui->textUrls->setText(urls.join(u'\n') + u"\n");
m_ui->textUrls->moveCursor(QTextCursor::End);
m_ui->buttonBox->setFocus();
}
if (const QSize dialogSize = m_storeDialogSize; dialogSize.isValid())
resize(dialogSize);

View File

@@ -0,0 +1,48 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QMetaEnum>
// Using `Q_ENUM_NS()` without a wrapper namespace in our case is not advised
// since `Q_NAMESPACE` cannot be used when the same namespace resides at different files.
// https://www.kdab.com/new-qt-5-8-meta-object-support-namespaces/#comment-143779
inline namespace FilterPatternFormatNS
{
Q_NAMESPACE
enum class FilterPatternFormat
{
PlainText,
Wildcards,
Regex
};
Q_ENUM_NS(FilterPatternFormat)
}

View File

@@ -0,0 +1,82 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#include "filterpatternformatmenu.h"
#include <QActionGroup>
FilterPatternFormatMenu::FilterPatternFormatMenu(const FilterPatternFormat format, QWidget *parent)
: QMenu(parent)
{
setTitle(tr("Pattern Format"));
auto *patternFormatGroup = new QActionGroup(this);
patternFormatGroup->setExclusive(true);
QAction *plainTextAction = addAction(tr("Plain text"));
plainTextAction->setCheckable(true);
patternFormatGroup->addAction(plainTextAction);
QAction *wildcardsAction = addAction(tr("Wildcards"));
wildcardsAction->setCheckable(true);
patternFormatGroup->addAction(wildcardsAction);
QAction *regexAction = addAction(tr("Regular expression"));
regexAction->setCheckable(true);
patternFormatGroup->addAction(regexAction);
switch (format)
{
case FilterPatternFormat::Wildcards:
default:
wildcardsAction->setChecked(true);
break;
case FilterPatternFormat::PlainText:
plainTextAction->setChecked(true);
break;
case FilterPatternFormat::Regex:
regexAction->setChecked(true);
break;
}
connect(plainTextAction, &QAction::toggled, this, [this](const bool checked)
{
if (checked)
emit patternFormatChanged(FilterPatternFormat::PlainText);
});
connect(wildcardsAction, &QAction::toggled, this, [this](const bool checked)
{
if (checked)
emit patternFormatChanged(FilterPatternFormat::Wildcards);
});
connect(regexAction, &QAction::toggled, this, [this](const bool checked)
{
if (checked)
emit patternFormatChanged(FilterPatternFormat::Regex);
});
}

View File

@@ -0,0 +1,45 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* In addition, as a special exception, the copyright holders give permission to
* link this program with the OpenSSL project's "OpenSSL" library (or with
* modified versions of it that use the same license as the "OpenSSL" library),
* and distribute the linked executables. You must obey the GNU General Public
* License in all respects for all of the code used other than "OpenSSL". If you
* modify file(s), you may extend this exception to your version of the file(s),
* but you are not obligated to do so. If you do not wish to do so, delete this
* exception statement from your version.
*/
#pragma once
#include <QMenu>
#include "filterpatternformat.h"
class FilterPatternFormatMenu final : public QMenu
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(FilterPatternFormatMenu)
public:
explicit FilterPatternFormatMenu(FilterPatternFormat format, QWidget *parent = nullptr);
signals:
void patternFormatChanged(FilterPatternFormat format);
};

View File

@@ -1,7 +1,8 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Mike Tzou (Chocobo1)
* Copyright (C) 2016 Eugene Shalygin
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022 Mike Tzou (Chocobo1)
* Copyright (C) 2016 Eugene Shalygin
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -32,6 +33,7 @@
#include <QCompleter>
#include <QContextMenuEvent>
#include <QDir>
#include <QFileIconProvider>
#include <QFileInfo>
#include <QFileSystemModel>
#include <QMenu>
@@ -159,36 +161,34 @@ QValidator::State Private::FileSystemPathValidator::validate(QString &input, [[m
}
Private::FileLineEdit::FileLineEdit(QWidget *parent)
: QLineEdit {parent}
, m_completerModel {new QFileSystemModel(this)}
, m_completer {new QCompleter(this)}
: QLineEdit(parent)
{
m_iconProvider.setOptions(QFileIconProvider::DontUseCustomDirectoryIcons);
m_completerModel->setIconProvider(&m_iconProvider);
m_completerModel->setOptions(QFileSystemModel::DontWatchForChanges);
m_completer->setModel(m_completerModel);
setCompleter(m_completer);
setCompleter(new QCompleter(this));
connect(this, &QLineEdit::textChanged, this, &FileLineEdit::validateText);
}
Private::FileLineEdit::~FileLineEdit()
{
delete m_completerModel; // has to be deleted before deleting the m_iconProvider object
delete m_iconProvider;
}
void Private::FileLineEdit::completeDirectoriesOnly(const bool completeDirsOnly)
{
const QDir::Filters filters = QDir::NoDotAndDotDot
| (completeDirsOnly ? QDir::Dirs : QDir::AllEntries);
m_completerModel->setFilter(filters);
m_completeDirectoriesOnly = completeDirsOnly;
if (m_completerModel)
{
const QDir::Filters filters = QDir::NoDotAndDotDot
| (completeDirsOnly ? QDir::Dirs : QDir::AllEntries);
m_completerModel->setFilter(filters);
}
}
void Private::FileLineEdit::setFilenameFilters(const QStringList &filters)
{
m_completerModel->setNameFilters(filters);
m_filenameFilters = filters;
if (m_completerModel)
m_completerModel->setNameFilters(m_filenameFilters);
}
void Private::FileLineEdit::setBrowseAction(QAction *action)
@@ -222,6 +222,22 @@ void Private::FileLineEdit::keyPressEvent(QKeyEvent *e)
if ((e->key() == Qt::Key_Space) && (e->modifiers() == Qt::CTRL))
{
if (!m_completerModel)
{
m_iconProvider = new QFileIconProvider;
m_iconProvider->setOptions(QFileIconProvider::DontUseCustomDirectoryIcons);
m_completerModel = new QFileSystemModel(this);
m_completerModel->setIconProvider(m_iconProvider);
m_completerModel->setOptions(QFileSystemModel::DontWatchForChanges);
m_completerModel->setNameFilters(m_filenameFilters);
const QDir::Filters filters = QDir::NoDotAndDotDot
| (m_completeDirectoriesOnly ? QDir::Dirs : QDir::AllEntries);
m_completerModel->setFilter(filters);
completer()->setModel(m_completerModel);
}
m_completerModel->setRootPath(Path(text()).data());
showCompletionPopup();
}
@@ -243,8 +259,8 @@ void Private::FileLineEdit::contextMenuEvent(QContextMenuEvent *event)
void Private::FileLineEdit::showCompletionPopup()
{
m_completer->setCompletionPrefix(text());
m_completer->complete();
completer()->setCompletionPrefix(text());
completer()->complete();
}
void Private::FileLineEdit::validateText()

View File

@@ -1,6 +1,7 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2016 Eugene Shalygin
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2016 Eugene Shalygin
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -29,7 +30,6 @@
#pragma once
#include <QComboBox>
#include <QFileIconProvider>
#include <QLineEdit>
#include <QtContainerFwd>
#include <QValidator>
@@ -37,8 +37,8 @@
#include "base/pathfwd.h"
class QAction;
class QCompleter;
class QContextMenuEvent;
class QFileIconProvider;
class QFileSystemModel;
class QKeyEvent;
@@ -140,10 +140,11 @@ namespace Private
static QString warningText(FileSystemPathValidator::TestResult result);
QFileSystemModel *m_completerModel = nullptr;
QCompleter *m_completer = nullptr;
QAction *m_browseAction = nullptr;
QAction *m_warningAction = nullptr;
QFileIconProvider m_iconProvider;
QFileIconProvider *m_iconProvider = nullptr;
bool m_completeDirectoriesOnly = false;
QStringList m_filenameFilters;
};
class FileComboEdit final : public QComboBox, public IFileEditorWithCompletion

View File

@@ -64,7 +64,8 @@ namespace
delta.setY(0);
dialogGeometry.translate(delta);
delta = screenGeometry.topLeft() - dialogGeometry.topLeft();
const QPoint frameOffset {10, 40};
delta = screenGeometry.topLeft() - dialogGeometry.topLeft() + frameOffset;
if (delta.x() < 0)
delta.setX(0);
if (delta.y() < 0)
@@ -225,14 +226,16 @@ bool GUIAddTorrentManager::processTorrent(const QString &source, const BitTorren
dlg->setAttribute(Qt::WA_DeleteOnClose);
m_dialogs[infoHash] = dlg;
connect(dlg, &QDialog::finished, this, [this, source, infoHash, dlg](int result)
connect(dlg, &AddNewTorrentDialog::torrentAccepted, this
, [this, source](const BitTorrent::TorrentDescriptor &torrentDescr, const BitTorrent::AddTorrentParams &addTorrentParams)
{
addTorrentToSession(source, torrentDescr, addTorrentParams);
});
connect(dlg, &QDialog::finished, this, [this, source, infoHash, dlg]
{
if (dlg->isDoNotDeleteTorrentChecked())
releaseTorrentFileGuard(source);
if (result == QDialog::Accepted)
addTorrentToSession(source, dlg->torrentDescriptor(), dlg->addTorrentParams());
m_dialogs.remove(infoHash);
});

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2020 Prince Gupta <jagannatharjun11@gmail.com>
* Copyright (C) 2019 sledgehammer999 <hammered999@gmail.com>
*
@@ -36,65 +37,26 @@
#include "base/global.h"
#include "gui/uithememanager.h"
namespace
{
const int MAX_VISIBLE_MESSAGES = 20000;
const int MAX_VISIBLE_MESSAGES = 20000;
QColor getTimestampColor()
{
return UIThemeManager::instance()->getColor(u"Log.TimeStamp"_s);
}
QColor getLogNormalColor()
{
return UIThemeManager::instance()->getColor(u"Log.Normal"_s);
}
QColor getLogInfoColor()
{
return UIThemeManager::instance()->getColor(u"Log.Info"_s);
}
QColor getLogWarningColor()
{
return UIThemeManager::instance()->getColor(u"Log.Warning"_s);
}
QColor getLogCriticalColor()
{
return UIThemeManager::instance()->getColor(u"Log.Critical"_s);
}
QColor getPeerBannedColor()
{
return UIThemeManager::instance()->getColor(u"Log.BannedPeer"_s);
}
}
BaseLogModel::Message::Message(const QString &time, const QString &message, const QColor &foreground, const Log::MsgType type)
: m_time(time)
, m_message(message)
, m_foreground(foreground)
, m_type(type)
BaseLogModel::Message::Message(const QString &time, const QString &message, const Log::MsgType type)
: m_time {time}
, m_message {message}
, m_type {type}
{
}
QVariant BaseLogModel::Message::time() const
QString BaseLogModel::Message::time() const
{
return m_time;
}
QVariant BaseLogModel::Message::message() const
QString BaseLogModel::Message::message() const
{
return m_message;
}
QVariant BaseLogModel::Message::foreground() const
{
return m_foreground;
}
QVariant BaseLogModel::Message::type() const
Log::MsgType BaseLogModel::Message::type() const
{
return m_type;
}
@@ -102,8 +64,9 @@ QVariant BaseLogModel::Message::type() const
BaseLogModel::BaseLogModel(QObject *parent)
: QAbstractListModel(parent)
, m_messages(MAX_VISIBLE_MESSAGES)
, m_timeForeground(getTimestampColor())
{
loadColors();
connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, &BaseLogModel::onUIThemeChanged);
}
int BaseLogModel::rowCount(const QModelIndex &) const
@@ -135,7 +98,7 @@ QVariant BaseLogModel::data(const QModelIndex &index, const int role) const
case TimeForegroundRole:
return m_timeForeground;
case MessageForegroundRole:
return message.foreground();
return messageForeground(message);
case TypeRole:
return message.type();
default:
@@ -160,6 +123,17 @@ void BaseLogModel::addNewMessage(const BaseLogModel::Message &message)
endInsertRows();
}
void BaseLogModel::onUIThemeChanged()
{
loadColors();
emit dataChanged(index(0, 0), index((rowCount() - 1), (columnCount() - 1)), {TimeForegroundRole, MessageForegroundRole});
}
void BaseLogModel::loadColors()
{
m_timeForeground = UIThemeManager::instance()->getColor(u"Log.TimeStamp"_s);
}
void BaseLogModel::reset()
{
beginResetModel();
@@ -169,14 +143,9 @@ void BaseLogModel::reset()
LogMessageModel::LogMessageModel(QObject *parent)
: BaseLogModel(parent)
, m_foregroundForMessageTypes
{
{Log::NORMAL, getLogNormalColor()},
{Log::INFO, getLogInfoColor()},
{Log::WARNING, getLogWarningColor()},
{Log::CRITICAL, getLogCriticalColor()}
}
{
loadColors();
for (const Log::Msg &msg : asConst(Logger::instance()->getMessages()))
handleNewMessage(msg);
connect(Logger::instance(), &Logger::newLogMessage, this, &LogMessageModel::handleNewMessage);
@@ -185,16 +154,38 @@ LogMessageModel::LogMessageModel(QObject *parent)
void LogMessageModel::handleNewMessage(const Log::Msg &message)
{
const QString time = QLocale::system().toString(QDateTime::fromSecsSinceEpoch(message.timestamp), QLocale::ShortFormat);
const QString messageText = message.message;
const QColor foreground = m_foregroundForMessageTypes[message.type];
addNewMessage({time, message.message, message.type});
}
addNewMessage({time, messageText, foreground, message.type});
QColor LogMessageModel::messageForeground(const Message &message) const
{
return m_foregroundForMessageTypes.value(message.type());
}
void LogMessageModel::onUIThemeChanged()
{
loadColors();
BaseLogModel::onUIThemeChanged();
}
void LogMessageModel::loadColors()
{
const auto *themeManager = UIThemeManager::instance();
const QColor normalColor = themeManager->getColor(u"Log.Normal"_s);
m_foregroundForMessageTypes =
{
{Log::NORMAL, normalColor.isValid() ? normalColor : QApplication::palette().color(QPalette::Active, QPalette::WindowText)},
{Log::INFO, themeManager->getColor(u"Log.Info"_s)},
{Log::WARNING, themeManager->getColor(u"Log.Warning"_s)},
{Log::CRITICAL, themeManager->getColor(u"Log.Critical"_s)}
};
}
LogPeerModel::LogPeerModel(QObject *parent)
: BaseLogModel(parent)
, m_bannedPeerForeground(getPeerBannedColor())
{
loadColors();
for (const Log::Peer &peer : asConst(Logger::instance()->getPeers()))
handleNewMessage(peer);
connect(Logger::instance(), &Logger::newLogPeer, this, &LogPeerModel::handleNewMessage);
@@ -207,5 +198,21 @@ void LogPeerModel::handleNewMessage(const Log::Peer &peer)
? tr("%1 was blocked. Reason: %2.", "0.0.0.0 was blocked. Reason: reason for blocking.").arg(peer.ip, peer.reason)
: tr("%1 was banned", "0.0.0.0 was banned").arg(peer.ip);
addNewMessage({time, message, m_bannedPeerForeground, Log::NORMAL});
addNewMessage({time, message, Log::NORMAL});
}
QColor LogPeerModel::messageForeground([[maybe_unused]] const Message &message) const
{
return m_bannedPeerForeground;
}
void LogPeerModel::onUIThemeChanged()
{
loadColors();
BaseLogModel::onUIThemeChanged();
}
void LogPeerModel::loadColors()
{
m_bannedPeerForeground = UIThemeManager::instance()->getColor(u"Log.BannedPeer"_s);
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2020 Prince Gupta <jagannatharjun11@gmail.com>
* Copyright (C) 2019 sledgehammer999 <hammered999@gmail.com>
*
@@ -62,25 +63,27 @@ protected:
class Message
{
public:
Message(const QString &time, const QString &message, const QColor &foreground, Log::MsgType type);
Message(const QString &time, const QString &message, Log::MsgType type);
QVariant time() const;
QVariant message() const;
QVariant foreground() const;
QVariant type() const;
QString time() const;
QString message() const;
Log::MsgType type() const;
private:
QVariant m_time;
QVariant m_message;
QVariant m_foreground;
QVariant m_type;
QString m_time;
QString m_message;
Log::MsgType m_type;
};
void addNewMessage(const Message &message);
virtual QColor messageForeground(const Message &message) const = 0;
virtual void onUIThemeChanged();
private:
void loadColors();
boost::circular_buffer_space_optimized<Message> m_messages;
const QColor m_timeForeground;
QColor m_timeForeground;
};
class LogMessageModel : public BaseLogModel
@@ -95,7 +98,11 @@ private slots:
void handleNewMessage(const Log::Msg &message);
private:
const QHash<int, QColor> m_foregroundForMessageTypes;
QColor messageForeground(const Message &message) const override;
void onUIThemeChanged() override;
void loadColors();
QHash<int, QColor> m_foregroundForMessageTypes;
};
class LogPeerModel : public BaseLogModel
@@ -110,5 +117,9 @@ private slots:
void handleNewMessage(const Log::Peer &peer);
private:
const QColor m_bannedPeerForeground;
QColor messageForeground(const Message &message) const override;
void onUIThemeChanged() override;
void loadColors();
QColor m_bannedPeerForeground;
};

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022-2023 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -125,18 +125,18 @@ namespace
MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, const QString &titleSuffix)
: GUIApplicationComponent(app)
, m_ui(new Ui::MainWindow)
, m_storeExecutionLogEnabled(EXECUTIONLOG_SETTINGS_KEY(u"Enabled"_s))
, m_storeDownloadTrackerFavicon(SETTINGS_KEY(u"DownloadTrackerFavicon"_s))
, m_storeExecutionLogTypes(EXECUTIONLOG_SETTINGS_KEY(u"Types"_s), Log::MsgType::ALL)
, m_ui {new Ui::MainWindow}
, m_downloadRate {Utils::Misc::friendlyUnit(0, true)}
, m_uploadRate {Utils::Misc::friendlyUnit(0, true)}
, m_storeExecutionLogEnabled {EXECUTIONLOG_SETTINGS_KEY(u"Enabled"_s)}
, m_storeDownloadTrackerFavicon {SETTINGS_KEY(u"DownloadTrackerFavicon"_s)}
, m_storeExecutionLogTypes {EXECUTIONLOG_SETTINGS_KEY(u"Types"_s), Log::MsgType::ALL}
#ifdef Q_OS_MACOS
, m_badger(std::make_unique<MacUtils::Badger>())
, m_badger {std::make_unique<MacUtils::Badger>()}
#endif // Q_OS_MACOS
{
m_ui->setupUi(this);
setTitleSuffix(titleSuffix);
Preferences *const pref = Preferences::instance();
m_uiLocked = pref->isUILocked();
m_displaySpeedInTitle = pref->speedInTitleBar();
@@ -145,6 +145,8 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
setWindowIcon(UIThemeManager::instance()->getIcon(u"qbittorrent"_s));
#endif // Q_OS_MACOS
setTitleSuffix(titleSuffix);
#if (defined(Q_OS_UNIX))
m_ui->actionOptions->setText(tr("Preferences"));
#endif
@@ -167,21 +169,37 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
m_ui->actionExit->setIcon(UIThemeManager::instance()->getIcon(u"application-exit"_s));
m_ui->actionLock->setIcon(UIThemeManager::instance()->getIcon(u"object-locked"_s));
m_ui->actionOptions->setIcon(UIThemeManager::instance()->getIcon(u"configure"_s, u"preferences-system"_s));
m_ui->actionPause->setIcon(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s));
m_ui->actionPauseAll->setIcon(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s));
m_ui->actionStart->setIcon(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s));
m_ui->actionStartAll->setIcon(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s));
m_ui->actionStop->setIcon(UIThemeManager::instance()->getIcon(u"torrent-stop"_s, u"media-playback-pause"_s));
m_ui->actionPauseSession->setIcon(UIThemeManager::instance()->getIcon(u"pause-session"_s, u"media-playback-pause"_s));
m_ui->actionResumeSession->setIcon(UIThemeManager::instance()->getIcon(u"torrent-start"_s, u"media-playback-start"_s));
m_ui->menuAutoShutdownOnDownloadsCompletion->setIcon(UIThemeManager::instance()->getIcon(u"task-complete"_s, u"application-exit"_s));
m_ui->actionManageCookies->setIcon(UIThemeManager::instance()->getIcon(u"browser-cookies"_s, u"preferences-web-browser-cookies"_s));
m_ui->menuLog->setIcon(UIThemeManager::instance()->getIcon(u"help-contents"_s));
m_ui->actionCheckForUpdates->setIcon(UIThemeManager::instance()->getIcon(u"view-refresh"_s));
m_ui->actionPauseSession->setVisible(!BitTorrent::Session::instance()->isPaused());
m_ui->actionResumeSession->setVisible(BitTorrent::Session::instance()->isPaused());
connect(BitTorrent::Session::instance(), &BitTorrent::Session::paused, this, [this]
{
m_ui->actionPauseSession->setVisible(false);
m_ui->actionResumeSession->setVisible(true);
refreshWindowTitle();
refreshTrayIconTooltip();
});
connect(BitTorrent::Session::instance(), &BitTorrent::Session::resumed, this, [this]
{
m_ui->actionPauseSession->setVisible(true);
m_ui->actionResumeSession->setVisible(false);
refreshWindowTitle();
refreshTrayIconTooltip();
});
auto *lockMenu = new QMenu(m_ui->menuView);
lockMenu->addAction(tr("&Set Password"), this, &MainWindow::defineUILockPassword);
lockMenu->addAction(tr("&Clear Password"), this, &MainWindow::clearUILockPassword);
m_ui->actionLock->setMenu(lockMenu);
// Creating Bittorrent session
updateAltSpeedsBtn(BitTorrent::Session::instance()->isAltGlobalSpeedLimitEnabled());
connect(BitTorrent::Session::instance(), &BitTorrent::Session::speedLimitModeChanged, this, &MainWindow::updateAltSpeedsBtn);
@@ -285,17 +303,14 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
// Transfer list slots
connect(m_ui->actionStart, &QAction::triggered, m_transferListWidget, &TransferListWidget::startSelectedTorrents);
connect(m_ui->actionStartAll, &QAction::triggered, m_transferListWidget, &TransferListWidget::resumeAllTorrents);
connect(m_ui->actionPause, &QAction::triggered, m_transferListWidget, &TransferListWidget::pauseSelectedTorrents);
connect(m_ui->actionPauseAll, &QAction::triggered, m_transferListWidget, &TransferListWidget::pauseAllTorrents);
connect(m_ui->actionStop, &QAction::triggered, m_transferListWidget, &TransferListWidget::stopSelectedTorrents);
connect(m_ui->actionPauseSession, &QAction::triggered, m_transferListWidget, &TransferListWidget::pauseSession);
connect(m_ui->actionResumeSession, &QAction::triggered, m_transferListWidget, &TransferListWidget::resumeSession);
connect(m_ui->actionDelete, &QAction::triggered, m_transferListWidget, &TransferListWidget::softDeleteSelectedTorrents);
connect(m_ui->actionTopQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::topQueuePosSelectedTorrents);
connect(m_ui->actionIncreaseQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::increaseQueuePosSelectedTorrents);
connect(m_ui->actionDecreaseQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::decreaseQueuePosSelectedTorrents);
connect(m_ui->actionBottomQueuePos, &QAction::triggered, m_transferListWidget, &TransferListWidget::bottomQueuePosSelectedTorrents);
#ifndef Q_OS_MACOS
connect(m_ui->actionToggleVisibility, &QAction::triggered, this, &MainWindow::toggleVisibility);
#endif
connect(m_ui->actionMinimize, &QAction::triggered, this, &MainWindow::minimizeWindow);
connect(m_ui->actionUseAlternativeSpeedLimits, &QAction::triggered, this, &MainWindow::toggleAlternativeSpeeds);
@@ -329,7 +344,7 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
// Configure BT session according to options
loadPreferences();
connect(BitTorrent::Session::instance(), &BitTorrent::Session::statsUpdated, this, &MainWindow::reloadSessionStats);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::statsUpdated, this, &MainWindow::loadSessionStats);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::torrentsUpdated, this, &MainWindow::reloadTorrentStats);
// Accept drag 'n drops
@@ -387,7 +402,7 @@ MainWindow::MainWindow(IGUIApplication *app, const WindowState initialState, con
// Load Window state and sizes
loadSettings();
app->desktopIntegration()->setMenu(createDesktopIntegrationMenu());
populateDesktopIntegrationMenu();
#ifndef Q_OS_MACOS
m_ui->actionLock->setVisible(app->desktopIntegration()->isActive());
connect(app->desktopIntegration(), &DesktopIntegration::stateChanged, this, [this, app]()
@@ -533,7 +548,7 @@ void MainWindow::setTitleSuffix(const QString &suffix)
m_windowTitle = QStringLiteral("qBittorrent " QBT_VERSION)
+ (!suffix.isEmpty() ? (separator + suffix) : QString());
setWindowTitle(m_windowTitle);
refreshWindowTitle();
}
void MainWindow::addToolbarContextMenu()
@@ -799,8 +814,11 @@ void MainWindow::saveSplitterSettings() const
void MainWindow::cleanup()
{
saveSettings();
saveSplitterSettings();
if (!m_neverShown)
{
saveSettings();
saveSplitterSettings();
}
// delete RSSWidget explicitly to avoid crash in
// handleRSSUnreadCountUpdated() at application shutdown
@@ -881,9 +899,9 @@ void MainWindow::createKeyboardShortcuts()
m_ui->actionOptions->setShortcut(Qt::ALT | Qt::Key_O);
m_ui->actionStatistics->setShortcut(Qt::CTRL | Qt::Key_I);
m_ui->actionStart->setShortcut(Qt::CTRL | Qt::Key_S);
m_ui->actionStartAll->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S);
m_ui->actionPause->setShortcut(Qt::CTRL | Qt::Key_P);
m_ui->actionPauseAll->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
m_ui->actionStop->setShortcut(Qt::CTRL | Qt::Key_P);
m_ui->actionPauseSession->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_P);
m_ui->actionResumeSession->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_S);
m_ui->actionBottomQueuePos->setShortcut(Qt::CTRL | Qt::SHIFT | Qt::Key_Minus);
m_ui->actionDecreaseQueuePos->setShortcut(Qt::CTRL | Qt::Key_Minus);
m_ui->actionIncreaseQueuePos->setShortcut(Qt::CTRL | Qt::Key_Plus);
@@ -1326,12 +1344,6 @@ void MainWindow::dragEnterEvent(QDragEnterEvent *event)
event->acceptProposedAction();
}
/*****************************************************
* *
* Torrent *
* *
*****************************************************/
// Display a dialog to allow user to add
// torrents to download list
void MainWindow::on_actionOpen_triggered()
@@ -1395,7 +1407,7 @@ void MainWindow::showFiltersSidebar(const bool show)
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersAdded, m_transferListFiltersWidget, &TransferListFiltersWidget::addTrackers);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersRemoved, m_transferListFiltersWidget, &TransferListFiltersWidget::removeTrackers);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackersChanged, m_transferListFiltersWidget, &TransferListFiltersWidget::refreshTrackers);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackerEntriesUpdated, m_transferListFiltersWidget, &TransferListFiltersWidget::trackerEntriesUpdated);
connect(BitTorrent::Session::instance(), &BitTorrent::Session::trackerEntryStatusesUpdated, m_transferListFiltersWidget, &TransferListFiltersWidget::trackerEntryStatusesUpdated);
m_splitter->insertWidget(0, m_transferListFiltersWidget);
m_splitter->setCollapsible(0, true);
@@ -1494,28 +1506,21 @@ void MainWindow::loadPreferences()
qDebug("GUI settings loaded");
}
void MainWindow::reloadSessionStats()
void MainWindow::loadSessionStats()
{
const BitTorrent::SessionStatus &status = BitTorrent::Session::instance()->status();
const QString downloadRate = Utils::Misc::friendlyUnit(status.payloadDownloadRate, true);
const QString uploadRate = Utils::Misc::friendlyUnit(status.payloadUploadRate, true);
const auto *btSession = BitTorrent::Session::instance();
const BitTorrent::SessionStatus &status = btSession->status();
m_downloadRate = Utils::Misc::friendlyUnit(status.payloadDownloadRate, true);
m_uploadRate = Utils::Misc::friendlyUnit(status.payloadUploadRate, true);
// update global information
#ifdef Q_OS_MACOS
m_badger->updateSpeed(status.payloadDownloadRate, status.payloadUploadRate);
#else
const auto toolTip = u"%1\n%2"_s.arg(
tr("DL speed: %1", "e.g: Download speed: 10 KiB/s").arg(downloadRate)
, tr("UP speed: %1", "e.g: Upload speed: 10 KiB/s").arg(uploadRate));
app()->desktopIntegration()->setToolTip(toolTip); // tray icon
refreshTrayIconTooltip();
#endif // Q_OS_MACOS
if (m_displaySpeedInTitle)
{
const QString title = tr("[D: %1, U: %2] %3", "D = Download; U = Upload; %3 is the rest of the window title")
.arg(downloadRate, uploadRate, m_windowTitle);
setWindowTitle(title);
}
refreshWindowTitle();
}
void MainWindow::reloadTorrentStats(const QVector<BitTorrent::Torrent *> &torrents)
@@ -1527,33 +1532,23 @@ void MainWindow::reloadTorrentStats(const QVector<BitTorrent::Torrent *> &torren
}
}
/*****************************************************
* *
* Utils *
* *
*****************************************************/
void MainWindow::downloadFromURLList(const QStringList &urlList)
{
for (const QString &url : urlList)
app()->addTorrentManager()->addTorrent(url);
}
/*****************************************************
* *
* Options *
* *
*****************************************************/
QMenu *MainWindow::createDesktopIntegrationMenu()
void MainWindow::populateDesktopIntegrationMenu()
{
auto *menu = new QMenu;
auto *menu = app()->desktopIntegration()->menu();
menu->clear();
#ifndef Q_OS_MACOS
connect(menu, &QMenu::aboutToShow, this, [this]()
{
m_ui->actionToggleVisibility->setText(isVisible() ? tr("Hide") : tr("Show"));
});
connect(m_ui->actionToggleVisibility, &QAction::triggered, this, &MainWindow::toggleVisibility);
menu->addAction(m_ui->actionToggleVisibility);
menu->addSeparator();
@@ -1567,8 +1562,8 @@ QMenu *MainWindow::createDesktopIntegrationMenu()
menu->addAction(m_ui->actionSetGlobalSpeedLimits);
menu->addSeparator();
menu->addAction(m_ui->actionStartAll);
menu->addAction(m_ui->actionPauseAll);
menu->addAction(m_ui->actionResumeSession);
menu->addAction(m_ui->actionPauseSession);
#ifndef Q_OS_MACOS
menu->addSeparator();
@@ -1577,8 +1572,6 @@ QMenu *MainWindow::createDesktopIntegrationMenu()
if (m_uiLocked)
menu->setEnabled(false);
return menu;
}
void MainWindow::updateAltSpeedsBtn(const bool alternative)
@@ -1631,10 +1624,7 @@ void MainWindow::on_actionSpeedInTitleBar_triggered()
{
m_displaySpeedInTitle = static_cast<QAction *>(sender())->isChecked();
Preferences::instance()->showSpeedInTitleBar(m_displaySpeedInTitle);
if (m_displaySpeedInTitle)
reloadSessionStats();
else
setWindowTitle(m_windowTitle);
refreshWindowTitle();
}
void MainWindow::on_actionRSSReader_triggered()
@@ -1696,12 +1686,6 @@ void MainWindow::on_actionSearchWidget_triggered()
displaySearchTab(m_ui->actionSearchWidget->isChecked());
}
/*****************************************************
* *
* HTTP Downloader *
* *
*****************************************************/
// Display an input dialog to prompt user for
// an url
void MainWindow::on_actionDownloadFromURL_triggered()
@@ -1887,10 +1871,10 @@ void MainWindow::updatePowerManagementState() const
const QVector<BitTorrent::Torrent *> allTorrents = BitTorrent::Session::instance()->torrents();
const bool inhibitSuspend = std::any_of(allTorrents.cbegin(), allTorrents.cend(), [&](const BitTorrent::Torrent *torrent)
{
if (preventFromSuspendWhenDownloading && (!torrent->isFinished() && !torrent->isPaused() && !torrent->isErrored() && torrent->hasMetadata()))
if (preventFromSuspendWhenDownloading && (!torrent->isFinished() && !torrent->isStopped() && !torrent->isErrored() && torrent->hasMetadata()))
return true;
if (preventFromSuspendWhenSeeding && (torrent->isFinished() && !torrent->isPaused()))
if (preventFromSuspendWhenSeeding && (torrent->isFinished() && !torrent->isStopped()))
return true;
return torrent->isMoving();
@@ -1905,6 +1889,45 @@ void MainWindow::applyTransferListFilter()
m_transferListWidget->applyFilter(m_columnFilterEdit->text(), m_columnFilterComboBox->currentData().value<TransferListModel::Column>());
}
void MainWindow::refreshWindowTitle()
{
const auto *btSession = BitTorrent::Session::instance();
if (btSession->isPaused())
{
const QString title = tr("[PAUSED] %1", "%1 is the rest of the window title").arg(m_windowTitle);
setWindowTitle(title);
}
else
{
if (m_displaySpeedInTitle)
{
const QString title = tr("[D: %1, U: %2] %3", "D = Download; U = Upload; %3 is the rest of the window title")
.arg(m_downloadRate, m_uploadRate, m_windowTitle);
setWindowTitle(title);
}
else
{
setWindowTitle(m_windowTitle);
}
}
}
void MainWindow::refreshTrayIconTooltip()
{
const auto *btSession = BitTorrent::Session::instance();
if (!btSession->isPaused())
{
const auto toolTip = u"%1\n%2"_s.arg(
tr("DL speed: %1", "e.g: Download speed: 10 KiB/s").arg(m_downloadRate)
, tr("UP speed: %1", "e.g: Upload speed: 10 KiB/s").arg(m_uploadRate));
app()->desktopIntegration()->setToolTip(toolTip);
}
else
{
app()->desktopIntegration()->setToolTip(tr("Paused"));
}
}
#if defined(Q_OS_WIN) || defined(Q_OS_MACOS)
void MainWindow::checkProgramUpdate(const bool invokedByUser)
{

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -127,7 +127,7 @@ private slots:
void displayRSSTab();
void displayExecutionLogTab();
void toggleFocusBetweenLineEdits();
void reloadSessionStats();
void loadSessionStats();
void reloadTorrentStats(const QVector<BitTorrent::Torrent *> &torrents);
void loadPreferences();
void optionsSaved();
@@ -170,7 +170,7 @@ private slots:
void on_actionDownloadFromURL_triggered();
void on_actionExit_triggered();
void on_actionLock_triggered();
// Check for unpaused downloading or seeding torrents and prevent system suspend/sleep according to preferences
// Check for non-stopped downloading or seeding torrents and prevent system suspend/sleep according to preferences
void updatePowerManagementState() const;
void toolbarMenuRequested();
@@ -186,7 +186,7 @@ private slots:
#endif
private:
QMenu *createDesktopIntegrationMenu();
void populateDesktopIntegrationMenu();
#ifdef Q_OS_WIN
void installPython();
#endif
@@ -203,14 +203,19 @@ private:
void showStatusBar(bool show);
void showFiltersSidebar(bool show);
void applyTransferListFilter();
void refreshWindowTitle();
void refreshTrayIconTooltip();
Ui::MainWindow *m_ui = nullptr;
QFileSystemWatcher *m_executableWatcher = nullptr;
// GUI related
QString m_windowTitle;
QString m_downloadRate;
QString m_uploadRate;
bool m_posInitialized = false;
bool m_neverShown = true;
QFileSystemWatcher *m_executableWatcher = nullptr;
// GUI related
QPointer<QTabWidget> m_tabs;
QPointer<StatusBar> m_statusBar;
QPointer<OptionsDialog> m_options;

View File

@@ -43,15 +43,16 @@
<string>&amp;Edit</string>
</property>
<addaction name="actionStart"/>
<addaction name="actionPause"/>
<addaction name="actionStartAll"/>
<addaction name="actionPauseAll"/>
<addaction name="actionStop"/>
<addaction name="separator"/>
<addaction name="actionDelete"/>
<addaction name="actionTopQueuePos"/>
<addaction name="actionIncreaseQueuePos"/>
<addaction name="actionDecreaseQueuePos"/>
<addaction name="actionBottomQueuePos"/>
<addaction name="separator"/>
<addaction name="actionPauseSession"/>
<addaction name="actionResumeSession"/>
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
@@ -153,7 +154,7 @@
<addaction name="actionDelete"/>
<addaction name="separator"/>
<addaction name="actionStart"/>
<addaction name="actionPause"/>
<addaction name="actionStop"/>
<addaction name="actionTopQueuePos"/>
<addaction name="actionIncreaseQueuePos"/>
<addaction name="actionDecreaseQueuePos"/>
@@ -188,22 +189,22 @@
</action>
<action name="actionStart">
<property name="text">
<string>&amp;Resume</string>
<string>Sta&amp;rt</string>
</property>
</action>
<action name="actionPause">
<action name="actionStop">
<property name="text">
<string>&amp;Pause</string>
<string>Sto&amp;p</string>
</property>
</action>
<action name="actionStartAll">
<action name="actionResumeSession">
<property name="text">
<string>R&amp;esume All</string>
<string>&amp;Resume Session</string>
</property>
</action>
<action name="actionPauseAll">
<action name="actionPauseSession">
<property name="text">
<string>P&amp;ause All</string>
<string>Pau&amp;se Session</string>
</property>
</action>
<action name="actionDelete">

View File

@@ -253,17 +253,17 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->comboHideZero->setCurrentIndex(pref->getHideZeroComboValues());
m_ui->comboHideZero->setEnabled(m_ui->checkHideZero->isChecked());
m_ui->actionTorrentDlOnDblClBox->setItemData(0, TOGGLE_PAUSE);
m_ui->actionTorrentDlOnDblClBox->setItemData(0, TOGGLE_STOP);
m_ui->actionTorrentDlOnDblClBox->setItemData(1, OPEN_DEST);
m_ui->actionTorrentDlOnDblClBox->setItemData(2, PREVIEW_FILE);
m_ui->actionTorrentDlOnDblClBox->setItemData(3, SHOW_OPTIONS);
m_ui->actionTorrentDlOnDblClBox->setItemData(4, NO_ACTION);
int actionDownloading = pref->getActionOnDblClOnTorrentDl();
if ((actionDownloading < 0) || (actionDownloading >= m_ui->actionTorrentDlOnDblClBox->count()))
actionDownloading = TOGGLE_PAUSE;
actionDownloading = TOGGLE_STOP;
m_ui->actionTorrentDlOnDblClBox->setCurrentIndex(m_ui->actionTorrentDlOnDblClBox->findData(actionDownloading));
m_ui->actionTorrentFnOnDblClBox->setItemData(0, TOGGLE_PAUSE);
m_ui->actionTorrentFnOnDblClBox->setItemData(0, TOGGLE_STOP);
m_ui->actionTorrentFnOnDblClBox->setItemData(1, OPEN_DEST);
m_ui->actionTorrentFnOnDblClBox->setItemData(2, PREVIEW_FILE);
m_ui->actionTorrentFnOnDblClBox->setItemData(3, SHOW_OPTIONS);
@@ -281,7 +281,6 @@ void OptionsDialog::loadBehaviorTabOptions()
m_ui->checkShowSplash->setChecked(!pref->isSplashScreenDisabled());
m_ui->checkProgramExitConfirm->setChecked(pref->confirmOnExit());
m_ui->checkProgramAutoExitConfirm->setChecked(!pref->dontConfirmAutoExit());
m_ui->checkConfirmPauseAndResumeAll->setChecked(pref->confirmPauseAndResumeAll());
m_ui->windowStateComboBox->addItem(tr("Normal"), QVariant::fromValue(WindowState::Normal));
m_ui->windowStateComboBox->addItem(tr("Minimized"), QVariant::fromValue(WindowState::Minimized));
@@ -381,7 +380,6 @@ void OptionsDialog::loadBehaviorTabOptions()
connect(m_ui->checkShowSplash, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkProgramExitConfirm, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkProgramAutoExitConfirm, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkConfirmPauseAndResumeAll, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkShowSystray, &QGroupBox::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkMinimizeToSysTray, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkCloseToSystray, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
@@ -464,7 +462,6 @@ void OptionsDialog::saveBehaviorTabOptions() const
pref->setSplashScreenDisabled(isSplashScreenDisabled());
pref->setConfirmOnExit(m_ui->checkProgramExitConfirm->isChecked());
pref->setDontConfirmAutoExit(!m_ui->checkProgramAutoExitConfirm->isChecked());
pref->setConfirmPauseAndResumeAll(m_ui->checkConfirmPauseAndResumeAll->isChecked());
#ifdef Q_OS_WIN
pref->setWinStartup(WinStartup());
@@ -522,7 +519,7 @@ void OptionsDialog::loadDownloadsTabOptions()
m_ui->contentLayoutComboBox->setCurrentIndex(static_cast<int>(session->torrentContentLayout()));
m_ui->checkAddToQueueTop->setChecked(session->isAddTorrentToQueueTop());
m_ui->checkStartPaused->setChecked(session->isAddTorrentPaused());
m_ui->checkAddStopped->setChecked(session->isAddTorrentStopped());
m_ui->stopConditionComboBox->setToolTip(
u"<html><body><p><b>" + tr("None") + u"</b> - " + tr("No stop condition is set.") + u"</p><p><b>" +
@@ -534,8 +531,8 @@ void OptionsDialog::loadDownloadsTabOptions()
m_ui->stopConditionComboBox->setItemData(1, QVariant::fromValue(BitTorrent::Torrent::StopCondition::MetadataReceived));
m_ui->stopConditionComboBox->setItemData(2, QVariant::fromValue(BitTorrent::Torrent::StopCondition::FilesChecked));
m_ui->stopConditionComboBox->setCurrentIndex(m_ui->stopConditionComboBox->findData(QVariant::fromValue(session->torrentStopCondition())));
m_ui->stopConditionLabel->setEnabled(!m_ui->checkStartPaused->isChecked());
m_ui->stopConditionComboBox->setEnabled(!m_ui->checkStartPaused->isChecked());
m_ui->stopConditionLabel->setEnabled(!m_ui->checkAddStopped->isChecked());
m_ui->stopConditionComboBox->setEnabled(!m_ui->checkAddStopped->isChecked());
m_ui->checkMergeTrackers->setChecked(session->isMergeTrackersEnabled());
m_ui->checkConfirmMergeTrackers->setEnabled(m_ui->checkAdditionDialog->isChecked());
@@ -655,8 +652,8 @@ void OptionsDialog::loadDownloadsTabOptions()
connect(m_ui->contentLayoutComboBox, qComboBoxCurrentIndexChanged, this, &ThisType::enableApplyButton);
connect(m_ui->checkAddToQueueTop, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkStartPaused, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkStartPaused, &QAbstractButton::toggled, this, [this](const bool checked)
connect(m_ui->checkAddStopped, &QAbstractButton::toggled, this, &ThisType::enableApplyButton);
connect(m_ui->checkAddStopped, &QAbstractButton::toggled, this, [this](const bool checked)
{
m_ui->stopConditionLabel->setEnabled(!checked);
m_ui->stopConditionComboBox->setEnabled(!checked);
@@ -732,7 +729,7 @@ void OptionsDialog::saveDownloadsTabOptions() const
session->setTorrentContentLayout(static_cast<BitTorrent::TorrentContentLayout>(m_ui->contentLayoutComboBox->currentIndex()));
session->setAddTorrentToQueueTop(m_ui->checkAddToQueueTop->isChecked());
session->setAddTorrentPaused(addTorrentsInPause());
session->setAddTorrentStopped(addTorrentsStopped());
session->setTorrentStopCondition(m_ui->stopConditionComboBox->currentData().value<BitTorrent::Torrent::StopCondition>());
TorrentFileGuard::setAutoDeleteMode(!m_ui->deleteTorrentBox->isChecked() ? TorrentFileGuard::Never
: !m_ui->deleteCancelledTorrentBox->isChecked() ? TorrentFileGuard::IfAdded
@@ -1687,9 +1684,9 @@ bool OptionsDialog::preAllocateAllFiles() const
return m_ui->checkPreallocateAll->isChecked();
}
bool OptionsDialog::addTorrentsInPause() const
bool OptionsDialog::addTorrentsStopped() const
{
return m_ui->checkStartPaused->isChecked();
return m_ui->checkAddStopped->isChecked();
}
// Proxy settings

View File

@@ -42,7 +42,7 @@ class AdvancedSettings;
// actions on double-click on torrents
enum DoubleClickAction
{
TOGGLE_PAUSE = 0,
TOGGLE_STOP = 0,
OPEN_DEST = 1,
PREVIEW_FILE = 2,
NO_ACTION = 3,
@@ -151,7 +151,7 @@ private:
// Downloads
bool preAllocateAllFiles() const;
bool useAdditionDialog() const;
bool addTorrentsInPause() const;
bool addTorrentsStopped() const;
Path getTorrentExportDir() const;
Path getFinishedTorrentExportDir() const;
// Connection options

View File

@@ -226,19 +226,6 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkConfirmPauseAndResumeAll">
<property name="toolTip">
<string>Shows a confirmation dialog upon pausing/resuming all the torrents</string>
</property>
<property name="text">
<string>Confirm &quot;Pause/Resume all&quot; actions</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkAltRowColors">
<property name="text">
@@ -267,7 +254,7 @@
</item>
<item>
<property name="text">
<string>Paused torrents only</string>
<string>Stopped torrents only</string>
</property>
</item>
</widget>
@@ -925,12 +912,12 @@
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkStartPaused">
<widget class="QCheckBox" name="checkAddStopped">
<property name="toolTip">
<string>The torrent will be added to download list in a paused state</string>
<string>The torrent will be added to download list in a stopped state</string>
</property>
<property name="text">
<string extracomment="The torrent will be added to download list in a paused state">Do not start the download automatically</string>
<string extracomment="The torrent will be added to download list in a stopped state">Do not start the download automatically</string>
</property>
</widget>
</item>
@@ -3043,7 +3030,7 @@ Disable encryption: Only connect to peers without protocol encryption</string>
</property>
<item>
<property name="text">
<string>Pause torrent</string>
<string>Stop torrent</string>
</property>
</item>
<item>
@@ -3069,7 +3056,7 @@ Disable encryption: Only connect to peers without protocol encryption</string>
<item>
<widget class="QGroupBox" name="checkEnableAddTrackers">
<property name="title">
<string>A&amp;utomatically add these trackers to new downloads:</string>
<string>A&amp;utomatically append these trackers to new downloads:</string>
</property>
<property name="checkable">
<bool>true</bool>
@@ -3897,7 +3884,7 @@ Use ';' to split multiple entries. Can use wildcard '*'.</string>
<tabstop>comboI18n</tabstop>
<tabstop>checkUseCustomTheme</tabstop>
<tabstop>customThemeFilePath</tabstop>
<tabstop>checkStartPaused</tabstop>
<tabstop>checkAddStopped</tabstop>
<tabstop>stopConditionComboBox</tabstop>
<tabstop>spinPort</tabstop>
<tabstop>checkUPnP</tabstop>

View File

@@ -29,6 +29,9 @@
#include "programupdater.h"
#include <libtorrent/version.hpp>
#include <QtCore/qconfig.h>
#include <QtSystemDetection>
#include <QDebug>
#include <QDesktopServices>
@@ -61,6 +64,20 @@ namespace
}
return (newVersion > currentVersion);
}
QString buildVariant()
{
#if defined(Q_OS_MACOS)
const auto BASE_OS = u"Mac OS X"_s;
#elif defined(Q_OS_WIN)
const auto BASE_OS = u"Windows x64"_s;
#endif
if constexpr ((QT_VERSION_MAJOR == 6) && (LIBTORRENT_VERSION_MAJOR == 1))
return BASE_OS;
return u"%1 (qt%2 lt%3%4)"_s.arg(BASE_OS, QString::number(QT_VERSION_MAJOR), QString::number(LIBTORRENT_VERSION_MAJOR), QString::number(LIBTORRENT_VERSION_MINOR));
}
}
void ProgramUpdater::checkForUpdates() const
@@ -97,12 +114,7 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
: QString {};
};
#ifdef Q_OS_MACOS
const QString OS_TYPE = u"Mac OS X"_s;
#elif defined(Q_OS_WIN)
const QString OS_TYPE = u"Windows x64"_s;
#endif
const QString variant = buildVariant();
bool inItem = false;
QString version;
QString updateLink;
@@ -128,7 +140,7 @@ void ProgramUpdater::rssDownloadFinished(const Net::DownloadResult &result)
{
if (inItem && (xml.name() == u"item"))
{
if (type.compare(OS_TYPE, Qt::CaseInsensitive) == 0)
if (type.compare(variant, Qt::CaseInsensitive) == 0)
{
qDebug("The last update available is %s", qUtf8Printable(version));
if (!version.isEmpty())

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -46,9 +47,9 @@ namespace
}
DownloadedPiecesBar::DownloadedPiecesBar(QWidget *parent)
: base {parent}
, m_dlPieceColor {dlPieceColor(pieceColor())}
: base(parent)
{
updateColorsImpl();
}
QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin, int reqSize)
@@ -128,25 +129,24 @@ QVector<float> DownloadedPiecesBar::bitfieldToFloatVector(const QBitArray &vecin
return result;
}
bool DownloadedPiecesBar::updateImage(QImage &image)
QImage DownloadedPiecesBar::renderImage()
{
// qDebug() << "updateImage";
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888);
if (image2.isNull())
QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
if (image.isNull())
{
qDebug() << "QImage image2() allocation failed, width():" << width();
return false;
qDebug() << "QImage allocation failed, width():" << width();
return image;
}
if (m_pieces.isEmpty())
{
image2.fill(backgroundColor());
image = image2;
return true;
image.fill(backgroundColor());
return image;
}
QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image2.width());
QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image2.width());
QVector<float> scaledPieces = bitfieldToFloatVector(m_pieces, image.width());
QVector<float> scaledPiecesDl = bitfieldToFloatVector(m_downloadedPieces, image.width());
// filling image
for (int x = 0; x < scaledPieces.size(); ++x)
@@ -161,15 +161,15 @@ bool DownloadedPiecesBar::updateImage(QImage &image)
QRgb mixedColor = mixTwoColors(pieceColor().rgb(), m_dlPieceColor.rgb(), ratio);
mixedColor = mixTwoColors(backgroundColor().rgb(), mixedColor, fillRatio);
image2.setPixel(x, 0, mixedColor);
image.setPixel(x, 0, mixedColor);
}
else
{
image2.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
image.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
}
}
image = image2;
return true;
return image;
}
void DownloadedPiecesBar::setProgress(const QBitArray &pieces, const QBitArray &downloadedPieces)
@@ -177,7 +177,7 @@ void DownloadedPiecesBar::setProgress(const QBitArray &pieces, const QBitArray &
m_pieces = pieces;
m_downloadedPieces = downloadedPieces;
requestImageUpdate();
redraw();
}
void DownloadedPiecesBar::clear()
@@ -198,3 +198,14 @@ QString DownloadedPiecesBar::simpleToolTipText() const
+ u"</table>";
}
void DownloadedPiecesBar::updateColors()
{
PiecesBar::updateColors();
updateColorsImpl();
}
void DownloadedPiecesBar::updateColorsImpl()
{
m_dlPieceColor = dlPieceColor(pieceColor());
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -52,11 +53,13 @@ public:
private:
// scale bitfield vector to float vector
QVector<float> bitfieldToFloatVector(const QBitArray &vecin, int reqSize);
bool updateImage(QImage &image) override;
QImage renderImage() override;
QString simpleToolTipText() const override;
void updateColors() override;
void updateColorsImpl();
// incomplete piece color
const QColor m_dlPieceColor;
QColor m_dlPieceColor;
// last used bitfields, uses to better resize redraw
// TODO: make a diff pieces to new pieces and update only changed pixels, speedup when update > 20x faster
QBitArray m_pieces;

View File

@@ -411,7 +411,7 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
return;
// Remove I2P peers since they will be completely reloaded.
for (QStandardItem *item : asConst(m_I2PPeerItems))
for (const QStandardItem *item : asConst(m_I2PPeerItems))
m_listModel->removeRow(item->row());
m_I2PPeerItems.clear();
@@ -466,10 +466,14 @@ void PeerListWidget::loadPeers(const BitTorrent::Torrent *torrent)
{
QStandardItem *item = m_peerItems.take(peerEndpoint);
QSet<QStandardItem *> &items = m_itemsByIP[peerEndpoint.address.ip];
items.remove(item);
if (items.isEmpty())
m_itemsByIP.remove(peerEndpoint.address.ip);
const auto items = m_itemsByIP.find(peerEndpoint.address.ip);
Q_ASSERT(items != m_itemsByIP.end());
if (items == m_itemsByIP.end()) [[unlikely]]
continue;
items->remove(item);
if (items->isEmpty())
m_itemsByIP.erase(items);
m_listModel->removeRow(item->row());
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -126,39 +127,38 @@ QVector<float> PieceAvailabilityBar::intToFloatVector(const QVector<int> &vecin,
return result;
}
bool PieceAvailabilityBar::updateImage(QImage &image)
QImage PieceAvailabilityBar::renderImage()
{
QImage image2(width() - 2 * borderWidth, 1, QImage::Format_RGB888);
if (image2.isNull())
QImage image {width() - 2 * borderWidth, 1, QImage::Format_RGB888};
if (image.isNull())
{
qDebug() << "QImage image2() allocation failed, width():" << width();
return false;
qDebug() << "QImage allocation failed, width():" << width();
return image;
}
if (m_pieces.empty())
{
image2.fill(backgroundColor());
image = image2;
return true;
image.fill(backgroundColor());
return image;
}
QVector<float> scaledPieces = intToFloatVector(m_pieces, image2.width());
QVector<float> scaledPieces = intToFloatVector(m_pieces, image.width());
// filling image
for (int x = 0; x < scaledPieces.size(); ++x)
{
float piecesToValue = scaledPieces.at(x);
image2.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
image.setPixel(x, 0, pieceColors()[piecesToValue * 255]);
}
image = image2;
return true;
return image;
}
void PieceAvailabilityBar::setAvailability(const QVector<int> &avail)
{
m_pieces = avail;
requestImageUpdate();
redraw();
}
void PieceAvailabilityBar::clear()

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -46,7 +47,7 @@ public:
void clear() override;
private:
bool updateImage(QImage &image) override;
QImage renderImage() override;
QString simpleToolTipText() const override;
// last used int vector, uses to better resize redraw

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2016 Eugene Shalygin
* Copyright (C) 2006 Christophe Dumez
*
@@ -41,6 +42,7 @@
#include "base/indexrange.h"
#include "base/path.h"
#include "base/utils/misc.h"
#include "gui/uithememanager.h"
namespace
{
@@ -114,10 +116,16 @@ namespace
}
PiecesBar::PiecesBar(QWidget *parent)
: QWidget {parent}
: QWidget(parent)
{
updatePieceColors();
setMouseTracking(true);
updateColorsImpl();
connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, [this]
{
updateColors();
redraw();
});
}
void PiecesBar::setTorrent(const BitTorrent::Torrent *torrent)
@@ -154,7 +162,7 @@ void PiecesBar::leaveEvent(QEvent *e)
{
m_hovered = false;
m_highlightedRegion = {};
requestImageUpdate();
redraw();
base::leaveEvent(e);
}
@@ -178,16 +186,17 @@ void PiecesBar::paintEvent(QPaintEvent *)
else
{
if (m_image.width() != imageRect.width())
updateImage(m_image);
{
if (const QImage image = renderImage(); !image.isNull())
m_image = image;
}
painter.drawImage(imageRect, m_image);
}
if (!m_highlightedRegion.isNull())
{
QColor highlightColor {this->palette().color(QPalette::Active, QPalette::Highlight)};
highlightColor.setAlphaF(0.35f);
QRect targetHighlightRect {m_highlightedRegion.adjusted(borderWidth, borderWidth, borderWidth, height() - 2 * borderWidth)};
painter.fillRect(targetHighlightRect, highlightColor);
painter.fillRect(targetHighlightRect, highlightedPieceColor());
}
QPainterPath border;
@@ -196,30 +205,40 @@ void PiecesBar::paintEvent(QPaintEvent *)
painter.drawPath(border);
}
void PiecesBar::requestImageUpdate()
void PiecesBar::redraw()
{
if (updateImage(m_image))
if (const QImage image = renderImage(); !image.isNull())
{
m_image = image;
update();
}
}
QColor PiecesBar::backgroundColor() const
{
return palette().color(QPalette::Base);
return palette().color(QPalette::Active, QPalette::Base);
}
QColor PiecesBar::borderColor() const
{
return palette().color(QPalette::Dark);
return palette().color(QPalette::Active, QPalette::Dark);
}
QColor PiecesBar::pieceColor() const
{
return palette().color(QPalette::Highlight);
return palette().color(QPalette::Active, QPalette::Highlight);
}
QColor PiecesBar::highlightedPieceColor() const
{
QColor col = palette().color(QPalette::Highlight).darker();
col.setAlphaF(0.35);
return col;
}
QColor PiecesBar::colorBoxBorderColor() const
{
return palette().color(QPalette::ToolTipText);
return palette().color(QPalette::Active, QPalette::ToolTipText);
}
const QVector<QRgb> &PiecesBar::pieceColors() const
@@ -325,12 +344,17 @@ void PiecesBar::highlightFile(int imagePos)
}
}
void PiecesBar::updatePieceColors()
void PiecesBar::updateColors()
{
updateColorsImpl();
}
void PiecesBar::updateColorsImpl()
{
m_pieceColors = QVector<QRgb>(256);
for (int i = 0; i < 256; ++i)
{
float ratio = (i / 255.0);
const float ratio = (i / 255.0);
m_pieceColors[i] = mixTwoColors(backgroundColor().rgb(), pieceColor().rgb(), ratio);
}
}

View File

@@ -1,5 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2016 Eugene Shalygin
* Copyright (C) 2006 Christophe Dumez
*
@@ -54,22 +55,22 @@ public:
virtual void clear();
// QObject interface
bool event(QEvent *e) override;
protected:
// QWidget interface
bool event(QEvent *e) override;
void enterEvent(QEnterEvent *e) override;
void leaveEvent(QEvent *e) override;
void mouseMoveEvent(QMouseEvent *e) override;
void paintEvent(QPaintEvent *e) override;
void requestImageUpdate();
virtual void updateColors();
void redraw();
QColor backgroundColor() const;
QColor borderColor() const;
QColor pieceColor() const;
QColor highlightedPieceColor() const;
QColor colorBoxBorderColor() const;
const QVector<QRgb> &pieceColors() const;
// mix two colors by light model, ratio <0, 1>
@@ -82,11 +83,9 @@ private:
void highlightFile(int imagePos);
virtual QString simpleToolTipText() const = 0;
virtual QImage renderImage() = 0;
// draw new image to replace the actual image
// returns true if image was successfully updated
virtual bool updateImage(QImage &image) = 0;
void updatePieceColors();
void updateColorsImpl();
const BitTorrent::Torrent *m_torrent = nullptr;
QImage m_image;

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -52,6 +52,7 @@
#include "base/utils/misc.h"
#include "base/utils/string.h"
#include "gui/autoexpandabledialog.h"
#include "gui/filterpatternformatmenu.h"
#include "gui/lineedit.h"
#include "gui/trackerlist/trackerlistwidget.h"
#include "gui/uithememanager.h"
@@ -66,6 +67,7 @@
PropertiesWidget::PropertiesWidget(QWidget *parent)
: QWidget(parent)
, m_ui {new Ui::PropertiesWidget}
, m_storeFilterPatternFormat {u"GUI/PropertiesWidget/FilterPatternFormat"_s}
{
m_ui->setupUi(this);
#ifndef Q_OS_MACOS
@@ -78,7 +80,9 @@ PropertiesWidget::PropertiesWidget(QWidget *parent)
m_contentFilterLine = new LineEdit(this);
m_contentFilterLine->setPlaceholderText(tr("Filter files..."));
m_contentFilterLine->setFixedWidth(300);
connect(m_contentFilterLine, &LineEdit::textChanged, m_ui->filesList, &TorrentContentWidget::setFilterPattern);
m_contentFilterLine->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_contentFilterLine, &QWidget::customContextMenuRequested, this, &PropertiesWidget::showContentFilterContextMenu);
connect(m_contentFilterLine, &LineEdit::textChanged, this, &PropertiesWidget::setContentFilterPattern);
m_ui->contentFilterLayout->insertWidget(3, m_contentFilterLine);
m_ui->filesList->setDoubleClickAction(TorrentContentWidget::DoubleClickAction::Open);
@@ -206,6 +210,7 @@ void PropertiesWidget::clear()
m_ui->labelSavePathVal->clear();
m_ui->labelCreatedOnVal->clear();
m_ui->labelTotalPiecesVal->clear();
m_ui->labelPrivateVal->clear();
m_ui->labelInfohash1Val->clear();
m_ui->labelInfohash2Val->clear();
m_ui->labelCommentVal->clear();
@@ -220,6 +225,7 @@ void PropertiesWidget::clear()
m_ui->labelConnectionsVal->clear();
m_ui->labelReannounceInVal->clear();
m_ui->labelShareRatioVal->clear();
m_ui->labelPopularityVal->clear();
m_ui->listWebSeeds->clear();
m_ui->labelETAVal->clear();
m_ui->labelSeedsVal->clear();
@@ -273,6 +279,28 @@ void PropertiesWidget::updateSavePath(BitTorrent::Torrent *const torrent)
m_ui->labelSavePathVal->setText(m_torrent->savePath().toString());
}
void PropertiesWidget::showContentFilterContextMenu()
{
QMenu *menu = m_contentFilterLine->createStandardContextMenu();
auto *formatMenu = new FilterPatternFormatMenu(m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards), menu);
connect(formatMenu, &FilterPatternFormatMenu::patternFormatChanged, this, [this](const FilterPatternFormat format)
{
m_storeFilterPatternFormat = format;
setContentFilterPattern();
});
menu->addSeparator();
menu->addMenu(formatMenu);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->popup(QCursor::pos());
}
void PropertiesWidget::setContentFilterPattern()
{
m_ui->filesList->setFilterPattern(m_contentFilterLine->text(), m_storeFilterPatternFormat.get(FilterPatternFormat::Wildcards));
}
void PropertiesWidget::updateTorrentInfos(BitTorrent::Torrent *const torrent)
{
if (torrent == m_torrent)
@@ -308,7 +336,14 @@ void PropertiesWidget::loadTorrentInfos(BitTorrent::Torrent *const torrent)
m_ui->labelCommentVal->setText(Utils::Misc::parseHtmlLinks(m_torrent->comment().toHtmlEscaped()));
m_ui->labelCreatedByVal->setText(m_torrent->creator());
m_ui->labelPrivateVal->setText(m_torrent->isPrivate() ? tr("Yes") : tr("No"));
}
else
{
m_ui->labelPrivateVal->setText(tr("N/A"));
}
// Load dynamic data
loadDynamicData();
}
@@ -407,6 +442,9 @@ void PropertiesWidget::loadDynamicData()
const qreal ratio = m_torrent->realRatio();
m_ui->labelShareRatioVal->setText(ratio > BitTorrent::Torrent::MAX_RATIO ? C_INFINITY : Utils::String::fromDouble(ratio, 2));
const qreal popularity = m_torrent->popularity();
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)")
.arg(QString::number(m_torrent->seedsCount())
, QString::number(m_torrent->totalSeedsCount())));
@@ -437,7 +475,7 @@ void PropertiesWidget::loadDynamicData()
m_ui->labelTotalPiecesVal->setText(tr("%1 x %2 (have %3)", "(torrent pieces) eg 152 x 4MB (have 25)").arg(m_torrent->piecesCount()).arg(Utils::Misc::friendlyUnit(m_torrent->pieceLength())).arg(m_torrent->piecesHave()));
if (!m_torrent->isFinished() && !m_torrent->isPaused() && !m_torrent->isQueued() && !m_torrent->isChecking())
if (!m_torrent->isFinished() && !m_torrent->isStopped() && !m_torrent->isQueued() && !m_torrent->isChecking())
{
// Pieces availability
showPiecesAvailability(true);

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2022 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2022-2024 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2006 Christophe Dumez <chris@qbittorrent.org>
*
* This program is free software; you can redistribute it and/or
@@ -32,7 +32,8 @@
#include <QList>
#include <QWidget>
#include "base/pathfwd.h"
#include "base/settingvalue.h"
#include "gui/filterpatternformat.h"
class QPushButton;
class QTreeView;
@@ -102,6 +103,8 @@ private slots:
private:
QPushButton *getButtonFromIndex(int index);
void showContentFilterContextMenu();
void setContentFilterPattern();
Ui::PropertiesWidget *m_ui = nullptr;
BitTorrent::Torrent *m_torrent = nullptr;
@@ -115,4 +118,6 @@ private:
PropTabBar *m_tabBar = nullptr;
LineEdit *m_contentFilterLine = nullptr;
int m_handleWidth = -1;
SettingValue<FilterPatternFormat> m_storeFilterPatternFormat;
};

View File

@@ -295,6 +295,25 @@
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelPopularity">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Ratio / Time Active (in months), indicates how popular the torrent is</string>
</property>
<property name="text">
<string>Popularity:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QLabel" name="labelConnectionsVal">
<property name="sizePolicy">
@@ -495,6 +514,22 @@
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="labelPopularityVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Ratio / Time Active (in months), indicates how popular the torrent is</string>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QLabel" name="labelUploaded">
<property name="sizePolicy">
@@ -546,7 +581,7 @@
</sizepolicy>
</property>
<property name="text">
<string extracomment="Time (duration) the torrent is active (not paused)">Time Active:</string>
<string extracomment="Time (duration) the torrent is active (not stopped)">Time Active:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
@@ -788,6 +823,38 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="labelPrivate">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Private:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1" colspan="5">
<widget class="QLabel" name="labelPrivateVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelInfohash1">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
@@ -803,71 +870,7 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="labelInfohash2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Info Hash v2:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="1" colspan="5">
<widget class="QLabel" name="labelInfohash2Val">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelSavePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Save Path:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelComment">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Comment:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="2" column="1" colspan="5">
<widget class="QLabel" name="labelInfohash1Val">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -883,7 +886,55 @@
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="labelInfohash2">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Info Hash v2:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="1" colspan="5">
<widget class="QLabel" name="labelInfohash2Val">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="textFormat">
<enum>Qt::PlainText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="labelSavePath">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Save Path:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="5" column="1" colspan="5">
<widget class="QLabel" name="labelSavePathVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
@@ -902,7 +953,23 @@
</property>
</widget>
</item>
<item row="5" column="1" colspan="5">
<item row="6" column="0">
<widget class="QLabel" name="labelComment">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Comment:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property>
</widget>
</item>
<item row="6" column="1" colspan="5">
<widget class="QLabel" name="labelCommentVal">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">

View File

@@ -1,6 +1,6 @@
/*
* Bittorrent Client using Qt and libtorrent.
* Copyright (C) 2017 Vladimir Golovnev <glassez@yandex.ru>
* Copyright (C) 2017-2024 Vladimir Golovnev <glassez@yandex.ru>
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
@@ -28,6 +28,7 @@
#include "articlelistwidget.h"
#include <QApplication>
#include <QListWidgetItem>
#include "base/global.h"
@@ -42,6 +43,12 @@ ArticleListWidget::ArticleListWidget(QWidget *parent)
setSelectionMode(QAbstractItemView::ExtendedSelection);
checkInvariant();
connect(UIThemeManager::instance(), &UIThemeManager::themeChanged, this, [this]
{
for (int row = 0; row < count(); ++row)
applyUITheme(item(row));
});
}
RSS::Article *ArticleListWidget::getRSSArticle(QListWidgetItem *item) const
@@ -100,11 +107,10 @@ void ArticleListWidget::handleArticleAdded(RSS::Article *rssArticle)
void ArticleListWidget::handleArticleRead(RSS::Article *rssArticle)
{
auto *item = mapRSSArticle(rssArticle);
if (!item) return;
if (!item)
return;
const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_s)};
item->setData(Qt::ForegroundRole, foregroundBrush);
item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_s, u"sphere"_s));
applyUITheme(item);
checkInvariant();
}
@@ -127,18 +133,25 @@ QListWidgetItem *ArticleListWidget::createItem(RSS::Article *article) const
item->setData(Qt::DisplayRole, article->title());
item->setData(Qt::UserRole, QVariant::fromValue(article));
if (article->isRead())
{
const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.ReadArticle"_s)};
item->setData(Qt::ForegroundRole, foregroundBrush);
item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_s, u"sphere"_s));
}
else
{
const QBrush foregroundBrush {UIThemeManager::instance()->getColor(u"RSS.UnreadArticle"_s)};
item->setData(Qt::ForegroundRole, foregroundBrush);
item->setData(Qt::DecorationRole, UIThemeManager::instance()->getIcon(u"loading"_s, u"sphere"_s));
}
applyUITheme(item);
return item;
}
void ArticleListWidget::applyUITheme(QListWidgetItem *item) const
{
const bool isRead = getRSSArticle(item)->isRead();
const auto *themeManager = UIThemeManager::instance();
if (isRead)
{
const QColor color = themeManager->getColor(u"RSS.ReadArticle"_s);
item->setData(Qt::ForegroundRole, (color.isValid() ? color : QApplication::palette().color(QPalette::Inactive, QPalette::WindowText)));
item->setData(Qt::DecorationRole, themeManager->getIcon(u"rss_read_article"_s, u"sphere"_s));
}
else
{
const QColor color = themeManager->getColor(u"RSS.UnreadArticle"_s);
item->setData(Qt::ForegroundRole, (color.isValid() ? color : QApplication::palette().color(QPalette::Active, QPalette::Link)));
item->setData(Qt::DecorationRole, themeManager->getIcon(u"rss_unread_article"_s, u"sphere"_s));
}
}

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