mirror of
https://github.com/aclist/dztui.git
synced 2026-10-31 07:02:50 +01:00
Compare commits
245
Commits
7.0.0b13
...
fc28d419bf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc28d419bf | ||
|
|
095af395f9 | ||
|
|
4ada06d305 | ||
|
|
5a0ab92985 | ||
|
|
f2501871ee | ||
|
|
cf70182733 | ||
|
|
42feeae8d0 | ||
|
|
28cd6b8c45 | ||
|
|
81e327fb35 | ||
|
|
37e247d1d9 | ||
|
|
286e3f2d80 | ||
|
|
2ef27a55d8 | ||
|
|
d0ac09ffe5 | ||
|
|
c0015c05ec | ||
|
|
682b5bb4c0 | ||
|
|
3551877192 | ||
|
|
b22aba8593 | ||
|
|
f85f1ef2a5 | ||
|
|
010d7984b6 | ||
|
|
70e1e9e740 | ||
|
|
734af7cf22 | ||
|
|
d8f4977ffe | ||
|
|
c9fd00f938 | ||
|
|
f4db1484d8 | ||
|
|
99ef9a31b9 | ||
|
|
4c392dc571 | ||
|
|
155ffea15d | ||
|
|
57d231331d | ||
|
|
d5fe3c35dc | ||
|
|
3cf547e903 | ||
|
|
bd0bfcb03d | ||
|
|
4f6c583294 | ||
|
|
5e5a6b4eaf | ||
|
|
ff59d6dc35 | ||
|
|
1c154a709d | ||
|
|
fb47564618 | ||
|
|
30e309442b | ||
|
|
3e4ed95908 | ||
|
|
93edbac5b0 | ||
|
|
c947bc2c9b | ||
|
|
06315972b1 | ||
|
|
4e36824ed7 | ||
|
|
0bb618b703 | ||
|
|
b8e11d5b59 | ||
|
|
db8b1acbc6 | ||
|
|
1e5d66c9be | ||
|
|
202620c062 | ||
|
|
673038a1ab | ||
|
|
c97e34c336 | ||
|
|
799ea263a0 | ||
|
|
08cbd88262 | ||
|
|
5b5e91c2fa | ||
|
|
f8be7e33d1 | ||
|
|
54c950fc41 | ||
|
|
1e266c3cd2 | ||
|
|
16959a692d | ||
|
|
7c79b17fdc | ||
|
|
87e0af33cc | ||
|
|
3b4a736ce6 | ||
|
|
b70428fe3d | ||
|
|
3f037cdad6 | ||
|
|
9b63d38c5a | ||
|
|
4c26d85afd | ||
|
|
35b8d21181 | ||
|
|
e368e98a1a | ||
|
|
2ea6490a9b | ||
|
|
7b57f95f5f | ||
|
|
a811e5945d | ||
|
|
9b305d806b | ||
|
|
1955abd3e6 | ||
|
|
aac29c0a5c | ||
|
|
218ed43403 | ||
|
|
380f6f82a1 | ||
|
|
cef9e1984f | ||
|
|
0520ced2ea | ||
|
|
4bbe179940 | ||
|
|
1527239567 | ||
|
|
64c8b905c4 | ||
|
|
52ab81f4ec | ||
|
|
ec7828df85 | ||
|
|
525de56036 | ||
|
|
7edb66c311 | ||
|
|
dc2a635d2f | ||
|
|
8528300851 | ||
|
|
daa4aa0059 | ||
|
|
71a0481600 | ||
|
|
ba1b980a7f | ||
|
|
1dc20b838d | ||
|
|
0c6421fa63 | ||
|
|
b47cb482f5 | ||
|
|
152999d728 | ||
|
|
cdcb3b794f | ||
|
|
cd23f403c9 | ||
|
|
a40bef8f82 | ||
|
|
a4f9a67da1 | ||
|
|
5c07a77b63 | ||
|
|
91773c933c | ||
|
|
6a30ed44fc | ||
|
|
bbaec35a5f | ||
|
|
c46477f0a7 | ||
|
|
f47446a2e1 | ||
|
|
cdb311cb1f | ||
|
|
11cdcb62b2 | ||
|
|
1913f69e7b | ||
|
|
8c2cea0fdc | ||
|
|
8c29b9b35a | ||
|
|
d8239f1c34 | ||
|
|
8c3ec4512a | ||
|
|
1a18c8ff02 | ||
|
|
92c23ec609 | ||
|
|
e9d4054e76 | ||
|
|
1c9e026f70 | ||
|
|
eb076b7dcd | ||
|
|
8629e02670 | ||
|
|
3ba38f30ad | ||
|
|
7c37fd8b6e | ||
|
|
1a065123bb | ||
|
|
2586306323 | ||
|
|
1ccb99d935 | ||
|
|
054fe968d1 | ||
|
|
1760e6e928 | ||
|
|
a2a39e9927 | ||
|
|
e664f68624 | ||
|
|
2e41856348 | ||
|
|
27d444ea6f | ||
|
|
26a2362172 | ||
|
|
50cf347164 | ||
|
|
dd69bc88b1 | ||
|
|
5c84b5ae0a | ||
|
|
6fdf5547dc | ||
|
|
fc1311579c | ||
|
|
da993db2e2 | ||
|
|
207d8fd407 | ||
|
|
72b10ad543 | ||
|
|
35680b1373 | ||
|
|
5121e6d005 | ||
|
|
2f61cc7ff7 | ||
|
|
8733094267 | ||
|
|
9d14b7f219 | ||
|
|
0084af4959 | ||
|
|
caf071f997 | ||
|
|
b50d061e1f | ||
|
|
e913c95c70 | ||
|
|
d6bf63886e | ||
|
|
ec8f6a3d36 | ||
|
|
b8fbbbb1cd | ||
|
|
fb2772cde2 | ||
|
|
6c36b741f4 | ||
|
|
aa43b017d1 | ||
|
|
fc86b75ca1 | ||
|
|
6a3b4b2bfb | ||
|
|
8652259053 | ||
|
|
e92df8c062 | ||
|
|
eff31e2bd3 | ||
|
|
494311e8df | ||
|
|
a2d1f9b0e1 | ||
|
|
b731fb5566 | ||
|
|
e8d991ccb5 | ||
|
|
f4aa6c749e | ||
|
|
3ed4747d08 | ||
|
|
d2042a3dde | ||
|
|
09ef0f4f9a | ||
|
|
a91ce17501 | ||
|
|
48f0f3913c | ||
|
|
33e367182e | ||
|
|
0ce3ab3915 | ||
|
|
90c7d9f73e | ||
|
|
5923d5d935 | ||
|
|
9f73c545ef | ||
|
|
041438cb7f | ||
|
|
c6e69f9aa0 | ||
|
|
8f1e4ff9d0 | ||
|
|
92792b0b4f | ||
|
|
a7cfd4c346 | ||
|
|
fa5f638697 | ||
|
|
78151b3bb3 | ||
|
|
2418d8be68 | ||
|
|
9fa7172056 | ||
|
|
d75f87768e | ||
|
|
5bb4271d36 | ||
|
|
6c86bff869 | ||
|
|
92ba7c2267 | ||
|
|
6c56f16633 | ||
|
|
2b87272b52 | ||
|
|
4de5147899 | ||
|
|
4c31cd7ecd | ||
|
|
61f90d6016 | ||
|
|
cd1fd1b179 | ||
|
|
1b25c8fadb | ||
|
|
18146bf66f | ||
|
|
8ae54d436a | ||
|
|
4d94551f87 | ||
|
|
1154789f8b | ||
|
|
cbccd4c5f6 | ||
|
|
8cac752a0d | ||
|
|
580273053c | ||
|
|
8d75c1f7b2 | ||
|
|
320c2f692e | ||
|
|
413d84b2b5 | ||
|
|
c751b277d0 | ||
|
|
7f36a3acad | ||
|
|
5780d3daff | ||
|
|
e692e1d139 | ||
|
|
38d44a7952 | ||
|
|
611e3e6e58 | ||
|
|
d5b415316e | ||
|
|
0eeec490c2 | ||
|
|
782157006a | ||
|
|
3be7bc0e02 | ||
|
|
9e7102aaba | ||
|
|
6516e7ae3c | ||
|
|
46c1da232e | ||
|
|
5280121e43 | ||
|
|
17c63cf85f | ||
|
|
488d98800f | ||
|
|
4eac5055cb | ||
|
|
81168d3952 | ||
|
|
ee90be42ee | ||
|
|
48f341bae2 | ||
|
|
0653f064b2 | ||
|
|
525dc24508 | ||
|
|
4e7c831a53 | ||
|
|
e41bef7b00 | ||
|
|
27136a0906 | ||
|
|
cd95a71782 | ||
|
|
0c014a7859 | ||
|
|
65c159505f | ||
|
|
e056ac924a | ||
|
|
d540680330 | ||
|
|
98c45afe7f | ||
|
|
e86040747f | ||
|
|
a559485d0a | ||
|
|
8695793af4 | ||
|
|
a2aeac0242 | ||
|
|
879d0650d4 | ||
|
|
5c85c746f1 | ||
|
|
fa8792ba69 | ||
|
|
5e3392ddec | ||
|
|
796f6b8435 | ||
|
|
780402d14f | ||
|
|
3ae0c8428a | ||
|
|
ed20a7cc84 | ||
|
|
9225f3bc06 | ||
|
|
075a2e5a74 | ||
|
|
58a7878f60 |
No files matched your search
+34
-28
@@ -1,6 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Added
|
## [7.0.0] Unreleased
|
||||||
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
@@ -41,8 +42,11 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
|
- Generate Steam shortcuts and cover art
|
||||||
|
- Warn user if background downloads are disabled
|
||||||
|
- Dynamic copy button
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
@@ -59,58 +63,60 @@
|
|||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
|
- Battlemetrics API support
|
||||||
- Debug mode
|
- Debug mode
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
|
- History table not being in lock-step with state file
|
||||||
|
|
||||||
## Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
## Developer-facing
|
### Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
## Changed
|
### Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
## Fixed
|
### Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
## Fixed
|
### Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
## Changed
|
### Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
## Added
|
### Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@@ -136,7 +142,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@@ -175,7 +181,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@@ -187,50 +193,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
### Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
## Added
|
### Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
## Changed
|
### Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
## Fixed
|
### Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
## Added
|
### Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
## Fixed
|
### Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
## Changed
|
### Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
## Dropped
|
### Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
## Changed
|
### Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Introduction
|
||||||
|
|
||||||
|
Thank you for your interest in DZGUI!
|
||||||
|
|
||||||
|
This guide goes over development conventions and best practices for contributors.
|
||||||
|
If you are a developer, you can skip to the end.
|
||||||
|
|
||||||
|
# Requesting help
|
||||||
|
|
||||||
|
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
||||||
|
[issue tracker](https://github.com/aclist/dztui/issues) under the
|
||||||
|
`troubleshooting` tag.
|
||||||
|
|
||||||
|
# How can I help the project?
|
||||||
|
|
||||||
|
There are several ways to help this project.
|
||||||
|
1. Report bugs that you find
|
||||||
|
2. Request features that you would like
|
||||||
|
3. Contribute features and fixes to the codebase
|
||||||
|
4. Contribute documentation to the project
|
||||||
|
|
||||||
|
Before making any contribution, please read the `CODE_OF_CONDUCT.md` and act
|
||||||
|
accordingly.
|
||||||
|
|
||||||
|
## Submitting a ticket
|
||||||
|
|
||||||
|
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues).
|
||||||
|
From there, follow the onscreen prompts. You will be asked questions such as:
|
||||||
|
|
||||||
|
- What version are you using?
|
||||||
|
- What distribution are you using?
|
||||||
|
- What is the issue that you found?
|
||||||
|
- How can we reproduce the issue?
|
||||||
|
|
||||||
|
You can also attach screenshots, logs, or other data that can help us.
|
||||||
|
|
||||||
|
## Requesting a feature
|
||||||
|
|
||||||
|
You can also request features via the same issue tracker. It is good practice to
|
||||||
|
first search for your idea to see if a similar one has already been posted.
|
||||||
|
If not, open a ticket where you describe your feature and its possible benefits.
|
||||||
|
|
||||||
|
Please note that this is a community project, so it takes time for us to develop
|
||||||
|
features. Putting in a feature request does not mean that it will be implemented,
|
||||||
|
but we will do our best to support as many cool ideas as possible.
|
||||||
|
|
||||||
|
## Contributing code or documentation
|
||||||
|
|
||||||
|
If you would like to assist with an issue on the issue tracker or
|
||||||
|
contribute a new change, please follow the guidelines below.
|
||||||
|
|
||||||
|
Fork this repository and check out the code up to the `dzgui7` (development) branch.
|
||||||
|
|
||||||
|
The following naming conventions apply for PRs:
|
||||||
|
- fix/<your-fix> - patch/hotfix branches
|
||||||
|
- feat/<your-feature> - feature branches
|
||||||
|
- doc/<your-doc-branch> - documentation branches
|
||||||
|
- infra/<your-infra-branch> - infrastructure branches
|
||||||
|
|
||||||
|
Implement your changes and test them locally. If they work, you may
|
||||||
|
open a merge request, and then we review your changes. If everything is OK, it
|
||||||
|
will be merged to `dzgui7`. After sufficient changes are consolidated into a new release,
|
||||||
|
this branch will be tagged to a certain point in time and a binary release published.
|
||||||
|
|
||||||
|
It is recommended to follow
|
||||||
|
[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), as this
|
||||||
|
integrates well with tooling and helps the project review your code.
|
||||||
|
|
||||||
|
Please see DEVELOPERS.md for further details.
|
||||||
@@ -24,13 +24,8 @@ Please refer to the documentation for installation and setup instructions:
|
|||||||
|
|
||||||
Geolocation records from [DB-IP](https://db-ip.com) under [CC 4.0 license](https://creativecommons.org/licenses/by/4.0/)
|
Geolocation records from [DB-IP](https://db-ip.com) under [CC 4.0 license](https://creativecommons.org/licenses/by/4.0/)
|
||||||
|
|
||||||
This tool uses [python-a2s](https://github.com/Yepoleb/python-a2s) and [dayzquery](https://github.com/Yepoleb/dayzquery) as submodules; licenses for these submodules can be found in the LICENSES file
|
Executable versions of DZGUI published as release binaries ship with various runtime dependencies and the Python interpreter built in.
|
||||||
of the project root.
|
Users wishing to review the licenses to these components can inspect the `LICENSE` file located in the release tarball.
|
||||||
|
|
||||||
Both the geolocation records and submodules listed above are not shipped with the source code, but are retrieved and assembled at runtime.
|
|
||||||
|
|
||||||
Finally, executable versions of DZGUI shipped as release binaries are thin wrappers around the Python interpreter, and also retrieve and assemble the above dependencies at runtime on the end-user's
|
|
||||||
machine, rather than using pre-compiled source code. Users wishing to review these dependencies can inspect the 'pyproject.toml' manifest in the project root.
|
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
Copyright (c) 2015 Rossen Georgiev <rossen@rgp.io>
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
||||||
|
of the Software, and to permit persons to whom the Software is furnished to do
|
||||||
|
so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -26,7 +26,7 @@ You can install it via the following set of commands:
|
|||||||
.. code:: console
|
.. code:: console
|
||||||
|
|
||||||
sudo apt update
|
sudo apt update
|
||||||
sudo apt install libgirepository-2.0
|
sudo apt install libgirepository-2.0-0
|
||||||
|
|
||||||
Next steps
|
Next steps
|
||||||
---------------
|
---------------
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+38
-15
@@ -1,12 +1,18 @@
|
|||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections.abc import Iterator
|
from collections.abc import Iterator
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
|
|
||||||
@deprecated("Use dzgui.api.steam.unsubscribe()")
|
logger = logging.getLogger(APP_NAME)
|
||||||
class WorkshopACF:
|
|
||||||
def __init__(self, file: str) -> None:
|
|
||||||
|
class ACF:
|
||||||
|
def __init__(self, file: Path) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.dict: dict[str, Any]
|
self.dict: dict[str, Any]
|
||||||
@@ -15,9 +21,10 @@ class WorkshopACF:
|
|||||||
def as_dict(self) -> dict[str, Any]:
|
def as_dict(self) -> dict[str, Any]:
|
||||||
return self.dict
|
return self.dict
|
||||||
|
|
||||||
def load(self, file: str) -> None:
|
def load(self, file: Path) -> None:
|
||||||
delimiter = r"\t\t"
|
delimiter = r"\t\t"
|
||||||
lines = []
|
lines = []
|
||||||
|
# TODO: illegal file handling
|
||||||
with open(file, "r", encoding="utf-8") as f:
|
with open(file, "r", encoding="utf-8") as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
@@ -27,6 +34,14 @@ class WorkshopACF:
|
|||||||
lines.append(els)
|
lines.append(els)
|
||||||
self.dict = self.parse(iter(lines))
|
self.dict = self.parse(iter(lines))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enquote(cls, s: str) -> str:
|
||||||
|
return f'"{s}"'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dequote(cls, s: str) -> str:
|
||||||
|
return s.rstrip('"').lstrip('"')
|
||||||
|
|
||||||
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
|
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
|
||||||
acf: dict[str, Any] = {}
|
acf: dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
@@ -54,6 +69,23 @@ class WorkshopACF:
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return acf
|
return acf
|
||||||
|
|
||||||
|
def get_allows_downloads(self) -> int | None:
|
||||||
|
try:
|
||||||
|
# TODO: consider coercing strs to ints on initial import
|
||||||
|
flag = int(self.dict["AppState"]["AllowOtherDownloadsWhileRunning"])
|
||||||
|
return flag
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated(
|
||||||
|
"Serializing workshop files is no longer supported, use dzgui.api.steam.unsubscribe"
|
||||||
|
)
|
||||||
|
class WorkshopACF(ACF):
|
||||||
|
def __init__(self, file: Path) -> None:
|
||||||
|
super().__init__(file)
|
||||||
|
|
||||||
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
||||||
t1 = "AppWorkshop"
|
t1 = "AppWorkshop"
|
||||||
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
||||||
@@ -78,18 +110,9 @@ class WorkshopACF:
|
|||||||
s += line + "\n"
|
s += line + "\n"
|
||||||
return s
|
return s
|
||||||
|
|
||||||
def to_file(self, file: str) -> None:
|
def to_file(self, file: Path) -> None:
|
||||||
s = self.unpack(self.dict)
|
s = self.unpack(self.dict)
|
||||||
with open(file, "w") as f:
|
file.write_text(s)
|
||||||
f.write(s)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def enquote(cls, s: str) -> str:
|
|
||||||
return f'"{s}"'
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def dequote(cls, s: str) -> str:
|
|
||||||
return s.rstrip('"').lstrip('"')
|
|
||||||
|
|
||||||
def delete(self, modid: int) -> None:
|
def delete(self, modid: int) -> None:
|
||||||
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
|
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import logging
|
|
||||||
import requests
|
|
||||||
from typing import Any, Optional, TYPE_CHECKING
|
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
|
||||||
from dzgui.const.endpoints import BM_SERVERS
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from dzgui.api.servers import Record
|
|
||||||
|
|
||||||
|
|
||||||
def get_attributes(key: str, uid: int) -> Any:
|
|
||||||
# TODO: handle if key is not set
|
|
||||||
# TODO: tests for malformed IDs/values
|
|
||||||
|
|
||||||
hdr = {"Authorization": "Bearer " + key}
|
|
||||||
payload: dict[str, str] = {
|
|
||||||
"filter[game]": "dayz",
|
|
||||||
"sort": "-players",
|
|
||||||
"filter[ids][whitelist]": str(uid),
|
|
||||||
}
|
|
||||||
res = requests.get(BM_SERVERS, params=payload, headers=hdr)
|
|
||||||
res.raise_for_status()
|
|
||||||
j = res.json()["data"][0]["attributes"]
|
|
||||||
return j
|
|
||||||
|
|
||||||
|
|
||||||
def map_id_to_record(key: str, uid: int) -> Optional["Record"]:
|
|
||||||
from dzgui.api.servers import Record
|
|
||||||
|
|
||||||
try:
|
|
||||||
record = get_attributes(key, uid)
|
|
||||||
ip = record["ip"]
|
|
||||||
port = int(record["port"])
|
|
||||||
qport = int(record["portQuery"])
|
|
||||||
return Record(ip, port, qport)
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
+19
-10
@@ -7,13 +7,12 @@ from concurrent.futures import ThreadPoolExecutor
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
from dzgui.api.steam import get_app_path
|
||||||
from dzgui.api.servers import get_rules, fqip_to_record
|
from dzgui.api.servers import get_rules, fqip_to_record
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
DAYZ_COMMUNITY_ROOT,
|
DAYZ_COMMUNITY_ROOT,
|
||||||
LIBRARYFOLDERS_PATH,
|
|
||||||
WORKSHOP_PATH,
|
WORKSHOP_PATH,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ def get_local_mod_ids(steam_path: Path) -> list[int]:
|
|||||||
|
|
||||||
|
|
||||||
def get_local_mod_path(steam_path: Path) -> Path:
|
def get_local_mod_path(steam_path: Path) -> Path:
|
||||||
p = PeFile.get_app_path(steam_path / Path(LIBRARYFOLDERS_PATH), APPID_DAYZ)
|
p = get_app_path(steam_path, APPID_DAYZ)
|
||||||
workshop_path = p / WORKSHOP_PATH
|
workshop_path = p / WORKSHOP_PATH
|
||||||
return workshop_path
|
return workshop_path
|
||||||
|
|
||||||
@@ -49,6 +48,7 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
|
|||||||
mods = [file for file in workshop_path.iterdir() if file.is_dir()]
|
mods = [file for file in workshop_path.iterdir() if file.is_dir()]
|
||||||
return mods
|
return mods
|
||||||
|
|
||||||
|
|
||||||
def is_mission(path: Path) -> bool:
|
def is_mission(path: Path) -> bool:
|
||||||
# TODO: parse integrity of other files
|
# TODO: parse integrity of other files
|
||||||
parent = path.parent.name
|
parent = path.parent.name
|
||||||
@@ -57,9 +57,10 @@ def is_mission(path: Path) -> bool:
|
|||||||
file = path / "init.c"
|
file = path / "init.c"
|
||||||
return file.exists()
|
return file.exists()
|
||||||
|
|
||||||
|
|
||||||
def tokenize(mod: Path) -> dict[str, Any] | None:
|
def tokenize(mod: Path) -> dict[str, Any] | None:
|
||||||
file = mod.joinpath("meta.cpp")
|
file = mod.joinpath("meta.cpp")
|
||||||
delimiter=r"\s*=\s*"
|
delimiter = r"\s*=\s*"
|
||||||
modmeta = {}
|
modmeta = {}
|
||||||
try:
|
try:
|
||||||
with open(file, "r", encoding="utf-8") as f:
|
with open(file, "r", encoding="utf-8") as f:
|
||||||
@@ -76,6 +77,7 @@ def tokenize(mod: Path) -> dict[str, Any] | None:
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_mod_size(path: Path) -> float:
|
def get_mod_size(path: Path) -> float:
|
||||||
s = 0
|
s = 0
|
||||||
for f in path.rglob("*"):
|
for f in path.rglob("*"):
|
||||||
@@ -123,20 +125,27 @@ def _hash(uid: str, use_custom: bool = False) -> str:
|
|||||||
return prefix + md5.hexdigest()[:8]
|
return prefix + md5.hexdigest()[:8]
|
||||||
|
|
||||||
|
|
||||||
def remove_stale_signatures(config: Path, versions: Path) -> None:
|
def remove_stale_signatures(
|
||||||
|
config: Path, versions: Path, ids: list[int] | None = None
|
||||||
|
) -> None:
|
||||||
if versions.is_file() is False:
|
if versions.is_file() is False:
|
||||||
logger.warning("Creating new version signatures file")
|
logger.warning("Creating new version signatures file")
|
||||||
versions.touch()
|
versions.touch()
|
||||||
return
|
return
|
||||||
|
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
steam_path = Path(path)
|
steam_path = Path(path)
|
||||||
ids = get_local_mod_ids(steam_path)
|
if ids is None:
|
||||||
|
local_ids = get_local_mod_ids(steam_path)
|
||||||
|
|
||||||
with open(versions, "r") as f:
|
with open(versions, "r") as f:
|
||||||
lines = f.readlines()
|
lines = f.readlines()
|
||||||
for line in lines:
|
|
||||||
uid = int(line.split(",")[0])
|
if ids is None:
|
||||||
if uid not in ids:
|
lines = [line for line in lines if int(line.split(",")[0]) in local_ids]
|
||||||
lines.remove(line)
|
else:
|
||||||
|
lines = [line for line in lines if int(line.split(",")[0]) not in ids]
|
||||||
|
|
||||||
with open(versions, "w") as f:
|
with open(versions, "w") as f:
|
||||||
for line in lines:
|
for line in lines:
|
||||||
f.write(line)
|
f.write(line)
|
||||||
|
|||||||
+2
-49
@@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import struct
|
import struct
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -12,9 +11,8 @@ from dzgui.const.constants import (
|
|||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP,
|
APPNAME_DAYZ_EXP,
|
||||||
DAYZ_BINARY,
|
DAYZ_BINARY,
|
||||||
LIBRARYFOLDERS_PATH,
|
|
||||||
)
|
)
|
||||||
from dzgui.api.steam import vdf2json
|
from dzgui.api.steam import get_app_path
|
||||||
|
|
||||||
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
|
# https://learn.microsoft.com/en-us/windows/win32/debug/pe-format
|
||||||
endian = "<"
|
endian = "<"
|
||||||
@@ -257,25 +255,6 @@ class PeFileError(Exception):
|
|||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AppNotInstalledError(Exception):
|
|
||||||
"""App not present in user's libraryfolders"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class AppMovedError(Exception):
|
|
||||||
"""VDF points to a nonexistent location on disk"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class VDFLoadError(Exception):
|
|
||||||
"""Malformed VDF or JSON conversion"""
|
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def parse_version_number(data: BinaryIO) -> FileVersion:
|
def parse_version_number(data: BinaryIO) -> FileVersion:
|
||||||
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
# https://learn.microsoft.com/en-us/windows/win32/api/verrsrc/ns-verrsrc-vs_fixedfileinfo
|
||||||
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
minor = struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF
|
||||||
@@ -419,37 +398,11 @@ def get_pefile_path(steam_path: Path, appid: int) -> Path:
|
|||||||
name = identifier[appid]
|
name = identifier[appid]
|
||||||
binary = DAYZ_BINARY
|
binary = DAYZ_BINARY
|
||||||
|
|
||||||
app_path = get_app_path(steam_path / LIBRARYFOLDERS_PATH, appid)
|
app_path = get_app_path(steam_path, appid)
|
||||||
pe_path = app_path / f"steamapps/common/{name}/{binary}"
|
pe_path = app_path / f"steamapps/common/{name}/{binary}"
|
||||||
return pe_path
|
return pe_path
|
||||||
|
|
||||||
|
|
||||||
def get_app_path(folders_path: Path, appid: int) -> Path:
|
|
||||||
app_path = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
j = json.loads(vdf2json(folders_path))
|
|
||||||
except Exception:
|
|
||||||
raise VDFLoadError("Failed to parse libraryfolders")
|
|
||||||
|
|
||||||
for obj in j["libraryfolders"]:
|
|
||||||
if str(appid) in j["libraryfolders"][obj]["apps"]:
|
|
||||||
app_path = j["libraryfolders"][obj]["path"]
|
|
||||||
if Path(app_path).exists():
|
|
||||||
break
|
|
||||||
|
|
||||||
if app_path is None:
|
|
||||||
raise AppNotInstalledError(
|
|
||||||
f"Failed to find a libraryfolder for the appid {appid}"
|
|
||||||
)
|
|
||||||
if Path(app_path).exists() is False:
|
|
||||||
raise AppMovedError(
|
|
||||||
f"The location '{app_path}' pointed to by '{appid}' no longer exists and may have been changed on the disk."
|
|
||||||
)
|
|
||||||
|
|
||||||
return Path(app_path)
|
|
||||||
|
|
||||||
|
|
||||||
def get_pretty_version(steam_path: Path, appid: int) -> str | None:
|
def get_pretty_version(steam_path: Path, appid: int) -> str | None:
|
||||||
try:
|
try:
|
||||||
pe_file_path = get_pefile_path(steam_path, appid)
|
pe_file_path = get_pefile_path(steam_path, appid)
|
||||||
|
|||||||
@@ -41,21 +41,6 @@ def test_ipdb() -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_bm_api(key: str) -> bool:
|
|
||||||
payload: dict[str, str] = {
|
|
||||||
"filter[game]": "dayz",
|
|
||||||
}
|
|
||||||
hdr = {"Authorization": "Bearer " + key}
|
|
||||||
try:
|
|
||||||
res = requests.get(
|
|
||||||
endpoints.BM_SERVERS, params=payload, headers=hdr, timeout=REQUEST_TIMEOUT
|
|
||||||
)
|
|
||||||
return is_remote_up(res)
|
|
||||||
except Exception as e:
|
|
||||||
logger.critical(e)
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def is_remote_up(res: "Response") -> bool:
|
def is_remote_up(res: "Response") -> bool:
|
||||||
if res.status_code == 200:
|
if res.status_code == 200:
|
||||||
return True
|
return True
|
||||||
|
|||||||
+2
-12
@@ -11,7 +11,6 @@ import threading
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, Optional, TYPE_CHECKING, Union
|
from typing import Any, Optional, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from dzgui.api.bm import map_id_to_record
|
|
||||||
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
|
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
|
||||||
from dzgui.const.endpoints import STEAM_SERVERS
|
from dzgui.const.endpoints import STEAM_SERVERS
|
||||||
from dzgui.util.format import format_exception
|
from dzgui.util.format import format_exception
|
||||||
@@ -289,13 +288,14 @@ def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict[str, Any] |
|
|||||||
|
|
||||||
|
|
||||||
def get_details(record: Record) -> Details:
|
def get_details(record: Record) -> Details:
|
||||||
|
# TODO: consolidate try/except statements
|
||||||
ip = record.ip
|
ip = record.ip
|
||||||
qport = record.qport
|
qport = record.qport
|
||||||
default_str = strings.none_provided
|
default_str = strings.none_provided
|
||||||
|
|
||||||
try:
|
try:
|
||||||
info = a2s.info((ip, qport))
|
info = a2s.info((ip, qport))
|
||||||
except TimeoutError:
|
except (TimeoutError, a2s.exceptions.BrokenMessageError):
|
||||||
return Details(None, default_str, False)
|
return Details(None, default_str, False)
|
||||||
try:
|
try:
|
||||||
rules = dayzquery.dayz_rules((ip, qport))
|
rules = dayzquery.dayz_rules((ip, qport))
|
||||||
@@ -477,16 +477,6 @@ def query_by_ip(addr: str) -> A2SInfo:
|
|||||||
return query_by_record(record, update_gameport=True)
|
return query_by_record(record, update_gameport=True)
|
||||||
|
|
||||||
|
|
||||||
def query_by_id(server_id: int, key: str) -> A2SInfo:
|
|
||||||
"""
|
|
||||||
Used with numeric Battlemetrics IDs
|
|
||||||
"""
|
|
||||||
record = map_id_to_record(key, server_id)
|
|
||||||
if record is None:
|
|
||||||
return A2SInfo(Record("0", 0, 0), None)
|
|
||||||
return query_by_record(record)
|
|
||||||
|
|
||||||
|
|
||||||
def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo:
|
def query_by_record(record: Record, update_gameport: bool = False) -> A2SInfo:
|
||||||
try:
|
try:
|
||||||
info = a2s.info((record.ip, record.qport), 3.0)
|
info = a2s.info((record.ip, record.qport), 3.0)
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
import binascii
|
||||||
|
import ctypes
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import vdf # type: ignore
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dzgui.api.steam import find_user_id_32
|
||||||
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
||||||
|
from dzgui.util.dirs import copy_dzgui_to_xdg_data
|
||||||
|
from dzgui.util.strings import unknown
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class ShortcutMetadata:
|
||||||
|
appname: str
|
||||||
|
appid: int
|
||||||
|
exe_path: str
|
||||||
|
start_dir: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
|
class Shortcuts:
|
||||||
|
def __init__(self, steam_path: Path) -> None:
|
||||||
|
self.user_config_path: Path
|
||||||
|
self.shortcuts_path = self.find_shortcuts_path(steam_path)
|
||||||
|
self._load_shortcuts(self.shortcuts_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_bpid(cls, uid: str) -> int:
|
||||||
|
u = uid.encode()
|
||||||
|
encoded = binascii.crc32(u)
|
||||||
|
return encoded | 0x80000000
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_signed_appid(cls, appid: int) -> int:
|
||||||
|
return ctypes.c_int(appid).value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_exe_uid(cls, appname: str, exe: Path) -> str:
|
||||||
|
wrapped_exe = f'"{exe}"'
|
||||||
|
return appname + wrapped_exe
|
||||||
|
|
||||||
|
def find_appname_by_unsigned_id(self, appid: int) -> str:
|
||||||
|
# NOTE: bitmask signed int back to 32-bit CRC
|
||||||
|
# varying client versions treat case sensitivity differently
|
||||||
|
try:
|
||||||
|
for key in self.shortcuts["shortcuts"].keys():
|
||||||
|
if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid:
|
||||||
|
try:
|
||||||
|
return str(self.shortcuts["shortcuts"][key]["appname"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
try:
|
||||||
|
return str(self.shortcuts["shortcuts"][key]["AppName"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return unknown
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return unknown
|
||||||
|
|
||||||
|
def get_shortcuts(self) -> Any:
|
||||||
|
return self.shortcuts
|
||||||
|
|
||||||
|
def _load_shortcuts(self, path: Path) -> None:
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
shortcuts = vdf.binary_load(f)
|
||||||
|
self.shortcuts = shortcuts
|
||||||
|
logger.debug("Loaded shortcuts file")
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def find_shortcuts_path(self, steam_path: Path) -> Path:
|
||||||
|
uid = find_user_id_32(steam_path)
|
||||||
|
self.user_config_path = steam_path.joinpath(f"userdata/{uid}/config")
|
||||||
|
return self.user_config_path.joinpath("shortcuts.vdf")
|
||||||
|
|
||||||
|
def add_shortcut(
|
||||||
|
self, appname: str, start_dir: Path, exe_path: Path, icon: Path
|
||||||
|
) -> None:
|
||||||
|
uid = self.gen_exe_uid(appname, exe_path)
|
||||||
|
self.bpid = self.gen_bpid(uid)
|
||||||
|
self.signed_appid = self.gen_signed_appid(self.bpid)
|
||||||
|
|
||||||
|
meta = ShortcutMetadata(
|
||||||
|
appname, self.signed_appid, str(exe_path), str(start_dir), str(icon)
|
||||||
|
)
|
||||||
|
entry = self._create_entry(meta)
|
||||||
|
self._insert_at_last_index(entry)
|
||||||
|
|
||||||
|
def add_grid_images(self, images: Path) -> None:
|
||||||
|
"""
|
||||||
|
{BPID}_hero.png: hero (splash)
|
||||||
|
{BPID}p.ping: portrait boxart in library
|
||||||
|
{BPID}_logo.png: logo overlay on top of hero
|
||||||
|
{BPID}.png: legacy Big Picture/landscape image
|
||||||
|
"""
|
||||||
|
|
||||||
|
grid_path = self.user_config_path.joinpath("grid")
|
||||||
|
for img in ("_hero", "p", "_logo"):
|
||||||
|
filename = str(self.bpid) + img
|
||||||
|
dest = grid_path.joinpath(filename).with_suffix(".png")
|
||||||
|
b = images.joinpath(img).with_suffix(".png").read_bytes()
|
||||||
|
dest.write_bytes(b)
|
||||||
|
|
||||||
|
# NOTE: legacy Big Picture header
|
||||||
|
b = images.joinpath("_hero.png").read_bytes()
|
||||||
|
dest = grid_path.joinpath(str(self.bpid)).with_suffix(".png")
|
||||||
|
dest.write_bytes(b)
|
||||||
|
|
||||||
|
def _insert_at_last_index(self, entry: dict[str, Any]) -> None:
|
||||||
|
try:
|
||||||
|
last = list(self.shortcuts["shortcuts"].keys())[-1]
|
||||||
|
n = int(last) + 1
|
||||||
|
except Exception:
|
||||||
|
n = 0
|
||||||
|
self.shortcuts["shortcuts"][str(n)] = entry
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_entry(cls, meta: ShortcutMetadata) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game
|
||||||
|
https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts
|
||||||
|
|
||||||
|
Key case is not internally consistent and varies between client versions
|
||||||
|
Most keys use Pascal case, but some do not
|
||||||
|
|
||||||
|
appid: signed int CRC
|
||||||
|
Exe: absolute path to the executable, must be wrapped in literal quotes
|
||||||
|
StartDir: directory the executable starts in, generally the parent
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEW_ENTRY: dict[str, Any] = {}
|
||||||
|
NEW_ENTRY["appid"] = meta.appid
|
||||||
|
NEW_ENTRY["AppName"] = meta.appname
|
||||||
|
NEW_ENTRY["Exe"] = f'"{meta.exe_path}"'
|
||||||
|
NEW_ENTRY["StartDir"] = f"{meta.start_dir}"
|
||||||
|
NEW_ENTRY["icon"] = meta.icon
|
||||||
|
NEW_ENTRY["ShortcutPath"] = ""
|
||||||
|
NEW_ENTRY["LaunchOptions"] = ""
|
||||||
|
NEW_ENTRY["IsHidden"] = 0
|
||||||
|
NEW_ENTRY["AllowDesktopConfig"] = 1
|
||||||
|
NEW_ENTRY["AllowOverlay"] = 1
|
||||||
|
NEW_ENTRY["OpenVR"] = 0
|
||||||
|
NEW_ENTRY["Devkit"] = 0
|
||||||
|
NEW_ENTRY["DevkitGameID"] = ""
|
||||||
|
NEW_ENTRY["DevkitOverrideAppID"] = 0
|
||||||
|
NEW_ENTRY["LastPlayTime"] = 0
|
||||||
|
NEW_ENTRY["FlatpakAppID"] = ""
|
||||||
|
NEW_ENTRY["sortas"] = ""
|
||||||
|
NEW_ENTRY["tags"] = {}
|
||||||
|
return NEW_ENTRY
|
||||||
|
|
||||||
|
def save_shortcuts(self) -> None:
|
||||||
|
try:
|
||||||
|
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||||
|
shutil.copy(self.shortcuts_path, backup)
|
||||||
|
with open(self.shortcuts_path, "wb") as f:
|
||||||
|
vdf.binary_dump(self.shortcuts, f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def add_steam_shortcut(steam_path: Path, exe_path: Path) -> None:
|
||||||
|
try:
|
||||||
|
start_dir = exe_path.parent
|
||||||
|
|
||||||
|
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
||||||
|
images = Path(str(traversable))
|
||||||
|
icon = images.joinpath("icon.png")
|
||||||
|
|
||||||
|
shortcuts = Shortcuts(Path(steam_path))
|
||||||
|
shortcuts.add_shortcut(APP_NAME, start_dir, exe_path, icon)
|
||||||
|
shortcuts.save_shortcuts()
|
||||||
|
shortcuts.add_grid_images(images)
|
||||||
|
|
||||||
|
copy_dzgui_to_xdg_data(exe_path)
|
||||||
|
logger.debug("Finished creating Steam shortcut")
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
+225
-67
@@ -1,34 +1,59 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import psutil
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
from typing import Union
|
import vdf # type: ignore
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Union
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
from shlex import shlex
|
from dzgui.api.acf import ACF
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.init.prereqs import has_steam_client
|
||||||
from dzgui.api.mods import _hash
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
|
APPID_DAYZ_EXP,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
|
DAYZ_BINARY,
|
||||||
DEBIAN_STEAM_PATH,
|
DEBIAN_STEAM_PATH,
|
||||||
DEFAULT_STEAM_PATH,
|
DEFAULT_STEAM_PATH,
|
||||||
FLATPAK_STEAM_PATH,
|
FLATPAK_STEAM_PATH,
|
||||||
|
LIBRARYFOLDERS_PATH,
|
||||||
UBUNTU_STEAM_PATH,
|
UBUNTU_STEAM_PATH,
|
||||||
REQUEST_TIMEOUT,
|
REQUEST_TIMEOUT,
|
||||||
VDF_PATH,
|
VDF_PATH,
|
||||||
)
|
)
|
||||||
from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_ENDPOINT
|
from dzgui.const.endpoints import (
|
||||||
|
APP_DETAILS,
|
||||||
|
SUB_ENDPOINT,
|
||||||
|
STEAM_PUBLISHED_FILES,
|
||||||
|
UNSUB_ENDPOINT,
|
||||||
|
)
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
from dzgui.util.bash import concat_bash_args
|
from dzgui.util.bash import concat_bash_args
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
class AppNotInstalledError(Exception):
|
||||||
|
"""App not present in user's libraryfolders"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppMovedError(Exception):
|
||||||
|
"""VDF points to a nonexistent location on disk"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class VDFLoadError(Exception):
|
||||||
|
"""Malformed VDF or JSON conversion"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_steam_paths() -> list[tuple[Path, str]]:
|
def get_steam_paths() -> list[tuple[Path, str]]:
|
||||||
paths = []
|
paths = []
|
||||||
if has_steam_client():
|
if has_steam_client():
|
||||||
@@ -58,6 +83,9 @@ def get_steam_paths() -> list[tuple[Path, str]]:
|
|||||||
|
|
||||||
|
|
||||||
def concat_mods(mods: list[str]) -> str:
|
def concat_mods(mods: list[str]) -> str:
|
||||||
|
# TODO: circular import workaround
|
||||||
|
from dzgui.api.mods import _hash
|
||||||
|
|
||||||
hashes = []
|
hashes = []
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
md5sum = _hash(mod)
|
md5sum = _hash(mod)
|
||||||
@@ -111,6 +139,9 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
|||||||
j = r.json()
|
j = r.json()
|
||||||
rows = j["response"]["publishedfiledetails"]
|
rows = j["response"]["publishedfiledetails"]
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
# NOTE: not a valid published file
|
||||||
|
if row["result"] == 9:
|
||||||
|
continue
|
||||||
title = str(row["title"])
|
title = str(row["title"])
|
||||||
_id = str(row["publishedfileid"])
|
_id = str(row["publishedfileid"])
|
||||||
time = int(row["time_updated"])
|
time = int(row["time_updated"])
|
||||||
@@ -170,64 +201,41 @@ def launch_offline(
|
|||||||
"-skipintro",
|
"-skipintro",
|
||||||
f"-name={name}",
|
f"-name={name}",
|
||||||
f"-mod={symlinks}",
|
f"-mod={symlinks}",
|
||||||
f"-mission={mission}",
|
|
||||||
]
|
]
|
||||||
|
if len(mission) > 0:
|
||||||
|
arg = f"-mission={mission}"
|
||||||
|
params.append(arg)
|
||||||
proc = subprocess.run([*client_args, *params])
|
proc = subprocess.run([*client_args, *params])
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def find_loginusers(path: Path) -> Path:
|
||||||
|
return path / "config" / "loginusers.vdf"
|
||||||
|
|
||||||
|
|
||||||
def find_user_id(path: Path) -> str | None:
|
def find_user_id(path: Path) -> str | None:
|
||||||
resolved_path = path / "config" / "loginusers.vdf"
|
resolved_path = find_loginusers(path)
|
||||||
try:
|
try:
|
||||||
vdf = vdf2json(resolved_path)
|
with open(resolved_path, "r") as f:
|
||||||
j = json.loads(vdf)
|
v = vdf.load(f)
|
||||||
for user in j["users"]:
|
# NOTE: beta client
|
||||||
if j["users"][user]["MostRecent"] == "1":
|
# /package/beta
|
||||||
|
if len(v["users"]) == 1:
|
||||||
|
return str(list(v["users"].keys())[0])
|
||||||
|
for user in v["users"]:
|
||||||
|
if v["users"][user]["MostRecent"] == "1":
|
||||||
return str(user)
|
return str(user)
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(e)
|
logger.debug(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def vdf2json(path: Path) -> str:
|
def find_user_id_32(path: Path) -> int:
|
||||||
def _istr(indent: int, string: str) -> str:
|
uid = find_user_id(path)
|
||||||
return (indent * " ") + string
|
if uid is None:
|
||||||
|
raise ValueError("Failed to parse a valid Steam32 ID")
|
||||||
jbuf = "{\n"
|
return int(uid) & 0xFFFFFFFF
|
||||||
indent = 1
|
|
||||||
|
|
||||||
with open(path, "r") as f:
|
|
||||||
st = f.read()
|
|
||||||
lex = shlex(st)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
tok = lex.get_token()
|
|
||||||
if not tok:
|
|
||||||
return jbuf + "}\n"
|
|
||||||
if tok == "}":
|
|
||||||
indent -= 1
|
|
||||||
jbuf += _istr(indent, "}")
|
|
||||||
ntok = lex.get_token()
|
|
||||||
if ntok is not None:
|
|
||||||
lex.push_token(ntok)
|
|
||||||
if ntok and ntok != "}":
|
|
||||||
jbuf += ","
|
|
||||||
jbuf += "\n"
|
|
||||||
else:
|
|
||||||
ntok = lex.get_token()
|
|
||||||
if ntok == "{":
|
|
||||||
jbuf += _istr(indent, tok + ": {\n")
|
|
||||||
indent += 1
|
|
||||||
else:
|
|
||||||
if ntok is not None:
|
|
||||||
jbuf += _istr(indent, tok + ": " + ntok)
|
|
||||||
ntok = lex.get_token()
|
|
||||||
if ntok is not None:
|
|
||||||
lex.push_token(ntok)
|
|
||||||
if ntok != "}":
|
|
||||||
jbuf += ","
|
|
||||||
jbuf += "\n"
|
|
||||||
|
|
||||||
|
|
||||||
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
||||||
@@ -253,21 +261,171 @@ def unsubscribe(key: str, mod: int) -> None:
|
|||||||
update_workshop(key, mod, UNSUB_ENDPOINT)
|
update_workshop(key, mod, UNSUB_ENDPOINT)
|
||||||
|
|
||||||
|
|
||||||
def gen_shortcut() -> None:
|
|
||||||
# TODO:
|
|
||||||
"""
|
|
||||||
during setup, prompt user to select matching user id from loginusers
|
|
||||||
show user account name, select outer steam id
|
|
||||||
steam/userdata/<steamid_32>/config/shortcuts.vdf
|
|
||||||
"""
|
|
||||||
# STEAMID_MAGIC = 76561197960265728
|
|
||||||
# STEAMID_64 - STEAMID_MAGIC = STEAMID32
|
|
||||||
# or get right-most 32 bits
|
|
||||||
# STEAMID_64 & 0xFFFFFFFF
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated("Use subscribe()")
|
@deprecated("Use subscribe()")
|
||||||
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
|
||||||
|
def get_registry() -> Any | None:
|
||||||
|
home = os.getenv("HOME")
|
||||||
|
try:
|
||||||
|
with open(f"{home}/.steam/registry.vdf") as f:
|
||||||
|
registry = vdf.load(f)
|
||||||
|
return registry
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dayz_running() -> bool:
|
||||||
|
registry = get_registry()
|
||||||
|
if registry is None:
|
||||||
|
return False
|
||||||
|
apps = registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"]["apps"].items()
|
||||||
|
for app in apps:
|
||||||
|
k, v = app
|
||||||
|
if k in (APPID_DAYZ, APPID_DAYZ_EXP):
|
||||||
|
try:
|
||||||
|
state = v["Running"]
|
||||||
|
# NOTE: 0 denotes False
|
||||||
|
return bool(int(state))
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _get_running_app() -> int | None:
|
||||||
|
registry = get_registry()
|
||||||
|
if registry is None:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return int(
|
||||||
|
registry["Registry"]["HKCU"]["Software"]["Valve"]["Steam"]["RunningAppID"]
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_running_app() -> int | None:
|
||||||
|
PROC_NAME = "steam"
|
||||||
|
SUBPROC_NAME = "reaper"
|
||||||
|
FLAG = "AppId"
|
||||||
|
# NOTE: may cause conflicts if multiple apps are running
|
||||||
|
try:
|
||||||
|
for proc in psutil.process_iter():
|
||||||
|
if proc.name() == PROC_NAME:
|
||||||
|
subprocs = proc.children()
|
||||||
|
filtered = (proc for proc in subprocs if proc.name() == SUBPROC_NAME)
|
||||||
|
try:
|
||||||
|
proc = next(filtered)
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
args = proc.cmdline()
|
||||||
|
appid = (row for row in args if FLAG in row)
|
||||||
|
try:
|
||||||
|
return int(next(appid).split("=")[1])
|
||||||
|
except StopIteration:
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
||||||
|
try:
|
||||||
|
root_path = get_app_path(path, appid)
|
||||||
|
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||||
|
flag = ACF(acf).get_allows_downloads()
|
||||||
|
except AppNotInstalledError:
|
||||||
|
flag = 0
|
||||||
|
match flag:
|
||||||
|
# NOTE: adheres to global client setting
|
||||||
|
case 0:
|
||||||
|
return get_client_allows_downloads(path)
|
||||||
|
# NOTE: always allow
|
||||||
|
case 1:
|
||||||
|
return True
|
||||||
|
# NOTE: never allow
|
||||||
|
case 2:
|
||||||
|
return False
|
||||||
|
# NOTE: no other known values at this time, fallback (assume permits)
|
||||||
|
case _:
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_config(path: Path) -> Path:
|
||||||
|
return path.joinpath("config/config.vdf")
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_allows_downloads(path: Path) -> bool:
|
||||||
|
config = get_config(path)
|
||||||
|
try:
|
||||||
|
with open(config) as f:
|
||||||
|
settings = vdf.load(f)
|
||||||
|
# NOTE: "1" denotes "allow"
|
||||||
|
allow = bool(
|
||||||
|
int(
|
||||||
|
settings["InstallConfigStore"]["Software"]["Valve"]["Steam"][
|
||||||
|
"AllowDownloadsDuringGameplay"
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return allow
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def is_dayz_running() -> bool:
|
||||||
|
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
||||||
|
procs = []
|
||||||
|
substring = DAYZ_BINARY
|
||||||
|
for proc in psutil.process_iter():
|
||||||
|
try:
|
||||||
|
procs.append(proc.cmdline())
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
continue
|
||||||
|
return any(substring in item for sublist in procs for item in sublist)
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_path(folders_path: Path, appid: int) -> Path:
|
||||||
|
app_path = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(folders_path / LIBRARYFOLDERS_PATH) as f:
|
||||||
|
folders = vdf.load(f)
|
||||||
|
except Exception:
|
||||||
|
raise VDFLoadError("Failed to parse libraryfolders")
|
||||||
|
|
||||||
|
for obj in folders["libraryfolders"]:
|
||||||
|
if str(appid) in folders["libraryfolders"][obj]["apps"]:
|
||||||
|
app_path = folders["libraryfolders"][obj]["path"]
|
||||||
|
if Path(app_path).exists():
|
||||||
|
break
|
||||||
|
|
||||||
|
if app_path is None:
|
||||||
|
raise AppNotInstalledError(
|
||||||
|
f"Failed to find a libraryfolder for the appid {appid}"
|
||||||
|
)
|
||||||
|
if Path(app_path).exists() is False:
|
||||||
|
raise AppMovedError(
|
||||||
|
f"The location '{app_path}' pointed to by '{appid}' no longer exists and may have been changed on the disk."
|
||||||
|
)
|
||||||
|
|
||||||
|
return Path(app_path)
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_name(appid: int) -> str | None:
|
||||||
|
payload = {"appids": [appid]}
|
||||||
|
res = requests.get(APP_DETAILS, params=payload)
|
||||||
|
if res.status_code != 200:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return str(res.json()[str(appid)]["data"]["name"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return None
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
|
from dzgui.const.enum import Preferences
|
||||||
|
from dzgui.config.query import lookup
|
||||||
|
from dzgui.config.userprefs import UserPrefs
|
||||||
|
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||||
|
from dzgui.init.migrate import (
|
||||||
|
has_new_config,
|
||||||
|
copy_state_files,
|
||||||
|
migrate_cols_file,
|
||||||
|
)
|
||||||
|
from dzgui.init.flock import lock_acquire
|
||||||
|
from dzgui.init.prereqs import has_steam_client
|
||||||
|
from dzgui.strings import boot
|
||||||
|
|
||||||
|
# from dzgui.util.map_count import get_map_count
|
||||||
|
from dzgui.util.deck import is_steam_deck, is_game_mode
|
||||||
|
from dzgui.util.dirs import make_parents
|
||||||
|
from dzgui.util.localize import set_locale
|
||||||
|
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||||
|
from dzgui.util.strings import init
|
||||||
|
|
||||||
|
from dzgui.views.base import App
|
||||||
|
from dzgui.views.dialogs.boot import BootWindow
|
||||||
|
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
||||||
|
from dzgui.views.dialogs.wizard import SetupWizard
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
# TODO: profile load time
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logger(log_path: "Path") -> None:
|
||||||
|
# TODO: put in consts?
|
||||||
|
_format = (
|
||||||
|
"%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s"
|
||||||
|
)
|
||||||
|
|
||||||
|
fh = logging.FileHandler(log_path)
|
||||||
|
formatter = logging.Formatter(_format)
|
||||||
|
fh.setFormatter(formatter)
|
||||||
|
fh.setLevel(logging.DEBUG)
|
||||||
|
|
||||||
|
_filter = RedactionFilter(patterns=REDACTION_PATTERNS)
|
||||||
|
fh.addFilter(_filter)
|
||||||
|
|
||||||
|
logger.setLevel(logging.DEBUG)
|
||||||
|
logger.addHandler(fh)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
|
||||||
|
# NOTE: temporary workaround for #375
|
||||||
|
conf = "config.json"
|
||||||
|
state = [
|
||||||
|
"dzg.history",
|
||||||
|
"dzg.versions",
|
||||||
|
"dzg.res.json",
|
||||||
|
"dzg.filters.json",
|
||||||
|
"dzg.columns.json",
|
||||||
|
"dzg.notes.json",
|
||||||
|
"ips.csv",
|
||||||
|
".month",
|
||||||
|
]
|
||||||
|
config_changed = False
|
||||||
|
state_changed = False
|
||||||
|
if config.parent.exists() is False:
|
||||||
|
new_file = config
|
||||||
|
old_file = config.parent.parent / conf
|
||||||
|
if old_file.is_file():
|
||||||
|
try:
|
||||||
|
make_parents(new_file)
|
||||||
|
shutil.copy(old_file, new_file)
|
||||||
|
config_changed = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
if resolution.parent.exists() is False:
|
||||||
|
state_path = resolution.parent
|
||||||
|
for state_file in state:
|
||||||
|
old_file = state_path.parent / state_file
|
||||||
|
if old_file.is_file():
|
||||||
|
try:
|
||||||
|
new_file = state_path / state_file
|
||||||
|
make_parents(new_file)
|
||||||
|
shutil.copy(old_file, new_file)
|
||||||
|
state_changed = True
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return (config_changed, state_changed)
|
||||||
|
|
||||||
|
|
||||||
|
def load_gui(version: str, is_debug: bool) -> None:
|
||||||
|
lock = lock_acquire() # noqa
|
||||||
|
|
||||||
|
set_locale()
|
||||||
|
# NOTE: consider aborting this check if steam deck
|
||||||
|
xdg_paths = get_xdg_paths()
|
||||||
|
XDG = parse_filepaths(xdg_paths)
|
||||||
|
|
||||||
|
copy_bare_configs(XDG.config, XDG.resolution)
|
||||||
|
|
||||||
|
if XDG.resolution.parent.is_dir() is False:
|
||||||
|
make_parents(XDG.resolution)
|
||||||
|
|
||||||
|
if XDG.debug.is_file() is False:
|
||||||
|
make_parents(XDG.debug)
|
||||||
|
|
||||||
|
_is_steam_deck = is_steam_deck()
|
||||||
|
_is_game_mode = is_game_mode() if _is_steam_deck else False
|
||||||
|
if _is_game_mode:
|
||||||
|
# NOTE: this may no longer be necessary on newer versions of SteamOS
|
||||||
|
del os.environ["GTK_IM_MODULE"]
|
||||||
|
|
||||||
|
if has_new_config(XDG.config) is False:
|
||||||
|
migrate_cols_file(XDG.columns)
|
||||||
|
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
||||||
|
# TODO: add logging inside wizard
|
||||||
|
wizard = SetupWizard(_is_steam_deck, XDG)
|
||||||
|
|
||||||
|
if not wizard.is_setup_complete():
|
||||||
|
return
|
||||||
|
|
||||||
|
setup_logger(XDG.debug)
|
||||||
|
with open(XDG.debug, "w") as f:
|
||||||
|
f.truncate(0)
|
||||||
|
|
||||||
|
if _is_steam_deck is False:
|
||||||
|
# TODO: sudo escalation dialog
|
||||||
|
# count = get_map_count()
|
||||||
|
# TODO: move into module
|
||||||
|
if has_steam_client() is False:
|
||||||
|
EarlyAlertDialog(init.requires_steam)
|
||||||
|
|
||||||
|
bootwin = BootWindow(XDG, version)
|
||||||
|
local_coords, latest_release = bootwin.get_results()
|
||||||
|
|
||||||
|
use_miles = lookup(XDG.config, Preferences.DIST)
|
||||||
|
prefs = UserPrefs(
|
||||||
|
is_steam_deck=_is_steam_deck,
|
||||||
|
is_game_mode=_is_game_mode,
|
||||||
|
is_debug=is_debug,
|
||||||
|
coords=local_coords,
|
||||||
|
version=version,
|
||||||
|
paths=XDG,
|
||||||
|
latest_release=latest_release,
|
||||||
|
use_miles=use_miles,
|
||||||
|
)
|
||||||
|
print(boot.all_ok)
|
||||||
|
App(prefs)
|
||||||
@@ -3,6 +3,8 @@ import shlex
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from dzgui.const.boilerplate import config_boilerplate
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Convert legacy dztuirc to config.json
|
Convert legacy dztuirc to config.json
|
||||||
@@ -40,6 +42,7 @@ def rc2json(file: Path) -> str:
|
|||||||
|
|
||||||
toggles = ["fullscreen"]
|
toggles = ["fullscreen"]
|
||||||
deprecated = [
|
deprecated = [
|
||||||
|
"api_key",
|
||||||
"staging_dir",
|
"staging_dir",
|
||||||
"src_path",
|
"src_path",
|
||||||
"steam_path",
|
"steam_path",
|
||||||
@@ -64,8 +67,6 @@ def rc2json(file: Path) -> str:
|
|||||||
value = str2bool(ntok)
|
value = str2bool(ntok)
|
||||||
elif tok == "preferred_client":
|
elif tok == "preferred_client":
|
||||||
tok = "client"
|
tok = "client"
|
||||||
elif tok == "api_key":
|
|
||||||
tok = "bm_api"
|
|
||||||
elif tok == "ip_list":
|
elif tok == "ip_list":
|
||||||
while True:
|
while True:
|
||||||
ntok = lex.get_token()
|
ntok = lex.get_token()
|
||||||
@@ -89,4 +90,8 @@ def rc2json(file: Path) -> str:
|
|||||||
keys["ip_list"] = ips
|
keys["ip_list"] = ips
|
||||||
keys["use_miles"] = False
|
keys["use_miles"] = False
|
||||||
keys["start_tab"] = 0
|
keys["start_tab"] = 0
|
||||||
|
# NOTE: workaround for DZGUI 6 logic (see #375)
|
||||||
|
for key in config_boilerplate.keys():
|
||||||
|
if key not in keys:
|
||||||
|
keys[key] = config_boilerplate[key]
|
||||||
return json.dumps(keys, indent=2)
|
return json.dumps(keys, indent=2)
|
||||||
@@ -1,18 +1,43 @@
|
|||||||
|
import stat
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
|
||||||
|
|
||||||
|
|
||||||
|
def get_share_path(exe_path: Path) -> Path:
|
||||||
|
return exe_path.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def write_desktop_file(exe_path: Path) -> Path:
|
||||||
|
icon = find_icon_resource()
|
||||||
|
|
||||||
def write_desktop_file(share_path: Path) -> None:
|
|
||||||
template = f"""\
|
template = f"""\
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Type=Application
|
Type=Application
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Exec={share_path}/launch.sh
|
Exec={exe_path}
|
||||||
Name=DZGUI
|
Name=DZGUI
|
||||||
Comment=dzgui
|
Comment=DayZ server browser and mod manager
|
||||||
Icon={share_path}/dzgui
|
Icon={icon}
|
||||||
Categories=Game"""
|
Categories=Game"""
|
||||||
|
|
||||||
file = share_path / "dzgui.desktop"
|
copy_dzgui_to_xdg_data(exe_path)
|
||||||
file.write_text(textwrap.dedent(template))
|
|
||||||
|
share_path = get_share_path(exe_path)
|
||||||
|
desktop_file = share_path.joinpath("applications/dzgui.desktop")
|
||||||
|
make_parents(desktop_file)
|
||||||
|
desktop_file.write_text(textwrap.dedent(template))
|
||||||
|
desktop_file.chmod(desktop_file.stat().st_mode | stat.S_IEXEC)
|
||||||
|
return desktop_file
|
||||||
|
|
||||||
|
|
||||||
|
def write_desktop_shortcut(desktop_file: Path) -> None:
|
||||||
|
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
|
||||||
|
link = Path.home().joinpath("Desktop/dzgui.desktop")
|
||||||
|
make_parents(link)
|
||||||
|
if link.exists():
|
||||||
|
link.unlink()
|
||||||
|
link.symlink_to(desktop_file)
|
||||||
+12
-6
@@ -18,6 +18,7 @@ class Xdg:
|
|||||||
debug: Path
|
debug: Path
|
||||||
ips: Path
|
ips: Path
|
||||||
filters: Path
|
filters: Path
|
||||||
|
shortcut: Path
|
||||||
|
|
||||||
|
|
||||||
def is_writeable(path_str: str) -> bool:
|
def is_writeable(path_str: str) -> bool:
|
||||||
@@ -29,9 +30,9 @@ def is_writeable(path_str: str) -> bool:
|
|||||||
if not path.exists():
|
if not path.exists():
|
||||||
try:
|
try:
|
||||||
path.mkdir(parents=True)
|
path.mkdir(parents=True)
|
||||||
|
path.unlink()
|
||||||
except OSError:
|
except OSError:
|
||||||
return False
|
return False
|
||||||
path.unlink()
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -55,17 +56,19 @@ def get_xdg_paths() -> dict:
|
|||||||
|
|
||||||
resolved_paths = {}
|
resolved_paths = {}
|
||||||
for path in xdg_paths:
|
for path in xdg_paths:
|
||||||
rp = os.environ.get(path)
|
real_path = os.environ.get(path)
|
||||||
if rp is not None and is_writeable(rp):
|
if real_path is not None and is_writeable(real_path):
|
||||||
resolved_paths[path] = Path(rp)
|
new_path = Path(real_path)
|
||||||
else:
|
else:
|
||||||
resolved_paths[path] = xdg_paths[path] / APP_NAME_LOWER
|
new_path = xdg_paths[path]
|
||||||
|
resolved_paths[path] = new_path / APP_NAME_LOWER
|
||||||
return resolved_paths
|
return resolved_paths
|
||||||
|
|
||||||
|
|
||||||
def parse_filepaths(xdg: dict) -> Xdg:
|
def parse_filepaths(xdg: dict) -> Xdg:
|
||||||
config = xdg["XDG_CONFIG_HOME"]
|
config = xdg["XDG_CONFIG_HOME"]
|
||||||
state = xdg["XDG_STATE_HOME"]
|
state = xdg["XDG_STATE_HOME"]
|
||||||
|
share = xdg["XDG_DATA_HOME"]
|
||||||
|
|
||||||
config = config / "config.json"
|
config = config / "config.json"
|
||||||
|
|
||||||
@@ -80,6 +83,8 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
ips = state / "ips.csv"
|
ips = state / "ips.csv"
|
||||||
filters = state / "dzg.filters.json"
|
filters = state / "dzg.filters.json"
|
||||||
|
|
||||||
|
shortcut = share / APP_NAME_LOWER
|
||||||
|
|
||||||
return Xdg(
|
return Xdg(
|
||||||
config,
|
config,
|
||||||
columns,
|
columns,
|
||||||
@@ -90,5 +95,6 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
system,
|
system,
|
||||||
debug,
|
debug,
|
||||||
ips,
|
ips,
|
||||||
filters
|
filters,
|
||||||
|
shortcut,
|
||||||
)
|
)
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
Generic defaults used when initializing a config file from scratch
|
Generic defaults used when initializing a config file from scratch
|
||||||
"""
|
"""
|
||||||
config_boilerplate = {
|
config_boilerplate = {
|
||||||
"bm_api": "",
|
|
||||||
"fav_server": "",
|
"fav_server": "",
|
||||||
"fav_label": "",
|
"fav_label": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ UDP_PORT = 27016
|
|||||||
VM_FILE = "/proc/sys/vm/max_map_count"
|
VM_FILE = "/proc/sys/vm/max_map_count"
|
||||||
MIN_COUNT = 1048576
|
MIN_COUNT = 1048576
|
||||||
|
|
||||||
RATE_LIMIT_THRESHOLD = 3
|
API_RATE_LIMIT = 1
|
||||||
|
CLIENT_RATE_LIMIT = 3
|
||||||
REQUEST_TIMEOUT = 10
|
REQUEST_TIMEOUT = 10
|
||||||
|
|
||||||
APPNAME_DAYZ = "DayZ"
|
APPNAME_DAYZ = "DayZ"
|
||||||
@@ -37,6 +38,7 @@ HEX_ORANGE = "#FFAC1C"
|
|||||||
CARET_DOWN = "go-down-symbolic"
|
CARET_DOWN = "go-down-symbolic"
|
||||||
CARET_UP = "go-up-symbolic"
|
CARET_UP = "go-up-symbolic"
|
||||||
CLIPBOARD = "edit-copy-symbolic"
|
CLIPBOARD = "edit-copy-symbolic"
|
||||||
|
CLOSE = "window-close-symbolic"
|
||||||
FOLDER = "folder-symbolic"
|
FOLDER = "folder-symbolic"
|
||||||
EDIT_DELETE = "edit-delete-symbolic"
|
EDIT_DELETE = "edit-delete-symbolic"
|
||||||
ERROR = "dialog-error-symbolic"
|
ERROR = "dialog-error-symbolic"
|
||||||
@@ -71,7 +73,9 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv"
|
|||||||
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
||||||
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
||||||
CHANGELOG_PATH = "data/CHANGELOG.md"
|
CHANGELOG_PATH = "data/CHANGELOG.md"
|
||||||
HERO_PATH = "data/images/hero.png"
|
IMAGES_PATH = "data/images"
|
||||||
|
|
||||||
|
HERO_PATH = "data/images/_hero.png"
|
||||||
CSS_PATH = "data/app.css"
|
CSS_PATH = "data/app.css"
|
||||||
VDF_PATH = "steamapps/libraryfolders.vdf"
|
VDF_PATH = "steamapps/libraryfolders.vdf"
|
||||||
DEFAULT_STEAM_PATH = ".local/share/Steam"
|
DEFAULT_STEAM_PATH = ".local/share/Steam"
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ STEAM_PUBLISHED_FILES = (
|
|||||||
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
|
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
|
||||||
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
||||||
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
||||||
|
APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
|
||||||
|
|
||||||
BM_SERVERS = "https://api.battlemetrics.com/servers?"
|
|
||||||
GITHUB = "https://github.com/aclist"
|
GITHUB = "https://github.com/aclist"
|
||||||
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
||||||
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
|
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
|
||||||
@@ -26,5 +26,3 @@ GITHUB_ISSUES = "https://github.com/aclist/dzgui/issues"
|
|||||||
FORUM = "https://old.reddit.com/r/dzgui"
|
FORUM = "https://old.reddit.com/r/dzgui"
|
||||||
SPONSORS = "https://github.com/sponsors/aclist"
|
SPONSORS = "https://github.com/sponsors/aclist"
|
||||||
STEAM_API_SETUP = "https://steamcommunity.com/dev/apikey"
|
STEAM_API_SETUP = "https://steamcommunity.com/dev/apikey"
|
||||||
BM_API_SETUP = "https://www.battlemetrics.com/developers"
|
|
||||||
BM_BROWSE = "https://www.battlemetrics.com/servers/dayz"
|
|
||||||
@@ -61,9 +61,6 @@ class Preferences(EnumWithAttrs):
|
|||||||
STEAM = {
|
STEAM = {
|
||||||
"key": "steam_api",
|
"key": "steam_api",
|
||||||
}
|
}
|
||||||
BM = {
|
|
||||||
"key": "bm_api",
|
|
||||||
}
|
|
||||||
CLIENT = {
|
CLIENT = {
|
||||||
"key": "client",
|
"key": "client",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,8 +81,8 @@ class Emitter(GObject.GObject):
|
|||||||
def distcalc_started(self) -> None:
|
def distcalc_started(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,))
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool, int))
|
||||||
def mod_page_toggled(self, state: bool) -> None:
|
def mod_page_toggled(self, state: bool, mods: int) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,))
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,))
|
||||||
@@ -172,3 +172,7 @@ class Emitter(GObject.GObject):
|
|||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
def invalid_custom_mods(self) -> None:
|
def invalid_custom_mods(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
|
def all_mods_synched(self) -> None:
|
||||||
|
pass
|
||||||
+13
-9
@@ -23,7 +23,7 @@ from dzgui.managers.contextmenu import ContextMenuManager
|
|||||||
from dzgui.managers.notes import NoteManager
|
from dzgui.managers.notes import NoteManager
|
||||||
from dzgui.model.servers import ServerModelManager
|
from dzgui.model.servers import ServerModelManager
|
||||||
from dzgui.util.diag import write_diagnostic
|
from dzgui.util.diag import write_diagnostic
|
||||||
from dzgui.util.format import format_player_count
|
from dzgui.util.format import format_exception, format_player_count
|
||||||
from dzgui.util.open_links import open_user_workshop, open_workshop_page
|
from dzgui.util.open_links import open_user_workshop, open_workshop_page
|
||||||
from dzgui.views.dialogs.filepicker import FilePicker
|
from dzgui.views.dialogs.filepicker import FilePicker
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
@@ -203,7 +203,9 @@ class Controller(GObject.GObject):
|
|||||||
try:
|
try:
|
||||||
# TODO: where to put config file check
|
# TODO: where to put config file check
|
||||||
self.mediator.grid.notebook.settings.populate_settings()
|
self.mediator.grid.notebook.settings.populate_settings()
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
msg = format_exception(e)
|
||||||
|
logger.critical(msg)
|
||||||
return
|
return
|
||||||
case ButtonType.MODS:
|
case ButtonType.MODS:
|
||||||
self.load_mods()
|
self.load_mods()
|
||||||
@@ -400,6 +402,9 @@ class Controller(GObject.GObject):
|
|||||||
def get_menu(self) -> "MenuTreeView":
|
def get_menu(self) -> "MenuTreeView":
|
||||||
return self.mediator.menu
|
return self.mediator.menu
|
||||||
|
|
||||||
|
def unset_fav(self) -> None:
|
||||||
|
self.config_man.unset_fav()
|
||||||
|
|
||||||
def has_favorites(self) -> bool:
|
def has_favorites(self) -> bool:
|
||||||
favs = self.config_man.get_favorites()
|
favs = self.config_man.get_favorites()
|
||||||
if len(favs) < 1:
|
if len(favs) < 1:
|
||||||
@@ -411,7 +416,7 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
def add_by_str(self, addr: str) -> None:
|
def add_by_str(self, addr: str) -> None:
|
||||||
saved_tree = self.get_servers().get_saved()
|
saved_tree = self.get_servers().get_saved()
|
||||||
ServerModelManager(self, saved_tree).add_by_str(addr)
|
ServerModelManager(self, saved_tree).add_by_ip(addr)
|
||||||
|
|
||||||
def add_by_record(self, record: "Record") -> None:
|
def add_by_record(self, record: "Record") -> None:
|
||||||
saved_tree = self.get_servers().get_saved()
|
saved_tree = self.get_servers().get_saved()
|
||||||
@@ -435,12 +440,6 @@ class Controller(GObject.GObject):
|
|||||||
ServerModelManager(self, tv).remove_from_history(record)
|
ServerModelManager(self, tv).remove_from_history(record)
|
||||||
|
|
||||||
def connect_by_str(self, addr: str) -> None:
|
def connect_by_str(self, addr: str) -> None:
|
||||||
if addr.isdigit():
|
|
||||||
config_man = self.get_config_man()
|
|
||||||
key = config_man.lookup(Preferences.BM)
|
|
||||||
self.connection_man = ConnectionManager(self)
|
|
||||||
self.connection_man.connect_by_id(int(addr), key)
|
|
||||||
else:
|
|
||||||
self.connection_man = ConnectionManager(self)
|
self.connection_man = ConnectionManager(self)
|
||||||
self.connection_man.connect_by_ip(addr)
|
self.connection_man.connect_by_ip(addr)
|
||||||
|
|
||||||
@@ -463,6 +462,9 @@ class Controller(GObject.GObject):
|
|||||||
open_workshop_page(mod, cmd)
|
open_workshop_page(mod, cmd)
|
||||||
|
|
||||||
def has_note(self) -> bool:
|
def has_note(self) -> bool:
|
||||||
|
# TODO: get record string, store in memory
|
||||||
|
# get note by record, get is in favs, etc. store as a block
|
||||||
|
# this is only used by context mixin
|
||||||
note = self.get_note()
|
note = self.get_note()
|
||||||
if len(note) > 0:
|
if len(note) > 0:
|
||||||
return True
|
return True
|
||||||
@@ -477,11 +479,13 @@ class Controller(GObject.GObject):
|
|||||||
return self.notes_man.get_note(record)
|
return self.notes_man.get_note(record)
|
||||||
|
|
||||||
def add_note(self, note: str) -> None:
|
def add_note(self, note: str) -> None:
|
||||||
|
# TODO: record should be cached upon creation of dialog
|
||||||
tv = self.get_active_treeview()
|
tv = self.get_active_treeview()
|
||||||
record = tv.get_record_string()
|
record = tv.get_record_string()
|
||||||
self.notes_man.add_note(record, note)
|
self.notes_man.add_note(record, note)
|
||||||
|
|
||||||
def delete_note(self) -> None:
|
def delete_note(self) -> None:
|
||||||
|
# TODO: record should be cached upon creation of dialog
|
||||||
tv = self.get_active_treeview()
|
tv = self.get_active_treeview()
|
||||||
record = tv.get_record_string()
|
record = tv.get_record_string()
|
||||||
self.notes_man.delete_note(record)
|
self.notes_man.delete_note(record)
|
||||||
|
|||||||
+31
-28
@@ -1,6 +1,7 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Added
|
## [7.0.0] Unreleased
|
||||||
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
@@ -41,8 +42,10 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
|
- Generate Steam shortcuts and cover art
|
||||||
|
- Warn user if background downloads are disabled
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
@@ -59,58 +62,58 @@
|
|||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
- Propagate subscribed mods to Steam client
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Debug mode
|
- Debug mode
|
||||||
- Branch switching
|
- Branch switching
|
||||||
- Manual mod install mode (describe rationale)
|
- Manual mod install mode (describe rationale)
|
||||||
- Force update mods
|
- Force update mods
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
- Center server title text on server dialogs
|
- Center server title text on server dialogs
|
||||||
- Rare segfaults when changing maps (threading)
|
- Rare segfaults when changing maps (threading)
|
||||||
- Moved dialogs out of threads
|
- Moved dialogs out of threads
|
||||||
|
|
||||||
## Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
## Developer-facing
|
### Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
## Changed
|
### Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
## Fixed
|
### Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
## Fixed
|
### Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
## Changed
|
### Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
## Added
|
### Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@@ -136,7 +139,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@@ -175,7 +178,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@@ -187,50 +190,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
### Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
## Added
|
### Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
## Changed
|
### Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
## Fixed
|
### Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
## Added
|
### Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
## Fixed
|
### Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
## Changed
|
### Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
## Dropped
|
### Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
## Changed
|
### Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
+3
-3
@@ -2,11 +2,11 @@ import logging
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.api.steam import get_app_path
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
from dzgui.const.constants import APPID_DAYZ, APP_NAME, LIBRARYFOLDERS_PATH
|
from dzgui.const.constants import APPID_DAYZ, APP_NAME
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
def is_dayz_installed(config: Path) -> None:
|
def is_dayz_installed(config: Path) -> None:
|
||||||
try:
|
try:
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
PeFile.get_app_path(Path(path) / LIBRARYFOLDERS_PATH, APPID_DAYZ)
|
get_app_path(Path(path), APPID_DAYZ)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
raise e
|
raise e
|
||||||
+4
-5
@@ -1,11 +1,12 @@
|
|||||||
import platform
|
import platform
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from pathlib import Path
|
from ctypes.util import find_library
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
LIB = "libgirepository-2.0"
|
LIB = "libgirepository-2.0"
|
||||||
|
|
||||||
|
|
||||||
@deprecated("currently unused")
|
@deprecated("currently unused")
|
||||||
def is_debian() -> bool:
|
def is_debian() -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -39,12 +40,10 @@ def is_debian() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def has_libgi() -> bool:
|
def has_libgi() -> bool:
|
||||||
parent_dir = Path("/usr/lib64")
|
return bool(find_library("girepository-2.0"))
|
||||||
exists = any(LIB in subdir.name for subdir in parent_dir.iterdir())
|
|
||||||
return exists
|
|
||||||
|
|
||||||
|
|
||||||
def test_missing_lib() -> bool:
|
def test_libgi_missing() -> None:
|
||||||
msg = f"System is missing the required library '{LIB}'. Install it via your system package manager."
|
msg = f"System is missing the required library '{LIB}'. Install it via your system package manager."
|
||||||
if has_libgi() is False:
|
if has_libgi() is False:
|
||||||
print(msg)
|
print(msg)
|
||||||
|
|||||||
+3
-37
@@ -1,11 +1,10 @@
|
|||||||
import psutil
|
import psutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
|
import logging
|
||||||
from warnings import deprecated
|
|
||||||
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
DAYZ_BINARY,
|
APP_NAME,
|
||||||
STEAM_CMD,
|
STEAM_CMD,
|
||||||
FLATPAK_APPID,
|
FLATPAK_APPID,
|
||||||
FLATPAK_CMD,
|
FLATPAK_CMD,
|
||||||
@@ -13,18 +12,7 @@ from dzgui.const.constants import (
|
|||||||
FLATPAK_SANDBOX,
|
FLATPAK_SANDBOX,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
# TODO: move to util.proc
|
|
||||||
def is_dayz_running() -> bool:
|
|
||||||
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
|
||||||
procs = []
|
|
||||||
substring = DAYZ_BINARY
|
|
||||||
for proc in psutil.process_iter():
|
|
||||||
try:
|
|
||||||
procs.append(proc.cmdline())
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
return any(substring in item for sublist in procs for item in sublist)
|
|
||||||
|
|
||||||
|
|
||||||
def is_steam_running(cmd: str) -> bool:
|
def is_steam_running(cmd: str) -> bool:
|
||||||
@@ -61,25 +49,3 @@ def has_cmd(cmd: str) -> bool:
|
|||||||
if shutil.which(cmd) is not None:
|
if shutil.which(cmd) is not None:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@deprecated("dropped in favor of Gtk native methods")
|
|
||||||
def foreground(cmd: str, pid: int) -> None:
|
|
||||||
if cmd == "wmctrl":
|
|
||||||
proc = subprocess.run(["wmctrl", "-ilp"], capture_output=True, text=True)
|
|
||||||
lines = proc.stdout.splitlines()
|
|
||||||
for line in lines:
|
|
||||||
els = line.split(" ")
|
|
||||||
if str(pid) in els:
|
|
||||||
wid = els[0]
|
|
||||||
break
|
|
||||||
subprocess.run(["wmctrl", "-ia", wid])
|
|
||||||
elif cmd == "xdotool":
|
|
||||||
args = [cmd, "search", "--pid", str(pid)]
|
|
||||||
proc = subprocess.run([*args], capture_output=True, text=True)
|
|
||||||
lines = proc.stdout.splitlines()
|
|
||||||
## NOTE: some forked subprocesses may fail, so skip over them
|
|
||||||
for line in lines:
|
|
||||||
subprocess.run(
|
|
||||||
["xdotool", "windowactivate", line], stderr=subprocess.DEVNULL
|
|
||||||
)
|
|
||||||
+6
-103
@@ -1,40 +1,11 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import APP_NAME
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.init.libgi import test_libgi_missing
|
||||||
from dzgui.config.query import lookup
|
|
||||||
from dzgui.config.userprefs import UserPrefs
|
|
||||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
|
||||||
from dzgui.init.flock import lock_acquire
|
|
||||||
from dzgui.init.migrate import (
|
|
||||||
has_new_config,
|
|
||||||
copy_state_files,
|
|
||||||
migrate_cols_file,
|
|
||||||
)
|
|
||||||
from dzgui.init.prefix import get_version
|
from dzgui.init.prefix import get_version
|
||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.util.strings import flags
|
||||||
from dzgui.strings import boot
|
|
||||||
|
|
||||||
# from dzgui.util.map_count import get_map_count
|
|
||||||
from dzgui.util.deck import is_steam_deck, is_game_mode
|
|
||||||
from dzgui.util.localize import set_locale
|
|
||||||
from dzgui.util.strings import init, flags
|
|
||||||
|
|
||||||
from dzgui.views.base import App
|
|
||||||
from dzgui.views.dialogs.boot import BootWindow
|
|
||||||
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
|
||||||
from dzgui.views.dialogs.wizard import SetupWizard
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description=flags.description)
|
parser = argparse.ArgumentParser(description=flags.description)
|
||||||
parser.add_argument("-v", "--version", action="store_true", help=flags.version)
|
parser.add_argument("-v", "--version", action="store_true", help=flags.version)
|
||||||
@@ -43,11 +14,6 @@ parser.add_argument("-d", "--debug", action="store_true", help=flags.debug)
|
|||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
# TODO: profile load time
|
|
||||||
def make_parents(path: "Path") -> None:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def uninstall() -> None:
|
def uninstall() -> None:
|
||||||
# TODO: uninstall data files (-u)
|
# TODO: uninstall data files (-u)
|
||||||
# -u removes state, log, freedesktop
|
# -u removes state, log, freedesktop
|
||||||
@@ -57,21 +23,7 @@ def uninstall() -> None:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(log_path: "Path") -> None:
|
|
||||||
_format = (
|
|
||||||
"%(asctime)s␞%(levelname)s␞%(filename)s::%(funcName)s::%(lineno)s␞%(message)s"
|
|
||||||
)
|
|
||||||
fh = logging.FileHandler(log_path)
|
|
||||||
formatter = logging.Formatter(_format)
|
|
||||||
fh.setFormatter(formatter)
|
|
||||||
fh.setLevel(logging.DEBUG)
|
|
||||||
logger.setLevel(logging.DEBUG)
|
|
||||||
logger.addHandler(fh)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
lock = lock_acquire() # noqa
|
|
||||||
|
|
||||||
if args.version is True:
|
if args.version is True:
|
||||||
print(get_version())
|
print(get_version())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
@@ -82,60 +34,11 @@ def main() -> None:
|
|||||||
warnings.filterwarnings("default", category=DeprecationWarning)
|
warnings.filterwarnings("default", category=DeprecationWarning)
|
||||||
|
|
||||||
version = get_version()
|
version = get_version()
|
||||||
set_locale()
|
|
||||||
|
|
||||||
print(f"{APP_NAME} {version}")
|
print(f"{APP_NAME} {version}")
|
||||||
|
test_libgi_missing()
|
||||||
|
|
||||||
# NOTE: consider aborting this check if steam deck
|
# NOTE: only import ligbirepository modules after the above check
|
||||||
xdg_paths = get_xdg_paths()
|
from dzgui.app_init import load_gui
|
||||||
XDG = parse_filepaths(xdg_paths)
|
|
||||||
|
|
||||||
if XDG.resolution.parent.is_dir() is False:
|
load_gui(version, args.debug)
|
||||||
make_parents(XDG.resolution)
|
|
||||||
|
|
||||||
if XDG.debug.is_file() is False:
|
|
||||||
make_parents(XDG.debug)
|
|
||||||
|
|
||||||
_is_steam_deck = is_steam_deck()
|
|
||||||
_is_game_mode = is_game_mode() if _is_steam_deck else False
|
|
||||||
if _is_game_mode:
|
|
||||||
# NOTE: this may no longer be necessary on newer versions of SteamOS
|
|
||||||
del os.environ["GTK_IM_MODULE"]
|
|
||||||
|
|
||||||
if has_new_config(XDG.config) is False:
|
|
||||||
migrate_cols_file(XDG.columns)
|
|
||||||
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
|
||||||
# TODO: add logging inside wizard
|
|
||||||
SetupWizard(_is_steam_deck, XDG.config)
|
|
||||||
|
|
||||||
# NOTE: implies that setup wizard failed or was closed
|
|
||||||
if has_new_config(XDG.config) is False:
|
|
||||||
return
|
|
||||||
|
|
||||||
setup_logger(XDG.debug)
|
|
||||||
with open(XDG.debug, "w") as f:
|
|
||||||
f.truncate(0)
|
|
||||||
|
|
||||||
if _is_steam_deck is False:
|
|
||||||
# TODO: sudo escalation dialog
|
|
||||||
# count = get_map_count()
|
|
||||||
# TODO: move into module
|
|
||||||
if has_steam_client() is False:
|
|
||||||
EarlyAlertDialog(init.requires_steam)
|
|
||||||
|
|
||||||
bootwin = BootWindow(XDG, version)
|
|
||||||
local_coords, latest_release = bootwin.get_results()
|
|
||||||
|
|
||||||
use_miles = lookup(XDG.config, Preferences.DIST)
|
|
||||||
prefs = UserPrefs(
|
|
||||||
is_steam_deck=_is_steam_deck,
|
|
||||||
is_game_mode=_is_game_mode,
|
|
||||||
is_debug=args.debug,
|
|
||||||
coords=local_coords,
|
|
||||||
version=version,
|
|
||||||
paths=XDG,
|
|
||||||
latest_release=latest_release,
|
|
||||||
use_miles=use_miles,
|
|
||||||
)
|
|
||||||
print(boot.all_ok)
|
|
||||||
App(prefs)
|
|
||||||
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
|
|||||||
WINDOW_DEFAULT_X,
|
WINDOW_DEFAULT_X,
|
||||||
WINDOW_DEFAULT_Y,
|
WINDOW_DEFAULT_Y,
|
||||||
)
|
)
|
||||||
from dzgui.api.probe import test_steam_api, test_bm_api
|
from dzgui.api.probe import test_steam_api
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
@@ -55,8 +55,11 @@ class ConfigManager:
|
|||||||
self.update_config(Preferences.IP_LIST, ips)
|
self.update_config(Preferences.IP_LIST, ips)
|
||||||
|
|
||||||
def update_history_file(self, fqip: str) -> None:
|
def update_history_file(self, fqip: str) -> None:
|
||||||
|
try:
|
||||||
with open(self.prefs.paths.history, "r") as f:
|
with open(self.prefs.paths.history, "r") as f:
|
||||||
ips = [line.rstrip() for line in f.readlines()]
|
ips = [line.rstrip() for line in f.readlines()]
|
||||||
|
except FileNotFoundError:
|
||||||
|
ips = []
|
||||||
|
|
||||||
seen = set()
|
seen = set()
|
||||||
ips.append(fqip)
|
ips.append(fqip)
|
||||||
@@ -99,8 +102,6 @@ class ConfigManager:
|
|||||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
def update_api_key(self, key: Preferences, text: str) -> None:
|
||||||
if key is Preferences.STEAM:
|
if key is Preferences.STEAM:
|
||||||
res = test_steam_api(text)
|
res = test_steam_api(text)
|
||||||
else:
|
|
||||||
res = test_bm_api(text)
|
|
||||||
if res is True:
|
if res is True:
|
||||||
self.update_config(key, text)
|
self.update_config(key, text)
|
||||||
else:
|
else:
|
||||||
@@ -159,6 +160,13 @@ class ConfigManager:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def unset_fav(self) -> None:
|
||||||
|
try:
|
||||||
|
self.write_config(Preferences.FAV_LBL, "")
|
||||||
|
self.write_config(Preferences.FAV_SRV, "")
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
|
||||||
def write_config(self, key: Preferences, value: str) -> None:
|
def write_config(self, key: Preferences, value: str) -> None:
|
||||||
try:
|
try:
|
||||||
real_key = self.enum_to_key(key)
|
real_key = self.enum_to_key(key)
|
||||||
|
|||||||
@@ -9,11 +9,15 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
import dzgui.api.servers as Servers
|
import dzgui.api.servers as Servers
|
||||||
|
from dzgui.api.shortcuts import Shortcuts
|
||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
get_remote_signatures,
|
get_app_allows_downloads,
|
||||||
|
get_app_name,
|
||||||
get_needs_update,
|
get_needs_update,
|
||||||
|
get_remote_signatures,
|
||||||
|
get_running_app,
|
||||||
load_to_menu,
|
load_to_menu,
|
||||||
subscribe,
|
subscribe,
|
||||||
)
|
)
|
||||||
@@ -25,17 +29,22 @@ from dzgui.api.mods import (
|
|||||||
update_signatures,
|
update_signatures,
|
||||||
)
|
)
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
|
API_RATE_LIMIT,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
RATE_LIMIT_THRESHOLD,
|
|
||||||
)
|
)
|
||||||
|
from dzgui.api.steam import is_dayz_running
|
||||||
from dzgui.const.enum import NotebookPage, Preferences
|
from dzgui.const.enum import NotebookPage, Preferences
|
||||||
from dzgui.init.proc import is_dayz_running, is_steam_running
|
from dzgui.init.proc import is_steam_running
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings.dialogs import waiting_for_launch, waiting_for_mods
|
from dzgui.strings.dialogs import (
|
||||||
|
waiting_for_launch,
|
||||||
|
waiting_for_mods,
|
||||||
|
waiting_for_directories,
|
||||||
|
)
|
||||||
from dzgui.strings.server_mods import checkmark, resync
|
from dzgui.strings.server_mods import checkmark, resync
|
||||||
from dzgui.util.format import format_mib
|
from dzgui.util.format import format_mib
|
||||||
from dzgui.util.strings import dialog, server_timeout
|
from dzgui.util.strings import dialog, server_timeout
|
||||||
@@ -77,6 +86,8 @@ class Prerequisites:
|
|||||||
mods: list[list[str]]
|
mods: list[list[str]]
|
||||||
game_mode: bool
|
game_mode: bool
|
||||||
is_last_server: bool
|
is_last_server: bool
|
||||||
|
invalid_mods: list[tuple[str, str]]
|
||||||
|
allows_downloads: tuple[bool, str]
|
||||||
|
|
||||||
|
|
||||||
class ConnectionManager:
|
class ConnectionManager:
|
||||||
@@ -93,11 +104,6 @@ class ConnectionManager:
|
|||||||
self.remote_mod_ids: list[str] = []
|
self.remote_mod_ids: list[str] = []
|
||||||
self.missing_mods: list[tuple[str, str, int, int]] = []
|
self.missing_mods: list[tuple[str, str, int, int]] = []
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
|
||||||
def connect_by_id(self, _id: int, key: str) -> None:
|
|
||||||
res = Servers.query_by_id(_id, key)
|
|
||||||
self._prepare_connection(res)
|
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def connect_by_ip(self, addr: str) -> None:
|
def connect_by_ip(self, addr: str) -> None:
|
||||||
res = Servers.query_by_ip(addr)
|
res = Servers.query_by_ip(addr)
|
||||||
@@ -144,21 +150,14 @@ class ConnectionManager:
|
|||||||
remote_mods: list[list[str]] = []
|
remote_mods: list[list[str]] = []
|
||||||
if res.is_modded():
|
if res.is_modded():
|
||||||
try:
|
try:
|
||||||
remote_mods = self._query_modlist(record)
|
remote_mods, missing_mods = self._query_modlist(record)
|
||||||
|
self.missing_mods = missing_mods
|
||||||
self.remote_mod_ids = [mod[1] for mod in remote_mods]
|
self.remote_mod_ids = [mod[1] for mod in remote_mods]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
|
self.thread_man.set_cleanup_func(failure_func, destroy_first=True)
|
||||||
return
|
return
|
||||||
|
|
||||||
hashes = get_remote_signatures(self.remote_mod_ids)
|
|
||||||
version_file = prefs.paths.version
|
|
||||||
self.missing_mods = get_needs_update(version_file, hashes)
|
|
||||||
|
|
||||||
for mod in remote_mods:
|
|
||||||
if any(mod[1] in tuple for tuple in self.missing_mods):
|
|
||||||
mod[2] = resync
|
|
||||||
|
|
||||||
if local_version is not None:
|
if local_version is not None:
|
||||||
pefile_path = PeFile.get_pefile_path(steam_path, info.game_id)
|
pefile_path = PeFile.get_pefile_path(steam_path, info.game_id)
|
||||||
total, used, free = shutil.disk_usage(pefile_path)
|
total, used, free = shutil.disk_usage(pefile_path)
|
||||||
@@ -169,6 +168,20 @@ class ConnectionManager:
|
|||||||
|
|
||||||
dayz_running = is_dayz_running()
|
dayz_running = is_dayz_running()
|
||||||
|
|
||||||
|
running_app = get_running_app()
|
||||||
|
if running_app is not None:
|
||||||
|
allows_dl = get_app_allows_downloads(steam_path, running_app)
|
||||||
|
running_app_name = get_app_name(running_app)
|
||||||
|
# NOTE: for out-of-range appid, assume NSG.
|
||||||
|
# dzgui.api.steam.get_app_name() will return None
|
||||||
|
if running_app_name is None:
|
||||||
|
s = Shortcuts(steam_path)
|
||||||
|
# NOTE: returns "Unknown" if unparseable
|
||||||
|
running_app_name = s.find_appname_by_unsigned_id(running_app)
|
||||||
|
allows_downloads = (allows_dl, running_app_name)
|
||||||
|
else:
|
||||||
|
allows_downloads = (True, "")
|
||||||
|
|
||||||
client_name = self.controller.get_steam_client_name()
|
client_name = self.controller.get_steam_client_name()
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
running = is_steam_running(client)
|
running = is_steam_running(client)
|
||||||
@@ -179,6 +192,9 @@ class ConnectionManager:
|
|||||||
|
|
||||||
is_last = self.is_last_server()
|
is_last = self.is_last_server()
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
invalid_mods = [(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"]
|
||||||
|
|
||||||
prereqs = Prerequisites(
|
prereqs = Prerequisites(
|
||||||
name=info.server_name,
|
name=info.server_name,
|
||||||
appid=info.game_id,
|
appid=info.game_id,
|
||||||
@@ -194,6 +210,8 @@ class ConnectionManager:
|
|||||||
mods=remote_mods,
|
mods=remote_mods,
|
||||||
game_mode=game_mode,
|
game_mode=game_mode,
|
||||||
is_last_server=is_last,
|
is_last_server=is_last,
|
||||||
|
invalid_mods=invalid_mods,
|
||||||
|
allows_downloads=allows_downloads,
|
||||||
)
|
)
|
||||||
|
|
||||||
func = StoredFunc(self.controller.open_connection_assistant, prereqs)
|
func = StoredFunc(self.controller.open_connection_assistant, prereqs)
|
||||||
@@ -222,10 +240,13 @@ class ConnectionManager:
|
|||||||
StoredFunc(self._present_details_dialog, details), destroy_first=True
|
StoredFunc(self._present_details_dialog, details), destroy_first=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def _query_modlist(self, record: Servers.Record) -> list[list[str]]:
|
def _query_modlist(
|
||||||
|
self, record: Servers.Record
|
||||||
|
) -> tuple[list[list[str]], list[tuple[str, str, int, int]]]:
|
||||||
mods = Servers.get_rules(record)
|
mods = Servers.get_rules(record)
|
||||||
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
local = get_local_mod_ids(steam_path)
|
local = get_local_mod_ids(Path(steam_path))
|
||||||
|
|
||||||
alpha_mods: list[list[str]] = [
|
alpha_mods: list[list[str]] = [
|
||||||
[
|
[
|
||||||
mod.name,
|
mod.name,
|
||||||
@@ -235,12 +256,28 @@ class ConnectionManager:
|
|||||||
for mod in mods
|
for mod in mods
|
||||||
]
|
]
|
||||||
alpha_mods.sort(key=lambda x: x[0])
|
alpha_mods.sort(key=lambda x: x[0])
|
||||||
return alpha_mods
|
|
||||||
|
prefs = self.controller.get_prefs()
|
||||||
|
version_file = prefs.paths.version
|
||||||
|
|
||||||
|
remote_mod_ids = [mod[1] for mod in alpha_mods]
|
||||||
|
hashes = get_remote_signatures(remote_mod_ids)
|
||||||
|
missing_mods = get_needs_update(version_file, hashes)
|
||||||
|
for mod in alpha_mods:
|
||||||
|
if any(mod[1] in tuple for tuple in missing_mods):
|
||||||
|
mod[2] = resync
|
||||||
|
for mod in alpha_mods:
|
||||||
|
# NOTE: if the mod is neither synched or out of date, it is a malformed mod
|
||||||
|
if mod[2] == "":
|
||||||
|
# TODO: strings
|
||||||
|
mod[2] = "Invalid mod"
|
||||||
|
|
||||||
|
return alpha_mods, missing_mods
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def query_modlist_and_present(self, record: Servers.Record) -> None:
|
def query_modlist_and_present(self, record: Servers.Record) -> None:
|
||||||
try:
|
try:
|
||||||
mods = self._query_modlist(record)
|
mods, missing_mods = self._query_modlist(record)
|
||||||
except Exception:
|
except Exception:
|
||||||
self.thread_man.set_cleanup_func(
|
self.thread_man.set_cleanup_func(
|
||||||
StoredFunc(self._server_timeout), destroy_first=True
|
StoredFunc(self._server_timeout), destroy_first=True
|
||||||
@@ -276,7 +313,6 @@ class ConnectionManager:
|
|||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.thread_man.show_cancel(False)
|
|
||||||
self.thread_man.update_dialog(waiting_for_launch)
|
self.thread_man.update_dialog(waiting_for_launch)
|
||||||
while True:
|
while True:
|
||||||
# FIXME: cancel should not be visible here per setting above
|
# FIXME: cancel should not be visible here per setting above
|
||||||
@@ -303,8 +339,9 @@ class ConnectionManager:
|
|||||||
if self.controller.is_cancel_pending():
|
if self.controller.is_cancel_pending():
|
||||||
return
|
return
|
||||||
subscribe(key, int(mod))
|
subscribe(key, int(mod))
|
||||||
time.sleep(RATE_LIMIT_THRESHOLD)
|
time.sleep(API_RATE_LIMIT)
|
||||||
|
|
||||||
|
self.thread_man.update_dialog(waiting_for_directories)
|
||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
mod_path = self.workshop / mod
|
mod_path = self.workshop / mod
|
||||||
|
|
||||||
@@ -322,8 +359,12 @@ class ConnectionManager:
|
|||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
update_signatures(self.missing_mods, prefs.paths.version)
|
update_signatures(self.missing_mods, prefs.paths.version)
|
||||||
# TODO: just push steam path directly
|
|
||||||
rebuild_symlinks(prefs.paths.config)
|
rebuild_symlinks(prefs.paths.config)
|
||||||
|
|
||||||
|
# NOTE: update table status in main loop
|
||||||
|
self.thread_man.update_emitter("all_mods_synched")
|
||||||
|
|
||||||
|
# TODO: just push steam path directly
|
||||||
self._connect_steam(menu_only)
|
self._connect_steam(menu_only)
|
||||||
|
|
||||||
@call_on_thread(waiting_for_mods, show_cancel=True)
|
@call_on_thread(waiting_for_mods, show_cancel=True)
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ class FilterManager:
|
|||||||
if maps is None:
|
if maps is None:
|
||||||
return
|
return
|
||||||
if len(maps) < 1:
|
if len(maps) < 1:
|
||||||
|
self.reinit_map_store()
|
||||||
return
|
return
|
||||||
|
|
||||||
self.reinit_map_store()
|
self.reinit_map_store()
|
||||||
|
|||||||
+20
-10
@@ -12,10 +12,10 @@ from dzgui.api.mods import (
|
|||||||
remove_stale_signatures,
|
remove_stale_signatures,
|
||||||
)
|
)
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
|
API_RATE_LIMIT,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
RATE_LIMIT_THRESHOLD,
|
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
@@ -23,6 +23,7 @@ from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
|||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util.format import format_mods
|
from dzgui.util.format import format_mods
|
||||||
from dzgui.util.strings import server_timeout
|
from dzgui.util.strings import server_timeout
|
||||||
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
|
||||||
|
|
||||||
@@ -59,16 +60,20 @@ class ModManager:
|
|||||||
|
|
||||||
@call_on_thread(dialogs.fetching_mods)
|
@call_on_thread(dialogs.fetching_mods)
|
||||||
def load_mods(self) -> None:
|
def load_mods(self) -> None:
|
||||||
mods = get_delimited_mods(self.path)
|
mods = get_delimited_mods(Path(self.path))
|
||||||
if len(mods) < 1:
|
if len(mods) < 1:
|
||||||
msg = self.format_mod_statusbar()
|
msg = self.format_mod_statusbar()
|
||||||
func = StoredFunc(lambda: self.emitter.emit("mods_updated", msg, 0))
|
func = StoredFunc(lambda: self.emitter.emit("mods_updated", msg, 0))
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
prefs = self.controller.get_prefs()
|
||||||
|
rebuild_symlinks(prefs.paths.config)
|
||||||
|
|
||||||
self.store = ModelFactory().make_mod_store()
|
self.store = ModelFactory().make_mod_store()
|
||||||
self.store.extend(mods)
|
self.store.extend(mods)
|
||||||
func = StoredFunc(self._on_mods_loaded)
|
func = StoredFunc(self._on_mods_loaded)
|
||||||
|
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def set_store(self, store: "FastInsertListStore") -> None:
|
def set_store(self, store: "FastInsertListStore") -> None:
|
||||||
@@ -93,6 +98,7 @@ class ModManager:
|
|||||||
continue
|
continue
|
||||||
mod, _iter = res
|
mod, _iter = res
|
||||||
mods.append((mod, _iter))
|
mods.append((mod, _iter))
|
||||||
|
self.thread_man.set_job_count(len(mods))
|
||||||
self.unsub_all_mods(mods)
|
self.unsub_all_mods(mods)
|
||||||
|
|
||||||
def get_mod_from_tree_path(
|
def get_mod_from_tree_path(
|
||||||
@@ -109,9 +115,10 @@ class ModManager:
|
|||||||
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||||
for mod, _iter in mods:
|
for mod, _iter in mods:
|
||||||
self.unsub_atomic_mod(mod)
|
self.unsub_atomic_mod(mod)
|
||||||
|
self.thread_man.increment_dialog()
|
||||||
|
time.sleep(API_RATE_LIMIT)
|
||||||
|
|
||||||
iters = [_iter for mod, _iter in mods]
|
func = StoredFunc(self._on_mods_unsubbed, mods)
|
||||||
func = StoredFunc(self._on_mods_unsubbed, iters)
|
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def unsub_atomic_mod(self, mod: str) -> None:
|
def unsub_atomic_mod(self, mod: str) -> None:
|
||||||
@@ -134,22 +141,25 @@ class ModManager:
|
|||||||
app_path_exp = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP)
|
app_path_exp = PeFile.get_nested_app_path(steam_path, APPID_DAYZ_EXP)
|
||||||
symlink = app_path_exp / md5
|
symlink = app_path_exp / md5
|
||||||
symlink.unlink()
|
symlink.unlink()
|
||||||
except PeFile.AppNotInstalledError:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
time.sleep(RATE_LIMIT_THRESHOLD)
|
time.sleep(API_RATE_LIMIT)
|
||||||
|
|
||||||
def _on_mods_unsubbed(self, iters: list[Gtk.TreeIter]) -> None:
|
def _on_mods_unsubbed(self, mod_iter: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||||
|
mods = [int(mod) for mod, _iter in mod_iter]
|
||||||
|
iters = [_iter for mod, _iter in mod_iter]
|
||||||
if self.store is None:
|
if self.store is None:
|
||||||
return
|
return
|
||||||
for _iter in iters:
|
for _iter in iters:
|
||||||
self.store.remove(_iter)
|
self.store.remove(_iter)
|
||||||
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version)
|
# TODO: process config path in called function
|
||||||
|
remove_stale_signatures(self.prefs.paths.config, self.prefs.paths.version, mods)
|
||||||
model = self.treeview.get_model()
|
model = self.treeview.get_model()
|
||||||
if model is None:
|
if model is None:
|
||||||
return
|
return
|
||||||
mods = len(model)
|
total_mods = len(model)
|
||||||
msg = self.format_mod_statusbar()
|
msg = self.format_mod_statusbar()
|
||||||
self.emitter.emit("mods_updated", msg, mods)
|
self.emitter.emit("mods_updated", msg, total_mods)
|
||||||
|
|
||||||
def uncolorize_mods(self) -> None:
|
def uncolorize_mods(self) -> None:
|
||||||
model = self.treeview.get_model()
|
model = self.treeview.get_model()
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ from typing import Callable, TYPE_CHECKING, Union
|
|||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
from dzgui.api.mods import is_mission, get_custom_mods
|
from dzgui.api.mods import is_mission, get_custom_mods
|
||||||
from dzgui.api.steam import launch_offline
|
from dzgui.api.steam import is_dayz_running, launch_offline
|
||||||
from dzgui.const.constants import APP_NAME, APPID_DAYZ_EXP
|
from dzgui.const.constants import APP_NAME, APPID_DAYZ_EXP
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.init.proc import is_dayz_running
|
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
@@ -77,7 +76,7 @@ class OfflineManager:
|
|||||||
window.set_sensitive(True)
|
window.set_sensitive(True)
|
||||||
self.emitter.emit("custom_mods_loaded", store, folder)
|
self.emitter.emit("custom_mods_loaded", store, folder)
|
||||||
|
|
||||||
@call_on_thread(dialogs.waiting_for_launch)
|
@call_on_thread(dialogs.waiting_for_launch, show_cancel=True)
|
||||||
def launch(
|
def launch(
|
||||||
self,
|
self,
|
||||||
appid: int,
|
appid: int,
|
||||||
|
|||||||
@@ -81,6 +81,11 @@ class ThreadingManager:
|
|||||||
def set_job_count(self, jobs: int) -> None:
|
def set_job_count(self, jobs: int) -> None:
|
||||||
self.jobs = jobs
|
self.jobs = jobs
|
||||||
|
|
||||||
|
def update_emitter(self, signal: str) -> None:
|
||||||
|
if self.controller:
|
||||||
|
emitter = self.controller.get_emitter()
|
||||||
|
GLib.idle_add(lambda: emitter.emit(signal))
|
||||||
|
|
||||||
def update_dialog(self, msg: str) -> None:
|
def update_dialog(self, msg: str) -> None:
|
||||||
GLib.idle_add(lambda: self.wait_dialog.update_text(msg))
|
GLib.idle_add(lambda: self.wait_dialog.update_text(msg))
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ from dataclasses import dataclass
|
|||||||
from typing import Any, Sequence, TYPE_CHECKING
|
from typing import Any, Sequence, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.enum import HELP_MENU_ROWS
|
from dzgui.const.enum import HELP_MENU_ROWS
|
||||||
from dzgui.util.redact import redact_log
|
|
||||||
from dzgui.util.strings import delimiter
|
from dzgui.util.strings import delimiter
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -134,9 +133,7 @@ class ModelFactory:
|
|||||||
with open(path, "r") as f:
|
with open(path, "r") as f:
|
||||||
lines = [line.split(delimiter) for line in f.read().splitlines()]
|
lines = [line.split(delimiter) for line in f.read().splitlines()]
|
||||||
for record in lines:
|
for record in lines:
|
||||||
# NOTE: strips PII and API keys
|
store.append(record)
|
||||||
clean = redact_log(record)
|
|
||||||
store.append(clean)
|
|
||||||
return store
|
return store
|
||||||
|
|
||||||
def new_model_from_class(self, cls: type) -> FastInsertListStore:
|
def new_model_from_class(self, cls: type) -> FastInsertListStore:
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from typing import Any, TYPE_CHECKING, Union
|
from typing import Any, TYPE_CHECKING, Union
|
||||||
from warnings import deprecated
|
|
||||||
|
|
||||||
from dzgui.const.enum import FilterMode
|
from dzgui.const.enum import FilterMode
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
@@ -86,8 +85,8 @@ class ProxyModelManager:
|
|||||||
if found is False:
|
if found is False:
|
||||||
self.control_model.append(history)
|
self.control_model.append(history)
|
||||||
|
|
||||||
# if len(self.control_model) == 11:
|
if len(self.control_model) == 11:
|
||||||
# del self.control_model[0]
|
del self.control_model[0]
|
||||||
|
|
||||||
self.filter(FilterMode.INITIAL, skip_cache=True)
|
self.filter(FilterMode.INITIAL, skip_cache=True)
|
||||||
|
|
||||||
@@ -259,7 +258,7 @@ class ProxyModelManager:
|
|||||||
def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None:
|
def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None:
|
||||||
self.filter_cache[filters] = filtered_rows
|
self.filter_cache[filters] = filtered_rows
|
||||||
|
|
||||||
@deprecated("Currently unused")
|
# @deprecated("Currently unused")
|
||||||
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
|
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
|
||||||
# return [[el for el in row] for row in model]
|
# return [[el for el in row] for row in model]
|
||||||
|
|
||||||
|
|||||||
+13
-18
@@ -15,7 +15,7 @@ from dzgui.const.constants import (
|
|||||||
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util.strings import api_warn_msg, dialog
|
from dzgui.util.strings import server_timeout, dialog
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -175,11 +175,18 @@ class ServerModelManager:
|
|||||||
# TODO: wrap except
|
# TODO: wrap except
|
||||||
if self.controller.get_exit_event().is_set():
|
if self.controller.get_exit_event().is_set():
|
||||||
return
|
return
|
||||||
res = future.result(timeout=API_TIMEOUT)
|
|
||||||
self.thread_man.increment_dialog()
|
self.thread_man.increment_dialog()
|
||||||
|
|
||||||
|
"""
|
||||||
|
Collects futures in linear order after completion
|
||||||
|
for lock-step representation of the state file
|
||||||
|
"""
|
||||||
|
for future in futures:
|
||||||
|
res = future.result(timeout=API_TIMEOUT)
|
||||||
# NOTE: failing entries are culled
|
# NOTE: failing entries are culled
|
||||||
if res is None:
|
if res is None:
|
||||||
# TODO: log which servers failed
|
# TODO: log which servers failed
|
||||||
|
# query_direct() currently does not return enough information
|
||||||
continue
|
continue
|
||||||
servers.append(res)
|
servers.append(res)
|
||||||
if len(servers) == 0:
|
if len(servers) == 0:
|
||||||
@@ -191,13 +198,6 @@ class ServerModelManager:
|
|||||||
parsed = Servers.parse_json(servers)
|
parsed = Servers.parse_json(servers)
|
||||||
self._push_data(parsed)
|
self._push_data(parsed)
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
|
||||||
def add_by_id(self, _id: str) -> None:
|
|
||||||
config_man = self.controller.get_config_man()
|
|
||||||
key = config_man.lookup(Preferences.BM)
|
|
||||||
res = Servers.query_by_id(int(_id), key)
|
|
||||||
self._parse_single_record(res)
|
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def add_by_ip(self, addr: str) -> None:
|
def add_by_ip(self, addr: str) -> None:
|
||||||
res = Servers.query_by_ip(addr)
|
res = Servers.query_by_ip(addr)
|
||||||
@@ -241,7 +241,6 @@ class ServerModelManager:
|
|||||||
|
|
||||||
self.first_iteration = False
|
self.first_iteration = False
|
||||||
self.emitter.emit("load_maps", store)
|
self.emitter.emit("load_maps", store)
|
||||||
# self.emitter.emit("servers_loaded_init")
|
|
||||||
|
|
||||||
# TODO: dataclass for record rows; check for other dict annotations
|
# TODO: dataclass for record rows; check for other dict annotations
|
||||||
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
|
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
|
||||||
@@ -263,12 +262,6 @@ class ServerModelManager:
|
|||||||
proxy_man.remove_row_from_control(record)
|
proxy_man.remove_row_from_control(record)
|
||||||
self.update_history()
|
self.update_history()
|
||||||
|
|
||||||
def add_by_str(self, addr: str) -> None:
|
|
||||||
if addr.isdigit():
|
|
||||||
self.add_by_id(addr)
|
|
||||||
else:
|
|
||||||
self.add_by_ip(addr)
|
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def update_playercount(
|
def update_playercount(
|
||||||
self, treeiter: Gtk.TreeIter, record: Servers.Record
|
self, treeiter: Gtk.TreeIter, record: Servers.Record
|
||||||
@@ -395,8 +388,10 @@ class ServerModelManager:
|
|||||||
self.first_iteration = False
|
self.first_iteration = False
|
||||||
|
|
||||||
def _cleanup_on_success(self) -> None:
|
def _cleanup_on_success(self) -> None:
|
||||||
proxy = self._get_proxy_man().get_proxy_model()
|
proxy = self.proxy_man.get_proxy_model()
|
||||||
self.tv.set_model(proxy)
|
self.tv.set_model(proxy)
|
||||||
|
if self.preserve_on_fail is True:
|
||||||
|
self.proxy_man.wipe_cache()
|
||||||
|
|
||||||
self.emitter.emit("servers_loaded", self.enum)
|
self.emitter.emit("servers_loaded", self.enum)
|
||||||
if self.first_iteration:
|
if self.first_iteration:
|
||||||
@@ -424,7 +419,7 @@ class ServerModelManager:
|
|||||||
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
|
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
|
||||||
# customize statusbar and dialog accordingly
|
# customize statusbar and dialog accordingly
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, api_warn_msg)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _push_data(self, data: list[Any]) -> None:
|
def _push_data(self, data: list[Any]) -> None:
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ add_tooltip="Add to Saved Servers"
|
|||||||
connect_button="Connect"
|
connect_button="Connect"
|
||||||
connect_tooltip="Connect to this server"
|
connect_tooltip="Connect to this server"
|
||||||
connect_entry_tooltip=(
|
connect_entry_tooltip=(
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
|
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
|
||||||
"- Battlemetrics: numeric server ID\ne.g. 123456"
|
|
||||||
)
|
)
|
||||||
connect_entry_placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)"
|
connect_entry_placeholder="Enter IP (IP:Query port)"
|
||||||
|
|
||||||
fav_heading="Favorite server"
|
fav_heading="Favorite server"
|
||||||
favs_empty="None set. Right click a server and select 'Set favorite' to set."
|
favs_empty="None set. Right click a server and select 'Set favorite' to set."
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
waiting_for_launch = "Waiting for DayZ to launch"
|
waiting_for_launch = "Waiting for DayZ to launch"
|
||||||
waiting_for_mods = "Waiting for Steam to update mods"
|
waiting_for_mods = "Queueing up mods (step 1/2)"
|
||||||
|
waiting_for_directories = "Steam is staging mods (step 2/2)"
|
||||||
|
|
||||||
fetching_update = "Fetching update"
|
fetching_update = "Fetching update"
|
||||||
failed_to_update = "Failed to update DZGUI executable"
|
failed_to_update = "Failed to update DZGUI executable"
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
heading = "Play offline"
|
heading = "Play offline"
|
||||||
|
heading_disclaimer = (
|
||||||
|
"Not all mod combinations will function offline. Use at your own discretion."
|
||||||
|
)
|
||||||
|
|
||||||
mission_button = "Select mission folder"
|
mission_button = "Select mission folder"
|
||||||
mission_frame = "Mission"
|
mission_frame = "Mission"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ warnings = "Warnings"
|
|||||||
errors = "Errors"
|
errors = "Errors"
|
||||||
mods = "Mods"
|
mods = "Mods"
|
||||||
total_mods = "Total mods: "
|
total_mods = "Total mods: "
|
||||||
|
up_to_date = "All mods are up to date."
|
||||||
all_updated = "All mods updated."
|
all_updated = "All mods updated."
|
||||||
"If you recently installed {build} or moved it to a different drive, "
|
"If you recently installed {build} or moved it to a different drive, "
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
"restart Steam to allow these changes to synchronize, then try again."
|
||||||
+19
-5
@@ -46,11 +46,6 @@ You must set up a Steam Web API key in order to browse the global server list.
|
|||||||
\nIf you don't have one already, it can be set up via the page below.
|
\nIf you don't have one already, it can be set up via the page below.
|
||||||
\nPlease refer to the DZGUI documentation for more instructions.
|
\nPlease refer to the DZGUI documentation for more instructions.
|
||||||
"""
|
"""
|
||||||
heading_bm_api = "Battlemetrics Web API key"
|
|
||||||
blurb_bm_api = """A Battlemetrics key is <b>optional</b>, but allows you to add/search for servers\n
|
|
||||||
by numeric ID on the web. For example, in the URL https://www.battlemetrics.net/servers/dayz/24819107,\n
|
|
||||||
the ID would be <b>24819107</b>.
|
|
||||||
"""
|
|
||||||
|
|
||||||
### PreferencesPage
|
### PreferencesPage
|
||||||
heading_prefs = "User preferences"
|
heading_prefs = "User preferences"
|
||||||
@@ -64,6 +59,25 @@ radio_mi = "mi (miles)"
|
|||||||
label_dist = "Distance display"
|
label_dist = "Distance display"
|
||||||
label_client = "Steam client"
|
label_client = "Steam client"
|
||||||
|
|
||||||
|
### Shortcuts page
|
||||||
|
heading_shortcuts = "Create shortcuts"
|
||||||
|
blurb_shortcuts = (
|
||||||
|
"You can optionally create shortcuts to facilitate launching DZGUI faster."
|
||||||
|
)
|
||||||
|
button_shortcuts = "Create shortcuts"
|
||||||
|
checkbox_steam_shortcut = (
|
||||||
|
"Add DZGUI to Steam library",
|
||||||
|
(
|
||||||
|
"This creates a standalone copy of DZGUI and adds it to Steam. "
|
||||||
|
"You <b>must restart Steam</b> for the shortcut to appear. This will not replace or update existing shortcuts. "
|
||||||
|
"If you update DZGUI when it is launched via Steam, updates will be applied to the Steam version."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
checkbox_start_menu = (
|
||||||
|
"Add a start menu shortcut",
|
||||||
|
"Depends on local window manager settings. Will work on Steam Deck.",
|
||||||
|
)
|
||||||
|
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
|
||||||
|
|
||||||
### Completion page
|
### Completion page
|
||||||
heading_completion = "Setup complete"
|
heading_completion = "Setup complete"
|
||||||
|
|||||||
+13
-6
@@ -1,3 +1,4 @@
|
|||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
|
|
||||||
@@ -9,7 +10,9 @@ from dzgui.const.constants import APP_NAME
|
|||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
from dzgui.init.prefix import get_version
|
from dzgui.init.prefix import get_version
|
||||||
from dzgui.util.redact import redact
|
from dzgui.util.redact import redact_home
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def get_cpu_model() -> str:
|
def get_cpu_model() -> str:
|
||||||
@@ -38,15 +41,19 @@ def print_mods(mods: list[int]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def write_diagnostic(config: Path, outfile: Path) -> None:
|
def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||||
# TODO: test availability on other distros
|
|
||||||
date = datetime.now().isoformat()
|
date = datetime.now().isoformat()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# FIXME: ID_LIKE not available on CachyOS
|
||||||
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
||||||
|
except Exception as e:
|
||||||
|
logger.warn(e)
|
||||||
|
distro = "Unknown"
|
||||||
|
|
||||||
kernel = os.uname().release
|
kernel = os.uname().release
|
||||||
cpu = get_cpu_model()
|
cpu = get_cpu_model()
|
||||||
version = get_version()
|
version = get_version()
|
||||||
|
|
||||||
branch = lookup(config, Preferences.BRANCH)
|
|
||||||
debug = lookup(config, Preferences.DEBUG)
|
debug = lookup(config, Preferences.DEBUG)
|
||||||
install = lookup(config, Preferences.INSTALL)
|
install = lookup(config, Preferences.INSTALL)
|
||||||
default = lookup(config, Preferences.DEFAULT)
|
default = lookup(config, Preferences.DEFAULT)
|
||||||
@@ -54,8 +61,8 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
steam_path = Path(default)
|
steam_path = Path(default)
|
||||||
workshop_path = get_local_mod_path(steam_path)
|
workshop_path = get_local_mod_path(steam_path)
|
||||||
|
|
||||||
steam_redacted = redact(default)
|
steam_redacted = redact_home(default)
|
||||||
workshop_redacted = redact(str(workshop_path))
|
workshop_redacted = redact_home(str(workshop_path))
|
||||||
|
|
||||||
mods = get_local_mod_ids(steam_path)
|
mods = get_local_mod_ids(steam_path)
|
||||||
mods_pretty = print_mods(mods)
|
mods_pretty = print_mods(mods)
|
||||||
@@ -65,7 +72,7 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
|
|
||||||
# FIXME: extraneous newlines in lists of mods
|
# FIXME: extraneous newlines in lists of mods
|
||||||
template = f"""\
|
template = f"""\
|
||||||
{APP_NAME} version {version} ({branch})
|
{APP_NAME} version {version}
|
||||||
Date: {date}
|
Date: {date}
|
||||||
===============================
|
===============================
|
||||||
Distribution: {distro}
|
Distribution: {distro}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def make_parents(path: "Path") -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_dzgui_to_xdg_data(exe_path: Path) -> None:
|
||||||
|
dzgui = os.getenv("PYAPP")
|
||||||
|
if dzgui is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
make_parents(exe_path)
|
||||||
|
shutil.copy(dzgui, exe_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def find_icon_resource() -> "Path":
|
||||||
|
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
||||||
|
images = Path(str(traversable))
|
||||||
|
icon = images.joinpath("icon.png")
|
||||||
|
return icon
|
||||||
+26
-16
@@ -1,20 +1,30 @@
|
|||||||
|
import logging
|
||||||
import re
|
import re
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
def redact(text: str) -> str:
|
api_filter = r"(.*&key=)([^&]*)(.*)"
|
||||||
r = r"(/home/)([^/])*"
|
home_filter = r"(/home/)([^\s'\/]*)(.*)"
|
||||||
cleaned = re.sub(r, r"/home/REDACTED", text)
|
user_filter = r"(.*Steam/userdata/)([^/]*)(.*)"
|
||||||
|
REDACTED = r"\1REDACTED\3"
|
||||||
|
REDACTION_PATTERNS = [api_filter, home_filter, user_filter]
|
||||||
|
|
||||||
|
|
||||||
|
def redact_home(text: str) -> str:
|
||||||
|
pat = re.compile(home_filter)
|
||||||
|
cleaned = pat.sub(REDACTED, text)
|
||||||
return cleaned
|
return cleaned
|
||||||
|
|
||||||
def redact_log(record: list) -> list[str]:
|
|
||||||
"""
|
class RedactionFilter(logging.Filter):
|
||||||
requests library includes Steam API key in URL params
|
def __init__(self, patterns: list[str] | None = None) -> None:
|
||||||
"""
|
super().__init__()
|
||||||
clean = []
|
self._patterns = [re.compile(pat) for pat in (patterns or [])]
|
||||||
for item in record:
|
|
||||||
if "&key=" in item:
|
def filter(self, record: logging.LogRecord) -> Literal[True]:
|
||||||
pat = r"(.*&key=)(\S+)(.*)"
|
for pattern in self._patterns:
|
||||||
scrubbed = re.sub(pat, r"\1REDACTED\3", item)
|
try:
|
||||||
clean.append(scrubbed)
|
record.msg = pattern.sub(REDACTED, record.msg)
|
||||||
else:
|
except TypeError:
|
||||||
clean.append(item)
|
exception_text = f"{type(record.msg).__name__}: {record.msg}"
|
||||||
return clean
|
record.msg = pattern.sub(REDACTED, exception_text)
|
||||||
|
return True
|
||||||
+9
-20
@@ -100,10 +100,7 @@ steam_missing = "Local Steam installation is not set, possibly malformed config
|
|||||||
build_corrupted = (
|
build_corrupted = (
|
||||||
"Steam settings or DayZ installation may be corrupted. Try restarting Steam."
|
"Steam settings or DayZ installation may be corrupted. Try restarting Steam."
|
||||||
)
|
)
|
||||||
api_warn_msg = """No servers returned. Please wait and try again.
|
server_timeout = "Timed out when querying server(s). Check IP or try again later."
|
||||||
This usually indicates a transitory network timeout. If this issue persists, your API key or network may be malfunctioning.
|
|
||||||
"""
|
|
||||||
server_timeout = "Timed out when querying server, check IP or try again later."
|
|
||||||
server_error = (
|
server_error = (
|
||||||
"Error while contacting server, possibly timed out. Please wait and try again."
|
"Error while contacting server, possibly timed out. Please wait and try again."
|
||||||
)
|
)
|
||||||
@@ -130,7 +127,6 @@ servers = {
|
|||||||
"Enter/space/double-click": "connect to server",
|
"Enter/space/double-click": "connect to server",
|
||||||
"Right-click/Ctrl-l": "additional context menus",
|
"Right-click/Ctrl-l": "additional context menus",
|
||||||
"Ctrl-r": "refresh players",
|
"Ctrl-r": "refresh players",
|
||||||
"Ctrl-p": "refresh ping",
|
|
||||||
"Ctrl-f": "jump to keyword search field",
|
"Ctrl-f": "jump to keyword search field",
|
||||||
"Ctrl-m": "jump to maps field",
|
"Ctrl-m": "jump to maps field",
|
||||||
"Ctrl-d": "select default LAN port",
|
"Ctrl-d": "select default LAN port",
|
||||||
@@ -148,8 +144,8 @@ servers = {
|
|||||||
vim = {
|
vim = {
|
||||||
"j": "Move down a row/scroll up",
|
"j": "Move down a row/scroll up",
|
||||||
"k": "Move up a row/scroll down",
|
"k": "Move up a row/scroll down",
|
||||||
"l": "Jump to main area from sidebar",
|
"h": "Jump to main area from sidebar",
|
||||||
"h": "Jump to sidebar from main area",
|
"l": "Jump to sidebar from main area",
|
||||||
"g": "Jump to first row/top of page",
|
"g": "Jump to first row/top of page",
|
||||||
"G": "Jump to last row/bottom of page",
|
"G": "Jump to last row/bottom of page",
|
||||||
}
|
}
|
||||||
@@ -287,11 +283,8 @@ class Thanks:
|
|||||||
class Options:
|
class Options:
|
||||||
header: str
|
header: str
|
||||||
steam_web: str
|
steam_web: str
|
||||||
bm_web: str
|
|
||||||
enter_steam: str
|
enter_steam: str
|
||||||
enter_bm: str
|
|
||||||
steam_placeholder: str
|
steam_placeholder: str
|
||||||
bm_placeholder: str
|
|
||||||
name_placeholder: str
|
name_placeholder: str
|
||||||
last_used: str
|
last_used: str
|
||||||
always_fs: str
|
always_fs: str
|
||||||
@@ -312,7 +305,7 @@ class Options:
|
|||||||
force_eventbox: str
|
force_eventbox: str
|
||||||
self_update: str
|
self_update: str
|
||||||
no_self_update: str
|
no_self_update: str
|
||||||
api_keys: str
|
api_key: str
|
||||||
prefs: str
|
prefs: str
|
||||||
mods: str
|
mods: str
|
||||||
version: str
|
version: str
|
||||||
@@ -334,8 +327,8 @@ mod_panel = ModPanelStrings(
|
|||||||
unhighlight_stale_tooltip="Clears highlight from stale mods",
|
unhighlight_stale_tooltip="Clears highlight from stale mods",
|
||||||
highlight_stale="Highlight stale",
|
highlight_stale="Highlight stale",
|
||||||
highlight_stale_tooltip=(
|
highlight_stale_tooltip=(
|
||||||
"Shows locally-installed mods which are not used by any server "
|
"Shows locally-installed mods which are not \n"
|
||||||
"in your Saved Servers"
|
"used by any server in your Saved Servers"
|
||||||
),
|
),
|
||||||
unsub_selected="Unsubscribe selected",
|
unsub_selected="Unsubscribe selected",
|
||||||
unsub_selected_tooltip="Unsubscribes from selected mods",
|
unsub_selected_tooltip="Unsubscribes from selected mods",
|
||||||
@@ -379,11 +372,8 @@ thanks = Thanks(
|
|||||||
options = Options(
|
options = Options(
|
||||||
header="Options",
|
header="Options",
|
||||||
steam_web="Steam API page",
|
steam_web="Steam API page",
|
||||||
bm_web="Battlemetrics API page",
|
|
||||||
enter_steam="Enter your Steam API key",
|
enter_steam="Enter your Steam API key",
|
||||||
enter_bm="Enter your Battlemetrics API key",
|
|
||||||
steam_placeholder="Steam API key",
|
steam_placeholder="Steam API key",
|
||||||
bm_placeholder="Battlemetrics API key",
|
|
||||||
name_placeholder="Identifies you to other players in-game",
|
name_placeholder="Identifies you to other players in-game",
|
||||||
last_used="Last used dimensions",
|
last_used="Last used dimensions",
|
||||||
always_fs="Always fullscreen",
|
always_fs="Always fullscreen",
|
||||||
@@ -412,7 +402,7 @@ options = Options(
|
|||||||
"In-app updates are disabled when DZGUI is "
|
"In-app updates are disabled when DZGUI is "
|
||||||
"installed globally (e.g., via package manager)."
|
"installed globally (e.g., via package manager)."
|
||||||
),
|
),
|
||||||
api_keys="API Keys",
|
api_key="API Key",
|
||||||
prefs="Preferences",
|
prefs="Preferences",
|
||||||
mods="Mods",
|
mods="Mods",
|
||||||
version="Version",
|
version="Version",
|
||||||
@@ -550,10 +540,9 @@ connect_panel = ConnectPanel(
|
|||||||
connect="Connect",
|
connect="Connect",
|
||||||
add="Add",
|
add="Add",
|
||||||
add_con="Add/connect",
|
add_con="Add/connect",
|
||||||
placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)",
|
placeholder="Enter IP (IP:Query port)",
|
||||||
entry_tooltip=(
|
entry_tooltip=(
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
|
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016"
|
||||||
"- Battlemetrics: numeric server ID\ne.g. 123456"
|
|
||||||
),
|
),
|
||||||
add_tooltip="Add to Saved Servers",
|
add_tooltip="Add to Saved Servers",
|
||||||
connect_tooltip="Connect to this server",
|
connect_tooltip="Connect to this server",
|
||||||
|
|||||||
+4
-1
@@ -54,7 +54,10 @@ class OuterWindow(Gtk.Window):
|
|||||||
MainController.register_widget("window", self)
|
MainController.register_widget("window", self)
|
||||||
|
|
||||||
# NOTE: steam deck taskbar may occlude elements
|
# NOTE: steam deck taskbar may occlude elements
|
||||||
if MainController.get_prefs().is_steam_deck is False:
|
is_deck = MainController.get_prefs().is_steam_deck
|
||||||
|
if is_deck:
|
||||||
|
self.maximize()
|
||||||
|
else:
|
||||||
self.set_titlebar(self.hb)
|
self.set_titlebar(self.hb)
|
||||||
|
|
||||||
self.connect("delete-event", self._on_delete_event)
|
self.connect("delete-event", self._on_delete_event)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from typing import Callable, Self, TYPE_CHECKING, Union
|
from typing import Callable, Literal, Self, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from dzgui.util.clip import copy_clipboard
|
from dzgui.util.clip import copy_clipboard
|
||||||
from dzgui.util.format import pluralize
|
from dzgui.util.format import pluralize
|
||||||
@@ -10,6 +10,7 @@ from dzgui.util.strings import (
|
|||||||
)
|
)
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
CLIPBOARD,
|
CLIPBOARD,
|
||||||
|
CLOSE,
|
||||||
INPUT_KEYBOARD,
|
INPUT_KEYBOARD,
|
||||||
LIST_ADD,
|
LIST_ADD,
|
||||||
REFRESH_ICON,
|
REFRESH_ICON,
|
||||||
@@ -66,6 +67,15 @@ class IconButton(Gtk.Button):
|
|||||||
# self.set_image_position(Gtk.PositionType.RIGHT)
|
# self.set_image_position(Gtk.PositionType.RIGHT)
|
||||||
self.set_focus_on_click(False)
|
self.set_focus_on_click(False)
|
||||||
|
|
||||||
|
def swap_icon(self, icon: str) -> None:
|
||||||
|
start = self.icon.get_margin_start()
|
||||||
|
end = self.icon.get_margin_end()
|
||||||
|
alt_icon = Icon(icon, margin_start=start, margin_end=end)
|
||||||
|
self.set_image(alt_icon)
|
||||||
|
|
||||||
|
def revert_icon(self) -> None:
|
||||||
|
self.set_image(self.icon)
|
||||||
|
|
||||||
|
|
||||||
class IconTextButton(IconButton):
|
class IconTextButton(IconButton):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -96,11 +106,18 @@ class ClipboardButton(IconTextButton):
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.connect("clicked", self._on_button_clicked, func)
|
self.connect("clicked", self._on_button_clicked, func)
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
self.set_tooltip_text("Copy to clipboard")
|
self.set_tooltip_text("Copy to clipboard")
|
||||||
|
|
||||||
def _on_button_clicked(self, button: Self, func: Callable) -> None:
|
def _on_button_clicked(self, button: Self, func: Callable) -> None:
|
||||||
|
def revert() -> Literal[False]:
|
||||||
|
self.revert_icon()
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.swap_icon("object-select-symbolic")
|
||||||
data = func()
|
data = func()
|
||||||
copy_clipboard(data)
|
copy_clipboard(data)
|
||||||
|
GLib.timeout_add(600, revert)
|
||||||
|
|
||||||
|
|
||||||
# TODO: determine when controller would be passed to this button or drop
|
# TODO: determine when controller would be passed to this button or drop
|
||||||
@@ -108,9 +125,18 @@ class CopyIpButton(ClipboardButton):
|
|||||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||||
super().__init__(controller, func)
|
super().__init__(controller, func)
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
self.set_tooltip_text("Copy IP to clipboard")
|
self.set_tooltip_text("Copy IP to clipboard")
|
||||||
|
|
||||||
|
|
||||||
|
class CloseButton(IconTextButton):
|
||||||
|
def __init__(self, label: str) -> None:
|
||||||
|
super().__init__(icon=CLOSE, label=label)
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
self.set_tooltip_text("Unset this server as favorite")
|
||||||
|
|
||||||
|
|
||||||
class WebButton(IconTextButton):
|
class WebButton(IconTextButton):
|
||||||
def __init__(self, label: str) -> None:
|
def __init__(self, label: str) -> None:
|
||||||
super().__init__(icon=WEB_BROWSER, label=label)
|
super().__init__(icon=WEB_BROWSER, label=label)
|
||||||
@@ -141,6 +167,8 @@ class RefreshButton(IconTextButton):
|
|||||||
|
|
||||||
def _on_refresh_clicked(self, button: Self) -> None:
|
def _on_refresh_clicked(self, button: Self) -> None:
|
||||||
"""Spawned in a thread"""
|
"""Spawned in a thread"""
|
||||||
|
if self.controller.get_active_treeview().is_loaded() is False:
|
||||||
|
return
|
||||||
self.is_clicked = True
|
self.is_clicked = True
|
||||||
self.controller.refresh_tree()
|
self.controller.refresh_tree()
|
||||||
# TODO: get server tab enum
|
# TODO: get server tab enum
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from dzgui.strings import connect_panel
|
|||||||
from dzgui.util.keys import is_ctrl_mask
|
from dzgui.util.keys import is_ctrl_mask
|
||||||
from dzgui.views.components.buttons import (
|
from dzgui.views.components.buttons import (
|
||||||
AddButton,
|
AddButton,
|
||||||
|
CloseButton,
|
||||||
CopyIpButton,
|
CopyIpButton,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
@@ -187,17 +188,27 @@ class FavPanel(Gtk.Frame):
|
|||||||
self.fav_button = SteamConnectButton()
|
self.fav_button = SteamConnectButton()
|
||||||
self.fav_button.connect("clicked", self._on_connect_clicked)
|
self.fav_button.connect("clicked", self._on_connect_clicked)
|
||||||
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
|
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
self.unset_button = CloseButton("Unset")
|
||||||
|
self.unset_button.connect("clicked", self._on_unset_clicked)
|
||||||
|
|
||||||
if favorite is None:
|
if favorite is None:
|
||||||
self.toggle_buttons(False)
|
self.toggle_buttons(False)
|
||||||
|
|
||||||
# NOTE: disable vscrollbar to prevent layout jumping behavior
|
# NOTE: disable vscrollbar to prevent layout jumping behavior
|
||||||
scrollable_label = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
|
self.scrollable_label = Gtk.ScrolledWindow(
|
||||||
scrollable_label.add(self.fav_label)
|
vscrollbar_policy=Gtk.PolicyType.NEVER, overlay_scrolling=False
|
||||||
|
)
|
||||||
|
self.scrollable_label.add(self.fav_label)
|
||||||
|
|
||||||
grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
|
grid = Gtk.Grid(margin=10, vexpand=False, column_spacing=15, row_spacing=5)
|
||||||
grid.attach(scrollable_label, 0, 0, 3, ROWS)
|
grid.attach(self.scrollable_label, 0, 0, 3, ROWS)
|
||||||
grid.attach_next_to(
|
grid.attach_next_to(
|
||||||
self.copy_button, scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS
|
self.unset_button, self.scrollable_label, Gtk.PositionType.RIGHT, COLS, ROWS
|
||||||
|
)
|
||||||
|
grid.attach_next_to(
|
||||||
|
self.copy_button, self.unset_button, Gtk.PositionType.RIGHT, COLS, ROWS
|
||||||
)
|
)
|
||||||
grid.attach_next_to(
|
grid.attach_next_to(
|
||||||
self.fav_button, self.copy_button, Gtk.PositionType.RIGHT, COLS, ROWS
|
self.fav_button, self.copy_button, Gtk.PositionType.RIGHT, COLS, ROWS
|
||||||
@@ -205,8 +216,16 @@ class FavPanel(Gtk.Frame):
|
|||||||
|
|
||||||
self.add(grid)
|
self.add(grid)
|
||||||
|
|
||||||
|
def _on_unset_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
try:
|
||||||
|
self.controller.unset_fav()
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
self.fav_label.set_text(connect_panel.favs_empty)
|
||||||
|
self.toggle_buttons(False)
|
||||||
|
|
||||||
def toggle_buttons(self, state: bool) -> None:
|
def toggle_buttons(self, state: bool) -> None:
|
||||||
for button in self.fav_button, self.copy_button:
|
for button in self.fav_button, self.copy_button, self.unset_button:
|
||||||
button.set_sensitive(state)
|
button.set_sensitive(state)
|
||||||
|
|
||||||
def _on_connect_clicked(self, button: Gtk.Button) -> None:
|
def _on_connect_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
|||||||
@@ -26,14 +26,11 @@ def validate_port(text: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def validate_ip_or_id(text: str) -> bool:
|
def validate_ip_truthy(text: str) -> bool:
|
||||||
try:
|
try:
|
||||||
validate_ip(text)
|
validate_ip(text)
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
if text.isdigit():
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@@ -124,7 +121,7 @@ class IpEntry(ValidatedEntry):
|
|||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
controller,
|
controller,
|
||||||
func=validate_ip_or_id,
|
func=validate_ip_truthy,
|
||||||
placeholder_text=connect_panel.placeholder,
|
placeholder_text=connect_panel.placeholder,
|
||||||
tooltip_text=connect_panel.entry_tooltip,
|
tooltip_text=connect_panel.entry_tooltip,
|
||||||
)
|
)
|
||||||
@@ -162,7 +159,8 @@ class APIEntry(Gtk.Box):
|
|||||||
self.entry.connect("icon-release", self._on_icon_release)
|
self.entry.connect("icon-release", self._on_icon_release)
|
||||||
self.entry.connect("activate", self._on_field_activated)
|
self.entry.connect("activate", self._on_field_activated)
|
||||||
|
|
||||||
self.submit = Gtk.Button(label="Submit")
|
# TODO: strings
|
||||||
|
self.submit = Gtk.Button(label="Validate")
|
||||||
self.submit.set_sensitive(False)
|
self.submit.set_sensitive(False)
|
||||||
self.submit.connect("clicked", self._on_submit)
|
self.submit.connect("clicked", self._on_submit)
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ class InfoEventBox(Gtk.EventBox):
|
|||||||
self.connect("leave-notify-event", self._on_leave_tooltip)
|
self.connect("leave-notify-event", self._on_leave_tooltip)
|
||||||
self.add(box)
|
self.add(box)
|
||||||
|
|
||||||
|
def set_icon_yalign(self, offset: float) -> None:
|
||||||
|
self.icon.set_property("yalign", offset)
|
||||||
|
|
||||||
def _on_enter_tooltip(
|
def _on_enter_tooltip(
|
||||||
self, eventbox: Gtk.EventBox, eventcrossing: Gdk.EventCrossing
|
self, eventbox: Gtk.EventBox, eventcrossing: Gdk.EventCrossing
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -80,14 +80,18 @@ class ModSelectionPanel(Gtk.Box):
|
|||||||
self.highlight_stale_button.set_sensitive(True)
|
self.highlight_stale_button.set_sensitive(True)
|
||||||
|
|
||||||
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
|
def _on_mods_updated(self, emitter: "Emitter", msg: str, mods: int) -> None:
|
||||||
if mods < 1:
|
self._toggle_panel_sensitivity(mods)
|
||||||
self.main_panel.set_sensitive(False)
|
|
||||||
self.stale_panel.set_sensitive(False)
|
def _toggle_panel_sensitivity(self, mods: int) -> None:
|
||||||
|
state = bool(mods)
|
||||||
|
for el in self.main_panel, self.stale_panel:
|
||||||
|
el.set_sensitive(state)
|
||||||
|
|
||||||
def _on_mods_highlighted(self, emitter: "Emitter") -> None:
|
def _on_mods_highlighted(self, emitter: "Emitter") -> None:
|
||||||
self.swap_sensitive(True)
|
self.swap_sensitive(True)
|
||||||
|
|
||||||
def _on_mod_page_toggled(self, emitter: "Emitter", state: bool) -> None:
|
def _on_mod_page_toggled(self, emitter: "Emitter", state: bool, mods: int) -> None:
|
||||||
|
self._toggle_panel_sensitivity(mods)
|
||||||
self.set_visible(state)
|
self.set_visible(state)
|
||||||
|
|
||||||
def swap_sensitive(self, state: bool) -> None:
|
def swap_sensitive(self, state: bool) -> None:
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class RightPanel(Gtk.Box):
|
|||||||
return
|
return
|
||||||
self.copying = True
|
self.copying = True
|
||||||
version = self.version_label.get_text()
|
version = self.version_label.get_text()
|
||||||
|
# TODO: strings
|
||||||
self.version_label.set_text("Copied!")
|
self.version_label.set_text("Copied!")
|
||||||
copy_clipboard(version)
|
copy_clipboard(version)
|
||||||
GLib.timeout_add_seconds(1, revert)
|
GLib.timeout_add_seconds(1, revert)
|
||||||
@@ -41,7 +41,7 @@ class BootDialog(Gtk.Dialog):
|
|||||||
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
|
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
title=dialog_header,
|
title=dialog_header,
|
||||||
parent=parent,
|
transient_for=parent,
|
||||||
modal=True,
|
modal=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class FilePicker(Gtk.FileChooserDialog):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
title=picker.title,
|
title=picker.title,
|
||||||
action=Gtk.FileChooserAction.SAVE,
|
action=Gtk.FileChooserAction.SAVE,
|
||||||
parent=parent,
|
transient_for=parent,
|
||||||
resizable=True,
|
resizable=True,
|
||||||
)
|
)
|
||||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
||||||
|
|||||||
@@ -107,23 +107,26 @@ class WaitDialog(GenericDialog):
|
|||||||
self.cur_job = 1
|
self.cur_job = 1
|
||||||
|
|
||||||
self.cancel = Gtk.Button(label="Cancel", halign=Gtk.Align.CENTER)
|
self.cancel = Gtk.Button(label="Cancel", halign=Gtk.Align.CENTER)
|
||||||
self.cancel.connect("clicked", lambda _: self.controller.set_cancel_event())
|
self.cancel.connect("clicked", self._on_cancel_clicked)
|
||||||
|
|
||||||
self.connect("delete-event", lambda widget, event: True)
|
self.connect("delete-event", lambda widget, event: True)
|
||||||
content = self.get_content_area()
|
content = self.get_content_area()
|
||||||
spinner = Gtk.Spinner()
|
|
||||||
self.prog = Gtk.ProgressBar()
|
self.prog = Gtk.ProgressBar()
|
||||||
|
|
||||||
content.pack_end(self.cancel, NO_EXPAND, NO_FILL, 0)
|
spinner_box = Gtk.Box(
|
||||||
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
|
orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER
|
||||||
|
)
|
||||||
|
spinner = Gtk.Spinner()
|
||||||
|
spinner_box.add(spinner)
|
||||||
|
|
||||||
if self.jobs > 1:
|
if self.jobs > 1:
|
||||||
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
|
content.pack_start(self.prog, NO_EXPAND, NO_FILL, 0)
|
||||||
else:
|
else:
|
||||||
|
content.pack_start(spinner_box, NO_EXPAND, NO_FILL, 0)
|
||||||
spinner.start()
|
spinner.start()
|
||||||
|
|
||||||
if show_cancel is False:
|
if show_cancel is True:
|
||||||
self.connect("realize", lambda _: self.cancel.set_visible(False))
|
content.pack_start(self.cancel, NO_EXPAND, NO_FILL, 0)
|
||||||
|
|
||||||
def update_text(self, msg: str) -> None:
|
def update_text(self, msg: str) -> None:
|
||||||
self.format_secondary_text(msg)
|
self.format_secondary_text(msg)
|
||||||
@@ -135,6 +138,11 @@ class WaitDialog(GenericDialog):
|
|||||||
self.prog.set_fraction(fraction)
|
self.prog.set_fraction(fraction)
|
||||||
self.cur_job += 1
|
self.cur_job += 1
|
||||||
|
|
||||||
|
def _on_cancel_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
# TODO: strings
|
||||||
|
GLib.idle_add(self.update_text, "Caught cancel signal, cleaning up")
|
||||||
|
self.controller.set_cancel_event()
|
||||||
|
|
||||||
def show_cancel(self, state: bool) -> None:
|
def show_cancel(self, state: bool) -> None:
|
||||||
self.cancel.set_visible(state)
|
self.cancel.set_visible(state)
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import html
|
||||||
from typing import Self, TYPE_CHECKING
|
from typing import Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import EXPAND, FILL
|
from dzgui.const.constants import EXPAND, FILL
|
||||||
@@ -65,6 +66,7 @@ class ServerDialog(GenericDialog):
|
|||||||
|
|
||||||
class ServerDetailsDialog(ServerDialog):
|
class ServerDetailsDialog(ServerDialog):
|
||||||
def __init__(self, controller: "Controller", details: "Details"):
|
def __init__(self, controller: "Controller", details: "Details"):
|
||||||
|
# TODO: server name should also be packed in details struct
|
||||||
name = controller.get_server_name()
|
name = controller.get_server_name()
|
||||||
super().__init__(controller, strings.server_details, name, menu=None)
|
super().__init__(controller, strings.server_details, name, menu=None)
|
||||||
|
|
||||||
@@ -109,7 +111,7 @@ class ServerDetailsDialog(ServerDialog):
|
|||||||
self.view.set_model(self.store)
|
self.view.set_model(self.store)
|
||||||
text = details.description
|
text = details.description
|
||||||
text = format_hyperlinks(text)
|
text = format_hyperlinks(text)
|
||||||
self.description.set_markup(text)
|
self.description.set_markup(html.escape(text))
|
||||||
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
|
|||||||
+152
-50
@@ -1,11 +1,13 @@
|
|||||||
|
import os
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Self
|
from typing import Any, Callable, Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.probe import test_steam_api, test_bm_api
|
from dzgui.api.probe import test_steam_api
|
||||||
|
from dzgui.api.shortcuts import add_steam_shortcut
|
||||||
from dzgui.api.steam import get_steam_paths
|
from dzgui.api.steam import get_steam_paths
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
@@ -14,7 +16,10 @@ from dzgui.const.constants import (
|
|||||||
LEGACY_CONFIG_PATH,
|
LEGACY_CONFIG_PATH,
|
||||||
)
|
)
|
||||||
from dzgui.const.boilerplate import config_boilerplate
|
from dzgui.const.boilerplate import config_boilerplate
|
||||||
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP
|
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||||
|
from dzgui.const.enum import Preferences
|
||||||
|
from dzgui.config import freedesktop
|
||||||
|
from dzgui.config.query import lookup
|
||||||
from dzgui.init.migrate import migrate_legacy_conf
|
from dzgui.init.migrate import migrate_legacy_conf
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
@@ -30,17 +35,27 @@ import gi
|
|||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
|
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dzgui.config.xdg import Xdg
|
||||||
|
|
||||||
|
|
||||||
class PageNum(Enum):
|
class PageNum(Enum):
|
||||||
INTRO = 1
|
INTRO = 1
|
||||||
HAS_CONFIG = 2
|
HAS_CONFIG = 2
|
||||||
STEAM_PATH = 3
|
STEAM_PATH = 3
|
||||||
STEAM_API = 4
|
STEAM_API = 4
|
||||||
BM_API = 5
|
USER_PREFS = 5
|
||||||
USER_PREFS = 6
|
SHORTCUTS = 6
|
||||||
FINAL = 7
|
FINAL = 7
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalPageMixin:
|
||||||
|
"""Marks optional pages as advanceable"""
|
||||||
|
|
||||||
|
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
||||||
|
EMITTER.emit("step_complete")
|
||||||
|
|
||||||
|
|
||||||
class DescriptionArea(Gtk.Box):
|
class DescriptionArea(Gtk.Box):
|
||||||
def __init__(self, text: str):
|
def __init__(self, text: str):
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL)
|
||||||
@@ -58,7 +73,7 @@ class Progress(Gtk.ProgressBar):
|
|||||||
|
|
||||||
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||||
def __init__(self, enum: PageNum, heading: str, description: str):
|
def __init__(self, enum: PageNum, heading: str, description: str):
|
||||||
super().__init__()
|
super().__init__(overlay_scrolling=False)
|
||||||
|
|
||||||
self.enum = enum
|
self.enum = enum
|
||||||
self.page_type: Gtk.AssistantPageType
|
self.page_type: Gtk.AssistantPageType
|
||||||
@@ -82,6 +97,7 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
margin_top=50,
|
margin_top=50,
|
||||||
spacing=20,
|
spacing=20,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
self.prog = Progress()
|
self.prog = Progress()
|
||||||
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
||||||
@@ -141,6 +157,10 @@ class NotificationFrame(Gtk.Frame):
|
|||||||
if error:
|
if error:
|
||||||
add_class(self, "error-frame")
|
add_class(self, "error-frame")
|
||||||
|
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
wrapped = textwrap.fill(text, width=80)
|
||||||
|
self.label.set_markup(wrapped)
|
||||||
|
|
||||||
|
|
||||||
class APIValidationPage(ScrolledWizardPage):
|
class APIValidationPage(ScrolledWizardPage):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -202,23 +222,6 @@ class APIValidationPage(ScrolledWizardPage):
|
|||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
|
|
||||||
|
|
||||||
class BMValidationPage(APIValidationPage):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(
|
|
||||||
enum=PageNum.BM_API,
|
|
||||||
heading=wizard.heading_bm_api,
|
|
||||||
description=wizard.blurb_bm_api,
|
|
||||||
link=BM_API_SETUP,
|
|
||||||
func=self._validate,
|
|
||||||
)
|
|
||||||
|
|
||||||
@call_on_thread("", show_dialog=False)
|
|
||||||
def _validate(self, key: str) -> None:
|
|
||||||
is_valid = test_bm_api(key.strip())
|
|
||||||
cleanup = StoredFunc(self._cleanup, is_valid, key)
|
|
||||||
self.thread_man.set_cleanup_func(cleanup)
|
|
||||||
|
|
||||||
|
|
||||||
class SteamValidationPage(APIValidationPage):
|
class SteamValidationPage(APIValidationPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -339,6 +342,7 @@ class ConfigMigrationPage(ScrolledWizardPage):
|
|||||||
def _on_import_clicked(self, button: Gtk.Button) -> None:
|
def _on_import_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.grid.set_sensitive(False)
|
self.grid.set_sensitive(False)
|
||||||
try:
|
try:
|
||||||
|
# TODO: this could be deferred to the final page (prevents accidental destruction of dialog via ESC)
|
||||||
migrate_legacy_conf(self.config)
|
migrate_legacy_conf(self.config)
|
||||||
self.migrated = True
|
self.migrated = True
|
||||||
self.success_box.set_visible(True)
|
self.success_box.set_visible(True)
|
||||||
@@ -423,23 +427,27 @@ class CompletionPage(ScrolledWizardPage):
|
|||||||
|
|
||||||
|
|
||||||
class Assistant(Gtk.Assistant):
|
class Assistant(Gtk.Assistant):
|
||||||
def __init__(self, is_deck: bool, config: Path):
|
def __init__(self, is_deck: bool, XDG: "Xdg"):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if is_deck:
|
if is_deck:
|
||||||
self.fullscreen()
|
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||||
|
self.set_default_size(1085, 670)
|
||||||
else:
|
else:
|
||||||
self.set_default_size(1500, 900)
|
self.set_default_size(1500, 900)
|
||||||
|
|
||||||
self.config_path = config
|
self.is_binary = False if os.getenv("PYAPP") is None else True
|
||||||
|
self.config_path = XDG.config
|
||||||
|
|
||||||
self.config_values: dict[str, Any] = config_boilerplate
|
self.config_values: dict[str, Any] = config_boilerplate
|
||||||
|
|
||||||
|
self.setup_complete = False
|
||||||
|
|
||||||
self.page1 = IntroductionPage()
|
self.page1 = IntroductionPage()
|
||||||
self.page2 = ConfigMigrationPage(config)
|
self.page2 = ConfigMigrationPage(XDG.config)
|
||||||
self.page3 = SteamPathPage()
|
self.page3 = SteamPathPage()
|
||||||
self.page4 = SteamValidationPage()
|
self.page4 = SteamValidationPage()
|
||||||
self.page5 = BMValidationPage()
|
self.page5 = PreferencesPage()
|
||||||
self.page6 = PreferencesPage()
|
self.page6 = ShortcutCreationPage(XDG.shortcut)
|
||||||
self.page7 = CompletionPage()
|
self.page7 = CompletionPage()
|
||||||
|
|
||||||
self.set_forward_page_func(self._advance_page)
|
self.set_forward_page_func(self._advance_page)
|
||||||
@@ -460,7 +468,13 @@ class Assistant(Gtk.Assistant):
|
|||||||
self.page7,
|
self.page7,
|
||||||
):
|
):
|
||||||
# NOTE: skip config migration page if no legacy config file
|
# NOTE: skip config migration page if no legacy config file
|
||||||
if page == self.page2 and self.has_legacy_config is False:
|
if (
|
||||||
|
isinstance(page, ConfigMigrationPage)
|
||||||
|
and self.has_legacy_config is False
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
# NOTE: disabled for now on system-provided packages
|
||||||
|
if isinstance(page, ShortcutCreationPage) and not self.is_binary:
|
||||||
continue
|
continue
|
||||||
self._add_page(page, page.get_page_type())
|
self._add_page(page, page.get_page_type())
|
||||||
|
|
||||||
@@ -475,30 +489,38 @@ class Assistant(Gtk.Assistant):
|
|||||||
|
|
||||||
def _advance_page(self, index: int) -> int:
|
def _advance_page(self, index: int) -> int:
|
||||||
page = self.get_nth_page(index)
|
page = self.get_nth_page(index)
|
||||||
# TODO: use enums
|
|
||||||
match page:
|
match page:
|
||||||
case self.page1:
|
case IntroductionPage():
|
||||||
pass
|
pass
|
||||||
case self.page2:
|
case ConfigMigrationPage():
|
||||||
if self.page2.is_migrated():
|
if page.is_migrated():
|
||||||
return self.get_n_pages() - 1
|
steam_path = lookup(self.config_path, Preferences.DEFAULT)
|
||||||
case self.page3:
|
self.page6.set_steam_path(steam_path)
|
||||||
|
offset = 1 if not self.is_binary else 2
|
||||||
|
self.setup_complete = True
|
||||||
|
return self.get_n_pages() - offset
|
||||||
|
case SteamPathPage():
|
||||||
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
||||||
case self.page4:
|
case SteamValidationPage():
|
||||||
self.config_values["steam_api"] = page.get_api_key()
|
self.config_values["steam_api"] = page.get_api_key()
|
||||||
case self.page5:
|
case PreferencesPage():
|
||||||
self.config_values["bm_api"] = page.get_api_key()
|
|
||||||
# NOTE: collects config values before advancing to last page
|
# NOTE: collects config values before advancing to last page
|
||||||
case self.page6:
|
name, use_miles, client = self.page5.get_prefs()
|
||||||
name, use_miles, client = self.page6.get_prefs()
|
|
||||||
self.config_values["name"] = name
|
self.config_values["name"] = name
|
||||||
self.config_values["use_miles"] = use_miles
|
self.config_values["use_miles"] = use_miles
|
||||||
self.config_values["client"] = client
|
self.config_values["client"] = client
|
||||||
self.write_config()
|
self.write_config()
|
||||||
|
self.page6.set_steam_path(self.config_values["default_steam_path"])
|
||||||
|
self.setup_complete = True
|
||||||
|
case ShortcutCreationPage():
|
||||||
|
page.create_shortcuts()
|
||||||
case _:
|
case _:
|
||||||
raise AttributeError("Trying to advance a non-canonical page")
|
raise AttributeError("Trying to advance a non-canonical page")
|
||||||
return index + 1
|
return index + 1
|
||||||
|
|
||||||
|
def is_setup_complete(self) -> bool:
|
||||||
|
return self.setup_complete
|
||||||
|
|
||||||
def destroy_and_quit(self, widget: Self) -> None:
|
def destroy_and_quit(self, widget: Self) -> None:
|
||||||
self.destroy()
|
self.destroy()
|
||||||
Gtk.main_quit()
|
Gtk.main_quit()
|
||||||
@@ -535,14 +557,91 @@ class Assistant(Gtk.Assistant):
|
|||||||
bar.set_fraction(fraction)
|
bar.set_fraction(fraction)
|
||||||
bar.set_text(f"{page_num}/{total}")
|
bar.set_text(f"{page_num}/{total}")
|
||||||
|
|
||||||
# NOTE: disable forward action
|
if not isinstance(page, IntroductionPage):
|
||||||
# TODO: use page enums
|
|
||||||
if page == self.page5:
|
|
||||||
return
|
|
||||||
if page != self.page1:
|
|
||||||
EMITTER.emit("step_pending")
|
EMITTER.emit("step_pending")
|
||||||
|
|
||||||
|
|
||||||
|
class CheckboxWithLabel(Gtk.Box):
|
||||||
|
def __init__(self, text: str, blurb_text: str) -> None:
|
||||||
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||||
|
|
||||||
|
self.button = Gtk.CheckButton(label=text)
|
||||||
|
self.button.set_active(True)
|
||||||
|
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20)
|
||||||
|
wrapped = textwrap.fill(blurb_text, width=100)
|
||||||
|
label.set_markup(f"- {wrapped}")
|
||||||
|
|
||||||
|
for el in self.button, label:
|
||||||
|
self.add(el)
|
||||||
|
|
||||||
|
def get_checkbox(self) -> Gtk.CheckButton:
|
||||||
|
return self.button
|
||||||
|
|
||||||
|
def get_active(self) -> bool:
|
||||||
|
return self.button.get_active()
|
||||||
|
|
||||||
|
def set_active(self, state: bool) -> None:
|
||||||
|
self.button.set_active(state)
|
||||||
|
|
||||||
|
|
||||||
|
class ShortcutCreationPage(OptionalPageMixin, ScrolledWizardPage): # type: ignore
|
||||||
|
def __init__(self, shortcut: Path) -> None:
|
||||||
|
super().__init__(
|
||||||
|
enum=PageNum.SHORTCUTS,
|
||||||
|
heading=wizard.heading_shortcuts,
|
||||||
|
description=wizard.blurb_shortcuts,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.steam_path: Path
|
||||||
|
self.shortcut_path = shortcut
|
||||||
|
self.checks_area = Gtk.Box(
|
||||||
|
orientation=Gtk.Orientation.VERTICAL, spacing=10, margin_top=20
|
||||||
|
)
|
||||||
|
self.page_type = Gtk.AssistantPageType.INTRO
|
||||||
|
|
||||||
|
label, blurb = wizard.checkbox_steam_shortcut
|
||||||
|
self.steam_checkbox = CheckboxWithLabel(label, blurb)
|
||||||
|
|
||||||
|
label, blurb = wizard.checkbox_start_menu
|
||||||
|
self.start_menu_checkbox = CheckboxWithLabel(label, blurb)
|
||||||
|
cb = self.start_menu_checkbox.get_checkbox()
|
||||||
|
cb.connect("toggled", self._on_start_menu_toggled)
|
||||||
|
|
||||||
|
label, blurb = wizard.checkbox_desktop_shortcut
|
||||||
|
self.desktop_checkbox = CheckboxWithLabel(label, blurb)
|
||||||
|
|
||||||
|
for el in (
|
||||||
|
self.steam_checkbox,
|
||||||
|
self.start_menu_checkbox,
|
||||||
|
self.desktop_checkbox,
|
||||||
|
):
|
||||||
|
self.checks_area.add(el)
|
||||||
|
|
||||||
|
self.add_start(self.checks_area)
|
||||||
|
self.show_all()
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_start_menu_toggled(self, button: Gtk.CheckButton) -> None:
|
||||||
|
state = button.get_active()
|
||||||
|
if not state:
|
||||||
|
self.desktop_checkbox.set_active(state)
|
||||||
|
self.desktop_checkbox.set_sensitive(state)
|
||||||
|
|
||||||
|
def set_steam_path(self, path: Path) -> None:
|
||||||
|
self.steam_path = path
|
||||||
|
|
||||||
|
def create_shortcuts(self) -> None:
|
||||||
|
# NOTE: best-effort, permissive even on failure (page is already marked as complete)
|
||||||
|
if self.steam_checkbox.get_active():
|
||||||
|
add_steam_shortcut(self.steam_path, self.shortcut_path)
|
||||||
|
|
||||||
|
if self.start_menu_checkbox.get_active():
|
||||||
|
desktop_file = freedesktop.write_desktop_file(self.shortcut_path)
|
||||||
|
|
||||||
|
if self.desktop_checkbox.get_active():
|
||||||
|
freedesktop.write_desktop_shortcut(desktop_file)
|
||||||
|
|
||||||
|
|
||||||
class SteamPathPage(ScrolledWizardPage):
|
class SteamPathPage(ScrolledWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
@@ -599,17 +698,20 @@ class SteamPathPage(ScrolledWizardPage):
|
|||||||
|
|
||||||
|
|
||||||
class SetupWizard(Gtk.Application):
|
class SetupWizard(Gtk.Application):
|
||||||
def __init__(self, is_deck: bool, config: Path) -> None:
|
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
GLib.set_prgname(APP_NAME)
|
GLib.set_prgname(APP_NAME)
|
||||||
Window(is_deck, config)
|
self.win = Window(is_deck, XDG)
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
|
def is_setup_complete(self) -> int:
|
||||||
|
return self.win.assistant.is_setup_complete()
|
||||||
|
|
||||||
|
|
||||||
class Window(Gtk.Window):
|
class Window(Gtk.Window):
|
||||||
def __init__(self, is_deck: bool, config: Path) -> None:
|
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
||||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||||
Assistant(is_deck, config)
|
self.assistant = Assistant(is_deck, XDG)
|
||||||
|
|
||||||
|
|
||||||
class Emitter(GObject.GObject):
|
class Emitter(GObject.GObject):
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ class CursorMixin:
|
|||||||
end = len(model) - 1
|
end = len(model) - 1
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
cur_row = self.get_focused_row_index() # type: ignore
|
cur_row = self.get_focused_row_index() # type: ignore
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
if position == CursorPosition.DOWN:
|
if position == CursorPosition.DOWN:
|
||||||
if cur_row == end:
|
if cur_row == end:
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
@@ -32,15 +33,79 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
|
|
||||||
# TODO: should long text be wrapped?
|
# TODO: should long text be wrapped?
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
formatted = format_pango(changelog)
|
self.box = Gtk.Box(
|
||||||
self.changelog_label = Gtk.Label(valign=Gtk.Align.START, margin=15)
|
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
|
||||||
self.changelog_label.set_markup(formatted)
|
)
|
||||||
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
self.box.add(self.changelog_label)
|
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
|
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("key-press-event", self._on_esc_keypress)
|
self.connect("key-press-event", self._on_esc_keypress)
|
||||||
|
|
||||||
|
changes = self._parse(changelog)
|
||||||
|
self._generate_nodes(changes)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.grab_focus()
|
self.grab_focus()
|
||||||
|
|
||||||
|
def _parse_version(self, version: str) -> tuple[str, str]:
|
||||||
|
pattern = r"(^##\s\[)(.+)(\] )(.+$)"
|
||||||
|
match = re.match(pattern, version)
|
||||||
|
if match:
|
||||||
|
version = match.group(2)
|
||||||
|
date = match.group(4)
|
||||||
|
return version, date
|
||||||
|
raise ValueError("Changelog parse error")
|
||||||
|
|
||||||
|
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
|
||||||
|
for header, changes in releases:
|
||||||
|
for ind in (0, -1):
|
||||||
|
if changes[ind] == "":
|
||||||
|
del changes[ind]
|
||||||
|
text = "\n".join(changes)
|
||||||
|
formatted = format_pango(text)
|
||||||
|
|
||||||
|
container = Gtk.Box(
|
||||||
|
valign=Gtk.Align.START, halign=Gtk.Align.START
|
||||||
|
)
|
||||||
|
label = Gtk.Label()
|
||||||
|
label.set_markup(formatted)
|
||||||
|
container.add(label)
|
||||||
|
|
||||||
|
version, date = self._parse_version(header)
|
||||||
|
button_text = f"{version} ({date})"
|
||||||
|
button = Gtk.Button(label=button_text, margin_start=5)
|
||||||
|
|
||||||
|
expander = Gtk.Expander()
|
||||||
|
expander.set_label_widget(button)
|
||||||
|
expander.add(container)
|
||||||
|
expander.connect("activate", self._on_expand, container)
|
||||||
|
self.box.add(expander)
|
||||||
|
|
||||||
|
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
|
||||||
|
"""
|
||||||
|
Wayland/some themes do not expect widgets to have a size allocation until expanded.
|
||||||
|
Trying to set margins a priori may cause negative allocation,
|
||||||
|
as the margins are subtracted from zero. In effect, the widget is not realized
|
||||||
|
until it is expanded for the first time.
|
||||||
|
"""
|
||||||
|
container.set_margin_start(15)
|
||||||
|
container.set_margin_top(15)
|
||||||
|
container.set_margin_bottom(15)
|
||||||
|
|
||||||
|
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
||||||
|
release = ""
|
||||||
|
releases: list[tuple[str, list[str]]] = []
|
||||||
|
release_notes: list[str] = []
|
||||||
|
for line in changelog.splitlines():
|
||||||
|
if line.startswith("# "):
|
||||||
|
continue
|
||||||
|
if line.startswith("## "):
|
||||||
|
if release != "":
|
||||||
|
releases.append((release, release_notes))
|
||||||
|
release_notes = []
|
||||||
|
release = ""
|
||||||
|
release = line
|
||||||
|
continue
|
||||||
|
release_notes.append(line.rstrip())
|
||||||
|
return releases
|
||||||
@@ -103,4 +103,4 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
|||||||
return grid
|
return grid
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.box.grab_focus()
|
self.grab_focus()
|
||||||
@@ -64,10 +64,15 @@ class Mods(Gtk.Box):
|
|||||||
self.controller.open_offline()
|
self.controller.open_offline()
|
||||||
|
|
||||||
def _on_unmap(self, widget: Self) -> None:
|
def _on_unmap(self, widget: Self) -> None:
|
||||||
self.emitter.emit("mod_page_toggled", False)
|
self.emitter.emit("mod_page_toggled", False, 0)
|
||||||
|
|
||||||
def _on_map(self, widget: Self) -> None:
|
def _on_map(self, widget: Self) -> None:
|
||||||
self.emitter.emit("mod_page_toggled", True)
|
model = self.tree.get_model()
|
||||||
|
if model is None:
|
||||||
|
mods = 0
|
||||||
|
else:
|
||||||
|
mods = len(model)
|
||||||
|
self.emitter.emit("mod_page_toggled", True, mods)
|
||||||
# TODO: delegation
|
# TODO: delegation
|
||||||
# NOTE: handles going back from offline mods page
|
# NOTE: handles going back from offline mods page
|
||||||
# more generic cache restoration method
|
# more generic cache restoration method
|
||||||
|
|||||||
@@ -41,9 +41,18 @@ class FolderError(Enum):
|
|||||||
FOLDER_CHANGED = 3
|
FOLDER_CHANGED = 3
|
||||||
|
|
||||||
|
|
||||||
class PageHeading(Gtk.Label):
|
class PageHeading(Gtk.Box):
|
||||||
def __init__(self, label: str) -> None:
|
def __init__(self, label: str, controller: "Controller") -> None:
|
||||||
super().__init__(label=label, halign=Gtk.Align.CENTER)
|
super().__init__(
|
||||||
|
halign=Gtk.Align.CENTER, orientation=Gtk.Orientation.HORIZONTAL
|
||||||
|
)
|
||||||
|
|
||||||
|
self.label = Gtk.Label(label=label)
|
||||||
|
self.eb = InfoEventBox(offline.heading_disclaimer, controller)
|
||||||
|
self.eb.set_icon_yalign(0.7)
|
||||||
|
|
||||||
|
for el in self.label, self.eb:
|
||||||
|
self.add(el)
|
||||||
|
|
||||||
css.add_class(self, "page-heading")
|
css.add_class(self, "page-heading")
|
||||||
|
|
||||||
@@ -434,7 +443,8 @@ class OfflineLoader(Gtk.Box):
|
|||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
self.offline_man = OfflineManager(controller)
|
self.offline_man = OfflineManager(controller)
|
||||||
|
|
||||||
self.add(PageHeading(offline.heading))
|
heading = PageHeading(offline.heading, controller)
|
||||||
|
self.add(heading)
|
||||||
|
|
||||||
self.local_frame = ModFrame(self, controller, offline.local_frame)
|
self.local_frame = ModFrame(self, controller, offline.local_frame)
|
||||||
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
||||||
@@ -511,3 +521,6 @@ class OfflineLoader(Gtk.Box):
|
|||||||
self.mission_frame.present_folder_changed(mission)
|
self.mission_frame.present_folder_changed(mission)
|
||||||
return
|
return
|
||||||
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
|
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
|
||||||
|
|
||||||
|
def grab_content_area(self) -> None:
|
||||||
|
self.local_frame.tree.grab_focus()
|
||||||
@@ -15,16 +15,19 @@ from dzgui.const.constants import (
|
|||||||
VIEW_CONCEAL,
|
VIEW_CONCEAL,
|
||||||
VIEW_REVEAL,
|
VIEW_REVEAL,
|
||||||
)
|
)
|
||||||
from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
|
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||||
from dzgui.const.enum import Preferences, ServerTab
|
from dzgui.const.enum import Preferences, ServerTab
|
||||||
from dzgui.strings import errors, options
|
from dzgui.strings import errors, options
|
||||||
from dzgui.util import strings, css, open_links
|
from dzgui.util import strings, css, open_links
|
||||||
|
|
||||||
|
from dzgui.views.components.box import VBox
|
||||||
|
from dzgui.views.components.eventbox import InfoEventBox
|
||||||
from dzgui.views.components.labels import LeftLabel
|
from dzgui.views.components.labels import LeftLabel
|
||||||
from dzgui.views.components.buttons import WebButton
|
from dzgui.views.components.buttons import WebButton
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.components.misc import ClientCombo
|
from dzgui.views.components.misc import ClientCombo
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||||
|
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -37,6 +40,37 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.controllers.emitter import Emitter
|
from dzgui.controllers.emitter import Emitter
|
||||||
|
|
||||||
|
|
||||||
|
class ToggleField(Gtk.Box):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
first_option: str,
|
||||||
|
second_option: str,
|
||||||
|
context: Preferences,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(spacing=5, halign=Gtk.Align.START)
|
||||||
|
|
||||||
|
self.controller = controller
|
||||||
|
self.context = context
|
||||||
|
self.radio1 = Gtk.RadioButton.new_with_label(None, first_option)
|
||||||
|
self.radio2 = Gtk.RadioButton.new_from_widget(self.radio1)
|
||||||
|
self.radio2.set_label(second_option)
|
||||||
|
self.pack_start(self.radio1, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||||
|
self.pack_start(self.radio2, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||||
|
|
||||||
|
def set_suboption_active(self, state: bool) -> None:
|
||||||
|
# NOTE: defer connection of signal until after state is set
|
||||||
|
self.radio2.set_active(state)
|
||||||
|
self.radio1.connect("toggled", self._on_radio_toggled, self.context)
|
||||||
|
|
||||||
|
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
||||||
|
self.controller.toggle_config(context)
|
||||||
|
|
||||||
|
def set_sensitive(self, state: bool) -> None:
|
||||||
|
for el in self.radio1, self.radio2:
|
||||||
|
el.set_sensitive(state)
|
||||||
|
|
||||||
|
|
||||||
class ShortHBox(Gtk.Box):
|
class ShortHBox(Gtk.Box):
|
||||||
def __init__(self, widget: Gtk.Widget) -> None:
|
def __init__(self, widget: Gtk.Widget) -> None:
|
||||||
super().__init__(spacing=5, halign=Gtk.Align.START)
|
super().__init__(spacing=5, halign=Gtk.Align.START)
|
||||||
@@ -44,10 +78,9 @@ class ShortHBox(Gtk.Box):
|
|||||||
self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING)
|
self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||||
|
|
||||||
|
|
||||||
class Options(Gtk.Box):
|
class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||||
def __init__(self, controller: "Controller"):
|
def __init__(self, controller: "Controller"):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
|
||||||
margin_start=10,
|
margin_start=10,
|
||||||
margin_end=10,
|
margin_end=10,
|
||||||
)
|
)
|
||||||
@@ -60,29 +93,16 @@ class Options(Gtk.Box):
|
|||||||
self.DEFAULT_WIDTH = 1
|
self.DEFAULT_WIDTH = 1
|
||||||
self.DEFAULT_HEIGHT = 1
|
self.DEFAULT_HEIGHT = 1
|
||||||
|
|
||||||
label = Gtk.Label(label=strings.options.header)
|
|
||||||
label.set_halign(Gtk.Align.CENTER)
|
|
||||||
css.add_class(label, "page-heading")
|
|
||||||
self.add(label)
|
|
||||||
|
|
||||||
self.steam_entry: Gtk.Entry
|
self.steam_entry: Gtk.Entry
|
||||||
self.bm_entry: Gtk.Entry
|
|
||||||
|
|
||||||
self.steam = WebButton(label=strings.options.steam_web)
|
self.steam = WebButton(label=strings.options.steam_web)
|
||||||
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
||||||
|
|
||||||
self.bm = WebButton(label=strings.options.bm_web)
|
|
||||||
self.bm.connect("clicked", self._on_link_button_clicked, BM_API_SETUP)
|
|
||||||
|
|
||||||
self.steam_box = self._make_submit_field(
|
self.steam_box = self._make_submit_field(
|
||||||
strings.options.enter_steam, Preferences.STEAM, True
|
strings.options.enter_steam, Preferences.STEAM, True
|
||||||
)
|
)
|
||||||
self.bm_box = self._make_submit_field(
|
|
||||||
strings.options.enter_bm, Preferences.BM, True
|
|
||||||
)
|
|
||||||
api_rows = [
|
api_rows = [
|
||||||
[LeftLabel(strings.options.steam_placeholder), self.steam_box],
|
[LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
|
||||||
[LeftLabel(strings.options.bm_placeholder), self.bm_box],
|
|
||||||
]
|
]
|
||||||
|
|
||||||
self.player_box = self._make_submit_field(
|
self.player_box = self._make_submit_field(
|
||||||
@@ -92,19 +112,26 @@ class Options(Gtk.Box):
|
|||||||
# TODO: make submit field a standalone class
|
# TODO: make submit field a standalone class
|
||||||
self.player_box.get_children()[0].set_width_chars(30) # type: ignore
|
self.player_box.get_children()[0].set_width_chars(30) # type: ignore
|
||||||
|
|
||||||
self.fullscreen_toggle = self.make_binary_radio(
|
self.fullscreen_toggle = ToggleField(
|
||||||
|
self.controller,
|
||||||
strings.options.last_used,
|
strings.options.last_used,
|
||||||
strings.options.always_fs,
|
strings.options.always_fs,
|
||||||
Preferences.WINDOW,
|
Preferences.WINDOW,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
eb = InfoEventBox(
|
||||||
|
"This option is not available on Steam Deck.", self.controller
|
||||||
|
)
|
||||||
|
self.fullscreen_toggle.add(eb)
|
||||||
|
|
||||||
self.client_combo = ClientCombo()
|
self.client_combo = ClientCombo()
|
||||||
self.client_combo.connect("changed", self._on_client_changed)
|
self.client_combo.connect("changed", self._on_client_changed)
|
||||||
|
|
||||||
client_hbox = ShortHBox(self.client_combo)
|
client_hbox = ShortHBox(self.client_combo)
|
||||||
|
|
||||||
self.distance_toggle = self.make_binary_radio(
|
self.distance_toggle = ToggleField(
|
||||||
strings.options.km, strings.options.mi, Preferences.DIST
|
self.controller, strings.options.km, strings.options.mi, Preferences.DIST
|
||||||
)
|
)
|
||||||
|
|
||||||
combo_store = Gtk.ListStore(str, object)
|
combo_store = Gtk.ListStore(str, object)
|
||||||
@@ -144,18 +171,6 @@ class Options(Gtk.Box):
|
|||||||
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
api_grid = self._make_grid(api_rows)
|
api_grid = self._make_grid(api_rows)
|
||||||
api_box.add(api_grid)
|
api_box.add(api_grid)
|
||||||
api_links_box = Gtk.Box(
|
|
||||||
orientation=Gtk.Orientation.HORIZONTAL,
|
|
||||||
margin_start=5,
|
|
||||||
margin_end=5,
|
|
||||||
margin_top=5,
|
|
||||||
margin_bottom=10,
|
|
||||||
homogeneous=True,
|
|
||||||
spacing=10,
|
|
||||||
)
|
|
||||||
api_links_box.add(self.steam)
|
|
||||||
api_links_box.add(self.bm)
|
|
||||||
api_box.add(api_links_box)
|
|
||||||
|
|
||||||
prefs_grid = self._make_grid(pref_rows)
|
prefs_grid = self._make_grid(pref_rows)
|
||||||
version_grid = self._make_grid(version_rows)
|
version_grid = self._make_grid(version_rows)
|
||||||
@@ -176,7 +191,7 @@ class Options(Gtk.Box):
|
|||||||
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
||||||
|
|
||||||
for pair in [
|
for pair in [
|
||||||
(api_box, strings.options.api_keys),
|
(api_box, strings.options.api_key),
|
||||||
(prefs_grid, strings.options.prefs),
|
(prefs_grid, strings.options.prefs),
|
||||||
(version_grid, strings.options.version),
|
(version_grid, strings.options.version),
|
||||||
]:
|
]:
|
||||||
@@ -185,9 +200,16 @@ class Options(Gtk.Box):
|
|||||||
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
self.scrollable = Gtk.ScrolledWindow(vexpand=True)
|
label = Gtk.Label(label=strings.options.header)
|
||||||
self.scrollable.add(grid)
|
label.set_halign(Gtk.Align.CENTER)
|
||||||
self.add(self.scrollable)
|
css.add_class(label, "page-heading")
|
||||||
|
|
||||||
|
box = VBox()
|
||||||
|
box.add(label)
|
||||||
|
box.add(grid)
|
||||||
|
self.add(box)
|
||||||
|
|
||||||
|
self.connect("key-press-event", self._on_keypress)
|
||||||
|
|
||||||
def get_client_name(self) -> str:
|
def get_client_name(self) -> str:
|
||||||
model = self.client_combo.get_model()
|
model = self.client_combo.get_model()
|
||||||
@@ -195,13 +217,11 @@ class Options(Gtk.Box):
|
|||||||
return str(model[ind][0])
|
return str(model[ind][0])
|
||||||
|
|
||||||
def block_text_entry(self) -> None:
|
def block_text_entry(self) -> None:
|
||||||
for entry in self.steam_entry, self.bm_entry:
|
self.steam_entry.set_position(-1)
|
||||||
entry.set_position(-1)
|
self.steam_entry.set_can_focus(False)
|
||||||
entry.set_can_focus(False)
|
|
||||||
|
|
||||||
def unblock_text_entry(self) -> None:
|
def unblock_text_entry(self) -> None:
|
||||||
for entry in self.steam_entry, self.bm_entry:
|
self.steam_entry.set_can_focus(True)
|
||||||
entry.set_can_focus(True)
|
|
||||||
|
|
||||||
def _on_developers_clicked(self, button: Gtk.Button) -> None:
|
def _on_developers_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.show_developers_page()
|
self.controller.show_developers_page()
|
||||||
@@ -234,8 +254,6 @@ class Options(Gtk.Box):
|
|||||||
|
|
||||||
if context == Preferences.STEAM:
|
if context == Preferences.STEAM:
|
||||||
self.steam_entry = entry
|
self.steam_entry = entry
|
||||||
else:
|
|
||||||
self.bm_entry = entry
|
|
||||||
|
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||||
box.add(entry)
|
box.add(entry)
|
||||||
@@ -282,12 +300,13 @@ class Options(Gtk.Box):
|
|||||||
case Preferences.NAME:
|
case Preferences.NAME:
|
||||||
value = entry.get_text().strip()
|
value = entry.get_text().strip()
|
||||||
self.controller.update_config(enum, value)
|
self.controller.update_config(enum, value)
|
||||||
case Preferences.BM | Preferences.STEAM:
|
case Preferences.STEAM:
|
||||||
text = "".join(entry.get_text().split())
|
text = "".join(entry.get_text().split())
|
||||||
self.controller.update_api_key(enum, text)
|
self.controller.update_api_key(enum, text)
|
||||||
|
|
||||||
def _on_api_change_failed(self, emitter: "Emitter") -> None:
|
def _on_api_change_failed(self, emitter: "Emitter") -> None:
|
||||||
self.old_entry.set_text(self.old_text)
|
self.old_entry.set_text(self.old_text)
|
||||||
|
# TODO: use popover
|
||||||
dialog = ExceptionDialog(self.controller, errors.api_validation_error)
|
dialog = ExceptionDialog(self.controller, errors.api_validation_error)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
@@ -297,9 +316,6 @@ class Options(Gtk.Box):
|
|||||||
def revert(self, mode: Preferences) -> None:
|
def revert(self, mode: Preferences) -> None:
|
||||||
if mode == Preferences.STEAM:
|
if mode == Preferences.STEAM:
|
||||||
self.steam_entry.set_text(self.old_steam)
|
self.steam_entry.set_text(self.old_steam)
|
||||||
else:
|
|
||||||
self.bm_entry.set_text(self.old_bm)
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
||||||
_iter = combo.get_active_iter()
|
_iter = combo.get_active_iter()
|
||||||
@@ -316,14 +332,6 @@ class Options(Gtk.Box):
|
|||||||
real_cmd = combo.get_model()[_iter][1]
|
real_cmd = combo.get_model()[_iter][1]
|
||||||
self.controller.update_config(Preferences.CLIENT, real_cmd)
|
self.controller.update_config(Preferences.CLIENT, real_cmd)
|
||||||
|
|
||||||
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
|
||||||
try:
|
|
||||||
self.controller.toggle_config(context)
|
|
||||||
except Exception:
|
|
||||||
button.handler_block_by_func(self._on_radio_toggled)
|
|
||||||
self.populate_settings()
|
|
||||||
button.handler_unblock_by_func(self._on_radio_toggled)
|
|
||||||
|
|
||||||
def _is_valid_text(self, text: str, context: Preferences) -> bool:
|
def _is_valid_text(self, text: str, context: Preferences) -> bool:
|
||||||
if text.isspace():
|
if text.isspace():
|
||||||
return False
|
return False
|
||||||
@@ -335,8 +343,6 @@ class Options(Gtk.Box):
|
|||||||
old = self.old_name
|
old = self.old_name
|
||||||
case Preferences.STEAM:
|
case Preferences.STEAM:
|
||||||
old = self.old_steam
|
old = self.old_steam
|
||||||
case Preferences.BM:
|
|
||||||
old = self.old_bm
|
|
||||||
if text == old:
|
if text == old:
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
@@ -369,30 +375,13 @@ class Options(Gtk.Box):
|
|||||||
state = self._is_valid_text(text, context)
|
state = self._is_valid_text(text, context)
|
||||||
button.set_sensitive(state)
|
button.set_sensitive(state)
|
||||||
|
|
||||||
def make_binary_radio(
|
|
||||||
self,
|
|
||||||
first_option: str,
|
|
||||||
second_option: str,
|
|
||||||
context: Preferences,
|
|
||||||
) -> Gtk.Box:
|
|
||||||
|
|
||||||
hbox = Gtk.Box(spacing=5, halign=Gtk.Align.START)
|
|
||||||
radio1 = Gtk.RadioButton.new_with_label(None, first_option)
|
|
||||||
radio2 = Gtk.RadioButton.new_from_widget(radio1)
|
|
||||||
radio2.set_label(second_option)
|
|
||||||
radio1.connect("toggled", self._on_radio_toggled, context)
|
|
||||||
hbox.pack_start(radio1, NO_EXPAND, NO_FILL, NO_PADDING)
|
|
||||||
hbox.pack_start(radio2, NO_EXPAND, NO_FILL, NO_PADDING)
|
|
||||||
|
|
||||||
return hbox
|
|
||||||
|
|
||||||
def populate_settings(self) -> None:
|
def populate_settings(self) -> None:
|
||||||
prefs = self.controller.get_prefs()
|
prefs = self.controller.get_prefs()
|
||||||
# NOTE: re-check in case file was removed by user between runs
|
# NOTE: re-check in case file was removed by user between runs
|
||||||
if prefs.paths.config.is_file() is False:
|
if prefs.paths.config.is_file() is False:
|
||||||
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
raise Exception
|
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||||
|
|
||||||
config = query.get_config(prefs.paths.config)
|
config = query.get_config(prefs.paths.config)
|
||||||
|
|
||||||
@@ -400,34 +389,31 @@ class Options(Gtk.Box):
|
|||||||
name = self.controller.query_config(Preferences.NAME)
|
name = self.controller.query_config(Preferences.NAME)
|
||||||
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
steam = self.controller.query_config(Preferences.STEAM)
|
steam = self.controller.query_config(Preferences.STEAM)
|
||||||
bm = self.controller.query_config(Preferences.BM)
|
|
||||||
|
|
||||||
steam_path = Path(default_steam_path)
|
steam_path = Path(default_steam_path)
|
||||||
|
|
||||||
self.old_steam = steam
|
self.old_steam = steam
|
||||||
self.old_bm = bm
|
|
||||||
self.old_name = name
|
self.old_name = name
|
||||||
|
|
||||||
self.steam_entry.set_text(steam)
|
self.steam_entry.set_text(steam)
|
||||||
self.bm_entry.set_text(bm)
|
|
||||||
p = self.player_box.get_children()[0]
|
p = self.player_box.get_children()[0]
|
||||||
if hasattr(p, "set_text"):
|
if hasattr(p, "set_text"):
|
||||||
p.set_text(name)
|
p.set_text(name)
|
||||||
|
|
||||||
# NOTE: suppress toggle signal until radios are built
|
fs = config["fullscreen"]
|
||||||
self._suppress_toggles(True)
|
miles = config["use_miles"]
|
||||||
for el, conf_state in [
|
|
||||||
(self.fullscreen_toggle, config["fullscreen"]),
|
if prefs.is_steam_deck:
|
||||||
(self.distance_toggle, config["use_miles"]),
|
self.fullscreen_toggle.set_sensitive(False)
|
||||||
]:
|
fs = True
|
||||||
el.get_children()[conf_state].set_active(True)
|
|
||||||
self._suppress_toggles(False)
|
self.fullscreen_toggle.set_suboption_active(fs)
|
||||||
|
self.distance_toggle.set_suboption_active(miles)
|
||||||
|
|
||||||
# NOTE: disable buttons if no text is set
|
# NOTE: disable buttons if no text is set
|
||||||
for field in (
|
for field in (
|
||||||
[name, self.player_box],
|
[name, self.player_box],
|
||||||
[steam, self.steam_box],
|
[steam, self.steam_box],
|
||||||
[bm, self.bm_box],
|
|
||||||
):
|
):
|
||||||
if field[0] == "":
|
if field[0] == "":
|
||||||
field[1].get_children()[1].set_sensitive(False)
|
field[1].get_children()[1].set_sensitive(False)
|
||||||
@@ -474,4 +460,4 @@ class Options(Gtk.Box):
|
|||||||
widget.set_visibility(state)
|
widget.set_visibility(state)
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
return
|
self.grab_focus()
|
||||||
@@ -23,6 +23,7 @@ from gi.repository import Gdk, Gtk # noqa E402
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.managers.connection import Prerequisites
|
from dzgui.managers.connection import Prerequisites
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
|
from dzgui.controllers.emitter import Emitter
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -191,9 +192,14 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
self.add(self.scrolled_box)
|
self.add(self.scrolled_box)
|
||||||
self.add(self.button_box)
|
self.add(self.button_box)
|
||||||
|
|
||||||
|
emitter = self.controller.get_emitter()
|
||||||
|
emitter.connect("all_mods_synched", self._on_mods_synched)
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("map", self._on_map)
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_mods_synched(self, emitter: "Emitter") -> None:
|
||||||
|
self.tree.mark_mods_synched()
|
||||||
|
|
||||||
def _on_map(self, widget: Self) -> None:
|
def _on_map(self, widget: Self) -> None:
|
||||||
widgets = (
|
widgets = (
|
||||||
self.tree_frame,
|
self.tree_frame,
|
||||||
@@ -233,6 +239,15 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
)
|
)
|
||||||
|
|
||||||
"""Errors"""
|
"""Errors"""
|
||||||
|
if len(prereqs.invalid_mods) > 0:
|
||||||
|
pairs = [": ".join(sub) for sub in prereqs.invalid_mods]
|
||||||
|
lines = "\n".join(pairs)
|
||||||
|
msg = (
|
||||||
|
"Server has invalid mods that are not recognized by Steam.\n"
|
||||||
|
"Contact the server owner and include these mod IDs in your report:\n"
|
||||||
|
f"{lines}"
|
||||||
|
)
|
||||||
|
errors.append(msg)
|
||||||
if prereqs.binary_missing:
|
if prereqs.binary_missing:
|
||||||
errors.append(
|
errors.append(
|
||||||
f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}"
|
f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}"
|
||||||
@@ -266,6 +281,15 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
"It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
"It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
allows_dl, running_app = prereqs.allows_downloads
|
||||||
|
if len(prereqs.mods) > 0 and allows_dl is False:
|
||||||
|
msg = (
|
||||||
|
f"The app '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
|
||||||
|
"Either stop the game first, or update your global Steam settings or the game's local settings.\n"
|
||||||
|
"Otherwise, mods may be queued for download but never update."
|
||||||
|
)
|
||||||
|
warnings.append(msg)
|
||||||
|
|
||||||
self.add_warnings(warnings)
|
self.add_warnings(warnings)
|
||||||
self.add_errors(errors)
|
self.add_errors(errors)
|
||||||
|
|
||||||
@@ -297,16 +321,16 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
self._hide_mod_area()
|
self._hide_mod_area()
|
||||||
else:
|
else:
|
||||||
self._show_mod_area()
|
self._show_mod_area()
|
||||||
msg = "All mods are up to date."
|
msg = preconnect.up_to_date
|
||||||
self.mod_count.set_text(msg)
|
self.mod_count.set_text(msg)
|
||||||
|
|
||||||
if prereqs.required_space == 0:
|
if prereqs.required_space != 0:
|
||||||
self.ok.set_label(preconnect.connect)
|
|
||||||
|
|
||||||
pretty = number(prereqs.required_space)
|
pretty = number(prereqs.required_space)
|
||||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
suffix = f" Need to download {pretty} MiB of mod updates."
|
||||||
prefix = preconnect.total_mods
|
prefix = preconnect.total_mods
|
||||||
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
||||||
|
else:
|
||||||
|
self.ok.set_label(preconnect.connect)
|
||||||
|
|
||||||
if prereqs.is_last_server:
|
if prereqs.is_last_server:
|
||||||
self.connect_last.show()
|
self.connect_last.show()
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from typing import Any, Optional, TYPE_CHECKING
|
from typing import Any, Optional, TYPE_CHECKING
|
||||||
from warnings import deprecated
|
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, SEPARATOR
|
from dzgui.const.constants import APP_NAME, SEPARATOR
|
||||||
from dzgui.util.keys import is_ctrl_mask, is_navkey
|
from dzgui.util.keys import is_ctrl_mask, is_navkey
|
||||||
@@ -173,7 +172,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
|||||||
(model, pathlist) = sels
|
(model, pathlist) = sels
|
||||||
return (model, pathlist)
|
return (model, pathlist)
|
||||||
|
|
||||||
@deprecated("Currently unused")
|
# @deprecated("Currently unused")
|
||||||
# def get_mpath(self) -> Optional[Gtk.TreePath]:
|
# def get_mpath(self) -> Optional[Gtk.TreePath]:
|
||||||
# (model, pathlist) = self.get_model_and_pathlist()
|
# (model, pathlist) = self.get_model_and_pathlist()
|
||||||
# if len(pathlist) < 1:
|
# if len(pathlist) < 1:
|
||||||
@@ -195,7 +194,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@deprecated("unused")
|
# @deprecated("unused")
|
||||||
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
|
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
|
||||||
# ind = self.get_selected_row_index()
|
# ind = self.get_selected_row_index()
|
||||||
# model = self.get_model()
|
# model = self.get_model()
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class LogTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
model = ModelFactory().new_model_from_logfile(filepath)
|
model = ModelFactory().new_model_from_logfile(filepath)
|
||||||
_filter = model.filter_new()
|
_filter = model.filter_new()
|
||||||
_filter.set_visible_func(self._filter_rows)
|
_filter.set_visible_func(self._filter_rows)
|
||||||
sortable = Gtk.TreeModelSort(_filter)
|
sortable = Gtk.TreeModelSort.new_with_model(_filter)
|
||||||
self.set_model(sortable)
|
self.set_model(sortable)
|
||||||
_filter.refilter()
|
_filter.refilter()
|
||||||
path = Gtk.TreePath.new_from_indices([0])
|
path = Gtk.TreePath.new_from_indices([0])
|
||||||
|
|||||||
@@ -48,6 +48,10 @@ class ServerModTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def mark_mods_synched(self) -> None:
|
||||||
|
for row in self.mod_store:
|
||||||
|
row[2] = server_mods.checkmark
|
||||||
|
|
||||||
# TODO: could be problematic if user downloads mods out of band
|
# TODO: could be problematic if user downloads mods out of band
|
||||||
def _on_row_activated(
|
def _on_row_activated(
|
||||||
self,
|
self,
|
||||||
|
|||||||
+8
-3
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b13"
|
version = "7.0.0b21"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -27,6 +27,7 @@ dependencies = [
|
|||||||
"psutil==7.1.3",
|
"psutil==7.1.3",
|
||||||
"python-a2s",
|
"python-a2s",
|
||||||
"requests==2.32.5",
|
"requests==2.32.5",
|
||||||
|
"vdf==3.4"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
@@ -75,7 +76,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
line-length = 88
|
line-length = 88
|
||||||
indent-width = 4
|
indent-width = 4
|
||||||
extend-exclude = ["*lib/a2s"]
|
extend-exclude = ["*lib/a2s"]
|
||||||
lint.unfixable = ["F401"]
|
lint.unfixable = ["F401", "I001"]
|
||||||
|
|
||||||
[tool.ruff.format]
|
[tool.ruff.format]
|
||||||
quote-style = "double"
|
quote-style = "double"
|
||||||
@@ -106,9 +107,13 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
markers = [
|
markers = [
|
||||||
|
"apitest: checks remote endpoints",
|
||||||
"config: config file keys/values",
|
"config: config file keys/values",
|
||||||
|
"realconfig: depends on actual generated local config file",
|
||||||
|
"mods: tests mod metadata/link creation",
|
||||||
"pefile: validate PE files",
|
"pefile: validate PE files",
|
||||||
"post_install: requires a completed installation",
|
"post_install: requires a completed installation",
|
||||||
|
"redact: log redaction mechanisms",
|
||||||
"slow: long-running tests",
|
"slow: long-running tests",
|
||||||
"webtest: checks remote endpoints"
|
"webtest: depends on remote endpoint",
|
||||||
]
|
]
|
||||||
@@ -12,6 +12,7 @@ This file includes the combined licenses to the following components:
|
|||||||
- psutil
|
- psutil
|
||||||
- PyGObject
|
- PyGObject
|
||||||
- python-a2s
|
- python-a2s
|
||||||
|
- vdf
|
||||||
|
|
||||||
When shipped as a binary installer, these components are collectively bundled
|
When shipped as a binary installer, these components are collectively bundled
|
||||||
as a turnkey application and used to run the DZGUI utility.\n\n
|
as a turnkey application and used to run the DZGUI utility.\n\n
|
||||||
|
|||||||
+13
@@ -0,0 +1,13 @@
|
|||||||
|
"InstallConfigStore"
|
||||||
|
{
|
||||||
|
"Software"
|
||||||
|
{
|
||||||
|
"Valve"
|
||||||
|
{
|
||||||
|
"Steam"
|
||||||
|
{
|
||||||
|
"AllowDownloadsDuringGameplay" "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
"InstallConfigStore"
|
||||||
|
{
|
||||||
|
"Software"
|
||||||
|
{
|
||||||
|
"Valve"
|
||||||
|
{
|
||||||
|
"Steam"
|
||||||
|
{
|
||||||
|
"AllowDownloadsDuringGameplay" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Vendored
File renamed without changes.
Vendored
File renamed without changes.
+13
@@ -0,0 +1,13 @@
|
|||||||
|
"users"
|
||||||
|
{
|
||||||
|
"0"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER"
|
||||||
|
"PersonaName" "STEAMUSER"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AutoLogin" "1"
|
||||||
|
"Timestamp" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
"users"
|
||||||
|
{
|
||||||
|
"0"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER"
|
||||||
|
"PersonaName" "STEAMUSER"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AllowAutoLogin" "1"
|
||||||
|
"MostRecent" "1"
|
||||||
|
"Timestamp" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"users"
|
||||||
|
{
|
||||||
|
"0"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER"
|
||||||
|
"PersonaName" "STEAMUSER"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AllowAutoLogin" "1"
|
||||||
|
"MostRecent" "0"
|
||||||
|
"Timestamp" "0"
|
||||||
|
}
|
||||||
|
"1"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER2"
|
||||||
|
"PersonaName" "STEAMUSER2"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AllowAutoLogin" "1"
|
||||||
|
"MostRecent" "1"
|
||||||
|
"Timestamp" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
Vendored
BIN
Binary file not shown.
File renamed without changes.
@@ -0,0 +1,53 @@
|
|||||||
|
"AppState"
|
||||||
|
{
|
||||||
|
"appid" "111"
|
||||||
|
"Universe" "1"
|
||||||
|
"name" "TEST APP"
|
||||||
|
"StateFlags" "4"
|
||||||
|
"installdir" "TESTAPP"
|
||||||
|
"LastUpdated" "0"
|
||||||
|
"LastPlayed" "0"
|
||||||
|
"SizeOnDisk" "0"
|
||||||
|
"StagingSize" "0"
|
||||||
|
"buildid" "0"
|
||||||
|
"LastOwner" "0"
|
||||||
|
"DownloadType" "2"
|
||||||
|
"UpdateResult" "0"
|
||||||
|
"BytesToDownload" "0"
|
||||||
|
"BytesDownloaded" "0"
|
||||||
|
"BytesToStage" "0"
|
||||||
|
"BytesStaged" "0"
|
||||||
|
"TargetBuildID" "0"
|
||||||
|
"AutoUpdateBehavior" "0"
|
||||||
|
"AllowOtherDownloadsWhileRunning" "1"
|
||||||
|
"ScheduledAutoUpdate" "0"
|
||||||
|
"FullValidateAfterNextUpdate" "1"
|
||||||
|
"InstalledDepots"
|
||||||
|
{
|
||||||
|
"1110"
|
||||||
|
{
|
||||||
|
"manifest" "0"
|
||||||
|
"size" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"SharedDepots"
|
||||||
|
{
|
||||||
|
"228983" "228980"
|
||||||
|
"228985" "228980"
|
||||||
|
"228988" "228980"
|
||||||
|
"228990" "228980"
|
||||||
|
"229004" "228980"
|
||||||
|
}
|
||||||
|
"UserConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
"MountedConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"AppState"
|
||||||
|
{
|
||||||
|
"appid" "222"
|
||||||
|
"Universe" "1"
|
||||||
|
"name" "TEST APP"
|
||||||
|
"StateFlags" "4"
|
||||||
|
"installdir" "TESTAPP"
|
||||||
|
"LastUpdated" "0"
|
||||||
|
"LastPlayed" "0"
|
||||||
|
"SizeOnDisk" "0"
|
||||||
|
"StagingSize" "0"
|
||||||
|
"buildid" "0"
|
||||||
|
"LastOwner" "0"
|
||||||
|
"DownloadType" "2"
|
||||||
|
"UpdateResult" "0"
|
||||||
|
"BytesToDownload" "0"
|
||||||
|
"BytesDownloaded" "0"
|
||||||
|
"BytesToStage" "0"
|
||||||
|
"BytesStaged" "0"
|
||||||
|
"TargetBuildID" "0"
|
||||||
|
"AutoUpdateBehavior" "0"
|
||||||
|
"AllowOtherDownloadsWhileRunning" "2"
|
||||||
|
"ScheduledAutoUpdate" "0"
|
||||||
|
"FullValidateAfterNextUpdate" "1"
|
||||||
|
"InstalledDepots"
|
||||||
|
{
|
||||||
|
"2220"
|
||||||
|
{
|
||||||
|
"manifest" "0"
|
||||||
|
"size" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"SharedDepots"
|
||||||
|
{
|
||||||
|
"228983" "228980"
|
||||||
|
"228985" "228980"
|
||||||
|
"228988" "228980"
|
||||||
|
"228990" "228980"
|
||||||
|
"229004" "228980"
|
||||||
|
}
|
||||||
|
"UserConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
"MountedConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
"AppState"
|
||||||
|
{
|
||||||
|
"appid" "333"
|
||||||
|
"Universe" "1"
|
||||||
|
"name" "TEST APP"
|
||||||
|
"StateFlags" "4"
|
||||||
|
"installdir" "TESTAPP"
|
||||||
|
"LastUpdated" "0"
|
||||||
|
"LastPlayed" "0"
|
||||||
|
"SizeOnDisk" "0"
|
||||||
|
"StagingSize" "0"
|
||||||
|
"buildid" "0"
|
||||||
|
"LastOwner" "0"
|
||||||
|
"DownloadType" "2"
|
||||||
|
"UpdateResult" "0"
|
||||||
|
"BytesToDownload" "0"
|
||||||
|
"BytesDownloaded" "0"
|
||||||
|
"BytesToStage" "0"
|
||||||
|
"BytesStaged" "0"
|
||||||
|
"TargetBuildID" "0"
|
||||||
|
"AutoUpdateBehavior" "0"
|
||||||
|
"AllowOtherDownloadsWhileRunning" "0"
|
||||||
|
"ScheduledAutoUpdate" "0"
|
||||||
|
"FullValidateAfterNextUpdate" "1"
|
||||||
|
"InstalledDepots"
|
||||||
|
{
|
||||||
|
"3330"
|
||||||
|
{
|
||||||
|
"manifest" "0"
|
||||||
|
"size" "0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"SharedDepots"
|
||||||
|
{
|
||||||
|
"228983" "228980"
|
||||||
|
"228985" "228980"
|
||||||
|
"228988" "228980"
|
||||||
|
"228990" "228980"
|
||||||
|
"229004" "228980"
|
||||||
|
}
|
||||||
|
"UserConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
"MountedConfig"
|
||||||
|
{
|
||||||
|
"language" "english"
|
||||||
|
"platform_override_dest" "linux"
|
||||||
|
"platform_override_source" "windows"
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Vendored
-3
@@ -1,3 +0,0 @@
|
|||||||
from dzgui.api.mods import tokenize
|
|
||||||
|
|
||||||
print(tokenize("meta4.cpp"))
|
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
1559212036,1771519119
|
||||||
|
1564026768,1770917948
|
||||||
|
2545327648,1780174455
|
||||||
|
2276010135,1756744083
|
||||||
|
1654462998,1780501511
|
||||||
|
3410710885,1752864732
|
||||||
|
2918418331,1780413260
|
||||||
|
3739934289,1780855136
|
||||||
File renamed without changes.
File renamed without changes.
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user