mirror of
https://github.com/aclist/dztui.git
synced 2026-10-25 12:12:31 +01:00
Compare commits
404
Commits
7.0.0b19
...
a6282e4401
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6282e4401 | ||
|
|
a9ae5f59b5 | ||
|
|
cdec81071d | ||
|
|
97b5c0e6ee | ||
|
|
67c19fb90d | ||
|
|
d9d3f747bb | ||
|
|
1440871819 | ||
|
|
761988d8c2 | ||
|
|
ab5637ae3b | ||
|
|
682532a867 | ||
|
|
d577ba0970 | ||
|
|
c2993ad289 | ||
|
|
edb87893f9 | ||
|
|
519e6c354d | ||
|
|
e3d1d62b4d | ||
|
|
1dcfdda657 | ||
|
|
019847dcf9 | ||
|
|
c2cfe2b4d9 | ||
|
|
01b3babdf5 | ||
|
|
e286f02342 | ||
|
|
9f3c39fdd6 | ||
|
|
0359aacd54 | ||
|
|
80e127889e | ||
|
|
da98b1d5b1 | ||
|
|
33869158de | ||
|
|
5cb8c68f5c | ||
|
|
2f92c2a44f | ||
|
|
da48834fa7 | ||
|
|
197c2eba06 | ||
|
|
0131ee6f0e | ||
|
|
b70edebd05 | ||
|
|
ae1c3a19d4 | ||
|
|
7822672cda | ||
|
|
f2a7bde432 | ||
|
|
00b5476aaf | ||
|
|
2a08b9ba82 | ||
|
|
0f8b6fcaad | ||
|
|
95988459d5 | ||
|
|
eed40e6f1a | ||
|
|
07460a1f8f | ||
|
|
a74d576387 | ||
|
|
052f7f0708 | ||
|
|
6feae44887 | ||
|
|
8cdb615796 | ||
|
|
31b6705017 | ||
|
|
ef1e7d17f5 | ||
|
|
3c52072eb5 | ||
|
|
bf69936891 | ||
|
|
fa180b750a | ||
|
|
96d50ffafa | ||
|
|
6c801ffec5 | ||
|
|
5eb70a1eb8 | ||
|
|
a3fff40f8f | ||
|
|
343630d2da | ||
|
|
d63c7ce3ec | ||
|
|
f7ad357e6f | ||
|
|
035a01fe34 | ||
|
|
7dd14804aa | ||
|
|
1cc1ba2864 | ||
|
|
8c4d0aecdc | ||
|
|
616f18452a | ||
|
|
1064f7a9d6 | ||
|
|
b0e85f0e35 | ||
|
|
4eec00e8e7 | ||
|
|
09ef3e446a | ||
|
|
4d02604bcf | ||
|
|
ebffbe39f2 | ||
|
|
61860f6aee | ||
|
|
191776ce74 | ||
|
|
7ae34747e9 | ||
|
|
1555ba2d64 | ||
|
|
4bac804fd7 | ||
|
|
748e1870da | ||
|
|
cd4c6fdfb2 | ||
|
|
1752018331 | ||
|
|
b3b05918b7 | ||
|
|
e6e0a3036d | ||
|
|
8c44a6e255 | ||
|
|
7fc1264b65 | ||
|
|
f748b09ba7 | ||
|
|
01be1247b4 | ||
|
|
27d5c4b4b2 | ||
|
|
045e47037e | ||
|
|
3bc2a54ee4 | ||
|
|
4e22b82703 | ||
|
|
492ed221c4 | ||
|
|
71a61a9201 | ||
|
|
516735defa | ||
|
|
9a3df70914 | ||
|
|
c41e47859f | ||
|
|
11bbe15ecb | ||
|
|
d40512dfa0 | ||
|
|
f07e503b74 | ||
|
|
3caeb823a2 | ||
|
|
6fcd887d56 | ||
|
|
c15b77d470 | ||
|
|
cee1e3b8af | ||
|
|
e8ccdd7cbb | ||
|
|
2629fe254e | ||
|
|
3b15bf58e7 | ||
|
|
e773a4909b | ||
|
|
95380681f9 | ||
|
|
18192a4667 | ||
|
|
f07a2dea5f | ||
|
|
0e58cf514e | ||
|
|
84c6a3868b | ||
|
|
7c0981981f | ||
|
|
e653c61955 | ||
|
|
d4bf9f71f6 | ||
|
|
cb5c233f45 | ||
|
|
ac286f4a72 | ||
|
|
e254821105 | ||
|
|
32b8c5cbc5 | ||
|
|
76ed7f2451 | ||
|
|
f135f6a35d | ||
|
|
eb5d5d5181 | ||
|
|
fef8c69f38 | ||
|
|
32395812a0 | ||
|
|
6d046ee719 | ||
|
|
1ebf253e99 | ||
|
|
ee31dce6e6 | ||
|
|
4aaf7fe195 | ||
|
|
b7e8982f40 | ||
|
|
c3b41ed9b4 | ||
|
|
e42fd26855 | ||
|
|
7a8ccca317 | ||
|
|
72e3054afb | ||
|
|
2f4606ede0 | ||
|
|
c6a75c1e78 | ||
|
|
bad7878c0a | ||
|
|
5014402e39 | ||
|
|
baade5b721 | ||
|
|
3c92108ac3 | ||
|
|
0f6f1c03ec | ||
|
|
39d8607f57 | ||
|
|
64d784f9a0 | ||
|
|
667203b4f9 | ||
|
|
00369837cb | ||
|
|
9bc5964df2 | ||
|
|
b7b83b3ca5 | ||
|
|
d04c20498a | ||
|
|
572ac5d7ce | ||
|
|
b4125c5a51 | ||
|
|
8bae2d5fb5 | ||
|
|
a85c632902 | ||
|
|
74ecc30692 | ||
|
|
33843becae | ||
|
|
07cbf7a36a | ||
|
|
687c5ac168 | ||
|
|
f3fd7ef6ea | ||
|
|
2383f7d2aa | ||
|
|
c839672f85 | ||
|
|
ca234b8dea | ||
|
|
8e3ce72295 | ||
|
|
f02d959ebc | ||
|
|
04e9e5dcf0 | ||
|
|
1b06fcae42 | ||
|
|
9759c39fd7 | ||
|
|
4451eee849 | ||
|
|
3f587f64a0 | ||
|
|
b00523661f | ||
|
|
f24b3574b8 | ||
|
|
fad8d2f43b | ||
|
|
e1e034fffd | ||
|
|
ea2ca684d6 | ||
|
|
2e2d3484c4 | ||
|
|
1254044ef8 | ||
|
|
7d651e9653 | ||
|
|
d9f392f8c9 | ||
|
|
6281ed37b3 | ||
|
|
0bb2ae2266 | ||
|
|
89b4e2ec98 | ||
|
|
b6d3f8d385 | ||
|
|
1b6ebac3a3 | ||
|
|
629c974b9d | ||
|
|
371eaa34ac | ||
|
|
c29346e134 | ||
|
|
79fd96e834 | ||
|
|
cc1948d225 | ||
|
|
e32c7fcacb | ||
|
|
76b3186ed7 | ||
|
|
493818b770 | ||
|
|
e2a6fa0c7e | ||
|
|
0a310f0b70 | ||
|
|
3f1aff7cc6 | ||
|
|
c348a499b0 | ||
|
|
5ff936f266 | ||
|
|
dfeafc3d28 | ||
|
|
96b91fd87c | ||
|
|
71d760dc56 | ||
|
|
aeffce2579 | ||
|
|
93d870b5bf | ||
|
|
63c2b5acd4 | ||
|
|
0d5e190b75 | ||
|
|
e9d064820e | ||
|
|
17b1a75745 | ||
|
|
e7ff025605 | ||
|
|
ec004aad5d | ||
|
|
bd022ea1d8 | ||
|
|
52b18f710f | ||
|
|
9ec3fa09e3 | ||
|
|
cb9ffb5b99 | ||
|
|
37c3937aee | ||
|
|
cf850c8f34 | ||
|
|
1332026c8c | ||
|
|
4eaedd0cdd | ||
|
|
b1f189bff8 | ||
|
|
b153ab875e | ||
|
|
de4e74daa1 | ||
|
|
0dfcc5a387 | ||
|
|
c64631cdca | ||
|
|
97ddbcdf0a | ||
|
|
ddc18e5705 | ||
|
|
6fb83b5c31 | ||
|
|
b5105ca9f9 | ||
|
|
05fab5774e | ||
|
|
203cf0da7f | ||
|
|
7dfdb4209a | ||
|
|
c03aa8f166 | ||
|
|
c34fa2fe03 | ||
|
|
0f4d611345 | ||
|
|
ca1958b0a6 | ||
|
|
c7bd6f635c | ||
|
|
2b394553b3 | ||
|
|
5b83a26f6a | ||
|
|
6ded9a3eec | ||
|
|
1a3154712f | ||
|
|
9f8b760c79 | ||
|
|
0f59d92bc3 | ||
|
|
cca1006efb | ||
|
|
027e6c0781 | ||
|
|
3932a3aed4 | ||
|
|
22c9be36a6 | ||
|
|
a2938c17f5 | ||
|
|
48b0660ebd | ||
|
|
5eb89a172d | ||
|
|
c037cf9c40 | ||
|
|
7cc8076df1 | ||
|
|
b300b614a0 | ||
|
|
0bb8f3206f | ||
|
|
8087f3a707 | ||
|
|
0ebc25b3b4 | ||
|
|
1e073af27a | ||
|
|
250da7a635 | ||
|
|
8098c4b68c | ||
|
|
1e8a423ec3 | ||
|
|
067b77bf92 | ||
|
|
d0ccc3bf4c | ||
|
|
38cc55530f | ||
|
|
5536180fec | ||
|
|
79bbc25714 | ||
|
|
456c6e62e8 | ||
|
|
eeffdb57f3 | ||
|
|
88ad8cf7c2 | ||
|
|
9f9a88d2ef | ||
|
|
f7e0b44379 | ||
|
|
ee33ce6662 | ||
|
|
a421d4c70b | ||
|
|
080b99cda8 | ||
|
|
afca9e5e6a | ||
|
|
a97c17f66b | ||
|
|
fe8bf611e1 | ||
|
|
70ccbf29f3 | ||
|
|
f5d595eccd | ||
|
|
fd7a549dbc | ||
|
|
6c810b28a9 | ||
|
|
1a1145c79c | ||
|
|
510719e3ec | ||
|
|
e06775c3c8 | ||
|
|
f5c71200a8 | ||
|
|
557e221993 | ||
|
|
dd30fa33d1 | ||
|
|
1753a40b94 | ||
|
|
a72d692c68 | ||
|
|
ce1cad6dcf | ||
|
|
9b24928f74 | ||
|
|
87f167bd53 | ||
|
|
4917c0ce2e | ||
|
|
cc485289e3 | ||
|
|
69cba3460d | ||
|
|
dfd6850d6d | ||
|
|
8d1d5bde80 | ||
|
|
533def5061 | ||
|
|
c179e3a9e9 | ||
|
|
2b8e0944a4 | ||
|
|
cd4bc7ff4e | ||
|
|
b209c9912d | ||
|
|
bfb0ffc9ad | ||
|
|
0eb47a0427 | ||
|
|
ea785960d8 | ||
|
|
7102c21f5a | ||
|
|
22294d7125 | ||
|
|
e9ce292368 | ||
|
|
5532e4e267 | ||
|
|
120703e702 | ||
|
|
b31896387a | ||
|
|
e5bb25cd60 | ||
|
|
e59b7e01fd | ||
|
|
7994bf988c | ||
|
|
90b1c875df | ||
|
|
b61e806009 | ||
|
|
b46d0614c9 | ||
|
|
c73864d545 | ||
|
|
314fed559c | ||
|
|
1c346a6c3c | ||
|
|
65e11ddd03 | ||
|
|
a1c0592c7d | ||
|
|
22c82677cb | ||
|
|
c5b41346ca | ||
|
|
c2fc9b2cca | ||
|
|
ba9d6e1d3b | ||
|
|
be6ab2c9cb | ||
|
|
2413dd1f14 | ||
|
|
9d8d37e110 | ||
|
|
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 | ||
|
|
a4f9a67da1 |
No files matched your search
+20
-1
@@ -3,6 +3,8 @@
|
|||||||
## [7.0.0] Unreleased
|
## [7.0.0] Unreleased
|
||||||
### Added
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
|
- Uninstall wizard
|
||||||
|
- Descriptive details panel to certain error dialogs
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
- Documentation ships with source
|
- Documentation ships with source
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
- Set favorite server from tables
|
- Set favorite server from tables
|
||||||
- Detailed/copyable trace in critical error dialogs
|
- Detailed/copyable trace in critical error dialogs
|
||||||
- Visual icons
|
- Visual icons
|
||||||
|
- Unset favorite server button
|
||||||
- Integrated server notebook
|
- Integrated server notebook
|
||||||
- Propagate width changes to all tables
|
- Propagate width changes to all tables
|
||||||
- Remember tree position in menus
|
- Remember tree position in menus
|
||||||
@@ -44,11 +47,20 @@
|
|||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
- Generate Steam shortcuts and cover art
|
- Generate Steam shortcuts and cover art
|
||||||
- Warn user if background downloads are disabled
|
- Warn user if background downloads are disabled
|
||||||
|
- Dynamic copy button
|
||||||
|
- Update map count (-u flag)
|
||||||
|
- Filter button tooltips
|
||||||
|
- Expand/collapse changelog versions
|
||||||
|
- Support local light/dark mode changes on system theme
|
||||||
|
- Server time calculator
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
|
- Enforce fullscreen mode on Steam Deck
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
|
- Changed some dialog error messages from popups to popovers
|
||||||
|
- Changed how filter buttons are packed in sidebar
|
||||||
- Bold labels inside frames
|
- Bold labels inside frames
|
||||||
- Sidebar buttons do not steal focus
|
- Sidebar buttons do not steal focus
|
||||||
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
||||||
@@ -61,18 +73,25 @@
|
|||||||
- Reduce size of geolocation DB on disk (~100MB)
|
- Reduce size of geolocation DB on disk (~100MB)
|
||||||
- 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
|
||||||
|
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||||
|
|
||||||
### Dropped
|
### Dropped
|
||||||
- Debug mode
|
- Battlemetrics API support
|
||||||
|
- Unused metadata from system log files
|
||||||
- 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
|
||||||
|
- In-game time field missing certain ranges of time at night
|
||||||
|
- Wrap long entries in changelog
|
||||||
|
- Certain server descriptions with special characters not rendering correctly
|
||||||
- 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
|
||||||
|
- Current keyword filter not being applied when refreshing table
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
|
|||||||
@@ -67,3 +67,7 @@ It is recommended to follow
|
|||||||
integrates well with tooling and helps the project review your code.
|
integrates well with tooling and helps the project review your code.
|
||||||
|
|
||||||
Please see DEVELOPERS.md for further details.
|
Please see DEVELOPERS.md for further details.
|
||||||
|
|
||||||
|
## License applicability to contributions
|
||||||
|
|
||||||
|
Code contributed to this repository becomes subject to the terms of the GPL license defined in the LICENSE file at the project root.
|
||||||
@@ -1,15 +1,23 @@
|
|||||||
## What this is
|
## What this is
|
||||||
DZGUI is a turnkey browser and mod manager for DayZ on Linux. It allows you to connect to both official and modded/community DayZ servers on Linux via a graphical user interface (GUI).
|
DZGUI is a turnkey server browser and mod manager for DayZ on Linux. It allows you to connect to both official and modded/community DayZ servers on Linux via a graphical user interface (GUI).
|
||||||
|
|
||||||
This overcomes certain limitations in the Linux client and helps prepare the game to launch by providing features like:
|
This overcomes certain limitations in the Linux client and helps prepare the game to launch by providing features like:
|
||||||
|
|
||||||
- Search for and display global and LAN servers via an interactive table
|
- Search for and display global and LAN servers via an interactive table
|
||||||
- Supports official, third-party, and modded DayZ servers
|
- Supports official, third-party, and modded DayZ servers
|
||||||
- Automatically find and prepare mods being requested by servers
|
- Automatically find and prepare mods being requested by servers
|
||||||
- Add/delete/manage favorite servers by IP or ID
|
|
||||||
- Quick-connect to favorite/recent servers
|
- Quick-connect to favorite/recent servers
|
||||||
- Manage local mods and symlinks
|
- Manage and delete local mods
|
||||||
- Prepare launch options to pass to Steam
|
- And more
|
||||||
|
|
||||||
|
## Design philosophy
|
||||||
|
|
||||||
|
DZGUI follows a specific set of guidelines, enumerated below:
|
||||||
|
|
||||||
|
- Provide a free and open-source tool under GPL
|
||||||
|
- Do not use undocumented, third-party servers to aggregate data; no central "DZGUI" server
|
||||||
|
- All connections go to Steam and DayZ servers directly; the tool should function in perpetuity without intermediary endpoints
|
||||||
|
- Target Linux desktop and Steam deck specifically; not a cross-platform tool
|
||||||
|
|
||||||
## Setup and usage
|
## Setup and usage
|
||||||
|
|
||||||
@@ -18,7 +26,7 @@ Please refer to the documentation for installation and setup instructions:
|
|||||||
- [GitHub](https://aclist.github.io/dzgui/index.html)
|
- [GitHub](https://aclist.github.io/dzgui/index.html)
|
||||||
- [Codeberg (Mirror)](https://aclist.codeberg.page)
|
- [Codeberg (Mirror)](https://aclist.codeberg.page)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ runtime environment are built-in.
|
|||||||
Turnkey installer
|
Turnkey installer
|
||||||
------------------------
|
------------------------
|
||||||
|
|
||||||
- Visit the project's `Releases page <https://github.com/aclist/dztui/releases>`_.
|
- Visit the project's `Releases page <https://github.com/aclist/dztui/releases/latest>`_.
|
||||||
- Extract the DZGUI tarball from the top of the **Assets** list.
|
- Extract the DZGUI tarball from the top of the **Assets** list.
|
||||||
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in
|
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in
|
||||||
a file explorer.
|
a file explorer.
|
||||||
|
|||||||
+37
-7
@@ -3,17 +3,22 @@ Knowledge Base
|
|||||||
|
|
||||||
.. _DZG-001:
|
.. _DZG-001:
|
||||||
|
|
||||||
DZG-001: Periodically getting dropped from servers, or servers time out in DZGUI
|
DZG-001: Timeouts occur while trying to query a specific server
|
||||||
------------------------------------------------------------------------------------
|
------------------------------------------------------------------------------------
|
||||||
DayZ opens a large number of connections while querying/connected to servers.
|
The leading cause of specific servers periodically timing out is local network configuration.
|
||||||
|
|
||||||
In addition, packets sent from server responses are expected to be a standard size (see warning below).
|
Many third-party DayZ servers use a server rental/hosting provider with DDoS protection.
|
||||||
|
This can cause responses from servers to originate from a server other than the one originally queried.
|
||||||
|
|
||||||
If your network does not have enough headroom or has settings departing from defaults, this may lead to getting dropped from servers,
|
Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation).
|
||||||
unresponsiveness, or a timeout.
|
By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue.
|
||||||
|
|
||||||
|
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for
|
||||||
|
the server's query port.
|
||||||
|
|
||||||
|
In addition, a uniform packet size is expected. If your router's MTU setting differs from the default of 1,500,
|
||||||
|
it could lead to incoming responses being discarded (see warning below).
|
||||||
|
|
||||||
If you are on Wi-Fi, try switching to a wired connection and see if the problem resolves itself. Consumer Wi-Fi routers
|
|
||||||
tend to have less headroom than their wired counterparts.
|
|
||||||
|
|
||||||
.. important::
|
.. important::
|
||||||
Ensure that MTU (maximum tranmission unit) on your network is set to the standard size of 1,500 bytes.
|
Ensure that MTU (maximum tranmission unit) on your network is set to the standard size of 1,500 bytes.
|
||||||
@@ -80,3 +85,28 @@ There is some misconception that a Steam Web API key could be used to gain infor
|
|||||||
A Steam Web API key is the most strict way of getting authentic, reliable, and consistent server information in a zero-trust model.
|
A Steam Web API key is the most strict way of getting authentic, reliable, and consistent server information in a zero-trust model.
|
||||||
|
|
||||||
You are responsible for the creation, storage, management, and revocation of your Web API key.
|
You are responsible for the creation, storage, management, and revocation of your Web API key.
|
||||||
|
|
||||||
|
DZG-008: Periodically getting dropped from servers while connected
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
In some cases, DayZ opens a large number of connections while connected to servers.
|
||||||
|
|
||||||
|
If your network does not have enough headroom or has settings departing from defaults, this may lead to getting dropped from servers,
|
||||||
|
unresponsiveness, or a timeout.
|
||||||
|
|
||||||
|
If you are on Wi-Fi, try switching to a wired connection and see if the problem resolves itself. Consumer Wi-Fi routers
|
||||||
|
tend to have less headroom for simultaneous connections than their wired counterparts.
|
||||||
|
|
||||||
|
DZG-009: Floating dialogs appear maximized on tiling window managers
|
||||||
|
-------------------------------------------------------------------
|
||||||
|
|
||||||
|
The main DZGUI window and its child dialogs are expected to be rendered as floating by your window manager.
|
||||||
|
DZGUI sends window manager hints to this effect, but tiling window managers designed to bisect the screen into quadrants (e.g., i3 window manager)
|
||||||
|
may try to always launch applications in fullscreen.
|
||||||
|
|
||||||
|
To resolve this, set specific exclusions or window hints in your WM's configuration file.
|
||||||
|
For example, for i3, add the following to your `XDG_CONFIG_HOME/i3/config` file (defaults to `$HOME/.config/i3/config`):
|
||||||
|
|
||||||
|
.. code:: console
|
||||||
|
|
||||||
|
for_window [instance="DZGUI"] floating enable, move position center
|
||||||
|
for_window [instance="DZGUI - Dialog"] floating enable, move position center
|
||||||
+4
-14
@@ -4,7 +4,7 @@ Setup
|
|||||||
DayZ license
|
DayZ license
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
Prepare a Steam account with a DayZ license (i.e., own the game). From Steam's right-click menu for the game options, under ``Compatibility``, enable a Proton version ≥ ``6.8`` (or use Proton
|
Prepare a Steam account with a DayZ license (i.e., own the game). From Steam's right-click menu for the game options, under ``Compatibility``, enable a Proton version ≥ ``6.8`` (or use Proton
|
||||||
Experimental for the latest version).
|
Experimental for the latest version).
|
||||||
|
|
||||||
As of this writing, any recent version of Proton should work, and it is encouraged to use the most recent one.
|
As of this writing, any recent version of Proton should work, and it is encouraged to use the most recent one.
|
||||||
@@ -33,20 +33,10 @@ Once configured, you can insert this key in the app when launching it for the fi
|
|||||||
|
|
||||||
Battlemetrics API key
|
Battlemetrics API key
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
**This step is optional, but recommended**. Using this key lets you also connect to and query servers by their shorthand ID instead of by IP.
|
.. warning::
|
||||||
|
|
||||||
Register for an API key at `BattleMetrics <https://www.battlemetrics.com/account/register?after=%2Fdevelopers>`_ (free).
|
This feature is no longer supported in DZGUI. Battlemetrics has switched to a paid service.
|
||||||
|
Battlemetrics queries are expected to fail in DZGUI 6. In DZGUI 7, this feature has been removed entirely.
|
||||||
From the ``Personal Access Tokens area``, select ``New Token``.
|
|
||||||
|
|
||||||
Give the token any name in the field at the top.
|
|
||||||
|
|
||||||
Leave **all options unchecked** and scroll to the bottom. Select ``Create Token``.
|
|
||||||
|
|
||||||
Once configured, you can insert this key in the app when launching it for the first time (optional), or later on when using the connect/query by ID methods in the app.
|
|
||||||
|
|
||||||
.. tip::
|
|
||||||
Each server has a unique ID. This is the string of numbers at the end of the URL. For example, in the URL ``https://www.battlemetrics.com/servers/dayz/8039514``, the ID is ``8039514``.
|
|
||||||
|
|
||||||
Next steps
|
Next steps
|
||||||
--------------
|
--------------
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
Steam integration
|
Steam integration
|
||||||
====================
|
====================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
In :doc:`DZGUI 7 <dzgui7>`, Steam shortcut creation and cover art setup are handled automatically during setup. You can skip this step unless you are using DZGUI 6.
|
||||||
|
|
||||||
DZGUI can be added to Steam as a "non-Steam game" in order to facilitate integration with Steam Deck or desktop environments.
|
DZGUI can be added to Steam as a "non-Steam game" in order to facilitate integration with Steam Deck or desktop environments.
|
||||||
|
|
||||||
First, launch Steam in the **Large** (default) view.
|
First, launch Steam in the **Large** (default) view.
|
||||||
@@ -74,7 +78,7 @@ The final result will create box art looking like the above.
|
|||||||
:scale: 100%
|
:scale: 100%
|
||||||
:alt: Adding a non-Steam game
|
:alt: Adding a non-Steam game
|
||||||
|
|
||||||
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
|
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
|
||||||
icon. Click this to open a file explorer, navigate to ``$HOME/.local/share/dzgui``, and select ``icon.png``. This will add a small icon to the list view.
|
icon. Click this to open a file explorer, navigate to ``$HOME/.local/share/dzgui``, and select ``icon.png``. This will add a small icon to the list view.
|
||||||
|
|
||||||
Finally, after you launch DZGUI for the first time, you should quit the application and return to the Library view. Select the **Recent Games** dropdown on the right-hand side.
|
Finally, after you launch DZGUI for the first time, you should quit the application and return to the Library view. Select the **Recent Games** dropdown on the right-hand side.
|
||||||
|
|||||||
@@ -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
|
|
||||||
+20
-8
@@ -255,16 +255,28 @@ class PeFileError(Exception):
|
|||||||
|
|
||||||
pass
|
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
|
"""
|
||||||
major = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
Note: structure differs from standard VS_FIXEDFILEINFO
|
||||||
build = struct.unpack("<L", data.read(4))[0] >> 0 & 0xFFFF
|
Major: lower 16 bits of first DWORD
|
||||||
# NOTE: 0-pad revision number
|
Minor: upper 16 bits of first DWORD
|
||||||
revision = str(struct.unpack("<L", data.read(4))[0] >> 16 & 0xFFFF)
|
Build: lower 16 bits of third DWORD
|
||||||
offset = 3 - len(revision)
|
Revision: upper 16 bits of second DWORD, zero-padded
|
||||||
padded = "0" * offset + revision
|
Lower 16 bits of revision DWORD appear unused.
|
||||||
return FileVersion(major, minor, build, padded)
|
"""
|
||||||
|
major_minor = struct.unpack("<I", data.read(4))[0]
|
||||||
|
revision_pair = struct.unpack("<I", data.read(4))[0]
|
||||||
|
build_dword = struct.unpack("<I", data.read(4))[0]
|
||||||
|
|
||||||
|
major = major_minor & 0xFFFF
|
||||||
|
minor = (major_minor >> 16) & 0xFFFF
|
||||||
|
build = build_dword & 0xFFFF
|
||||||
|
revision = (revision_pair >> 16) & 0xFFFF
|
||||||
|
revision = f"{revision:03d}"
|
||||||
|
|
||||||
|
return FileVersion(major, minor, build, revision)
|
||||||
|
|
||||||
|
|
||||||
def seek_to_hex(address: str, data: BinaryIO) -> None:
|
def seek_to_hex(address: str, data: BinaryIO) -> None:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
+29
-40
@@ -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
|
||||||
@@ -55,19 +54,16 @@ class Res:
|
|||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
class Details:
|
class Details:
|
||||||
data: Union[list, None]
|
data: list[list[str]]
|
||||||
|
name: str
|
||||||
description: str
|
description: str
|
||||||
success: bool
|
gametime: str
|
||||||
|
day_accel: float
|
||||||
|
night_accel: float
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
class Record:
|
class Record:
|
||||||
# TODO: investigate whether this enum is still being used;
|
|
||||||
# can this be frozen?
|
|
||||||
"""
|
|
||||||
The gameport field is manipulated by the RowType.CONN_BY_IP method
|
|
||||||
"""
|
|
||||||
|
|
||||||
ip: str
|
ip: str
|
||||||
gameport: int
|
gameport: int
|
||||||
qport: int
|
qport: int
|
||||||
@@ -284,43 +280,41 @@ def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict[str, Any] |
|
|||||||
info = a2s.info((ip, qport), timeout)
|
info = a2s.info((ip, qport), timeout)
|
||||||
return source_info_to_dict(ip, qport, info)
|
return source_info_to_dict(ip, qport, info)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.debug(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_details(record: Record) -> Details:
|
def get_details(record: Record) -> Details | None:
|
||||||
# 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, a2s.exceptions.BrokenMessageError):
|
|
||||||
return Details(None, default_str, False)
|
|
||||||
try:
|
|
||||||
rules = dayzquery.dayz_rules((ip, qport))
|
rules = dayzquery.dayz_rules((ip, qport))
|
||||||
except Exception:
|
|
||||||
return Details(None, default_str, False)
|
|
||||||
|
|
||||||
try:
|
|
||||||
keywords = info.keywords.split(",")
|
keywords = info.keywords.split(",")
|
||||||
except AttributeError:
|
except Exception as e:
|
||||||
return Details(None, default_str, False)
|
logger.critical(e)
|
||||||
|
return None
|
||||||
|
|
||||||
battleye = strings.disabled
|
battleye = strings.disabled
|
||||||
if "battleye" in keywords:
|
if "battleye" in keywords:
|
||||||
battleye = strings.enabled
|
battleye = strings.enabled
|
||||||
|
|
||||||
day_accel = 0.0
|
name = info.server_name
|
||||||
night_accel = 0.0
|
day_accel = 1.0
|
||||||
|
night_accel = 1.0
|
||||||
|
gametime = "00:00"
|
||||||
for keyword in keywords:
|
for keyword in keywords:
|
||||||
if "etm" in keyword:
|
if "etm" in keyword:
|
||||||
day_accel = float(keyword.lstrip("etm"))
|
day_accel = float(keyword.lstrip("etm"))
|
||||||
day_accel = float(f"{day_accel:g}")
|
day_accel = float(f"{day_accel:g}")
|
||||||
if "entm" in keywords:
|
if "entm" in keyword:
|
||||||
night_accel = float(keyword.lstrip("entm"))
|
night_accel = float(keyword.lstrip("entm"))
|
||||||
night_accel = float(f"{night_accel:g}")
|
night_accel = float(f"{night_accel:g}")
|
||||||
|
time_reg = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]$)"
|
||||||
|
if re.match(time_reg, keyword):
|
||||||
|
gametime = keyword
|
||||||
|
|
||||||
try:
|
try:
|
||||||
password = info.password_protected
|
password = info.password_protected
|
||||||
@@ -347,10 +341,15 @@ def get_details(record: Record) -> Details:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
dlc = rules.dlc_flags
|
dlc = rules.dlc_flags
|
||||||
if dlc == 0:
|
match dlc:
|
||||||
dlc = strings.none
|
case 0:
|
||||||
if dlc == 2:
|
dlc = strings.dlc_none
|
||||||
dlc = strings.dlc_frostline
|
case 2:
|
||||||
|
dlc = strings.dlc_frostline
|
||||||
|
case 4:
|
||||||
|
dlc = strings.dlc_badlands
|
||||||
|
case _:
|
||||||
|
dlc = strings.unspecified
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
dlc = strings.unspecified
|
dlc = strings.unspecified
|
||||||
|
|
||||||
@@ -381,7 +380,7 @@ def get_details(record: Record) -> Details:
|
|||||||
["Version", version],
|
["Version", version],
|
||||||
]
|
]
|
||||||
|
|
||||||
return Details(rows, description, True)
|
return Details(rows, name, description, gametime, day_accel, night_accel)
|
||||||
|
|
||||||
|
|
||||||
def ping(ip: str, qport: int) -> int:
|
def ping(ip: str, qport: int) -> int:
|
||||||
@@ -478,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)
|
||||||
|
|||||||
+16
-1
@@ -11,7 +11,7 @@ from typing import Any
|
|||||||
|
|
||||||
from dzgui.api.steam import find_user_id_32
|
from dzgui.api.steam import find_user_id_32
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
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.dirs import copy_dzgui_to_xdg_data, make_parents
|
||||||
from dzgui.util.strings import unknown
|
from dzgui.util.strings import unknown
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
@@ -75,6 +75,8 @@ class Shortcuts:
|
|||||||
shortcuts = vdf.binary_load(f)
|
shortcuts = vdf.binary_load(f)
|
||||||
self.shortcuts = shortcuts
|
self.shortcuts = shortcuts
|
||||||
logger.debug("Loaded shortcuts file")
|
logger.debug("Loaded shortcuts file")
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.shortcuts = {"shortcuts": {}}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
raise e
|
raise e
|
||||||
@@ -109,6 +111,7 @@ class Shortcuts:
|
|||||||
for img in ("_hero", "p", "_logo"):
|
for img in ("_hero", "p", "_logo"):
|
||||||
filename = str(self.bpid) + img
|
filename = str(self.bpid) + img
|
||||||
dest = grid_path.joinpath(filename).with_suffix(".png")
|
dest = grid_path.joinpath(filename).with_suffix(".png")
|
||||||
|
make_parents(dest)
|
||||||
b = images.joinpath(img).with_suffix(".png").read_bytes()
|
b = images.joinpath(img).with_suffix(".png").read_bytes()
|
||||||
dest.write_bytes(b)
|
dest.write_bytes(b)
|
||||||
|
|
||||||
@@ -156,14 +159,26 @@ class Shortcuts:
|
|||||||
NEW_ENTRY["DevkitOverrideAppID"] = 0
|
NEW_ENTRY["DevkitOverrideAppID"] = 0
|
||||||
NEW_ENTRY["LastPlayTime"] = 0
|
NEW_ENTRY["LastPlayTime"] = 0
|
||||||
NEW_ENTRY["FlatpakAppID"] = ""
|
NEW_ENTRY["FlatpakAppID"] = ""
|
||||||
|
|
||||||
NEW_ENTRY["sortas"] = ""
|
NEW_ENTRY["sortas"] = ""
|
||||||
NEW_ENTRY["tags"] = {}
|
NEW_ENTRY["tags"] = {}
|
||||||
return NEW_ENTRY
|
return NEW_ENTRY
|
||||||
|
|
||||||
|
def delete_shortcut(self, start_path: Path) -> None:
|
||||||
|
shortcuts = self.shortcuts["shortcuts"]
|
||||||
|
for s in shortcuts:
|
||||||
|
if shortcuts[s]["StartDir"] == str(start_path):
|
||||||
|
del shortcuts[s]
|
||||||
|
break
|
||||||
|
self.save_shortcuts()
|
||||||
|
|
||||||
def save_shortcuts(self) -> None:
|
def save_shortcuts(self) -> None:
|
||||||
try:
|
try:
|
||||||
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||||
shutil.copy(self.shortcuts_path, backup)
|
shutil.copy(self.shortcuts_path, backup)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
try:
|
||||||
with open(self.shortcuts_path, "wb") as f:
|
with open(self.shortcuts_path, "wb") as f:
|
||||||
vdf.binary_dump(self.shortcuts, f)
|
vdf.binary_dump(self.shortcuts, f)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
+88
-14
@@ -1,23 +1,29 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import psutil
|
import psutil
|
||||||
|
import re
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
import vdf # type: ignore
|
import vdf # type: ignore
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Union
|
from requests.exceptions import HTTPError
|
||||||
|
from typing import Any, Generator, Union
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
from dzgui.api.acf import ACF
|
from dzgui.api.acf import ACF
|
||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.init.prereqs import has_any_client
|
||||||
|
from dzgui.init.proc import has_cmd
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
DAYZ_BINARY,
|
DAYZ_BINARY,
|
||||||
|
DAYZ_LAUNCHER,
|
||||||
DEBIAN_STEAM_PATH,
|
DEBIAN_STEAM_PATH,
|
||||||
DEFAULT_STEAM_PATH,
|
DEFAULT_STEAM_PATH,
|
||||||
|
DOWNLOADS_PATH,
|
||||||
|
FLATPAK_CMD,
|
||||||
FLATPAK_STEAM_PATH,
|
FLATPAK_STEAM_PATH,
|
||||||
LIBRARYFOLDERS_PATH,
|
LIBRARYFOLDERS_PATH,
|
||||||
UBUNTU_STEAM_PATH,
|
UBUNTU_STEAM_PATH,
|
||||||
@@ -31,6 +37,7 @@ from dzgui.const.endpoints import (
|
|||||||
UNSUB_ENDPOINT,
|
UNSUB_ENDPOINT,
|
||||||
)
|
)
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
|
from dzgui.strings.errors import api_validation_error, workshop_endpoint_error
|
||||||
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)
|
||||||
@@ -56,7 +63,7 @@ class VDFLoadError(Exception):
|
|||||||
|
|
||||||
def get_steam_paths() -> list[tuple[Path, str]]:
|
def get_steam_paths() -> list[tuple[Path, str]]:
|
||||||
paths = []
|
paths = []
|
||||||
if has_steam_client():
|
if has_any_client():
|
||||||
HOME = Path.home()
|
HOME = Path.home()
|
||||||
env = os.environ.get("XDG_DATA_HOME")
|
env = os.environ.get("XDG_DATA_HOME")
|
||||||
if env is not None:
|
if env is not None:
|
||||||
@@ -77,6 +84,8 @@ def get_steam_paths() -> list[tuple[Path, str]]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH:
|
for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH:
|
||||||
|
if path is FLATPAK_PATH and has_cmd(FLATPAK_CMD) is False:
|
||||||
|
continue
|
||||||
if path.joinpath(VDF_PATH).is_file():
|
if path.joinpath(VDF_PATH).is_file():
|
||||||
paths.append((path, human[path]))
|
paths.append((path, human[path]))
|
||||||
return paths
|
return paths
|
||||||
@@ -139,6 +148,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"])
|
||||||
@@ -147,6 +159,25 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
|||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
|
def connect_debug(
|
||||||
|
client: str, addr: str, appid: int, name: str, mods: list[str]
|
||||||
|
) -> str:
|
||||||
|
concat = concat_mods(mods)
|
||||||
|
client_args = concat_bash_args(client)
|
||||||
|
params = [
|
||||||
|
"-applaunch",
|
||||||
|
str(appid),
|
||||||
|
f"-connect={addr}",
|
||||||
|
"-nolauncher",
|
||||||
|
"-nosplash",
|
||||||
|
"-skipintro",
|
||||||
|
f"-name={name}",
|
||||||
|
f"-mod={concat}",
|
||||||
|
]
|
||||||
|
client_args.extend(params)
|
||||||
|
return " ".join(client_args)
|
||||||
|
|
||||||
|
|
||||||
# TODO: set config to name=user, use official server and no mods,
|
# TODO: set config to name=user, use official server and no mods,
|
||||||
# ensure that formatted string is identical to fixture with same hash
|
# ensure that formatted string is identical to fixture with same hash
|
||||||
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
def connect(client: str, addr: str, appid: int, name: str, mods: list[str]) -> int:
|
||||||
@@ -171,6 +202,7 @@ def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int:
|
|||||||
"""Loads to the menu screen with the selected mods; used for AFK/pre-joining"""
|
"""Loads to the menu screen with the selected mods; used for AFK/pre-joining"""
|
||||||
concat = concat_mods(mods)
|
concat = concat_mods(mods)
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
|
logger.debug(f"Setting client arguments to: {client_args}")
|
||||||
params = [
|
params = [
|
||||||
"-applaunch",
|
"-applaunch",
|
||||||
str(appid),
|
str(appid),
|
||||||
@@ -205,28 +237,33 @@ def launch_offline(
|
|||||||
proc = subprocess.run([*client_args, *params])
|
proc = subprocess.run([*client_args, *params])
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
def find_loginusers(path: Path) -> Path:
|
def find_loginusers(path: Path) -> Path:
|
||||||
return path / "config" / "loginusers.vdf"
|
return path / "config" / "loginusers.vdf"
|
||||||
|
|
||||||
|
|
||||||
def find_user_id(path: Path) -> str | None:
|
def find_user_id(path: Path) -> str | None:
|
||||||
resolved_path = find_loginusers(path)
|
resolved_path = find_loginusers(path)
|
||||||
try:
|
try:
|
||||||
with open(resolved_path, "r") as f:
|
with open(resolved_path, "r") as f:
|
||||||
v = vdf.load(f)
|
v = vdf.load(f)
|
||||||
# NOTE: beta client
|
users = v["users"]
|
||||||
# /package/beta
|
# NOTE: beta client: /package/beta
|
||||||
if len(v["users"]) == 1:
|
last_user = str(next(iter(users)))
|
||||||
return str(list(v["users"].keys())[0])
|
if len(users) == 1:
|
||||||
for user in v["users"]:
|
return last_user
|
||||||
if v["users"][user]["MostRecent"] == "1":
|
last_stamp = int(users[last_user]["timestamp"])
|
||||||
return str(user)
|
for user in users:
|
||||||
return None
|
stamp = int(users[user]["timestamp"])
|
||||||
|
if stamp > last_stamp:
|
||||||
|
last_stamp = stamp
|
||||||
|
last_user = user
|
||||||
|
return last_user
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.debug(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def find_user_id_32(path: Path) -> int:
|
def find_user_id_32(path: Path) -> int:
|
||||||
uid = find_user_id(path)
|
uid = find_user_id(path)
|
||||||
if uid is None:
|
if uid is None:
|
||||||
@@ -234,6 +271,16 @@ def find_user_id_32(path: Path) -> int:
|
|||||||
return int(uid) & 0xFFFFFFFF
|
return int(uid) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
class SteamAPIKeyError(Exception):
|
||||||
|
def __init__(self, msg: str | None = None) -> None:
|
||||||
|
super().__init__(msg or api_validation_error)
|
||||||
|
|
||||||
|
|
||||||
|
class SteamWorkshopEndpointError(Exception):
|
||||||
|
def __init__(self, msg: str | None = None) -> None:
|
||||||
|
super().__init__(msg or workshop_endpoint_error)
|
||||||
|
|
||||||
|
|
||||||
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
||||||
payload: dict[str, Union[int, str]] = {
|
payload: dict[str, Union[int, str]] = {
|
||||||
"publishedfileid": mod,
|
"publishedfileid": mod,
|
||||||
@@ -245,8 +292,19 @@ def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
|||||||
try:
|
try:
|
||||||
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
|
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
except HTTPError as e:
|
||||||
|
logger.critical(e)
|
||||||
|
match res.status_code:
|
||||||
|
case 403:
|
||||||
|
raise SteamAPIKeyError()
|
||||||
|
case 404:
|
||||||
|
raise SteamWorkshopEndpointError()
|
||||||
|
case _:
|
||||||
|
# TODO: apply redaction filter
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def subscribe(key: str, mod: int) -> None:
|
def subscribe(key: str, mod: int) -> None:
|
||||||
@@ -378,14 +436,30 @@ def get_client_allows_downloads(path: Path) -> bool:
|
|||||||
def is_dayz_running() -> bool:
|
def is_dayz_running() -> bool:
|
||||||
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
||||||
procs = []
|
procs = []
|
||||||
substring = DAYZ_BINARY
|
substring = [DAYZ_BINARY, DAYZ_LAUNCHER]
|
||||||
for proc in psutil.process_iter():
|
for proc in psutil.process_iter():
|
||||||
try:
|
try:
|
||||||
procs.append(proc.cmdline())
|
procs.append(proc.cmdline())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(e)
|
logger.warning(e)
|
||||||
continue
|
continue
|
||||||
return any(substring in item for sublist in procs for item in sublist)
|
return any(s in item for sublist in procs for item in sublist for s in substring)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pending_downloads(steam_path: Path, appid: int) -> Generator[Path, None, None]:
|
||||||
|
root_path = get_app_path(steam_path, appid)
|
||||||
|
downloads_dir = root_path / DOWNLOADS_PATH
|
||||||
|
files = downloads_dir.iterdir()
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def is_dayz_downloading(steam_path: Path, appid: int) -> bool:
|
||||||
|
files = get_pending_downloads(steam_path, appid)
|
||||||
|
reg = re.compile(rf"^state_(?:{APPID_DAYZ_EXP}|{APPID_DAYZ})_.*\.patch$")
|
||||||
|
pending = [f for f in files if reg.match(f.name)]
|
||||||
|
if len(pending) > 0:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_app_path(folders_path: Path, appid: int) -> Path:
|
def get_app_path(folders_path: Path, appid: int) -> Path:
|
||||||
|
|||||||
+35
-7
@@ -1,10 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
from warnings import deprecated
|
||||||
|
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import APP_NAME, STEAM_ICON, STEAM_ICONS_PATH
|
||||||
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.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
@@ -15,15 +18,15 @@ from dzgui.init.migrate import (
|
|||||||
migrate_cols_file,
|
migrate_cols_file,
|
||||||
)
|
)
|
||||||
from dzgui.init.flock import lock_acquire
|
from dzgui.init.flock import lock_acquire
|
||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.init.prereqs import has_any_client
|
||||||
from dzgui.strings import boot
|
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.deck import is_steam_deck, is_game_mode
|
||||||
from dzgui.util.dirs import make_parents
|
from dzgui.util.dirs import make_parents
|
||||||
from dzgui.util.localize import set_locale
|
from dzgui.util.localize import set_locale
|
||||||
|
from dzgui.util.map_count import test_map_count
|
||||||
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||||
from dzgui.util.strings import init
|
from dzgui.util.strings import init, steam_icon_missing
|
||||||
|
|
||||||
from dzgui.views.base import App
|
from dzgui.views.base import App
|
||||||
from dzgui.views.dialogs.boot import BootWindow
|
from dzgui.views.dialogs.boot import BootWindow
|
||||||
@@ -96,6 +99,28 @@ def copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
|
|||||||
return (config_changed, state_changed)
|
return (config_changed, state_changed)
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Currently unused")
|
||||||
|
def load_steam_icon(config: Path) -> None:
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk # noqa
|
||||||
|
|
||||||
|
default = lookup(config, Preferences.DEFAULT)
|
||||||
|
icon_path = Path(default).joinpath(STEAM_ICONS_PATH)
|
||||||
|
try:
|
||||||
|
theme = Gtk.IconTheme.get_default()
|
||||||
|
paths = theme.get_search_path()
|
||||||
|
theme.set_search_path(paths + [str(icon_path)])
|
||||||
|
paths = theme.get_search_path()
|
||||||
|
icons = theme.list_icons(None)
|
||||||
|
if STEAM_ICON not in icons:
|
||||||
|
logger.warning(steam_icon_missing)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
logger.warning(e)
|
||||||
|
|
||||||
|
|
||||||
def load_gui(version: str, is_debug: bool) -> None:
|
def load_gui(version: str, is_debug: bool) -> None:
|
||||||
lock = lock_acquire() # noqa
|
lock = lock_acquire() # noqa
|
||||||
|
|
||||||
@@ -119,10 +144,11 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
del os.environ["GTK_IM_MODULE"]
|
del os.environ["GTK_IM_MODULE"]
|
||||||
|
|
||||||
if has_new_config(XDG.config) is False:
|
if has_new_config(XDG.config) is False:
|
||||||
|
test_map_count()
|
||||||
migrate_cols_file(XDG.columns)
|
migrate_cols_file(XDG.columns)
|
||||||
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
||||||
# TODO: add logging inside wizard
|
# TODO: add logging inside wizard
|
||||||
wizard = SetupWizard(_is_steam_deck, XDG)
|
wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths)
|
||||||
|
|
||||||
if not wizard.is_setup_complete():
|
if not wizard.is_setup_complete():
|
||||||
return
|
return
|
||||||
@@ -135,13 +161,14 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
# TODO: sudo escalation dialog
|
# TODO: sudo escalation dialog
|
||||||
# count = get_map_count()
|
# count = get_map_count()
|
||||||
# TODO: move into module
|
# TODO: move into module
|
||||||
if has_steam_client() is False:
|
if has_any_client() is False:
|
||||||
EarlyAlertDialog(init.requires_steam)
|
EarlyAlertDialog(init.requires_steam)
|
||||||
|
|
||||||
bootwin = BootWindow(XDG, version)
|
bootwin = BootWindow(XDG, version)
|
||||||
local_coords, latest_release = bootwin.get_results()
|
local_coords, latest_release = bootwin.get_results()
|
||||||
|
|
||||||
use_miles = lookup(XDG.config, Preferences.DIST)
|
use_miles = lookup(XDG.config, Preferences.DIST)
|
||||||
|
|
||||||
prefs = UserPrefs(
|
prefs = UserPrefs(
|
||||||
is_steam_deck=_is_steam_deck,
|
is_steam_deck=_is_steam_deck,
|
||||||
is_game_mode=_is_game_mode,
|
is_game_mode=_is_game_mode,
|
||||||
@@ -153,4 +180,5 @@ def load_gui(version: str, is_debug: bool) -> None:
|
|||||||
use_miles=use_miles,
|
use_miles=use_miles,
|
||||||
)
|
)
|
||||||
print(boot.all_ok)
|
print(boot.all_ok)
|
||||||
App(prefs)
|
app = App(prefs)
|
||||||
|
app.run(None)
|
||||||
@@ -42,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",
|
||||||
@@ -66,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()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import textwrap
|
|||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
|
||||||
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
|
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +20,7 @@ def write_desktop_file(exe_path: Path) -> Path:
|
|||||||
Terminal=false
|
Terminal=false
|
||||||
Exec={exe_path}
|
Exec={exe_path}
|
||||||
Name=DZGUI
|
Name=DZGUI
|
||||||
Comment=dzgui
|
Comment=DayZ server browser and mod manager
|
||||||
Icon={icon}
|
Icon={icon}
|
||||||
Categories=Game"""
|
Categories=Game"""
|
||||||
|
|
||||||
@@ -37,7 +36,8 @@ def write_desktop_file(exe_path: Path) -> Path:
|
|||||||
|
|
||||||
def write_desktop_shortcut(desktop_file: Path) -> None:
|
def write_desktop_shortcut(desktop_file: Path) -> None:
|
||||||
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
|
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
|
||||||
link = Path.home().joinpath(f"Desktop/{APP_NAME}.desktop")
|
link = Path.home().joinpath("Desktop/dzgui.desktop")
|
||||||
|
make_parents(link)
|
||||||
if link.exists():
|
if link.exists():
|
||||||
link.unlink()
|
link.unlink()
|
||||||
link.symlink_to(desktop_file)
|
link.symlink_to(desktop_file)
|
||||||
@@ -4,8 +4,6 @@ from pathlib import Path
|
|||||||
from dzgui.util._json import read_json
|
from dzgui.util._json import read_json
|
||||||
|
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.const.constants import STEAM_CMD, FLATPAK_RUN_CMD, FLATPAK_SANDBOX
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigFileError(Exception):
|
class ConfigFileError(Exception):
|
||||||
pass
|
pass
|
||||||
@@ -40,23 +38,5 @@ def get_favorites(path: Path) -> list[str]:
|
|||||||
return list(conf["ip_list"])
|
return list(conf["ip_list"])
|
||||||
|
|
||||||
|
|
||||||
# def is_in_favs(record: str, path: Path) -> bool:
|
|
||||||
# favs = get_favorites(path)
|
|
||||||
# if record in favs:
|
|
||||||
# return True
|
|
||||||
# return False
|
|
||||||
|
|
||||||
|
|
||||||
def enum_to_key(enum: Preferences) -> str:
|
def enum_to_key(enum: Preferences) -> str:
|
||||||
return str(enum.dict["key"])
|
return str(enum.dict["key"])
|
||||||
|
|
||||||
|
|
||||||
def get_client_index(client: str) -> int:
|
|
||||||
if client == STEAM_CMD:
|
|
||||||
return 0
|
|
||||||
elif client == FLATPAK_RUN_CMD:
|
|
||||||
return 1
|
|
||||||
elif client == FLATPAK_SANDBOX:
|
|
||||||
return 2
|
|
||||||
else:
|
|
||||||
return 0
|
|
||||||
+10
-2
@@ -3,7 +3,13 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG
|
from dzgui.const.constants import (
|
||||||
|
APP_NAME_LOWER,
|
||||||
|
DEBUG_LOG,
|
||||||
|
FLATPAK_ROOT_PATH,
|
||||||
|
SYSTEM_LOG,
|
||||||
|
)
|
||||||
|
from dzgui.init.proc import is_sandboxed
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -53,10 +59,12 @@ def get_xdg_paths() -> dict:
|
|||||||
"XDG_DATA_HOME": home / ".local" / "share",
|
"XDG_DATA_HOME": home / ".local" / "share",
|
||||||
"XDG_CACHE_HOME": home / ".cache",
|
"XDG_CACHE_HOME": home / ".cache",
|
||||||
}
|
}
|
||||||
|
|
||||||
resolved_paths = {}
|
resolved_paths = {}
|
||||||
for path in xdg_paths:
|
for path in xdg_paths:
|
||||||
real_path = os.environ.get(path)
|
real_path = os.environ.get(path)
|
||||||
|
if real_path is not None and is_sandboxed():
|
||||||
|
search_string = FLATPAK_ROOT_PATH + "/"
|
||||||
|
real_path = real_path.replace(search_string, "")
|
||||||
if real_path is not None and is_writeable(real_path):
|
if real_path is not None and is_writeable(real_path):
|
||||||
new_path = Path(real_path)
|
new_path = Path(real_path)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -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": "",
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ APPNAME_DAYZ = "DayZ"
|
|||||||
APPNAME_DAYZ_EXP = "DayZ Exp"
|
APPNAME_DAYZ_EXP = "DayZ Exp"
|
||||||
APPNAME_DAYZ_EXP_HUMAN = "DayZ Experimental"
|
APPNAME_DAYZ_EXP_HUMAN = "DayZ Experimental"
|
||||||
DAYZ_BINARY = "DayZ_x64.exe"
|
DAYZ_BINARY = "DayZ_x64.exe"
|
||||||
|
DAYZ_LAUNCHER = "DayZLauncher.exe"
|
||||||
|
|
||||||
LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf"
|
LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf"
|
||||||
|
DOWNLOADS_PATH = "steamapps/downloading"
|
||||||
WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)
|
WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)
|
||||||
|
|
||||||
STEAM_CMD = "steam"
|
STEAM_CMD = "steam"
|
||||||
@@ -29,15 +31,18 @@ BETA_REPO = "testing"
|
|||||||
APP_NAME = "DZGUI"
|
APP_NAME = "DZGUI"
|
||||||
APP_NAME_LOWER = "dzgui"
|
APP_NAME_LOWER = "dzgui"
|
||||||
APP_NAME_ABBR = "dzg"
|
APP_NAME_ABBR = "dzg"
|
||||||
|
GTK_APP_ID = "io.github.aclist.dzgui"
|
||||||
|
|
||||||
HEX_GREEN = "#32CD32"
|
HEX_GREEN = "#32CD32"
|
||||||
HEX_RED = "#FF0000"
|
HEX_RED = "#FF0000"
|
||||||
HEX_ORANGE = "#FFAC1C"
|
HEX_ORANGE = "#FFAC1C"
|
||||||
|
|
||||||
# FIXME: normalize icon names
|
# TODO: normalize icon names
|
||||||
|
CALCULATOR = "accessories-calculator-symbolic"
|
||||||
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"
|
||||||
@@ -51,6 +56,8 @@ VIEW_CONCEAL = "view-conceal-symbolic"
|
|||||||
VIEW_REVEAL = "view-reveal-symbolic"
|
VIEW_REVEAL = "view-reveal-symbolic"
|
||||||
WARNING = "dialog-warning-symbolic"
|
WARNING = "dialog-warning-symbolic"
|
||||||
WEB_BROWSER = "web-browser-symbolic"
|
WEB_BROWSER = "web-browser-symbolic"
|
||||||
|
DAY_ICON = "weather-clear-symbolic"
|
||||||
|
NIGHT_ICON = "weather-clear-night-symbolic"
|
||||||
|
|
||||||
SEPARATOR = "SEPARATOR"
|
SEPARATOR = "SEPARATOR"
|
||||||
|
|
||||||
@@ -78,9 +85,11 @@ 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"
|
||||||
FLATPAK_STEAM_PATH = ".var/app/com.valvesoftware.Steam/data/Steam"
|
FLATPAK_ROOT_PATH = ".var/app/com.valvesoftware.Steam"
|
||||||
|
FLATPAK_STEAM_PATH = FLATPAK_ROOT_PATH + "/data/Steam"
|
||||||
UBUNTU_STEAM_PATH = ".steam/steam"
|
UBUNTU_STEAM_PATH = ".steam/steam"
|
||||||
DEBIAN_STEAM_PATH = ".steam/debian-installation"
|
DEBIAN_STEAM_PATH = ".steam/debian-installation"
|
||||||
|
STEAM_ICONS_PATH = "public"
|
||||||
|
|
||||||
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
|
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
|
||||||
|
|
||||||
@@ -89,3 +98,8 @@ TMP_TARBALL = "/tmp/dzgui.tar.gz"
|
|||||||
TMP_EXE = "/tmp/dzgui/dzgui"
|
TMP_EXE = "/tmp/dzgui/dzgui"
|
||||||
|
|
||||||
DAYZ_COMMUNITY_ROOT = "Missions"
|
DAYZ_COMMUNITY_ROOT = "Missions"
|
||||||
|
|
||||||
|
GIO_SETTINGS_INTERFACE = "org.gnome.desktop.interface"
|
||||||
|
GIO_SETTINGS_PROP = "color-scheme"
|
||||||
|
GIO_SETTINGS_KEY = "gtk-application-prefer-dark-theme"
|
||||||
|
GIO_SETTINGS_VAL = "prefer-dark"
|
||||||
@@ -7,7 +7,6 @@ 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?"
|
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"
|
||||||
@@ -27,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",
|
||||||
}
|
}
|
||||||
@@ -76,12 +73,6 @@ class Preferences(EnumWithAttrs):
|
|||||||
NAME = {
|
NAME = {
|
||||||
"key": "name",
|
"key": "name",
|
||||||
}
|
}
|
||||||
INSTALL = {
|
|
||||||
"key": "auto_install",
|
|
||||||
}
|
|
||||||
DEBUG = {
|
|
||||||
"key": "debug",
|
|
||||||
}
|
|
||||||
DEFAULT = {
|
DEFAULT = {
|
||||||
"key": "default_steam_path",
|
"key": "default_steam_path",
|
||||||
}
|
}
|
||||||
@@ -94,9 +85,6 @@ class Preferences(EnumWithAttrs):
|
|||||||
IP_LIST = {
|
IP_LIST = {
|
||||||
"key": "ip_list",
|
"key": "ip_list",
|
||||||
}
|
}
|
||||||
BRANCH = {
|
|
||||||
"key": "branch",
|
|
||||||
}
|
|
||||||
START_TAB = {
|
START_TAB = {
|
||||||
"key": "start_tab",
|
"key": "start_tab",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,6 +145,10 @@ class Emitter(GObject.GObject):
|
|||||||
def api_change_failed(self) -> None:
|
def api_change_failed(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
|
def api_change_successful(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
def already_saved_server(self) -> None:
|
def already_saved_server(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|||||||
+30
-21
@@ -7,11 +7,12 @@ import dzgui.util._json as JSON # noqa
|
|||||||
|
|
||||||
|
|
||||||
from dzgui.const.enum import (
|
from dzgui.const.enum import (
|
||||||
FilterMode,
|
|
||||||
Preferences,
|
|
||||||
NotebookPage,
|
|
||||||
ButtonType,
|
ButtonType,
|
||||||
ContextMenu,
|
ContextMenu,
|
||||||
|
FilterMode,
|
||||||
|
NotebookPage,
|
||||||
|
Preferences,
|
||||||
|
ServerTab,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dzgui.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
@@ -37,7 +38,6 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
from dzgui.const.enum import ServerTab
|
|
||||||
from dzgui.managers.connection import Prerequisites
|
from dzgui.managers.connection import Prerequisites
|
||||||
from dzgui.managers.filter import FilterManager
|
from dzgui.managers.filter import FilterManager
|
||||||
from dzgui.util.dist import Haversine
|
from dzgui.util.dist import Haversine
|
||||||
@@ -88,7 +88,6 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
# NOTE: suppress requests until entire UI is loaded
|
# NOTE: suppress requests until entire UI is loaded
|
||||||
self.loaded = False
|
self.loaded = False
|
||||||
self.pending_jobs = 1
|
|
||||||
|
|
||||||
self.exit_event = threading.Event()
|
self.exit_event = threading.Event()
|
||||||
self.cancel_event = threading.Event()
|
self.cancel_event = threading.Event()
|
||||||
@@ -115,9 +114,6 @@ class Controller(GObject.GObject):
|
|||||||
def query_config(self, key: Preferences) -> Any:
|
def query_config(self, key: Preferences) -> Any:
|
||||||
return self.config_man.lookup(key)
|
return self.config_man.lookup(key)
|
||||||
|
|
||||||
def is_auto_install(self) -> bool:
|
|
||||||
return bool(self.query_config(Preferences.INSTALL))
|
|
||||||
|
|
||||||
def suppress_signal(
|
def suppress_signal(
|
||||||
self, owner: Any, child: Any, func_name: str, state: bool
|
self, owner: Any, child: Any, func_name: str, state: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -267,6 +263,7 @@ class Controller(GObject.GObject):
|
|||||||
self.open_page(NotebookPage.LOG)
|
self.open_page(NotebookPage.LOG)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dialog = ExceptionDialog(self, str(e))
|
dialog = ExceptionDialog(self, str(e))
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def select_colorized(self) -> None:
|
def select_colorized(self) -> None:
|
||||||
@@ -300,10 +297,11 @@ class Controller(GObject.GObject):
|
|||||||
write_diagnostic(self.prefs.paths.config, file)
|
write_diagnostic(self.prefs.paths.config, file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
dialog = ExceptionDialog(self, str(e))
|
dialog = ExceptionDialog(self, str(e))
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
def update_steam_api_key(self, text: str) -> None:
|
||||||
self.config_man.update_api_key(key, text)
|
self.config_man.update_steam_api_key(text)
|
||||||
|
|
||||||
def set_resolution(self, window: "OuterWindow") -> None:
|
def set_resolution(self, window: "OuterWindow") -> None:
|
||||||
self.config_man.set_resolution(window)
|
self.config_man.set_resolution(window)
|
||||||
@@ -402,6 +400,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:
|
||||||
@@ -413,7 +414,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()
|
||||||
@@ -437,14 +438,8 @@ 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():
|
self.connection_man = ConnectionManager(self)
|
||||||
config_man = self.get_config_man()
|
self.connection_man.connect_by_ip(addr)
|
||||||
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.connect_by_ip(addr)
|
|
||||||
|
|
||||||
def connect_by_record(self, record: "Record") -> None:
|
def connect_by_record(self, record: "Record") -> None:
|
||||||
self.connection_man = ConnectionManager(self)
|
self.connection_man = ConnectionManager(self)
|
||||||
@@ -465,6 +460,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
|
||||||
@@ -479,11 +477,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)
|
||||||
@@ -498,6 +498,9 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
def set_active_keyword(self, keyword: str) -> None:
|
def set_active_keyword(self, keyword: str) -> None:
|
||||||
self.get_filter_man().set_active_keyword(keyword)
|
self.get_filter_man().set_active_keyword(keyword)
|
||||||
|
ServerModelManager(self, self.get_active_treeview()).refilter(
|
||||||
|
FilterMode.TOGGLE_ON
|
||||||
|
)
|
||||||
|
|
||||||
def get_modtreeview(self) -> "ModTreeView":
|
def get_modtreeview(self) -> "ModTreeView":
|
||||||
return self.mediator.modtreeview
|
return self.mediator.modtreeview
|
||||||
@@ -523,8 +526,12 @@ class Controller(GObject.GObject):
|
|||||||
|
|
||||||
def set_start_tab(self) -> None:
|
def set_start_tab(self) -> None:
|
||||||
ind = self.config_man.get_start_tab()
|
ind = self.config_man.get_start_tab()
|
||||||
|
logger.info(f"Setting start tab to index {ind}")
|
||||||
self.get_servers().notebook.set_current_page(ind)
|
self.get_servers().notebook.set_current_page(ind)
|
||||||
|
|
||||||
|
def get_debug_args(self) -> str:
|
||||||
|
return self.connection_man.get_debug_args()
|
||||||
|
|
||||||
def update_and_load_to_menu(self) -> None:
|
def update_and_load_to_menu(self) -> None:
|
||||||
self.connection_man.update_and_connect(menu_only=True)
|
self.connection_man.update_and_connect(menu_only=True)
|
||||||
|
|
||||||
@@ -534,8 +541,10 @@ class Controller(GObject.GObject):
|
|||||||
def update_status(self) -> None:
|
def update_status(self) -> None:
|
||||||
self.mediator.preconnect.mark_finished()
|
self.mediator.preconnect.mark_finished()
|
||||||
|
|
||||||
def get_steam_client_name(self) -> str:
|
def get_client_name(self) -> str:
|
||||||
return self.mediator.options.get_client_name()
|
# NOTE: only possible values are "Steam" and "Flatpak"
|
||||||
|
cmd = self.query_config(Preferences.CLIENT)
|
||||||
|
return str(cmd.split()[0].capitalize())
|
||||||
|
|
||||||
def present_window(self) -> None:
|
def present_window(self) -> None:
|
||||||
self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
|
self.mediator.window.present_with_time(Gdk.CURRENT_TIME)
|
||||||
|
|||||||
+20
-1
@@ -3,6 +3,8 @@
|
|||||||
## [7.0.0] Unreleased
|
## [7.0.0] Unreleased
|
||||||
### Added
|
### Added
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
|
- Uninstall wizard
|
||||||
|
- Descriptive details panel to certain error dialogs
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
- Documentation ships with source
|
- Documentation ships with source
|
||||||
@@ -24,6 +26,7 @@
|
|||||||
- Set favorite server from tables
|
- Set favorite server from tables
|
||||||
- Detailed/copyable trace in critical error dialogs
|
- Detailed/copyable trace in critical error dialogs
|
||||||
- Visual icons
|
- Visual icons
|
||||||
|
- Unset favorite server button
|
||||||
- Integrated server notebook
|
- Integrated server notebook
|
||||||
- Propagate width changes to all tables
|
- Propagate width changes to all tables
|
||||||
- Remember tree position in menus
|
- Remember tree position in menus
|
||||||
@@ -44,11 +47,20 @@
|
|||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
- Generate Steam shortcuts and cover art
|
- Generate Steam shortcuts and cover art
|
||||||
- Warn user if background downloads are disabled
|
- Warn user if background downloads are disabled
|
||||||
|
- Dynamic copy button
|
||||||
|
- Update map count (-u flag)
|
||||||
|
- Filter button tooltips
|
||||||
|
- Expand/collapse changelog versions
|
||||||
|
- Support local light/dark mode changes on system theme
|
||||||
|
- Server time calculator
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
|
- Enforce fullscreen mode on Steam Deck
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
|
- Changed some dialog error messages from popups to popovers
|
||||||
|
- Changed how filter buttons are packed in sidebar
|
||||||
- Bold labels inside frames
|
- Bold labels inside frames
|
||||||
- Sidebar buttons do not steal focus
|
- Sidebar buttons do not steal focus
|
||||||
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
|
||||||
@@ -61,18 +73,25 @@
|
|||||||
- Reduce size of geolocation DB on disk (~100MB)
|
- Reduce size of geolocation DB on disk (~100MB)
|
||||||
- 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
|
||||||
|
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||||
|
|
||||||
### Dropped
|
### Dropped
|
||||||
- Debug mode
|
- Battlemetrics API support
|
||||||
|
- Unused metadata from system log files
|
||||||
- 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
|
||||||
|
- In-game time field missing certain ranges of time at night
|
||||||
|
- Wrap long entries in changelog
|
||||||
|
- Certain server descriptions with special characters not rendering correctly
|
||||||
- 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
|
||||||
|
- Current keyword filter not being applied when refreshing table
|
||||||
|
|
||||||
### Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
|
|||||||
@@ -24,6 +24,10 @@
|
|||||||
.left-label {
|
.left-label {
|
||||||
font-size: 1.3rem;
|
font-size: 1.3rem;
|
||||||
}
|
}
|
||||||
|
.server-subheading {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
.details-heading {
|
.details-heading {
|
||||||
font-size: 1.2rem
|
font-size: 1.2rem
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-2
@@ -4,17 +4,24 @@ from pathlib import Path
|
|||||||
|
|
||||||
from dzgui.api.steam import get_app_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
|
from dzgui.const.constants import APPID_DAYZ, APP_NAME, APPNAME_DAYZ
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
|
|
||||||
|
from dzgui.strings.preconnect import resync
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
class VDFSyncError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
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)
|
||||||
get_app_path(Path(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
|
msg = resync.format(APPNAME_DAYZ)
|
||||||
|
raise VDFSyncError(msg)
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import json
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -7,11 +8,10 @@ from dzgui.config.convert import rc2json
|
|||||||
from dzgui.util._json import read_json, write_json
|
from dzgui.util._json import read_json, write_json
|
||||||
|
|
||||||
|
|
||||||
def migrate_legacy_conf(config: Path) -> None:
|
def migrate_legacy_conf(config: Path) -> Any:
|
||||||
old_conf = Path.home() / LEGACY_CONFIG_PATH
|
old_conf = Path.home() / LEGACY_CONFIG_PATH
|
||||||
j = rc2json(old_conf)
|
j = rc2json(old_conf)
|
||||||
config.parent.mkdir(parents=True, exist_ok=True)
|
return json.loads(j)
|
||||||
config.write_text(j)
|
|
||||||
|
|
||||||
|
|
||||||
def has_new_config(config: Path) -> bool:
|
def has_new_config(config: Path) -> bool:
|
||||||
|
|||||||
+20
-7
@@ -7,19 +7,32 @@ from dzgui.const.constants import APP_NAME, FLATPAK_APPID, FLATPAK_CMD, STEAM_CM
|
|||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def has_steam_client() -> bool:
|
def has_flatpak_client() -> bool:
|
||||||
if shutil.which(STEAM_CMD):
|
|
||||||
return True
|
|
||||||
if not shutil.which(FLATPAK_CMD):
|
|
||||||
return False
|
|
||||||
try:
|
try:
|
||||||
res = subprocess.run(
|
res = subprocess.run(
|
||||||
[FLATPAK_CMD, "list"], capture_output=True, text=True, check=True
|
[FLATPAK_CMD, "list", "--columns=application"],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=True,
|
||||||
)
|
)
|
||||||
if FLATPAK_APPID in res.stdout:
|
if FLATPAK_APPID in res.stdout:
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
except subprocess.CalledProcessError as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def has_steam_client() -> bool:
|
||||||
|
if shutil.which(STEAM_CMD):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def has_any_client() -> bool:
|
||||||
|
if shutil.which(STEAM_CMD):
|
||||||
|
return True
|
||||||
|
if not shutil.which(FLATPAK_CMD):
|
||||||
|
return False
|
||||||
|
return has_flatpak_client()
|
||||||
+42
-16
@@ -1,7 +1,8 @@
|
|||||||
import psutil
|
|
||||||
import subprocess
|
|
||||||
import shutil
|
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
@@ -10,42 +11,67 @@ from dzgui.const.constants import (
|
|||||||
FLATPAK_CMD,
|
FLATPAK_CMD,
|
||||||
FLATPAK_RUN_CMD,
|
FLATPAK_RUN_CMD,
|
||||||
FLATPAK_SANDBOX,
|
FLATPAK_SANDBOX,
|
||||||
|
FLATPAK_ROOT_PATH,
|
||||||
|
FLATPAK_STEAM_PATH,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def is_steam_running(cmd: str) -> bool:
|
def is_steam_running(cmd: str) -> bool:
|
||||||
if cmd == STEAM_CMD:
|
if cmd in (FLATPAK_RUN_CMD, FLATPAK_SANDBOX):
|
||||||
if has_cmd(STEAM_CMD) is False:
|
|
||||||
return False
|
|
||||||
return is_running(STEAM_CMD)
|
|
||||||
elif cmd == FLATPAK_RUN_CMD or FLATPAK_SANDBOX:
|
|
||||||
return is_flatpak_steam_running()
|
return is_flatpak_steam_running()
|
||||||
|
elif cmd == STEAM_CMD:
|
||||||
|
return is_native_steam_running()
|
||||||
else:
|
else:
|
||||||
raise TypeError("Not a valid Steam client selection")
|
raise TypeError("Not a valid Steam client selection")
|
||||||
|
|
||||||
|
|
||||||
# CHORE: test alternate clients
|
def is_sandboxed() -> bool:
|
||||||
# TODO: has_flatpak_steam
|
return bool(os.environ.get("FLATPAK_ID"))
|
||||||
|
|
||||||
|
|
||||||
def is_flatpak_steam_running() -> bool:
|
def is_flatpak_steam_running() -> bool:
|
||||||
|
if is_sandboxed():
|
||||||
|
return True
|
||||||
if has_cmd(FLATPAK_CMD) is False:
|
if has_cmd(FLATPAK_CMD) is False:
|
||||||
return False
|
return False
|
||||||
proc = subprocess.check_output([FLATPAK_CMD, "ps"], text=True)
|
proc = subprocess.check_output(
|
||||||
|
[FLATPAK_CMD, "ps", "--columns=application"], text=True
|
||||||
|
)
|
||||||
lines = proc.splitlines()
|
lines = proc.splitlines()
|
||||||
if FLATPAK_APPID in lines:
|
if FLATPAK_APPID in lines:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_running(proc: str) -> bool:
|
def get_steam_procs() -> list[list[str]]:
|
||||||
for process in psutil.process_iter(["pid", "name"]):
|
return [
|
||||||
if process.info["name"] == proc:
|
p.cmdline()
|
||||||
return True
|
for p in psutil.process_iter(["pid", "name"])
|
||||||
return False
|
if STEAM_CMD in p.info["name"]
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def is_native_steam_running() -> bool:
|
||||||
|
if has_cmd(STEAM_CMD) is False:
|
||||||
|
return False
|
||||||
|
substrings = [FLATPAK_ROOT_PATH, "steam-runtime-launcher-service"]
|
||||||
|
steam_procs = get_steam_procs()
|
||||||
|
native_steam_procs = [
|
||||||
|
proc
|
||||||
|
for proc in steam_procs
|
||||||
|
if not any(sub in item for item in proc for sub in substrings)
|
||||||
|
]
|
||||||
|
return bool(native_steam_procs)
|
||||||
|
|
||||||
|
|
||||||
def has_cmd(cmd: str) -> bool:
|
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
|
||||||
|
|
||||||
|
|
||||||
|
def path_to_steam_cmd(path: str) -> str:
|
||||||
|
cmd = FLATPAK_RUN_CMD if FLATPAK_STEAM_PATH in path else STEAM_CMD
|
||||||
|
return cmd
|
||||||
+16
-13
@@ -5,33 +5,36 @@ import warnings
|
|||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import APP_NAME
|
||||||
from dzgui.init.libgi import test_libgi_missing
|
from dzgui.init.libgi import test_libgi_missing
|
||||||
from dzgui.init.prefix import get_version
|
from dzgui.init.prefix import get_version
|
||||||
|
from dzgui.util.deck import is_steam_deck
|
||||||
|
from dzgui.util.map_count import set_map_count
|
||||||
from dzgui.util.strings import flags
|
from dzgui.util.strings import flags
|
||||||
|
|
||||||
|
from dzgui.views.dialogs.uninstall import UninstallWizard
|
||||||
|
from dzgui.config.xdg import get_xdg_paths
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description=flags.description)
|
parser = argparse.ArgumentParser(description=flags.description)
|
||||||
parser.add_argument("-v", "--version", action="store_true", help=flags.version)
|
group = parser.add_mutually_exclusive_group()
|
||||||
parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall)
|
group.add_argument("-d", "--debug", action="store_true", help=flags.debug)
|
||||||
parser.add_argument("-d", "--debug", action="store_true", help=flags.debug)
|
group.add_argument("-m", "--map", action="store_true", help=flags.map_count)
|
||||||
|
group.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall)
|
||||||
|
group.add_argument("-v", "--version", action="store_true", help=flags.version)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
def uninstall() -> None:
|
|
||||||
# TODO: uninstall data files (-u)
|
|
||||||
# -u removes state, log, freedesktop
|
|
||||||
# XDG_SHARE_HOME/dzgui
|
|
||||||
# XDG_STATE_HOME/dzgui
|
|
||||||
# XDG_DATA_HOME/dzgui
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
if args.version is True:
|
if args.version is True:
|
||||||
print(get_version())
|
print(get_version())
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if args.uninstall is True:
|
if args.uninstall is True:
|
||||||
uninstall()
|
paths = get_xdg_paths()
|
||||||
|
_is_steam_deck = is_steam_deck()
|
||||||
|
UninstallWizard(_is_steam_deck, paths)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
if args.debug is True:
|
if args.debug is True:
|
||||||
warnings.filterwarnings("default", category=DeprecationWarning)
|
warnings.filterwarnings("default", category=DeprecationWarning)
|
||||||
|
if args.map is True:
|
||||||
|
set_map_count()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
version = get_version()
|
version = get_version()
|
||||||
|
|
||||||
|
|||||||
+23
-28
@@ -6,24 +6,16 @@ from typing import Any, TYPE_CHECKING
|
|||||||
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
FLATPAK_RUN_CMD,
|
|
||||||
FLATPAK_SANDBOX,
|
|
||||||
STEAM_CMD,
|
|
||||||
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
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util._json import read_json, write_json
|
from dzgui.util._json import read_json, write_json
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository.Gtk import main_quit # noqa E402
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -98,14 +90,13 @@ class ConfigManager:
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
@call_on_thread(dialogs.checking_api)
|
@call_on_thread(dialogs.checking_api, show_dialog=False)
|
||||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
def update_steam_api_key(self, text: str) -> None:
|
||||||
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(Preferences.STEAM, text)
|
||||||
|
func = StoredFunc(lambda: self.emitter.emit("api_change_successful"))
|
||||||
|
self.thread_man.set_cleanup_func(func)
|
||||||
else:
|
else:
|
||||||
self.thread_man.set_cleanup_func(
|
self.thread_man.set_cleanup_func(
|
||||||
StoredFunc(lambda: self.emitter.emit("api_change_failed"))
|
StoredFunc(lambda: self.emitter.emit("api_change_failed"))
|
||||||
@@ -135,15 +126,6 @@ class ConfigManager:
|
|||||||
def enum_to_key(self, enum: Preferences) -> str:
|
def enum_to_key(self, enum: Preferences) -> str:
|
||||||
return str(enum.dict["key"])
|
return str(enum.dict["key"])
|
||||||
|
|
||||||
def get_client_index(self, client: str) -> int:
|
|
||||||
if client == STEAM_CMD:
|
|
||||||
return 0
|
|
||||||
if client == FLATPAK_RUN_CMD:
|
|
||||||
return 1
|
|
||||||
if client == FLATPAK_SANDBOX:
|
|
||||||
return 2
|
|
||||||
return 0
|
|
||||||
|
|
||||||
def toggle_config(self, key: Preferences) -> None:
|
def toggle_config(self, key: Preferences) -> None:
|
||||||
try:
|
try:
|
||||||
real_key = self.enum_to_key(key)
|
real_key = self.enum_to_key(key)
|
||||||
@@ -162,6 +144,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)
|
||||||
@@ -172,8 +161,8 @@ class ConfigManager:
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
trace = traceback.format_exc()
|
trace = traceback.format_exc()
|
||||||
dialog = ExceptionDialog(self.controller, trace)
|
dialog = ExceptionDialog(self.controller, trace)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
raise e
|
|
||||||
|
|
||||||
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
||||||
columns = tv.get_columns()
|
columns = tv.get_columns()
|
||||||
@@ -200,7 +189,10 @@ class ConfigManager:
|
|||||||
|
|
||||||
logger.info("Normal user exit")
|
logger.info("Normal user exit")
|
||||||
if window.props.is_maximized:
|
if window.props.is_maximized:
|
||||||
main_quit()
|
app = window.get_application()
|
||||||
|
if app is None:
|
||||||
|
return
|
||||||
|
app.quit()
|
||||||
return
|
return
|
||||||
|
|
||||||
w, h = window.get_size()
|
w, h = window.get_size()
|
||||||
@@ -212,7 +204,10 @@ class ConfigManager:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
|
|
||||||
main_quit()
|
app = window.get_application()
|
||||||
|
if app is None:
|
||||||
|
return
|
||||||
|
app.quit()
|
||||||
|
|
||||||
def set_resolution(self, window: "OuterWindow") -> None:
|
def set_resolution(self, window: "OuterWindow") -> None:
|
||||||
if self.prefs.is_game_mode:
|
if self.prefs.is_game_mode:
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ from dzgui.api.shortcuts import Shortcuts
|
|||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
|
connect_debug,
|
||||||
|
is_dayz_downloading,
|
||||||
get_app_allows_downloads,
|
get_app_allows_downloads,
|
||||||
get_app_name,
|
get_app_name,
|
||||||
get_needs_update,
|
get_needs_update,
|
||||||
@@ -45,8 +47,9 @@ from dzgui.strings.dialogs import (
|
|||||||
waiting_for_mods,
|
waiting_for_mods,
|
||||||
waiting_for_directories,
|
waiting_for_directories,
|
||||||
)
|
)
|
||||||
|
from dzgui.strings import kb
|
||||||
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_exception, format_mib
|
||||||
from dzgui.util.strings import dialog, server_timeout
|
from dzgui.util.strings import dialog, server_timeout
|
||||||
from dzgui.util.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
@@ -82,10 +85,12 @@ class Prerequisites:
|
|||||||
available_space: float
|
available_space: float
|
||||||
passworded: bool
|
passworded: bool
|
||||||
dayz_running: bool
|
dayz_running: bool
|
||||||
|
download_paused: bool
|
||||||
steam_proc: SteamProcess
|
steam_proc: SteamProcess
|
||||||
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]
|
allows_downloads: tuple[bool, str]
|
||||||
|
|
||||||
|
|
||||||
@@ -103,11 +108,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)
|
||||||
@@ -171,6 +171,7 @@ class ConnectionManager:
|
|||||||
free_mib = format_mib(free)
|
free_mib = format_mib(free)
|
||||||
|
|
||||||
dayz_running = is_dayz_running()
|
dayz_running = is_dayz_running()
|
||||||
|
download_paused = is_dayz_downloading(steam_path, self.appid)
|
||||||
|
|
||||||
running_app = get_running_app()
|
running_app = get_running_app()
|
||||||
if running_app is not None:
|
if running_app is not None:
|
||||||
@@ -186,9 +187,8 @@ class ConnectionManager:
|
|||||||
else:
|
else:
|
||||||
allows_downloads = (True, "")
|
allows_downloads = (True, "")
|
||||||
|
|
||||||
|
|
||||||
client_name = self.controller.get_steam_client_name()
|
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
|
client_name = self.controller.get_client_name()
|
||||||
running = is_steam_running(client)
|
running = is_steam_running(client)
|
||||||
steam_proc = SteamProcess(client_name, running)
|
steam_proc = SteamProcess(client_name, running)
|
||||||
self.client = client
|
self.client = client
|
||||||
@@ -197,6 +197,11 @@ 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,
|
||||||
@@ -208,10 +213,12 @@ class ConnectionManager:
|
|||||||
available_space=free_mib,
|
available_space=free_mib,
|
||||||
passworded=info.password_protected,
|
passworded=info.password_protected,
|
||||||
dayz_running=dayz_running,
|
dayz_running=dayz_running,
|
||||||
|
download_paused=download_paused,
|
||||||
steam_proc=steam_proc,
|
steam_proc=steam_proc,
|
||||||
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,
|
allows_downloads=allows_downloads,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -232,7 +239,7 @@ class ConnectionManager:
|
|||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def query_details(self, record: Servers.Record) -> None:
|
def query_details(self, record: Servers.Record) -> None:
|
||||||
details = Servers.get_details(record)
|
details = Servers.get_details(record)
|
||||||
if details.success is False:
|
if details is None:
|
||||||
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
|
||||||
)
|
)
|
||||||
@@ -267,6 +274,11 @@ class ConnectionManager:
|
|||||||
for mod in alpha_mods:
|
for mod in alpha_mods:
|
||||||
if any(mod[1] in tuple for tuple in missing_mods):
|
if any(mod[1] in tuple for tuple in missing_mods):
|
||||||
mod[2] = resync
|
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
|
return alpha_mods, missing_mods
|
||||||
|
|
||||||
@@ -292,10 +304,24 @@ class ConnectionManager:
|
|||||||
dialog = ServerDetailsDialog(self.controller, details)
|
dialog = ServerDetailsDialog(self.controller, details)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
|
def _workshop_timeout(self, msg: str) -> None:
|
||||||
|
dialog = ExceptionDialog(self.controller, msg)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run()
|
||||||
|
|
||||||
def _server_timeout(self) -> None:
|
def _server_timeout(self) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
|
def get_debug_args(self) -> str:
|
||||||
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
|
return connect_debug(
|
||||||
|
self.client, addr, self.appid, playername, self.remote_mod_ids
|
||||||
|
)
|
||||||
|
|
||||||
def _connect_steam(self, menu_only: bool) -> None:
|
def _connect_steam(self, menu_only: bool) -> None:
|
||||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
playername = self.controller.query_config(Preferences.NAME)
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
@@ -334,8 +360,14 @@ class ConnectionManager:
|
|||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
if self.controller.is_cancel_pending():
|
if self.controller.is_cancel_pending():
|
||||||
return
|
return
|
||||||
subscribe(key, int(mod))
|
try:
|
||||||
time.sleep(API_RATE_LIMIT)
|
subscribe(key, int(mod))
|
||||||
|
time.sleep(API_RATE_LIMIT)
|
||||||
|
except Exception as e:
|
||||||
|
msg = format_exception(e)
|
||||||
|
func = StoredFunc(self._workshop_timeout, msg)
|
||||||
|
self.thread_man.set_cleanup_func(func, destroy_first=True)
|
||||||
|
return
|
||||||
|
|
||||||
self.thread_man.update_dialog(waiting_for_directories)
|
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:
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import gi
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -14,10 +13,6 @@ from dzgui.views.trees.tree_servers import ServerTreeView
|
|||||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||||
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk, Gdk, GLib, GObject # noqa E402
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
@@ -21,12 +21,30 @@ class FilterManager:
|
|||||||
strings.filter_3pp: True,
|
strings.filter_3pp: True,
|
||||||
strings.filter_night: True,
|
strings.filter_night: True,
|
||||||
strings.filter_full: show_full,
|
strings.filter_full: show_full,
|
||||||
strings.filter_lowpop: True,
|
|
||||||
strings.filter_nonascii: False,
|
|
||||||
strings.filter_duplicate: False,
|
|
||||||
strings.filter_official: True,
|
strings.filter_official: True,
|
||||||
strings.filter_unofficial: True,
|
|
||||||
strings.filter_modded: True,
|
strings.filter_modded: True,
|
||||||
|
strings.filter_lowpop: True,
|
||||||
|
strings.filter_unofficial: True,
|
||||||
|
strings.filter_unmodded: True,
|
||||||
|
strings.filter_duplicate: False,
|
||||||
|
strings.filter_nonascii: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# TODO: strings
|
||||||
|
self.tooltips = {
|
||||||
|
strings.filter_1pp: "First-person perspective",
|
||||||
|
strings.filter_day: "In-game time between 0600 and 1759",
|
||||||
|
strings.filter_empty: "Servers with no players",
|
||||||
|
strings.filter_3pp: "Third-person perspective",
|
||||||
|
strings.filter_night: "In-game time between 1800 and 0559",
|
||||||
|
strings.filter_full: "Servers with no open slots",
|
||||||
|
strings.filter_official: "Bohemia official servers",
|
||||||
|
strings.filter_nonascii: "Server names using non-standard, complex glyphs",
|
||||||
|
strings.filter_lowpop: "Current population is under 30% of total",
|
||||||
|
strings.filter_unofficial: "Third-party servers",
|
||||||
|
strings.filter_duplicate: "Duplicates of existing servers (usually spoofed)",
|
||||||
|
strings.filter_modded: "Servers with one or more mods",
|
||||||
|
strings.filter_unmodded: "Servers with no mods",
|
||||||
}
|
}
|
||||||
|
|
||||||
self.active_keyword = ""
|
self.active_keyword = ""
|
||||||
@@ -36,6 +54,9 @@ class FilterManager:
|
|||||||
self.filters: list
|
self.filters: list
|
||||||
self.enabled_filters = dict(self.default_filters)
|
self.enabled_filters = dict(self.default_filters)
|
||||||
|
|
||||||
|
def get_tooltips(self) -> dict[str, str]:
|
||||||
|
return self.tooltips
|
||||||
|
|
||||||
def set_prior_map(self, name: str) -> None:
|
def set_prior_map(self, name: str) -> None:
|
||||||
self.prior_map = name
|
self.prior_map = name
|
||||||
|
|
||||||
@@ -86,6 +107,7 @@ class FilterManager:
|
|||||||
self.reinit_map_store()
|
self.reinit_map_store()
|
||||||
maps.sort()
|
maps.sort()
|
||||||
for m in maps:
|
for m in maps:
|
||||||
|
# TODO: strings
|
||||||
if m == "All maps":
|
if m == "All maps":
|
||||||
continue
|
continue
|
||||||
self.append_map([m])
|
self.append_map([m])
|
||||||
|
|||||||
@@ -22,9 +22,7 @@ from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManage
|
|||||||
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
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.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
|
||||||
|
|
||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
@@ -124,7 +122,11 @@ class ModManager:
|
|||||||
def unsub_atomic_mod(self, mod: str) -> None:
|
def unsub_atomic_mod(self, mod: str) -> None:
|
||||||
config_man = self.controller.get_config_man()
|
config_man = self.controller.get_config_man()
|
||||||
key = config_man.lookup(Preferences.STEAM)
|
key = config_man.lookup(Preferences.STEAM)
|
||||||
unsubscribe(key, int(mod))
|
try:
|
||||||
|
unsubscribe(key, int(mod))
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return
|
||||||
|
|
||||||
steam_path = Path(self.path)
|
steam_path = Path(self.path)
|
||||||
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
@@ -213,10 +215,6 @@ class ModManager:
|
|||||||
func = StoredFunc(self._on_stale_mods_found, stale)
|
func = StoredFunc(self._on_stale_mods_found, stale)
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def _server_timeout(self) -> None:
|
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
|
||||||
dialog.run()
|
|
||||||
|
|
||||||
def select_colorized(self) -> None:
|
def select_colorized(self) -> None:
|
||||||
model = self.treeview.get_model()
|
model = self.treeview.get_model()
|
||||||
if model is None:
|
if model is None:
|
||||||
|
|||||||
@@ -64,4 +64,5 @@ class UpdateManager:
|
|||||||
|
|
||||||
def _on_update_failure(self, msg: str) -> None:
|
def _on_update_failure(self, msg: str) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, msg)
|
dialog = ExceptionDialog(self.controller, msg)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
+12
-12
@@ -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
|
||||||
@@ -13,6 +12,9 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
from dzgui.model.servers import NewPlayerCount
|
from dzgui.model.servers import NewPlayerCount
|
||||||
|
|
||||||
|
DAY_REG = r"([0][6-9]|[1][0-7])"
|
||||||
|
NIGHT_REG = r"([0][0-5]|[1][8-9]|[2][0-3])"
|
||||||
|
|
||||||
|
|
||||||
class ProxyModelManager:
|
class ProxyModelManager:
|
||||||
"""
|
"""
|
||||||
@@ -86,8 +88,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)
|
||||||
|
|
||||||
@@ -232,17 +234,17 @@ class ProxyModelManager:
|
|||||||
final.append(row)
|
final.append(row)
|
||||||
rows = final
|
rows = final
|
||||||
case strings.filter_day:
|
case strings.filter_day:
|
||||||
reg = r"([0][0-9]|[1][0-6])"
|
rows = [row for row in rows if not re.match(DAY_REG, row[3])]
|
||||||
rows = [row for row in rows if not re.match(reg, row[3])]
|
|
||||||
case strings.filter_night:
|
case strings.filter_night:
|
||||||
reg = r"([0][0-4]|[1][8]|[2][0-3])"
|
rows = [row for row in rows if not re.match(NIGHT_REG, row[3])]
|
||||||
rows = [row for row in rows if not re.match(reg, row[3])]
|
|
||||||
case strings.filter_nonascii:
|
case strings.filter_nonascii:
|
||||||
rows = [row for row in rows if row[0].isascii()]
|
rows = [row for row in rows if row[0].isascii()]
|
||||||
case strings.filter_lowpop:
|
case strings.filter_lowpop:
|
||||||
rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
|
rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
|
||||||
case strings.filter_modded:
|
case strings.filter_modded:
|
||||||
rows = [row for row in rows if not row[11]]
|
rows = [row for row in rows if not row[11]]
|
||||||
|
case strings.filter_unmodded:
|
||||||
|
rows = [row for row in rows if row[11]]
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
def filter_toggle_on(self, filters: tuple[str]) -> list:
|
def filter_toggle_on(self, filters: tuple[str]) -> list:
|
||||||
@@ -259,10 +261,6 @@ 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")
|
|
||||||
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
|
|
||||||
# return [[el for el in row] for row in model]
|
|
||||||
|
|
||||||
def set_filtered(self, rows: list | None) -> None:
|
def set_filtered(self, rows: list | None) -> None:
|
||||||
if rows is None:
|
if rows is None:
|
||||||
self.filtered = []
|
self.filtered = []
|
||||||
@@ -298,4 +296,6 @@ class ProxyModelManager:
|
|||||||
def push(self, data: list[Any]) -> None:
|
def push(self, data: list[Any]) -> None:
|
||||||
self.wipe_cache()
|
self.wipe_cache()
|
||||||
self.set_control(data)
|
self.set_control(data)
|
||||||
self.filter(FilterMode.INITIAL)
|
cur_keyword = self.filter_man.get_active_keyword()
|
||||||
|
mode = FilterMode.TOGGLE_ON if len(cur_keyword) > 0 else FilterMode.INITIAL
|
||||||
|
self.filter(mode)
|
||||||
+27
-29
@@ -14,14 +14,14 @@ 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, kb
|
||||||
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
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk # noqa E402
|
from gi.repository import Gtk, GLib # noqa E402
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.api.servers import A2SInfo, Record
|
from dzgui.api.servers import A2SInfo, Record
|
||||||
@@ -175,29 +175,29 @@ 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()
|
||||||
# NOTE: failing entries are culled
|
|
||||||
if res is None:
|
"""
|
||||||
# TODO: log which servers failed
|
Collects futures in linear order after completion
|
||||||
continue
|
for lock-step representation of the state file
|
||||||
servers.append(res)
|
"""
|
||||||
if len(servers) == 0:
|
for future in futures:
|
||||||
self.thread_man.set_cleanup_func(
|
res = future.result(timeout=API_TIMEOUT)
|
||||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
# NOTE: failing entries are culled
|
||||||
)
|
if res is None:
|
||||||
return
|
# TODO: log which servers failed
|
||||||
|
# query_direct() currently does not return enough information
|
||||||
|
continue
|
||||||
|
servers.append(res)
|
||||||
|
if len(servers) == 0:
|
||||||
|
self.thread_man.set_cleanup_func(
|
||||||
|
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
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
|
||||||
@@ -397,6 +390,8 @@ class ServerModelManager:
|
|||||||
def _cleanup_on_success(self) -> None:
|
def _cleanup_on_success(self) -> None:
|
||||||
proxy = self.proxy_man.get_proxy_model()
|
proxy = self.proxy_man.get_proxy_model()
|
||||||
self.tv.set_model(proxy)
|
self.tv.set_model(proxy)
|
||||||
|
if self.tv.enum == ServerTab.BROWSER:
|
||||||
|
GLib.idle_add(self.tv.wipe_cache)
|
||||||
if self.preserve_on_fail is True:
|
if self.preserve_on_fail is True:
|
||||||
self.proxy_man.wipe_cache()
|
self.proxy_man.wipe_cache()
|
||||||
|
|
||||||
@@ -412,6 +407,7 @@ class ServerModelManager:
|
|||||||
|
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
||||||
@@ -426,7 +422,9 @@ 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.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
def _push_data(self, data: list[Any]) -> None:
|
def _push_data(self, data: list[Any]) -> None:
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
heading_local_time = "Local time"
|
||||||
|
heading_server_time = "In-game time"
|
||||||
|
heading_target_time = "Target time"
|
||||||
|
heading_remainder_time = "Remainder until target"
|
||||||
|
heading_adjusted_time = "Adjusted local time"
|
||||||
|
calculate = "Calculate"
|
||||||
|
disclaimer = "Times shown are approximations.\nServer time may differ based on time of year in game."
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
collapse_all = "Collapse all"
|
||||||
|
expand_all = "Expand all"
|
||||||
@@ -1,24 +1,22 @@
|
|||||||
add_button="Add"
|
from dzgui.const.constants import UDP_PORT
|
||||||
add_con_heading="Add/connect"
|
|
||||||
|
add_button = "Add"
|
||||||
|
add_con_heading = "Add/connect"
|
||||||
add_popover = "This address is already in your Saved Servers"
|
add_popover = "This address is already in your Saved Servers"
|
||||||
add_tooltip="Add to Saved Servers"
|
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_placeholder = f"Enter IP:Query port (e.g., 192.168.1.1:{UDP_PORT})"
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
|
|
||||||
"- Battlemetrics: numeric server ID\ne.g. 123456"
|
|
||||||
)
|
|
||||||
connect_entry_placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)"
|
|
||||||
|
|
||||||
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."
|
||||||
|
|
||||||
lan_heading="LAN query port"
|
lan_heading = "LAN query port"
|
||||||
lan_default_button="Default port (27016)"
|
lan_default_button = f"Default port ({UDP_PORT})"
|
||||||
lan_custom_button="Custom port"
|
lan_custom_button = "Custom port"
|
||||||
lan_scan_button="Scan"
|
lan_scan_button = "Scan"
|
||||||
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
||||||
lan_checkbox = "Stop scanning on first hit"
|
lan_checkbox = "Stop scanning on first hit"
|
||||||
lan_placeholder="Enter the query port (1-65535)"
|
lan_entry_placeholder = "Enter the query port (1-65535)"
|
||||||
lan_entry_tooltip="Specify the port to search for DayZ servers on the local network"
|
lan_entry_tooltip = "Specify the port to search for DayZ servers on the local network"
|
||||||
lan_scan_tooltip="Scan for servers"
|
lan_scan_tooltip = "Scan for servers"
|
||||||
@@ -20,3 +20,11 @@ ok = "OK"
|
|||||||
|
|
||||||
mission_dialog = "Set a mission folder location"
|
mission_dialog = "Set a mission folder location"
|
||||||
custom_mod_dialog = "Set location to a custom mods folder"
|
custom_mod_dialog = "Set location to a custom mods folder"
|
||||||
|
|
||||||
|
debug_heading = "Debug"
|
||||||
|
debug_secondary = "Launch arguments"
|
||||||
|
|
||||||
|
toggle_calc = "Toggle calculator"
|
||||||
|
|
||||||
|
server_details = "Server details"
|
||||||
|
server_message = "Server message"
|
||||||
@@ -4,3 +4,4 @@ api_validation_error = (
|
|||||||
|
|
||||||
api_popover = "API key validation failed."
|
api_popover = "API key validation failed."
|
||||||
dayz_not_found = "Could not find a valid DayZ installation."
|
dayz_not_found = "Could not find a valid DayZ installation."
|
||||||
|
workshop_endpoint_error = "Steam Workshop service unavailable, try again later."
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
DZG_006 = """The leading cause of specific servers periodically timing out is local network configuration.
|
||||||
|
|
||||||
|
Many third-party DayZ servers use a server rental/hosting provider with DDoS protection.
|
||||||
|
|
||||||
|
This can cause responses from servers to originate from a server other than the one originally queried.
|
||||||
|
|
||||||
|
Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation).
|
||||||
|
|
||||||
|
By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue.
|
||||||
|
|
||||||
|
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port.
|
||||||
|
|
||||||
|
In addition, a uniform packet size is expected. If your router's MTU (maximum transmission unit) setting differs from the default of 1500, it could lead to incoming responses being discarded."""
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
exit_msg = (
|
||||||
|
"System map count is not high enough to run DayZ. "
|
||||||
|
"Please exit and run 'dzgui -m' to update map count."
|
||||||
|
)
|
||||||
|
failed_to_parse = (
|
||||||
|
"Failed to parse system map count.\n"
|
||||||
|
"This usually indicates that systemd is not installed."
|
||||||
|
)
|
||||||
|
failed_to_update = (
|
||||||
|
"Failed to update system map count.\n"
|
||||||
|
"Please report the issue upstream and provide the following traceback.\n\n"
|
||||||
|
"{0}"
|
||||||
|
)
|
||||||
|
meets_minimum = "System map count of {0} already meets the minimum."
|
||||||
|
prompt = "Updated map count will be written to the file {0}."
|
||||||
|
user_exit = "User exit"
|
||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
developers = "Developers"
|
developers = "Developers"
|
||||||
start_tab = "Start tab"
|
start_tab = "Start tab"
|
||||||
workshop_label = "Subscribed mods"
|
workshop_label = "Subscribed mods"
|
||||||
workshop_eventbox = "If you manually subscribed to mods on Steam prior to using DZGUI, you can unsubscribe via this link."
|
|
||||||
steam_combo = "Steam"
|
steam_combo = "Steam"
|
||||||
flatpak_combo = "Flatpak"
|
flatpak_combo = "Flatpak"
|
||||||
flatpak_container_combo = "Flatpak (container)"
|
flatpak_container_combo = "Flatpak (container)"
|
||||||
|
|
||||||
|
steam_placeholder = "Enter your Steam API key"
|
||||||
|
name_placeholder = "Identifies you to other players"
|
||||||
|
|
||||||
server_combo = "Server browser"
|
server_combo = "Server browser"
|
||||||
saved_combo = "Saved servers"
|
saved_combo = "Saved servers"
|
||||||
recent_combo = "Recent"
|
recent_combo = "Recent"
|
||||||
lan_combo = "LAN"
|
lan_combo = "LAN"
|
||||||
|
|
||||||
|
fullscreen_eventbox = "This option is not available on Steam Deck."
|
||||||
|
save_button = "Save"
|
||||||
|
|
||||||
|
api_failed = "API validation failed"
|
||||||
|
|
||||||
|
flatpak_label = "Flatpak client"
|
||||||
|
flatpak_standard = "Standard"
|
||||||
|
flatpak_container = "Containerized (flatpak-spawn --host)"
|
||||||
|
flatpak_info = (
|
||||||
|
"Containerized mode is used on certain distributions. May have side effects."
|
||||||
|
)
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
update_mods = "Update mods and connect"
|
update_mods = "Update mods and connect"
|
||||||
connect = "Connect"
|
connect = "Connect"
|
||||||
connect_last = "Load to menu screen"
|
connect_last = "Load to menu screen"
|
||||||
connect_last_tooltip = (
|
connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
||||||
"Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
debug = "Debug"
|
||||||
)
|
|
||||||
back = "Back"
|
back = "Back"
|
||||||
cancel = "Cancel"
|
cancel = "Cancel"
|
||||||
warnings = "Warnings"
|
warnings = "Warnings"
|
||||||
@@ -12,5 +11,43 @@ mods = "Mods"
|
|||||||
total_mods = "Total mods: "
|
total_mods = "Total mods: "
|
||||||
up_to_date = "All mods are up to date."
|
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, "
|
required_space = " Need to download {0} MiB of mod updates."
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
|
placeholder_no_mods = "This server has no mods."
|
||||||
|
placeholder_no_warnings = "No warnings."
|
||||||
|
|
||||||
|
|
||||||
|
# Errors
|
||||||
|
invalid_mods = (
|
||||||
|
"Server has invalid mods that are not recognized by Steam.\n"
|
||||||
|
"Contact the server owner and include these mod IDs in your report:\n"
|
||||||
|
"{0}"
|
||||||
|
)
|
||||||
|
not_enough_space = (
|
||||||
|
"Need to update {0} MiB of mods, but installation path only has {1} MiB."
|
||||||
|
)
|
||||||
|
resync = (
|
||||||
|
"If you recently installed {0} or moved it to a different drive,\n"
|
||||||
|
"restart Steam to allow these changes to synchronize, then try again."
|
||||||
|
)
|
||||||
|
steam_not_running = "'{0}' is set as the default Steam client, but it is either not installed or not running."
|
||||||
|
use_desktop_mode = "Use Desktop Mode to download mods on Steam Deck."
|
||||||
|
version_mismatch = "Local client version '{0}' does not match remote version '{1}'."
|
||||||
|
version_missing = "Remote server is running the build '{0}', but it is not installed."
|
||||||
|
no_errors = "No errors."
|
||||||
|
|
||||||
|
|
||||||
|
# Warnings
|
||||||
|
protected_server = (
|
||||||
|
"Protected: you will be prompted for a password when connecting to this server."
|
||||||
|
)
|
||||||
|
dayz_running = "It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
||||||
|
running_app = (
|
||||||
|
"The app '{0}' 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."
|
||||||
|
)
|
||||||
|
downloads_paused = (
|
||||||
|
"It looks like DayZ may have a pending update on Steam that was paused.\n"
|
||||||
|
"To enqueue mods for download, unpause any pending DayZ update and allow it to complete."
|
||||||
|
)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
pg1_title = "Choose uninstall targets"
|
||||||
|
pg1_heading = "Choose uninstall targets"
|
||||||
|
pg1_blurb = "Here you can choose which DZGUI configuration/state files to remove. "
|
||||||
|
|
||||||
|
pg2_title = "Uninstall complete"
|
||||||
|
pg2_heading = "Uninstall complete"
|
||||||
|
pg2_blurb = "Finished removing the selected files. See the console for any errors."
|
||||||
|
|
||||||
|
config_label = "Configuration files"
|
||||||
|
config_details = (
|
||||||
|
"User preferences and API key. "
|
||||||
|
"Keep this file if you want to import it again later."
|
||||||
|
)
|
||||||
|
desktop_label = "Desktop shortcut"
|
||||||
|
desktop_details = "Shortcut set on the desktop, if applicable."
|
||||||
|
|
||||||
|
start_menu_label = "Start menu shortcut"
|
||||||
|
start_menu_details = (
|
||||||
|
"Shortcut set under the Games category of the start menu, if applicable."
|
||||||
|
)
|
||||||
|
state_label = "Long-term state files"
|
||||||
|
state_details = (
|
||||||
|
"UI size, filter preferences, column size, server notes, and mod signatures."
|
||||||
|
)
|
||||||
|
steam_shortcut = "Steam shortcut"
|
||||||
|
steam_details = "Shortcut to DZGUI installed within Steam, if applicable. Requires Steam to be restarted to reflect the change."
|
||||||
|
|
||||||
|
|
||||||
|
path_remove_prefix = "The following path will be removed:"
|
||||||
|
not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall."
|
||||||
|
standalone_uninstall = (
|
||||||
|
"It looks like you are running the standalone version of DZGUI.\n"
|
||||||
|
"To finalize the uninstall process, invoke './dzgui self remove',\n"
|
||||||
|
"then remove the 'dzgui' file."
|
||||||
|
)
|
||||||
|
system_uninstall = (
|
||||||
|
"It looks like DZGUI was installed via your system package manager or directly from source code.\n"
|
||||||
|
"To finalize the uninstall process, follow the standard removal procedure for that context."
|
||||||
|
)
|
||||||
+15
-6
@@ -3,6 +3,8 @@ title_intro = "Welcome"
|
|||||||
blurb_intro = """
|
blurb_intro = """
|
||||||
This wizard will help you set up some common config options before launching the application.
|
This wizard will help you set up some common config options before launching the application.
|
||||||
"""
|
"""
|
||||||
|
error_sandbox = "Setup cannot be run from inside Flatpak sandbox.\nPlease launch the application directly."
|
||||||
|
welcome = "Welcome to {0}!"
|
||||||
|
|
||||||
### SteamPathPage
|
### SteamPathPage
|
||||||
heading_steam_path = "Steam path"
|
heading_steam_path = "Steam path"
|
||||||
@@ -46,11 +48,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"
|
||||||
@@ -75,7 +72,8 @@ checkbox_steam_shortcut = (
|
|||||||
(
|
(
|
||||||
"This creates a standalone copy of DZGUI and adds it to Steam. "
|
"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. "
|
"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."
|
"If you update DZGUI when it is launched via Steam, updates will be applied to the Steam version. "
|
||||||
|
"Not available if you selected the Flatpak client as your Steam default."
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
checkbox_start_menu = (
|
checkbox_start_menu = (
|
||||||
@@ -84,6 +82,17 @@ checkbox_start_menu = (
|
|||||||
)
|
)
|
||||||
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
|
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
|
||||||
|
|
||||||
|
### Flatpak page
|
||||||
|
heading_flatpak = "Flatpak sandbox overrides"
|
||||||
|
blurb_flatpak = (
|
||||||
|
"You chose to add a shortcut directly to Flatpak Steam. "
|
||||||
|
"To launch DZGUI from Flatpak's sandbox, you must grant "
|
||||||
|
"permission to read from and write to the following locations."
|
||||||
|
)
|
||||||
|
tree_column = "Paths"
|
||||||
|
overrides_applied = "Overrides applied."
|
||||||
|
button_flatpak = "Grant permissions"
|
||||||
|
|
||||||
### Completion page
|
### Completion page
|
||||||
heading_completion = "Setup complete"
|
heading_completion = "Setup complete"
|
||||||
blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI."
|
blurb_completion = "Configuration completed successfully. Click 'Close' to close this dialog and launch DZGUI."
|
||||||
@@ -1,5 +1,12 @@
|
|||||||
import shlex
|
import shlex
|
||||||
|
|
||||||
|
from dzgui.const.constants import STEAM_CMD
|
||||||
|
from dzgui.init.proc import is_sandboxed
|
||||||
|
|
||||||
|
|
||||||
def concat_bash_args(command: str) -> list[str]:
|
def concat_bash_args(command: str) -> list[str]:
|
||||||
|
# NOTE: NSGs spawned from sandbox cannot access host application commands,
|
||||||
|
# so fallback to Steam internal URIs
|
||||||
|
if is_sandboxed():
|
||||||
|
return [STEAM_CMD]
|
||||||
return shlex.split(command)
|
return shlex.split(command)
|
||||||
@@ -54,8 +54,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
cpu = get_cpu_model()
|
cpu = get_cpu_model()
|
||||||
version = get_version()
|
version = get_version()
|
||||||
|
|
||||||
debug = lookup(config, Preferences.DEBUG)
|
|
||||||
install = lookup(config, Preferences.INSTALL)
|
|
||||||
default = lookup(config, Preferences.DEFAULT)
|
default = lookup(config, Preferences.DEFAULT)
|
||||||
|
|
||||||
steam_path = Path(default)
|
steam_path = Path(default)
|
||||||
@@ -79,8 +77,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
|||||||
Kernel: {kernel}
|
Kernel: {kernel}
|
||||||
CPU: {cpu}
|
CPU: {cpu}
|
||||||
|
|
||||||
Debug: {debug}
|
|
||||||
Auto-install: {install}
|
|
||||||
Steam path: {steam_redacted}
|
Steam path: {steam_redacted}
|
||||||
Workshop path: {workshop_redacted}
|
Workshop path: {workshop_redacted}
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,5 @@ class CalcDist:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# TODO: handle failed remote dist
|
|
||||||
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
haversine = Haversine(local.lat, local.lon, remote.lat, remote.lon)
|
||||||
return haversine
|
return haversine
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def apply_overrides(xdg: dict[str, str]) -> None:
|
||||||
|
try:
|
||||||
|
# NOTE: D-Bus registration required for sandbox
|
||||||
|
args = [
|
||||||
|
"flatpak",
|
||||||
|
"--user",
|
||||||
|
"override",
|
||||||
|
f"--own-name={GTK_APP_ID}",
|
||||||
|
FLATPAK_APPID,
|
||||||
|
]
|
||||||
|
subprocess.run([*args], capture_output=True)
|
||||||
|
|
||||||
|
for path in xdg.values():
|
||||||
|
args = [
|
||||||
|
"flatpak",
|
||||||
|
"--user",
|
||||||
|
"override",
|
||||||
|
f"--filesystem={path}",
|
||||||
|
FLATPAK_APPID,
|
||||||
|
]
|
||||||
|
subprocess.run([*args], capture_output=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
+1
-9
@@ -2,14 +2,13 @@ import a2s
|
|||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from warnings import deprecated
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.servers import Record
|
from dzgui.api.servers import Record
|
||||||
from dzgui.const.constants import REQUEST_TIMEOUT
|
from dzgui.const.constants import REQUEST_TIMEOUT
|
||||||
from dzgui.const.endpoints import COORDS_API, IP_ECHO
|
from dzgui.const.endpoints import IP_ECHO
|
||||||
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -127,10 +126,3 @@ def is_valid_port(port: str) -> bool:
|
|||||||
if not port.isdigit() or int(port) == 0 or int(port[0]) == 0 or int(port) > 65535:
|
if not port.isdigit() or int(port) == 0 or int(port[0]) == 0 or int(port) > 65535:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
@deprecated("use ips.csv")
|
|
||||||
def get_local_coords(ip: str) -> str:
|
|
||||||
url = COORDS_API + "/" + ip
|
|
||||||
# local res=$(curl -Ls "$url" | jq -r '"\(.lat)\n\(.lon)"')
|
|
||||||
return url
|
|
||||||
+59
-16
@@ -1,28 +1,71 @@
|
|||||||
|
import logging
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import traceback
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dzgui.const.constants import VM_FILE, MIN_COUNT
|
from dzgui.const.constants import APP_NAME, VM_FILE, MIN_COUNT
|
||||||
|
from dzgui.util.bash import concat_bash_args
|
||||||
|
from dzgui.strings import map_count
|
||||||
|
from dzgui.views.dialogs.early_alert import EarlyIgnoreDialog
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def is_map_count_valid(count: int | None) -> bool:
|
||||||
|
if count is None:
|
||||||
|
# NOTE: permit if count was unreadable
|
||||||
|
return True
|
||||||
|
return count >= MIN_COUNT
|
||||||
|
|
||||||
|
|
||||||
def get_map_count() -> int | None:
|
def get_map_count() -> int | None:
|
||||||
path = Path(VM_FILE)
|
path = Path(VM_FILE)
|
||||||
if path.is_file() is False:
|
try:
|
||||||
|
count = int(path.read_text())
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
return None
|
return None
|
||||||
count = int(path.read_text())
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
# TODO: unfinished
|
|
||||||
#if count < MIN_COUNT:
|
|
||||||
# print("needs sudo escalation")
|
|
||||||
# # pop prompt
|
|
||||||
# # get response
|
|
||||||
# set_map_count()
|
|
||||||
#return count
|
|
||||||
|
|
||||||
# TODO: unfinished
|
def test_map_count() -> None:
|
||||||
|
count = get_map_count()
|
||||||
|
if is_map_count_valid(count):
|
||||||
|
return
|
||||||
|
msg = map_count.exit_msg
|
||||||
|
EarlyIgnoreDialog(msg)
|
||||||
|
|
||||||
|
|
||||||
def set_map_count() -> None:
|
def set_map_count() -> None:
|
||||||
|
count = get_map_count()
|
||||||
|
if count is None:
|
||||||
|
print(map_count.failed_to_parse)
|
||||||
|
return
|
||||||
|
if is_map_count_valid(count):
|
||||||
|
msg = map_count.meets_minimum.format(count)
|
||||||
|
print(msg)
|
||||||
|
return
|
||||||
|
|
||||||
conf = "/etc/sysctl.d/dayz.conf"
|
conf = "/etc/sysctl.d/dayz.conf"
|
||||||
count = f"vm.max_map_count={MIN_COUNT}"
|
value = f"vm.max_map_count={MIN_COUNT}"
|
||||||
with open(conf, "w") as f:
|
try:
|
||||||
f.write(count)
|
msg = map_count.prompt.format(conf)
|
||||||
# TODO: use concat_bash_args()
|
print(msg)
|
||||||
subprocess.run(["/usr/bin/sudo", "sysctl", "-p", conf])
|
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||||
|
tmp = f.name
|
||||||
|
Path(tmp).write_text(value)
|
||||||
|
|
||||||
|
mv_cmd = f"sudo mv {tmp} {conf}"
|
||||||
|
reload_cmd = f"sudo sysctl -p {conf}"
|
||||||
|
for cmd in mv_cmd, reload_cmd:
|
||||||
|
args = concat_bash_args(cmd)
|
||||||
|
subprocess.run([*args])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
trace = traceback.format_exc()
|
||||||
|
print(map_count.failed_to_update.format(trace))
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print(map_count.user_exit)
|
||||||
|
sys.exit(0)
|
||||||
@@ -4,8 +4,9 @@ from typing import Literal
|
|||||||
|
|
||||||
api_filter = r"(.*&key=)([^&]*)(.*)"
|
api_filter = r"(.*&key=)([^&]*)(.*)"
|
||||||
home_filter = r"(/home/)([^\s'\/]*)(.*)"
|
home_filter = r"(/home/)([^\s'\/]*)(.*)"
|
||||||
|
user_filter = r"(.*Steam/userdata/)([^/]*)(.*)"
|
||||||
REDACTED = r"\1REDACTED\3"
|
REDACTED = r"\1REDACTED\3"
|
||||||
REDACTION_PATTERNS = [api_filter, home_filter]
|
REDACTION_PATTERNS = [api_filter, home_filter, user_filter]
|
||||||
|
|
||||||
|
|
||||||
def redact_home(text: str) -> str:
|
def redact_home(text: str) -> str:
|
||||||
|
|||||||
+13
-99
@@ -1,27 +1,6 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from dzgui.const.constants import SYSTEM_LOG
|
from dzgui.const.constants import SYSTEM_LOG
|
||||||
|
|
||||||
# TODO: move to util.format.py
|
|
||||||
|
|
||||||
|
|
||||||
def build_missing(build: str) -> str:
|
|
||||||
msg = (
|
|
||||||
f"This server is running {build}. You can install "
|
|
||||||
f"{build} by searching for it in your Steam library. "
|
|
||||||
f"If you recently installed {build} or moved it to a different drive, "
|
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
def build_path_invalid(build: str) -> str:
|
|
||||||
msg = (
|
|
||||||
f"Steam is reporting that {build} is installed at a non-existent location. "
|
|
||||||
f"If you recently installed {build} or moved it to a different drive, "
|
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
)
|
|
||||||
return msg
|
|
||||||
|
|
||||||
|
|
||||||
# General
|
# General
|
||||||
dialog_header = "DZGUI - Dialog"
|
dialog_header = "DZGUI - Dialog"
|
||||||
@@ -32,7 +11,6 @@ delimiter = "␞"
|
|||||||
# GenericDialog
|
# GenericDialog
|
||||||
main_menu = "Return to main menu"
|
main_menu = "Return to main menu"
|
||||||
exit_app = "Exit"
|
exit_app = "Exit"
|
||||||
server_details = "Server details"
|
|
||||||
modlist = "Modlist"
|
modlist = "Modlist"
|
||||||
input_required = "User input required"
|
input_required = "User input required"
|
||||||
confirm = "Confirmation"
|
confirm = "Confirmation"
|
||||||
@@ -80,7 +58,6 @@ browser_cols = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
# DetailsDialog
|
# DetailsDialog
|
||||||
server_message = "Server message"
|
|
||||||
workshop = "Enter/double click a row to open in Steam Workshop."
|
workshop = "Enter/double click a row to open in Steam Workshop."
|
||||||
|
|
||||||
# LanDialog
|
# LanDialog
|
||||||
@@ -100,10 +77,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."
|
||||||
)
|
)
|
||||||
@@ -182,6 +156,7 @@ filter_duplicate = "Duplicate"
|
|||||||
filter_official = "Official"
|
filter_official = "Official"
|
||||||
filter_unofficial = "Unofficial"
|
filter_unofficial = "Unofficial"
|
||||||
filter_modded = "Modded"
|
filter_modded = "Modded"
|
||||||
|
filter_unmodded = "Unmodded"
|
||||||
|
|
||||||
# maps
|
# maps
|
||||||
all_maps = "All maps"
|
all_maps = "All maps"
|
||||||
@@ -192,11 +167,12 @@ none_provided = "None provided"
|
|||||||
disabled = "Disabled"
|
disabled = "Disabled"
|
||||||
enabled = "Enabled"
|
enabled = "Enabled"
|
||||||
null = "-"
|
null = "-"
|
||||||
none = "None"
|
|
||||||
unspecified = "not specified"
|
unspecified = "not specified"
|
||||||
|
|
||||||
# DLC
|
# DLC
|
||||||
dlc_frostline = "Frostline"
|
dlc_frostline = "Frostline"
|
||||||
|
dlc_badlands = "Badlands"
|
||||||
|
dlc_none = "None"
|
||||||
|
|
||||||
# platform
|
# platform
|
||||||
windows = "Windows"
|
windows = "Windows"
|
||||||
@@ -286,11 +262,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
|
||||||
@@ -311,7 +284,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
|
||||||
@@ -333,8 +306,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",
|
||||||
@@ -378,11 +351,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",
|
||||||
@@ -411,7 +381,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",
|
||||||
@@ -476,13 +446,15 @@ class Flags:
|
|||||||
version: str
|
version: str
|
||||||
uninstall: str
|
uninstall: str
|
||||||
debug: str
|
debug: str
|
||||||
|
map_count: str
|
||||||
|
|
||||||
|
|
||||||
flags = Flags(
|
flags = Flags(
|
||||||
description="DayZ server browser and mod manager",
|
description="DayZ server browser and mod manager",
|
||||||
version="Print version information",
|
version="print version information",
|
||||||
uninstall="Uninstall data files (use prior to 'pip uninstall')",
|
uninstall="clean up state/config files",
|
||||||
debug="Enables developer debugging features",
|
debug="enable developer debugging features",
|
||||||
|
map_count="check and update system map count value",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -534,64 +506,6 @@ server_labels = ServerLabels(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class ConnectPanel:
|
|
||||||
connect: str
|
|
||||||
add: str
|
|
||||||
add_con: str
|
|
||||||
placeholder: str
|
|
||||||
entry_tooltip: str
|
|
||||||
add_tooltip: str
|
|
||||||
connect_tooltip: str
|
|
||||||
|
|
||||||
|
|
||||||
connect_panel = ConnectPanel(
|
|
||||||
connect="Connect",
|
|
||||||
add="Add",
|
|
||||||
add_con="Add/connect",
|
|
||||||
placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)",
|
|
||||||
entry_tooltip=(
|
|
||||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
|
|
||||||
"- Battlemetrics: numeric server ID\ne.g. 123456"
|
|
||||||
),
|
|
||||||
add_tooltip="Add to Saved Servers",
|
|
||||||
connect_tooltip="Connect to this server",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class FavPanel:
|
|
||||||
heading: str
|
|
||||||
no_fav: str
|
|
||||||
|
|
||||||
|
|
||||||
fav_panel = FavPanel(
|
|
||||||
heading="Favorite server",
|
|
||||||
no_fav="None set. Right click a server and select 'Set favorite' to set.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
|
||||||
class LanPanel:
|
|
||||||
heading: str
|
|
||||||
default_button: str
|
|
||||||
custom_button: str
|
|
||||||
scan_button: str
|
|
||||||
placeholder: str
|
|
||||||
entry_tooltip: str
|
|
||||||
scan_tooltip: str
|
|
||||||
|
|
||||||
|
|
||||||
lan_panel = LanPanel(
|
|
||||||
heading="LAN query port",
|
|
||||||
default_button="Default port (27016)",
|
|
||||||
custom_button="Custom port",
|
|
||||||
scan_button="Scan",
|
|
||||||
placeholder="Enter the query port (1-65535)",
|
|
||||||
entry_tooltip="Specify the port to search for DayZ servers on the local network",
|
|
||||||
scan_tooltip="Scan for servers",
|
|
||||||
)
|
|
||||||
|
|
||||||
distance_suffix = "Distance: calculating..."
|
distance_suffix = "Distance: calculating..."
|
||||||
dialog_error = "ERROR"
|
dialog_error = "ERROR"
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -2,7 +2,12 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
||||||
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME, DAYZ_COMMUNITY_ROOT
|
from dzgui.const.constants import (
|
||||||
|
APPID_DAYZ,
|
||||||
|
APPID_DAYZ_EXP,
|
||||||
|
APP_NAME,
|
||||||
|
DAYZ_COMMUNITY_ROOT,
|
||||||
|
)
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
|
|
||||||
@@ -20,6 +25,7 @@ def expunge_link(file: Path) -> None:
|
|||||||
if str(file.stem)[:2] == "@C":
|
if str(file.stem)[:2] == "@C":
|
||||||
file.unlink()
|
file.unlink()
|
||||||
|
|
||||||
|
|
||||||
def rebuild_symlinks(config: Path) -> None:
|
def rebuild_symlinks(config: Path) -> None:
|
||||||
# TODO: pass direct path as argument
|
# TODO: pass direct path as argument
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
@@ -63,10 +69,9 @@ def symlink_mission(steam_path: Path, target: str) -> str:
|
|||||||
stem = path.name
|
stem = path.name
|
||||||
parent = path.parent
|
parent = path.parent
|
||||||
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
|
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
|
||||||
try:
|
if source.exists():
|
||||||
source.symlink_to(parent)
|
source.unlink()
|
||||||
except Exception:
|
source.symlink_to(parent)
|
||||||
return ""
|
|
||||||
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
|
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+25
-29
@@ -1,19 +1,23 @@
|
|||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
import warnings
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Literal
|
from typing import TYPE_CHECKING, Literal, Self
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, STEAM_ICON
|
from dzgui.const.constants import (
|
||||||
|
APP_NAME,
|
||||||
|
APP_NAME_LOWER,
|
||||||
|
GTK_APP_ID,
|
||||||
|
)
|
||||||
from dzgui.const.enum import NotebookPage
|
from dzgui.const.enum import NotebookPage
|
||||||
from dzgui.controllers.emitter import Emitter
|
from dzgui.controllers.emitter import Emitter
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
from dzgui.util import css, strings
|
from dzgui.util import css
|
||||||
from dzgui.util.keys import is_ctrl_mask
|
from dzgui.util.keys import is_ctrl_mask
|
||||||
from dzgui.views.components.connect_panel import ConnectPanel
|
from dzgui.views.components.connect_panel import ConnectPanel
|
||||||
from dzgui.views.components.crumbs import Breadcrumbs
|
from dzgui.views.components.crumbs import Breadcrumbs
|
||||||
from dzgui.views.components.right_panel import RightPanel
|
from dzgui.views.components.right_panel import RightPanel
|
||||||
from dzgui.views.components.statusbar import Statusbar
|
from dzgui.views.components.statusbar import Statusbar
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||||
|
|
||||||
# TODO: import notebook only and add components there?
|
# TODO: import notebook only and add components there?
|
||||||
@@ -41,20 +45,21 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
# TODO: drop
|
|
||||||
# https://bugzilla.gnome.org/show_bug.cgi?id=708676
|
|
||||||
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
|
||||||
|
|
||||||
|
class OuterWindow(Gtk.ApplicationWindow):
|
||||||
class OuterWindow(Gtk.Window):
|
def __init__(self, app: Gtk.Application) -> None:
|
||||||
def __init__(self) -> None:
|
super().__init__(
|
||||||
super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER)
|
application=app, title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER
|
||||||
|
)
|
||||||
|
|
||||||
self.hb = AppHeaderBar()
|
self.hb = AppHeaderBar()
|
||||||
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)
|
||||||
@@ -66,8 +71,8 @@ class OuterWindow(Gtk.Window):
|
|||||||
self.show_all()
|
self.show_all()
|
||||||
css.load_css()
|
css.load_css()
|
||||||
|
|
||||||
MainController.open_page(NotebookPage.SERVERS)
|
|
||||||
MainController.set_start_tab()
|
MainController.set_start_tab()
|
||||||
|
MainController.open_page(NotebookPage.SERVERS)
|
||||||
self.grid.hide_widgets_on_init()
|
self.grid.hide_widgets_on_init()
|
||||||
|
|
||||||
MainController.loaded = True
|
MainController.loaded = True
|
||||||
@@ -276,13 +281,18 @@ class Grid(Gtk.Grid):
|
|||||||
self.right_panel.refresh_button.set_visible(state)
|
self.right_panel.refresh_button.set_visible(state)
|
||||||
|
|
||||||
|
|
||||||
class App(Gtk.Application):
|
class App(ColorAwareApp, Gtk.Application): # type: ignore
|
||||||
def __init__(self, prefs: "UserPrefs") -> None:
|
def __init__(self, prefs: "UserPrefs") -> None:
|
||||||
|
super().__init__(application_id=GTK_APP_ID)
|
||||||
|
|
||||||
GLib.set_prgname(APP_NAME)
|
GLib.set_prgname(APP_NAME)
|
||||||
MainController.set_prefs(prefs)
|
MainController.set_prefs(prefs)
|
||||||
|
|
||||||
self.win = OuterWindow()
|
self._setup_signals()
|
||||||
|
self.connect("activate", self._on_activate)
|
||||||
|
|
||||||
|
def _on_activate(self, app: Self) -> None:
|
||||||
|
self.win = OuterWindow(app)
|
||||||
|
|
||||||
accel = Gtk.AccelGroup()
|
accel = Gtk.AccelGroup()
|
||||||
accel.connect(
|
accel.connect(
|
||||||
@@ -293,9 +303,6 @@ class App(Gtk.Application):
|
|||||||
)
|
)
|
||||||
self.win.add_accel_group(accel)
|
self.win.add_accel_group(accel)
|
||||||
|
|
||||||
self._setup_signals()
|
|
||||||
Gtk.main()
|
|
||||||
|
|
||||||
def _setup_signals(self) -> None:
|
def _setup_signals(self) -> None:
|
||||||
SIGNAL_ADD = "signal_add"
|
SIGNAL_ADD = "signal_add"
|
||||||
SIGNAL_ADD_FULL = "signal_add_full"
|
SIGNAL_ADD_FULL = "signal_add_full"
|
||||||
@@ -324,15 +331,4 @@ class App(Gtk.Application):
|
|||||||
) -> None:
|
) -> None:
|
||||||
self.win.halt_proc_and_quit()
|
self.win.halt_proc_and_quit()
|
||||||
|
|
||||||
|
|
||||||
MainController = Controller()
|
MainController = Controller()
|
||||||
|
|
||||||
try:
|
|
||||||
theme = Gtk.IconTheme.get_default()
|
|
||||||
icons = theme.list_icons(None)
|
|
||||||
if STEAM_ICON not in icons:
|
|
||||||
logger.warn(strings.steam_icon_missing)
|
|
||||||
warnings.warn(strings.steam_icon_missing, stacklevel=2)
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(e)
|
|
||||||
warnings.warn(strings.gtk_theme_missing, stacklevel=2)
|
|
||||||
@@ -23,3 +23,11 @@ class HBox(GenericBox):
|
|||||||
class VBox(GenericBox):
|
class VBox(GenericBox):
|
||||||
def __init__(self, spacing: int = 0) -> None:
|
def __init__(self, spacing: int = 0) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
||||||
|
|
||||||
|
|
||||||
|
class ShortHBox(HBox):
|
||||||
|
def __init__(self, widget: Gtk.Widget) -> None:
|
||||||
|
super().__init__(spacing=5)
|
||||||
|
|
||||||
|
self.set_halign(Gtk.Align.START)
|
||||||
|
self.pack_start(widget, expand=False, fill=False, padding=0)
|
||||||
@@ -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
|
||||||
@@ -6,10 +6,10 @@ from dzgui.strings import buttons
|
|||||||
from dzgui.util.strings import (
|
from dzgui.util.strings import (
|
||||||
alert_button_tooltip,
|
alert_button_tooltip,
|
||||||
atomic_buttons,
|
atomic_buttons,
|
||||||
connect_panel,
|
|
||||||
)
|
)
|
||||||
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,
|
||||||
@@ -17,6 +17,7 @@ from dzgui.const.constants import (
|
|||||||
WARNING,
|
WARNING,
|
||||||
WEB_BROWSER,
|
WEB_BROWSER,
|
||||||
)
|
)
|
||||||
|
from dzgui.strings import connect_panel
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -63,8 +64,17 @@ class IconButton(Gtk.Button):
|
|||||||
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
|
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
|
||||||
self.set_image(self.icon)
|
self.set_image(self.icon)
|
||||||
self.set_image_position(position)
|
self.set_image_position(position)
|
||||||
# self.set_image_position(Gtk.PositionType.RIGHT)
|
|
||||||
self.set_focus_on_click(False)
|
self.set_focus_on_click(False)
|
||||||
|
self.set_always_show_image(True)
|
||||||
|
|
||||||
|
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):
|
||||||
@@ -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)
|
||||||
@@ -196,7 +222,7 @@ class KeysButton(IconTextButton):
|
|||||||
|
|
||||||
class SteamConnectButton(LargeIconTextButton):
|
class SteamConnectButton(LargeIconTextButton):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(icon=STEAM_ICON, label=connect_panel.connect)
|
super().__init__(icon=STEAM_ICON, label=connect_panel.connect_button)
|
||||||
self.set_tooltip_text(connect_panel.connect_tooltip)
|
self.set_tooltip_text(connect_panel.connect_tooltip)
|
||||||
|
|
||||||
|
|
||||||
@@ -216,7 +242,7 @@ class SteamWorkshopButton(SteamTextButton):
|
|||||||
|
|
||||||
class AddButton(IconTextButton):
|
class AddButton(IconTextButton):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(icon=LIST_ADD, label=connect_panel.add)
|
super().__init__(icon=LIST_ADD, label=connect_panel.add_button)
|
||||||
self.set_tooltip_text(connect_panel.add_tooltip)
|
self.set_tooltip_text(connect_panel.add_tooltip)
|
||||||
|
|
||||||
|
|
||||||
@@ -241,3 +267,38 @@ class LoggerAlertsButton(IconTextButton):
|
|||||||
self.set_halign(Gtk.Align.END)
|
self.set_halign(Gtk.Align.END)
|
||||||
self.set_hexpand(True)
|
self.set_hexpand(True)
|
||||||
self.set_tooltip_text(alert_button_tooltip)
|
self.set_tooltip_text(alert_button_tooltip)
|
||||||
|
|
||||||
|
|
||||||
|
class SpinnerButton(Gtk.Button):
|
||||||
|
def __init__(self, label: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.text = label
|
||||||
|
self.label = Gtk.Label(label=label, halign=Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
self.spinner = Gtk.Spinner()
|
||||||
|
self.spinner.set_halign(Gtk.Align.END)
|
||||||
|
self.spinner.set_sensitive(False)
|
||||||
|
|
||||||
|
grid = Gtk.Grid(column_spacing=10)
|
||||||
|
for el in self.label, self.spinner:
|
||||||
|
grid.add(el)
|
||||||
|
|
||||||
|
self.add(grid)
|
||||||
|
|
||||||
|
self.connect("clicked", self._on_button_clicked)
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_map(self, button: Self) -> None:
|
||||||
|
self.spinner.set_visible(False)
|
||||||
|
|
||||||
|
def _on_button_clicked(self, button: Self) -> None:
|
||||||
|
self.start_spinner()
|
||||||
|
|
||||||
|
def stop_spinner(self) -> None:
|
||||||
|
self.spinner.set_visible(False)
|
||||||
|
self.spinner.stop()
|
||||||
|
|
||||||
|
def start_spinner(self) -> None:
|
||||||
|
self.spinner.set_visible(True)
|
||||||
|
self.spinner.start()
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
from dzgui.strings import preconnect
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gdk, Gtk # noqa E402
|
|
||||||
|
|
||||||
|
|
||||||
class ConnectBox(Gtk.Box):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
|
||||||
|
|
||||||
self.back = Gtk.Button(
|
|
||||||
label=preconnect.back, halign=Gtk.Align.END, hexpand=True
|
|
||||||
)
|
|
||||||
|
|
||||||
# TODO: dynamically update this
|
|
||||||
self.ok = Gtk.Button(label="Launch", halign=Gtk.Align.END)
|
|
||||||
|
|
||||||
for button in self.back, self.ok:
|
|
||||||
self.add(button)
|
|
||||||
|
|
||||||
def get_back_button(self) -> Gtk.Button:
|
|
||||||
return self.back
|
|
||||||
|
|
||||||
def get_ok_button(self) -> Gtk.Button:
|
|
||||||
return self.ok
|
|
||||||
|
|
||||||
|
|
||||||
class PreconnectBox(Gtk.Box):
|
|
||||||
def __init__(self) -> None:
|
|
||||||
super().__init__(
|
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
|
||||||
valign=Gtk.Align.END,
|
|
||||||
vexpand=True,
|
|
||||||
spacing=5,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.button_hbox = ConnectBox()
|
|
||||||
self.ok = self.button_hbox.get_ok_button()
|
|
||||||
self.ok.set_label(preconnect.update_mods)
|
|
||||||
|
|
||||||
self.connect_last = Gtk.Button(
|
|
||||||
label=preconnect.connect_last,
|
|
||||||
halign=Gtk.Align.END,
|
|
||||||
tooltip_text=preconnect.connect_last_tooltip,
|
|
||||||
)
|
|
||||||
self.button_hbox.add(self.connect_last)
|
|
||||||
|
|
||||||
self.raise_window = Gtk.CheckButton(
|
|
||||||
# TODO: strings
|
|
||||||
label="Foreground DZGUI while downloading",
|
|
||||||
halign=Gtk.Align.END,
|
|
||||||
hexpand=True,
|
|
||||||
valign=Gtk.Align.END,
|
|
||||||
visible=False,
|
|
||||||
has_tooltip=True,
|
|
||||||
sensitive=False,
|
|
||||||
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
|
|
||||||
active=True,
|
|
||||||
)
|
|
||||||
self.add(self.button_hbox)
|
|
||||||
self.add(self.raise_window)
|
|
||||||
|
|
||||||
def get_back_button(self) -> Gtk.Button:
|
|
||||||
return self.button_hbox.get_back_button()
|
|
||||||
|
|
||||||
def get_ok_button(self) -> Gtk.Button:
|
|
||||||
return self.ok
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from typing import Any, Callable, TYPE_CHECKING
|
|||||||
from dzgui.api.servers import validate_ip
|
from dzgui.api.servers import validate_ip
|
||||||
from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL
|
from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL
|
||||||
from dzgui.util.css import add_class, remove_class
|
from dzgui.util.css import add_class, remove_class
|
||||||
from dzgui.util.strings import connect_panel, lan_panel
|
from dzgui.strings import connect_panel
|
||||||
from dzgui.strings.errors import api_popover
|
from dzgui.strings.errors import api_popover
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
@@ -26,15 +26,12 @@ 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 False
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class ValidatedEntry(Gtk.Entry):
|
class ValidatedEntry(Gtk.Entry):
|
||||||
@@ -124,12 +121,9 @@ 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.connect_entry_placeholder,
|
||||||
tooltip_text=connect_panel.entry_tooltip,
|
|
||||||
)
|
)
|
||||||
self.set_placeholder_text(connect_panel.placeholder)
|
|
||||||
self.set_tooltip_text(connect_panel.entry_tooltip)
|
|
||||||
|
|
||||||
|
|
||||||
class PortEntry(ValidatedEntry):
|
class PortEntry(ValidatedEntry):
|
||||||
@@ -137,11 +131,9 @@ class PortEntry(ValidatedEntry):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
controller,
|
controller,
|
||||||
func=validate_port,
|
func=validate_port,
|
||||||
placeholder_text=connect_panel.placeholder,
|
placeholder_text=connect_panel.lan_entry_placeholder,
|
||||||
tooltip_text=connect_panel.entry_tooltip,
|
tooltip_text=connect_panel.lan_entry_tooltip,
|
||||||
)
|
)
|
||||||
self.set_placeholder_text(lan_panel.placeholder)
|
|
||||||
self.set_tooltip_text(lan_panel.entry_tooltip)
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: backport to Options page
|
# TODO: backport to Options page
|
||||||
@@ -162,7 +154,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:
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ from dzgui.const.constants import (
|
|||||||
NO_PADDING,
|
NO_PADDING,
|
||||||
SEARCH_ICON,
|
SEARCH_ICON,
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import FilterMode
|
|
||||||
from dzgui.model.servers import ServerModelManager
|
|
||||||
from dzgui.util.format import format_exception
|
from dzgui.util.format import format_exception
|
||||||
from dzgui.util.strings import all_maps
|
from dzgui.util.strings import all_maps
|
||||||
from dzgui.views.components.maps_combo import MapsCombo
|
from dzgui.views.components.maps_combo import MapsCombo
|
||||||
@@ -29,21 +27,33 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class ButtonGrid(Gtk.Grid):
|
class ButtonGrid(Gtk.Grid):
|
||||||
def __init__(self, controller: "Controller", defaults: dict) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
defaults: dict[str, str],
|
||||||
|
tooltips: dict[str, str],
|
||||||
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
halign=Gtk.Align.CENTER, column_spacing=5, column_homogeneous=True
|
halign=Gtk.Align.CENTER, column_spacing=5, column_homogeneous=True
|
||||||
)
|
)
|
||||||
row = 1
|
|
||||||
col = 0
|
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
self.checks: list[Gtk.CheckButton] = []
|
self.checks: list[Gtk.CheckButton] = []
|
||||||
|
|
||||||
# TODO: use enumerated checks
|
flowbox = Gtk.FlowBox(
|
||||||
|
halign=Gtk.Align.CENTER, min_children_per_line=3, max_children_per_line=3, selection_mode=Gtk.SelectionMode.NONE
|
||||||
|
)
|
||||||
|
|
||||||
for check in defaults.keys():
|
for check in defaults.keys():
|
||||||
checkbox = Gtk.CheckButton(label=check)
|
checkbox = Gtk.CheckButton(label=check)
|
||||||
|
try:
|
||||||
|
tt = tooltips[check]
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
tt = ""
|
||||||
|
|
||||||
|
checkbox.set_tooltip_text(tt)
|
||||||
label = checkbox.get_child()
|
label = checkbox.get_child()
|
||||||
if label is not None:
|
if label is not None:
|
||||||
label.set_ellipsize(Pango.EllipsizeMode.END) # type: ignore
|
label.set_ellipsize(Pango.EllipsizeMode.END) # type: ignore
|
||||||
@@ -51,14 +61,12 @@ class ButtonGrid(Gtk.Grid):
|
|||||||
if defaults[check]:
|
if defaults[check]:
|
||||||
checkbox.set_active(True)
|
checkbox.set_active(True)
|
||||||
|
|
||||||
col = col + 1
|
|
||||||
if col > 3:
|
|
||||||
row += 1
|
|
||||||
col = 1
|
|
||||||
self.attach(checkbox, col, row, 1, 1)
|
|
||||||
checkbox.connect("toggled", self._on_check_toggled)
|
checkbox.connect("toggled", self._on_check_toggled)
|
||||||
|
flowbox.add(checkbox)
|
||||||
self.checks.append(checkbox)
|
self.checks.append(checkbox)
|
||||||
|
|
||||||
|
self.add(flowbox)
|
||||||
|
|
||||||
def block_toggles(self, state: bool) -> None:
|
def block_toggles(self, state: bool) -> None:
|
||||||
for check in self.checks:
|
for check in self.checks:
|
||||||
self.controller.suppress_signal(
|
self.controller.suppress_signal(
|
||||||
@@ -138,10 +146,6 @@ class KeywordEntry(Gtk.Entry):
|
|||||||
self.controller.set_active_keyword(keyword)
|
self.controller.set_active_keyword(keyword)
|
||||||
logger.info(f"User filtered by keyword '{keyword}'")
|
logger.info(f"User filtered by keyword '{keyword}'")
|
||||||
|
|
||||||
ServerModelManager(
|
|
||||||
self.controller, self.controller.get_active_treeview()
|
|
||||||
).refilter(FilterMode.TOGGLE_ON)
|
|
||||||
|
|
||||||
|
|
||||||
class FilterPanel(Gtk.Box):
|
class FilterPanel(Gtk.Box):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
@@ -163,11 +167,12 @@ class FilterPanel(Gtk.Box):
|
|||||||
|
|
||||||
filter_man = self.controller.get_filter_man()
|
filter_man = self.controller.get_filter_man()
|
||||||
defaults = filter_man.get_default_filters()
|
defaults = filter_man.get_default_filters()
|
||||||
|
tooltips = filter_man.get_tooltips()
|
||||||
self.map_store = filter_man.get_map_store()
|
self.map_store = filter_man.get_map_store()
|
||||||
self.enabled_filters = defaults
|
self.enabled_filters = defaults
|
||||||
|
|
||||||
self.keyword_entry = KeywordEntry(self.controller)
|
self.keyword_entry = KeywordEntry(self.controller)
|
||||||
self.button_grid = ButtonGrid(self.controller, defaults)
|
self.button_grid = ButtonGrid(self.controller, defaults, tooltips)
|
||||||
|
|
||||||
# TODO: strings
|
# TODO: strings
|
||||||
self.filters_label = BoldLabel("Filters")
|
self.filters_label = BoldLabel("Filters")
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ class Icon(Gtk.Image):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
|
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_icon_name(self, name: str) -> None:
|
||||||
|
super().set_from_icon_name(name, Gtk.IconSize.BUTTON)
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
|
from typing import Self
|
||||||
|
from warnings import deprecated
|
||||||
|
|
||||||
|
from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
||||||
|
from dzgui.init.prereqs import has_steam_client, has_flatpak_client
|
||||||
from dzgui.strings import options
|
from dzgui.strings import options
|
||||||
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
from dzgui.views.components.box import HBox
|
||||||
|
from dzgui.views.components.buttons import Icon
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -7,29 +13,71 @@ gi.require_version("Gtk", "3.0")
|
|||||||
from gi.repository import Gtk # noqa
|
from gi.repository import Gtk # noqa
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Client choice is determined at setup time")
|
||||||
class ClientCombo(Gtk.ComboBox):
|
class ClientCombo(Gtk.ComboBox):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.client_store = Gtk.ListStore(str, str)
|
has_steam = has_steam_client()
|
||||||
clients = (
|
has_flatpak = has_flatpak_client()
|
||||||
|
self.client_store = Gtk.ListStore(str, str, bool)
|
||||||
|
self.clients = (
|
||||||
(
|
(
|
||||||
options.steam_combo,
|
options.steam_combo,
|
||||||
STEAM_CMD,
|
STEAM_CMD,
|
||||||
|
has_steam,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
options.flatpak_combo,
|
options.flatpak_combo,
|
||||||
FLATPAK_RUN_CMD,
|
FLATPAK_RUN_CMD,
|
||||||
|
has_flatpak,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
options.flatpak_container_combo,
|
options.flatpak_container_combo,
|
||||||
FLATPAK_SANDBOX,
|
FLATPAK_SANDBOX,
|
||||||
|
has_flatpak,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
for client in clients:
|
for client in self.clients:
|
||||||
self.client_store.append(client)
|
self.client_store.append(client)
|
||||||
|
|
||||||
self.set_model(self.client_store)
|
self.set_model(self.client_store)
|
||||||
renderer_text = Gtk.CellRendererText()
|
renderer_text = Gtk.CellRendererText()
|
||||||
self.pack_start(renderer_text, True)
|
self.pack_start(renderer_text, True)
|
||||||
self.add_attribute(renderer_text, "text", 0)
|
self.add_attribute(renderer_text, "text", 0)
|
||||||
self.set_active(0)
|
self.set_active(0)
|
||||||
|
|
||||||
|
self.set_cell_data_func(self.get_cells()[0], self.data_func)
|
||||||
|
|
||||||
|
def data_func(
|
||||||
|
self,
|
||||||
|
combo: Self,
|
||||||
|
cell: Gtk.CellRendererText,
|
||||||
|
model: Gtk.ListStore,
|
||||||
|
_iter: Gtk.TreeIter,
|
||||||
|
) -> None:
|
||||||
|
sens = model[_iter][2]
|
||||||
|
cell.set_property("sensitive", sens)
|
||||||
|
|
||||||
|
def get_client_human_readable(self, cmd: str) -> str:
|
||||||
|
for client in self.clients:
|
||||||
|
human, command, sens = client
|
||||||
|
if command == cmd:
|
||||||
|
return human
|
||||||
|
raise TypeError("Not a valid Steam client selection")
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorPopover(Gtk.Popover):
|
||||||
|
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
||||||
|
super().__init__(relative_to=relative_to, position=position)
|
||||||
|
|
||||||
|
self.hbox = HBox()
|
||||||
|
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
||||||
|
error_icon = Icon(ERROR, margin_start=10)
|
||||||
|
self.hbox.extend([error_icon, self.label])
|
||||||
|
self.add(self.hbox)
|
||||||
|
self.show_all()
|
||||||
|
self.popdown()
|
||||||
|
|
||||||
|
def set_label(self, label: str) -> None:
|
||||||
|
self.label.set_label(label)
|
||||||
@@ -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)
|
||||||
@@ -4,6 +4,9 @@ from typing import Any, Literal, Self, TYPE_CHECKING
|
|||||||
# import time
|
# import time
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
|
||||||
# TODO: import dialog titles
|
# TODO: import dialog titles
|
||||||
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
from dzgui.api.mods import remove_stale_signatures as remove_stale
|
||||||
from dzgui.config.ipdb import get_ipdb
|
from dzgui.config.ipdb import get_ipdb
|
||||||
@@ -37,11 +40,11 @@ class Success(Enum):
|
|||||||
|
|
||||||
# TODO: strings for "Running", "Failed", etc.
|
# TODO: strings for "Running", "Failed", etc.
|
||||||
# TODO: add margins to tree
|
# TODO: add margins to tree
|
||||||
class BootDialog(Gtk.Dialog):
|
class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
||||||
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,7 +102,7 @@ class BootDialog(Gtk.Dialog):
|
|||||||
margin_top=30,
|
margin_top=30,
|
||||||
margin_bottom=30,
|
margin_bottom=30,
|
||||||
)
|
)
|
||||||
self.error_label = Gtk.Label()
|
self.error_label = Gtk.Label(margin_start=15, margin_end=15)
|
||||||
|
|
||||||
# TODO: abstract class
|
# TODO: abstract class
|
||||||
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
||||||
|
|||||||
@@ -0,0 +1,457 @@
|
|||||||
|
import gi
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from datetime import datetime, time, timedelta
|
||||||
|
from typing import Literal
|
||||||
|
|
||||||
|
from dzgui.const.constants import DAY_ICON, NIGHT_ICON
|
||||||
|
from dzgui.strings import calc
|
||||||
|
from dzgui.views.components.icon import Icon
|
||||||
|
from dzgui.views.components.labels import BoldLabel
|
||||||
|
from dzgui.views.components.box import VBox, HBox
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk, GLib, GObject # type: ignore # noqa E402
|
||||||
|
|
||||||
|
DAY_START_TIME = time(hour=6, minute=0, second=0)
|
||||||
|
DAY_END_TIME = time(hour=18, minute=0, second=0)
|
||||||
|
NIGHT_START_TIME = time(hour=18, minute=0, second=0)
|
||||||
|
NIGHT_END_TIME = time(hour=6, minute=0, second=0)
|
||||||
|
EXTENDED_TIME_FORMAT = "%H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class Time:
|
||||||
|
time: str
|
||||||
|
day_accel: float
|
||||||
|
night_accel: float
|
||||||
|
|
||||||
|
|
||||||
|
class Emitter(GObject.GObject):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object, object))
|
||||||
|
def local_time_incremented(self, time_now: datetime, elapsed: timedelta) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def server_time_incremented(self, server_time: datetime) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
|
def calculate_button_clicked(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def target_time_changed(self, time: timedelta) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def target_time_reset(self, time: timedelta) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def remaining_time_changed(self, time: datetime) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def adjusted_local_time_changed(self, time: datetime) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LocalClock:
|
||||||
|
def __init__(self, emitter: Emitter) -> None:
|
||||||
|
self.emitter = emitter
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
# NOTE: highest theoretical accel interval is 24 * 64
|
||||||
|
GLib.timeout_add(500, self.check_time)
|
||||||
|
|
||||||
|
def get_time(self) -> str:
|
||||||
|
return datetime.now().time().strftime(EXTENDED_TIME_FORMAT)
|
||||||
|
|
||||||
|
def get_elapsed_time(self) -> timedelta:
|
||||||
|
return datetime.now() - self.start_time
|
||||||
|
|
||||||
|
def check_time(self) -> Literal[True]:
|
||||||
|
time_now = datetime.now()
|
||||||
|
elapsed = datetime.now() - self.start_time
|
||||||
|
self.emitter.emit("local_time_incremented", time_now, elapsed)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ServerClock:
|
||||||
|
def __init__(
|
||||||
|
self, server_time: Time, emitter: Emitter, local_clock: LocalClock
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
self.local_clock = local_clock
|
||||||
|
self.emitter = emitter
|
||||||
|
|
||||||
|
hour, minute = self.split_time(server_time)
|
||||||
|
self.day_accel = server_time.day_accel
|
||||||
|
self.night_accel = server_time.night_accel
|
||||||
|
|
||||||
|
self.start_time = datetime.combine(datetime.today(), time(hour, minute))
|
||||||
|
self.cur_time = datetime.combine(datetime.today(), time(hour, minute))
|
||||||
|
|
||||||
|
self._last_elapsed = self.local_clock.get_elapsed_time()
|
||||||
|
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def split_time(server_time: Time) -> tuple[int, int]:
|
||||||
|
split = server_time.time.split(":")
|
||||||
|
return int(split[0]), int(split[1])
|
||||||
|
|
||||||
|
def is_day(self) -> bool:
|
||||||
|
if DAY_START_TIME <= self.cur_time.time() <= DAY_END_TIME:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _on_local_time_incremented(
|
||||||
|
self,
|
||||||
|
emitter: Emitter,
|
||||||
|
time: datetime,
|
||||||
|
elapsed: timedelta,
|
||||||
|
) -> Literal[True]:
|
||||||
|
delta = elapsed - self._last_elapsed
|
||||||
|
self._last_elapsed = elapsed
|
||||||
|
factor = self.get_accel_factor(self.cur_time)
|
||||||
|
self.cur_time += delta * factor
|
||||||
|
self.emitter.emit("server_time_incremented", self.cur_time)
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_accel_factor(self, dt: datetime) -> float:
|
||||||
|
return (
|
||||||
|
self.day_accel
|
||||||
|
if DAY_START_TIME <= dt.time() < DAY_END_TIME
|
||||||
|
else (self.night_accel * self.day_accel)
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_values(self) -> tuple[int, int]:
|
||||||
|
return self.cur_time.hour, self.cur_time.minute
|
||||||
|
|
||||||
|
def get_datetime(self) -> datetime:
|
||||||
|
return self.cur_time
|
||||||
|
|
||||||
|
def get_time(self) -> time:
|
||||||
|
return self.cur_time.time()
|
||||||
|
|
||||||
|
def calc_delta(self, end_time: datetime) -> timedelta:
|
||||||
|
total = 0.0
|
||||||
|
current_time = self.cur_time
|
||||||
|
|
||||||
|
if current_time > end_time:
|
||||||
|
end_time += timedelta(days=1)
|
||||||
|
|
||||||
|
while current_time < end_time:
|
||||||
|
factor = self.get_accel_factor(current_time)
|
||||||
|
day_start = datetime.combine(current_time.date(), DAY_START_TIME)
|
||||||
|
day_end = datetime.combine(current_time.date(), DAY_END_TIME)
|
||||||
|
|
||||||
|
if current_time.time() < DAY_START_TIME:
|
||||||
|
edge = day_start
|
||||||
|
elif current_time.time() < DAY_END_TIME:
|
||||||
|
edge = day_end
|
||||||
|
else:
|
||||||
|
edge = datetime.combine(
|
||||||
|
current_time.date() + timedelta(days=1), DAY_START_TIME
|
||||||
|
)
|
||||||
|
|
||||||
|
chunk_end = min(end_time, edge)
|
||||||
|
total += (chunk_end - current_time).total_seconds() / factor
|
||||||
|
current_time = chunk_end
|
||||||
|
return timedelta(seconds=total)
|
||||||
|
|
||||||
|
|
||||||
|
class RemainderClock:
|
||||||
|
def __init__(self, emitter: Emitter) -> None:
|
||||||
|
self.emitter = emitter
|
||||||
|
self.emitter.connect("target_time_changed", self._on_target_time_changed)
|
||||||
|
self.emitter.connect("target_time_reset", self._on_target_time_reset)
|
||||||
|
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
|
||||||
|
self.reset_time(True)
|
||||||
|
|
||||||
|
self.previous = timedelta(0)
|
||||||
|
|
||||||
|
def reset_time(self, zero_out: bool = False) -> None:
|
||||||
|
now = datetime.now()
|
||||||
|
second, ms = (0, 0) if zero_out else (now.second, now.microsecond)
|
||||||
|
self.time = datetime.combine(
|
||||||
|
now, time(hour=0, minute=0, second=second, microsecond=ms)
|
||||||
|
)
|
||||||
|
|
||||||
|
def _on_local_time_incremented(
|
||||||
|
self, emitter: Emitter, time: datetime, elapsed: timedelta
|
||||||
|
) -> None:
|
||||||
|
if self.time == datetime.min.time():
|
||||||
|
return
|
||||||
|
|
||||||
|
delta = elapsed - self.previous
|
||||||
|
# NOTE: clamp to midnight
|
||||||
|
self.time = max(
|
||||||
|
self.time - delta, self.time.replace(hour=0, minute=0, second=0)
|
||||||
|
)
|
||||||
|
self.previous = elapsed
|
||||||
|
self.emitter.emit("remaining_time_changed", self.time)
|
||||||
|
|
||||||
|
def _on_target_time_reset(self, emitter: Emitter, time: timedelta) -> None:
|
||||||
|
self.reset_time(True)
|
||||||
|
self.emitter.emit("remaining_time_changed", self.time)
|
||||||
|
# NOTE: restore adjusted time to current time
|
||||||
|
self.emitter.emit("adjusted_local_time_changed", datetime.now())
|
||||||
|
|
||||||
|
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
|
||||||
|
self.reset_time(True)
|
||||||
|
self.time += time
|
||||||
|
self.emitter.emit("remaining_time_changed", self.time)
|
||||||
|
|
||||||
|
def get_time(self) -> str:
|
||||||
|
return self.time.strftime(EXTENDED_TIME_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
class AdjustedClock:
|
||||||
|
def __init__(self, emitter: Emitter) -> None:
|
||||||
|
self.emitter = emitter
|
||||||
|
self.emitter.connect("target_time_changed", self._on_target_time_changed)
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
|
||||||
|
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
|
||||||
|
self.start_time = datetime.now()
|
||||||
|
adjusted = self.start_time + time
|
||||||
|
self.emitter.emit("adjusted_local_time_changed", adjusted)
|
||||||
|
|
||||||
|
def get_time(self) -> str:
|
||||||
|
return self.start_time.strftime(EXTENDED_TIME_FORMAT)
|
||||||
|
|
||||||
|
|
||||||
|
class VerticalSpinBox(Gtk.SpinButton):
|
||||||
|
def __init__(self, value: int, upper: int) -> None:
|
||||||
|
super().__init__(
|
||||||
|
adjustment=Gtk.Adjustment(
|
||||||
|
value=value,
|
||||||
|
lower=0,
|
||||||
|
upper=upper,
|
||||||
|
step_increment=1,
|
||||||
|
page_increment=1,
|
||||||
|
page_size=0,
|
||||||
|
),
|
||||||
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HourSpinBox(VerticalSpinBox):
|
||||||
|
def __init__(self, hour: int) -> None:
|
||||||
|
super().__init__(value=hour, upper=23)
|
||||||
|
|
||||||
|
|
||||||
|
class MinuteSpinBox(VerticalSpinBox):
|
||||||
|
def __init__(self, minute: int) -> None:
|
||||||
|
super().__init__(value=minute, upper=59)
|
||||||
|
|
||||||
|
self.get_adjustment().set_page_increment(10)
|
||||||
|
|
||||||
|
|
||||||
|
class TimePicker(Gtk.Box):
|
||||||
|
def __init__(self, hour: int, minute: int) -> None:
|
||||||
|
super().__init__(
|
||||||
|
orientation=Gtk.Orientation.HORIZONTAL,
|
||||||
|
halign=Gtk.Align.CENTER,
|
||||||
|
valign=Gtk.Align.CENTER,
|
||||||
|
)
|
||||||
|
self.hour_spin = HourSpinBox(hour)
|
||||||
|
self.minute_spin = MinuteSpinBox(minute)
|
||||||
|
self.add(self.hour_spin)
|
||||||
|
self.add(self.minute_spin)
|
||||||
|
|
||||||
|
self.hour_spin.connect("output", self._on_output)
|
||||||
|
self.minute_spin.connect("output", self._on_output)
|
||||||
|
|
||||||
|
def _on_output(self, spin_button: VerticalSpinBox) -> Literal[True]:
|
||||||
|
val = spin_button.get_value()
|
||||||
|
spin_button.set_text(f"{int(val):02d}")
|
||||||
|
# NOTE: indicates that rendering completed
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_values(self) -> tuple[int, int]:
|
||||||
|
hour = self.get_hour()
|
||||||
|
minute = self.get_minute()
|
||||||
|
return hour, minute
|
||||||
|
|
||||||
|
def get_hour(self) -> int:
|
||||||
|
return int(self.hour_spin.get_adjustment().get_value())
|
||||||
|
|
||||||
|
def get_minute(self) -> int:
|
||||||
|
return int(self.minute_spin.get_adjustment().get_value())
|
||||||
|
|
||||||
|
def get_time(self) -> datetime:
|
||||||
|
"""
|
||||||
|
NOTE: interpolating seconds/microseconds from the current time
|
||||||
|
may prevent abrupt visual changes when setting a new TimePicker time
|
||||||
|
while a clock tick is updating the local remainder label.
|
||||||
|
However, this can introduce false negatives when running tests due to microsecond drift.
|
||||||
|
Ideally, tests should be decoupled from this logic.
|
||||||
|
"""
|
||||||
|
d = datetime.now()
|
||||||
|
return datetime.combine(
|
||||||
|
d,
|
||||||
|
time(
|
||||||
|
hour=self.get_hour(),
|
||||||
|
minute=self.get_minute(),
|
||||||
|
second=0,
|
||||||
|
microsecond=0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_time(self, hour: int, minute: int) -> None:
|
||||||
|
self.hour_spin.get_adjustment().set_value(hour)
|
||||||
|
self.minute_spin.get_adjustment().set_value(minute)
|
||||||
|
|
||||||
|
|
||||||
|
class AdjustedTimeFrame(Gtk.Frame):
|
||||||
|
def __init__(self, emitter: Emitter) -> None:
|
||||||
|
super().__init__(margin_start=50, margin_end=50)
|
||||||
|
|
||||||
|
self.emitter = emitter
|
||||||
|
self.emitter.connect(
|
||||||
|
"adjusted_local_time_changed", self._on_adjusted_time_changed
|
||||||
|
)
|
||||||
|
self.emitter.connect("remaining_time_changed", self._on_remainder_changed)
|
||||||
|
|
||||||
|
self.remainder_clock = RemainderClock(emitter)
|
||||||
|
self.remainder_heading = BoldLabel(calc.heading_remainder_time)
|
||||||
|
self.remaining_time = Gtk.Label(label=self.remainder_clock.get_time())
|
||||||
|
|
||||||
|
self.adjusted_clock = AdjustedClock(emitter)
|
||||||
|
self.adjusted_heading = BoldLabel(calc.heading_adjusted_time)
|
||||||
|
self.adjusted_local_time = Gtk.Label(label=self.adjusted_clock.get_time())
|
||||||
|
|
||||||
|
flowbox = Gtk.FlowBox(
|
||||||
|
row_spacing=20,
|
||||||
|
column_spacing=50,
|
||||||
|
min_children_per_line=2,
|
||||||
|
max_children_per_line=2,
|
||||||
|
margin=15,
|
||||||
|
halign=Gtk.Align.CENTER,
|
||||||
|
selection_mode=Gtk.SelectionMode.NONE,
|
||||||
|
)
|
||||||
|
flowbox.add(self.remainder_heading)
|
||||||
|
flowbox.add(self.adjusted_heading)
|
||||||
|
flowbox.add(self.remaining_time)
|
||||||
|
flowbox.add(self.adjusted_local_time)
|
||||||
|
|
||||||
|
recalc = Gtk.Button(
|
||||||
|
label=calc.calculate, halign=Gtk.Align.CENTER, margin_bottom=20
|
||||||
|
)
|
||||||
|
recalc.connect("clicked", self._on_recalc_clicked)
|
||||||
|
|
||||||
|
vbox = VBox()
|
||||||
|
vbox.extend([flowbox, recalc])
|
||||||
|
self.add(vbox)
|
||||||
|
|
||||||
|
def _on_remainder_changed(self, emitter: Emitter, time: datetime) -> None:
|
||||||
|
strtime = str(time.strftime(EXTENDED_TIME_FORMAT))
|
||||||
|
self.remaining_time.set_text(strtime)
|
||||||
|
|
||||||
|
def _on_adjusted_time_changed(self, emitter: Emitter, time: datetime) -> None:
|
||||||
|
strtime = str(time.strftime(EXTENDED_TIME_FORMAT))
|
||||||
|
self.adjusted_local_time.set_text(strtime)
|
||||||
|
|
||||||
|
def _on_recalc_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.emitter.emit("calculate_button_clicked")
|
||||||
|
|
||||||
|
|
||||||
|
class TimePickerFrame(Gtk.Frame):
|
||||||
|
def __init__(self, emitter: Emitter, stime: Time) -> None:
|
||||||
|
super().__init__(margin_start=50, margin_end=50)
|
||||||
|
|
||||||
|
self.emitter = emitter
|
||||||
|
self.emitter.connect("local_time_incremented", self._on_local_clock_increment)
|
||||||
|
self.emitter.connect("server_time_incremented", self._on_server_clock_increment)
|
||||||
|
self.emitter.connect(
|
||||||
|
"calculate_button_clicked", self._on_calculate_button_clicked
|
||||||
|
)
|
||||||
|
|
||||||
|
self.local_clock = LocalClock(self.emitter)
|
||||||
|
local_clock_time = self.local_clock.get_time()
|
||||||
|
|
||||||
|
self.server_time = ServerClock(stime, self.emitter, self.local_clock)
|
||||||
|
hour, minute = self.server_time.get_values()
|
||||||
|
self.picker = TimePicker(hour, minute)
|
||||||
|
|
||||||
|
self.local_timelabel = Gtk.Label(label=local_clock_time)
|
||||||
|
self.server_timelabel = Gtk.Label(label=stime.time, halign=Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
h0 = BoldLabel(calc.heading_local_time)
|
||||||
|
h1 = BoldLabel(calc.heading_server_time)
|
||||||
|
h2 = BoldLabel(calc.heading_target_time)
|
||||||
|
|
||||||
|
self.icon_indicator = Icon(DAY_ICON)
|
||||||
|
self.update_server_icon()
|
||||||
|
|
||||||
|
server_time_box = HBox(10)
|
||||||
|
server_time_box.set_halign(Gtk.Align.CENTER)
|
||||||
|
server_time_box.extend([self.server_timelabel, self.icon_indicator])
|
||||||
|
|
||||||
|
flowbox = Gtk.FlowBox(
|
||||||
|
row_spacing=10,
|
||||||
|
column_spacing=50,
|
||||||
|
min_children_per_line=3,
|
||||||
|
max_children_per_line=3,
|
||||||
|
margin=15,
|
||||||
|
halign=Gtk.Align.CENTER,
|
||||||
|
selection_mode=Gtk.SelectionMode.NONE,
|
||||||
|
)
|
||||||
|
flowbox.add(h0)
|
||||||
|
flowbox.add(h1)
|
||||||
|
flowbox.add(h2)
|
||||||
|
flowbox.add(self.local_timelabel)
|
||||||
|
flowbox.add(server_time_box)
|
||||||
|
flowbox.add(self.picker)
|
||||||
|
|
||||||
|
self.add(flowbox)
|
||||||
|
|
||||||
|
def update_server_icon(self) -> None:
|
||||||
|
icon = DAY_ICON if self.server_time.is_day() else NIGHT_ICON
|
||||||
|
self.icon_indicator.set_icon_name(icon)
|
||||||
|
|
||||||
|
def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None:
|
||||||
|
t = time_now.time().strftime("%H:%M")
|
||||||
|
self.server_timelabel.set_text(t)
|
||||||
|
self.update_server_icon()
|
||||||
|
|
||||||
|
def _on_local_clock_increment(
|
||||||
|
self, emitter: Emitter, time_now: datetime, elapsed: timedelta
|
||||||
|
) -> None:
|
||||||
|
t = time_now.time().strftime(EXTENDED_TIME_FORMAT)
|
||||||
|
self.local_timelabel.set_text(t)
|
||||||
|
|
||||||
|
def _on_calculate_button_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
if (self.picker.get_values()) == (self.server_time.get_values()):
|
||||||
|
total_time = timedelta(seconds=0)
|
||||||
|
self.emitter.emit("target_time_reset", total_time)
|
||||||
|
else:
|
||||||
|
picker_time = self.picker.get_time()
|
||||||
|
total_time = self.server_time.calc_delta(picker_time)
|
||||||
|
self.emitter.emit("target_time_changed", total_time)
|
||||||
|
|
||||||
|
|
||||||
|
class ServerTimeCalculator(Gtk.ScrolledWindow):
|
||||||
|
def __init__(self, stime: Time) -> None:
|
||||||
|
super().__init__(propagate_natural_width=True)
|
||||||
|
|
||||||
|
self.emitter = Emitter()
|
||||||
|
outerbox = Gtk.Box(
|
||||||
|
halign=Gtk.Align.CENTER,
|
||||||
|
valign=Gtk.Align.START,
|
||||||
|
spacing=20,
|
||||||
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
|
)
|
||||||
|
time_picker_frame = TimePickerFrame(self.emitter, stime)
|
||||||
|
adjusted_time_frame = AdjustedTimeFrame(self.emitter)
|
||||||
|
disclaimer = Gtk.Label(
|
||||||
|
label=calc.disclaimer,
|
||||||
|
justify=Gtk.Justification.CENTER,
|
||||||
|
)
|
||||||
|
for el in time_picker_frame, adjusted_time_frame, disclaimer:
|
||||||
|
outerbox.add(el)
|
||||||
|
self.add(outerbox)
|
||||||
@@ -3,6 +3,7 @@ import sys
|
|||||||
|
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from dzgui.util.strings import dialog_error, dialog_header
|
from dzgui.util.strings import dialog_error, dialog_header
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -10,21 +11,26 @@ gi.require_version("Gtk", "3.0")
|
|||||||
from gi.repository import Gtk # noqa E402
|
from gi.repository import Gtk # noqa E402
|
||||||
|
|
||||||
|
|
||||||
class EarlyAlertDialog(Gtk.MessageDialog):
|
class AbortDialog(ColorAwareApp, Gtk.MessageDialog): # type: ignore
|
||||||
def __init__(self, string: str) -> None:
|
def __init__(self, string: str, buttons: Gtk.ButtonsType) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
title=dialog_header,
|
title=dialog_header,
|
||||||
text=dialog_error,
|
text=dialog_error,
|
||||||
transient_for=None,
|
transient_for=None,
|
||||||
buttons=Gtk.ButtonsType.OK,
|
buttons=buttons,
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = textwrap.fill(string, 50)
|
msg = textwrap.fill(string, 50)
|
||||||
self.format_secondary_text(msg)
|
self.format_secondary_text(msg)
|
||||||
|
|
||||||
|
ma = self.get_message_area()
|
||||||
|
label = ma.get_children()[1] # type: ignore
|
||||||
|
label.set_justify(Gtk.Justification.CENTER)
|
||||||
|
|
||||||
aa = self.get_action_area()
|
aa = self.get_action_area()
|
||||||
aa.set_margin_bottom(20)
|
aa.set_margin_bottom(20)
|
||||||
# self.action_area.set_margin_bottom(20)
|
aa.set_layout(Gtk.ButtonBoxStyle.CENTER) # type: ignore
|
||||||
|
|
||||||
self.outer = self.get_content_area()
|
self.outer = self.get_content_area()
|
||||||
self.outer.set_margin_start(30)
|
self.outer.set_margin_start(30)
|
||||||
self.outer.set_margin_end(30)
|
self.outer.set_margin_end(30)
|
||||||
@@ -32,26 +38,30 @@ class EarlyAlertDialog(Gtk.MessageDialog):
|
|||||||
self.set_default_size(250, 100)
|
self.set_default_size(250, 100)
|
||||||
|
|
||||||
abort = self.get_widget_for_response(Gtk.ResponseType.OK)
|
abort = self.get_widget_for_response(Gtk.ResponseType.OK)
|
||||||
|
ignore = self.get_widget_for_response(Gtk.ResponseType.CANCEL)
|
||||||
if abort is not None and hasattr(abort, "set_label"):
|
if abort is not None and hasattr(abort, "set_label"):
|
||||||
abort.set_label("Exit")
|
abort.set_label("Exit")
|
||||||
|
if ignore is not None and hasattr(ignore, "set_label"):
|
||||||
|
ignore.set_label("Ignore")
|
||||||
|
|
||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
|
|
||||||
self.run()
|
self.run()
|
||||||
self.destroy()
|
self.destroy()
|
||||||
|
|
||||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||||
match response:
|
match response:
|
||||||
case Gtk.ResponseType.OK:
|
case Gtk.ResponseType.OK | Gtk.ResponseType.DELETE_EVENT:
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
case Gtk.ResponseType.CANCEL:
|
case Gtk.ResponseType.CANCEL:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
class EarlyIgnoreDialog(EarlyAlertDialog):
|
class EarlyAlertDialog(AbortDialog):
|
||||||
def __init__(self, string: str) -> None:
|
def __init__(self, string: str) -> None:
|
||||||
super().__init__(string=string)
|
super().__init__(string=string, buttons=Gtk.ButtonsType.OK)
|
||||||
|
|
||||||
# TODO: reverse order
|
|
||||||
self.add_button("Ignore", Gtk.ResponseType.CANCEL)
|
|
||||||
|
|
||||||
# TODO: if exit, sys.exit(1), else pass
|
class EarlyIgnoreDialog(AbortDialog):
|
||||||
|
def __init__(self, string: str) -> None:
|
||||||
|
super().__init__(string=string, buttons=Gtk.ButtonsType.OK_CANCEL)
|
||||||
@@ -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)
|
||||||
|
|||||||
+152
-51
@@ -1,6 +1,7 @@
|
|||||||
from typing import Literal, Self, TYPE_CHECKING
|
from typing import Literal, Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL
|
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL
|
||||||
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util import strings
|
from dzgui.util import strings
|
||||||
from dzgui.views.components.buttons import ClipboardButton
|
from dzgui.views.components.buttons import ClipboardButton
|
||||||
|
|
||||||
@@ -107,23 +108,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 +139,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)
|
||||||
|
|
||||||
@@ -153,6 +162,7 @@ class QuitDialog(GenericDialog):
|
|||||||
self.add_button(strings.exit_app, Gtk.ResponseType.OK)
|
self.add_button(strings.exit_app, Gtk.ResponseType.OK)
|
||||||
|
|
||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
|
# TODO: superfluous with the above
|
||||||
self.connect("delete-event", self._on_response)
|
self.connect("delete-event", self._on_response)
|
||||||
|
|
||||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||||
@@ -160,58 +170,30 @@ class QuitDialog(GenericDialog):
|
|||||||
self.controller.save_res_and_quit()
|
self.controller.save_res_and_quit()
|
||||||
|
|
||||||
|
|
||||||
class ExceptionDialog(GenericDialog):
|
class TextBufferDialog(GenericDialog):
|
||||||
"""
|
def __init__(
|
||||||
Error dialog with rich traceback.
|
self,
|
||||||
Usage:
|
controller: "Controller",
|
||||||
try:
|
mtype: Gtk.MessageType,
|
||||||
foo()
|
heading: str,
|
||||||
except Exception:
|
secondary: str,
|
||||||
trace = traceback.format_exc()
|
text: str,
|
||||||
dialog = ExceptionDialog(Controller, trace)
|
):
|
||||||
dialog.run()
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, controller: "Controller", trace: str):
|
|
||||||
super().__init__(
|
super().__init__(
|
||||||
controller=controller,
|
controller=controller,
|
||||||
text=strings.error_heading,
|
text=heading,
|
||||||
mtype=Gtk.MessageType.ERROR,
|
mtype=mtype,
|
||||||
buttons=Gtk.ButtonsType.NONE,
|
buttons=Gtk.ButtonsType.NONE,
|
||||||
secondary=strings.something_wrong,
|
secondary=secondary,
|
||||||
)
|
)
|
||||||
|
self.text = text
|
||||||
self.trace = trace
|
copy_button = ClipboardButton(controller, self.get_text)
|
||||||
# NOTE: box expands to end of content area
|
|
||||||
scrollable = Gtk.ScrolledWindow(
|
|
||||||
propagate_natural_height=True, max_content_height=500
|
|
||||||
)
|
|
||||||
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
|
||||||
# TODO: wrap/truncate long messages
|
|
||||||
textview = Gtk.TextView(
|
|
||||||
wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10
|
|
||||||
)
|
|
||||||
textview.set_buffer(Gtk.TextBuffer(text=self.trace))
|
|
||||||
box.pack_start(textview, EXPAND, FILL, 10)
|
|
||||||
scrollable.add(box)
|
|
||||||
|
|
||||||
content = self.get_content_area()
|
|
||||||
content.set_spacing(0)
|
|
||||||
# TODO: padding around top of content area when traceback is long
|
|
||||||
content.add(scrollable)
|
|
||||||
|
|
||||||
copy_button = ClipboardButton(controller, self.get_trace)
|
|
||||||
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
||||||
self.add_button("OK", Gtk.ResponseType.OK)
|
self.add_button("OK", Gtk.ResponseType.OK)
|
||||||
|
|
||||||
self.show_all()
|
|
||||||
self.ok = self.get_widget_for_response(Gtk.ResponseType.OK)
|
|
||||||
if self.ok is not None:
|
|
||||||
self.ok.grab_focus()
|
|
||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
|
|
||||||
def get_trace(self) -> str:
|
def get_text(self) -> str:
|
||||||
return self.trace
|
return self.text
|
||||||
|
|
||||||
def _on_response(
|
def _on_response(
|
||||||
self, dialog: Self, response: Gtk.ResponseType
|
self, dialog: Self, response: Gtk.ResponseType
|
||||||
@@ -227,3 +209,122 @@ class ExceptionDialog(GenericDialog):
|
|||||||
return None
|
return None
|
||||||
case _:
|
case _:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class DebugDialog(TextBufferDialog):
|
||||||
|
def __init__(self, controller: "Controller", debug: str):
|
||||||
|
super().__init__(
|
||||||
|
controller=controller,
|
||||||
|
mtype=Gtk.MessageType.INFO,
|
||||||
|
heading=dialogs.debug_heading,
|
||||||
|
secondary=dialogs.debug_secondary,
|
||||||
|
text=debug,
|
||||||
|
)
|
||||||
|
scrollable = Gtk.ScrolledWindow(
|
||||||
|
propagate_natural_height=False,
|
||||||
|
min_content_height=200,
|
||||||
|
max_content_height=200,
|
||||||
|
margin_bottom=10,
|
||||||
|
)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.CHAR, editable=False, left_margin=10, right_margin=10
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=debug))
|
||||||
|
scrollable.add(textview)
|
||||||
|
content = self.get_content_area()
|
||||||
|
content.set_spacing(0)
|
||||||
|
content.add(scrollable)
|
||||||
|
|
||||||
|
|
||||||
|
class ExceptionDialog(TextBufferDialog):
|
||||||
|
"""
|
||||||
|
Error dialog with rich traceback.
|
||||||
|
Usage:
|
||||||
|
try:
|
||||||
|
foo()
|
||||||
|
except Exception:
|
||||||
|
trace = traceback.format_exc()
|
||||||
|
dialog = ExceptionDialog(Controller, trace)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run()
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, controller: "Controller", trace: str):
|
||||||
|
super().__init__(
|
||||||
|
controller=controller,
|
||||||
|
heading=strings.error_heading,
|
||||||
|
secondary=strings.something_wrong,
|
||||||
|
text=trace,
|
||||||
|
mtype=Gtk.MessageType.ERROR,
|
||||||
|
)
|
||||||
|
|
||||||
|
# NOTE: box expands to end of content area
|
||||||
|
scrollable = Gtk.ScrolledWindow(
|
||||||
|
propagate_natural_height=True, max_content_height=500
|
||||||
|
)
|
||||||
|
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.WORD,
|
||||||
|
editable=False,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
top_margin=15,
|
||||||
|
bottom_margin=10,
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=trace))
|
||||||
|
box.pack_start(textview, EXPAND, FILL, 0)
|
||||||
|
|
||||||
|
self.error_details = Gtk.ScrolledWindow(
|
||||||
|
overlay_scrolling=False,
|
||||||
|
max_content_height=150,
|
||||||
|
propagate_natural_height=False,
|
||||||
|
)
|
||||||
|
details_box = Gtk.Box(
|
||||||
|
hexpand=True,
|
||||||
|
vexpand=True,
|
||||||
|
margin_right=5,
|
||||||
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.details_buffer = Gtk.TextBuffer()
|
||||||
|
details_textview = Gtk.TextView(
|
||||||
|
wrap_mode=Gtk.WrapMode.WORD_CHAR,
|
||||||
|
editable=False,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
top_margin=15,
|
||||||
|
)
|
||||||
|
|
||||||
|
details_textview.set_buffer(self.details_buffer)
|
||||||
|
details_box.pack_start(details_textview, EXPAND, FILL, 10)
|
||||||
|
self.error_details.add(details_box)
|
||||||
|
|
||||||
|
self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15)
|
||||||
|
self.error_notebook.append_page(box, Gtk.Label(label="Error"))
|
||||||
|
self.error_notebook.append_page(self.error_details, Gtk.Label(label="Details"))
|
||||||
|
self.error_notebook.connect("switch-page", self._on_page_changed)
|
||||||
|
scrollable.add(self.error_notebook)
|
||||||
|
|
||||||
|
content = self.get_content_area()
|
||||||
|
content.set_spacing(0)
|
||||||
|
content.add(scrollable)
|
||||||
|
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_map(self, dialog: Self) -> None:
|
||||||
|
button = self.get_widget_for_response(Gtk.ResponseType.OK)
|
||||||
|
if button is None:
|
||||||
|
return
|
||||||
|
button.grab_focus()
|
||||||
|
|
||||||
|
def _on_page_changed(
|
||||||
|
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
|
||||||
|
) -> None:
|
||||||
|
if child == self.error_details:
|
||||||
|
GLib.idle_add(self.error_details.set_propagate_natural_height, True)
|
||||||
|
else:
|
||||||
|
GLib.idle_add(self.error_details.set_propagate_natural_height, False)
|
||||||
|
|
||||||
|
def set_details_buffer(self, text: str) -> None:
|
||||||
|
self.details_buffer.set_text(text)
|
||||||
|
self.error_notebook.set_show_tabs(True)
|
||||||
@@ -1,19 +1,21 @@
|
|||||||
|
import gi
|
||||||
|
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 CALCULATOR
|
||||||
from dzgui.const.enum import ContextMenuGroup
|
from dzgui.const.enum import ContextMenuGroup
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
from dzgui.util import css
|
from dzgui.util import css
|
||||||
from dzgui.util import strings
|
|
||||||
from dzgui.util.format import format_hyperlinks, format_server_mods
|
from dzgui.util.format import format_hyperlinks, format_server_mods
|
||||||
from dzgui.strings import server_mods
|
from dzgui.strings import server_mods, dialogs
|
||||||
|
from dzgui.views.components.buttons import IconTextButton
|
||||||
|
from dzgui.views.components.box import VBox
|
||||||
|
from dzgui.views.dialogs.calc import Time, ServerTimeCalculator
|
||||||
from dzgui.views.dialogs.generic import GenericDialog
|
from dzgui.views.dialogs.generic import GenericDialog
|
||||||
from dzgui.views.trees.tree_base import TreeView
|
from dzgui.views.trees.tree_base import TreeView
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa
|
from gi.repository import Gtk, Gdk, Pango # noqa
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.api.servers import Details
|
from dzgui.api.servers import Details
|
||||||
@@ -45,12 +47,19 @@ class ServerDialog(GenericDialog):
|
|||||||
self.connect("response", self._on_response)
|
self.connect("response", self._on_response)
|
||||||
self.view.connect("key-press-event", self._on_keypress)
|
self.view.connect("key-press-event", self._on_keypress)
|
||||||
|
|
||||||
self.scrollable_tree = Gtk.ScrolledWindow()
|
self.scrollable_tree = Gtk.ScrolledWindow(vexpand=True)
|
||||||
self.scrollable_tree.add(self.view)
|
self.scrollable_tree.add(self.view)
|
||||||
self.scrollable_tree.set_size_request(700, 400)
|
self.scrollable_tree.set_size_request(700, 500)
|
||||||
|
|
||||||
|
self.stack = Gtk.Stack()
|
||||||
|
self.content_box = VBox(10)
|
||||||
|
self.content_box.set_valign(Gtk.Align.FILL)
|
||||||
|
self.content_box.add(self.scrollable_tree)
|
||||||
|
|
||||||
|
self.stack.add_named(self.content_box, "METADATA")
|
||||||
|
|
||||||
self.content = self.get_content_area()
|
self.content = self.get_content_area()
|
||||||
self.content.pack_start(self.scrollable_tree, EXPAND, FILL, 0)
|
self.content.pack_start(self.stack, expand=True, fill=True, padding=0)
|
||||||
|
|
||||||
def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> bool:
|
def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> bool:
|
||||||
# NOTE: ESC normally unfocuses treeview instead of destroying dialog
|
# NOTE: ESC normally unfocuses treeview instead of destroying dialog
|
||||||
@@ -65,8 +74,7 @@ class ServerDialog(GenericDialog):
|
|||||||
|
|
||||||
class ServerDetailsDialog(ServerDialog):
|
class ServerDetailsDialog(ServerDialog):
|
||||||
def __init__(self, controller: "Controller", details: "Details"):
|
def __init__(self, controller: "Controller", details: "Details"):
|
||||||
name = controller.get_server_name()
|
super().__init__(controller, dialogs.server_details, details.name, menu=None)
|
||||||
super().__init__(controller, strings.server_details, name, menu=None)
|
|
||||||
|
|
||||||
self.store = Gtk.ListStore(str, str, Pango.Weight)
|
self.store = Gtk.ListStore(str, str, Pango.Weight)
|
||||||
self.view.connect("row-activated", self._on_row_activated)
|
self.view.connect("row-activated", self._on_row_activated)
|
||||||
@@ -83,14 +91,9 @@ class ServerDetailsDialog(ServerDialog):
|
|||||||
column.set_sort_column_id(i)
|
column.set_sort_column_id(i)
|
||||||
column.set_expand(True)
|
column.set_expand(True)
|
||||||
|
|
||||||
# NOTE: constrain tree area to logical height of metadata
|
scrollable_message = Gtk.ScrolledWindow(vexpand=True)
|
||||||
# leaving room for server message
|
desc = Gtk.Label(label=dialogs.server_message, valign=Gtk.Align.START)
|
||||||
self.scrollable_tree.set_size_request(700, 150)
|
css.add_class(desc, "server-subheading")
|
||||||
|
|
||||||
# TODO: make "Server message" text boldface
|
|
||||||
scrollable_message = Gtk.ScrolledWindow()
|
|
||||||
desc = Gtk.Label(label=strings.server_message, valign=Gtk.Align.START)
|
|
||||||
css.add_class(desc, "details-heading")
|
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
|
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
|
||||||
self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
|
self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
|
||||||
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
|
||||||
@@ -98,21 +101,41 @@ class ServerDetailsDialog(ServerDialog):
|
|||||||
for el in desc, sep, self.description:
|
for el in desc, sep, self.description:
|
||||||
box.add(el)
|
box.add(el)
|
||||||
scrollable_message.add(box)
|
scrollable_message.add(box)
|
||||||
|
self.content_box.add(scrollable_message)
|
||||||
self.pack(scrollable_message)
|
|
||||||
|
|
||||||
if details.data is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
for row in details.data:
|
for row in details.data:
|
||||||
self.store.append(row + [Pango.Weight.BOLD])
|
self.store.append(row + [Pango.Weight.BOLD])
|
||||||
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))
|
||||||
|
|
||||||
|
calc_button = IconTextButton(CALCULATOR, dialogs.toggle_calc)
|
||||||
|
calc_button.connect("clicked", self._on_calc_pressed)
|
||||||
|
calc_button.set_halign(Gtk.Align.END)
|
||||||
|
calc_button.set_valign(Gtk.Align.START)
|
||||||
|
self.content.pack_start(calc_button, False, False, 0)
|
||||||
|
self.content.reorder_child(calc_button, 2)
|
||||||
|
|
||||||
|
self.scrollable_tree.set_size_request(700, 250)
|
||||||
|
self.calc = ServerTimeCalculator(
|
||||||
|
Time(details.gametime, details.day_accel, details.night_accel)
|
||||||
|
)
|
||||||
|
self.stack.add_named(self.calc, "CALCULATOR")
|
||||||
|
|
||||||
|
self.connect("map", self._on_map)
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
|
def _on_map(self, dialog: Self) -> None:
|
||||||
|
if widget := self.get_widget_for_response(Gtk.ResponseType.OK):
|
||||||
|
widget.grab_focus()
|
||||||
|
|
||||||
|
def _on_calc_pressed(self, button: Gtk.Button) -> None:
|
||||||
|
if self.stack.get_visible_child_name() == "METADATA":
|
||||||
|
self.stack.set_visible_child_name("CALCULATOR")
|
||||||
|
else:
|
||||||
|
self.stack.set_visible_child_name("METADATA")
|
||||||
|
|
||||||
def _on_row_activated(
|
def _on_row_activated(
|
||||||
self,
|
self,
|
||||||
treeview: Gtk.TreeView,
|
treeview: Gtk.TreeView,
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Self
|
||||||
|
|
||||||
|
from dzgui.api.shortcuts import Shortcuts
|
||||||
|
from dzgui.const.constants import APP_NAME
|
||||||
|
from dzgui.strings import uninstall
|
||||||
|
from dzgui.util._json import read_json
|
||||||
|
from dzgui.util.css import load_css
|
||||||
|
from dzgui.util.format import format_exception
|
||||||
|
from dzgui.views.components.box import HBox
|
||||||
|
from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel
|
||||||
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk, GLib, Pango # noqa E402
|
||||||
|
|
||||||
|
|
||||||
|
class Assistant(Gtk.Assistant):
|
||||||
|
def __init__(self, is_deck: bool, paths: dict[str, str]):
|
||||||
|
super().__init__()
|
||||||
|
if is_deck:
|
||||||
|
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||||
|
self.set_default_size(1085, 670)
|
||||||
|
else:
|
||||||
|
self.set_default_size(1500, 900)
|
||||||
|
|
||||||
|
self.set_forward_page_func(self._advance_page)
|
||||||
|
|
||||||
|
pyapp = os.getenv("PYAPP")
|
||||||
|
|
||||||
|
self.page1 = UninstallPage(paths, pyapp)
|
||||||
|
self.page2 = CompletionPage(pyapp)
|
||||||
|
|
||||||
|
self.append_page(self.page1)
|
||||||
|
self.set_page_type(self.page1, Gtk.AssistantPageType.INTRO)
|
||||||
|
self.set_page_title(self.page1, uninstall.pg1_title)
|
||||||
|
self.set_page_complete(self.page1, True)
|
||||||
|
|
||||||
|
self.append_page(self.page2)
|
||||||
|
self.set_page_title(self.page2, uninstall.pg2_title)
|
||||||
|
self.set_page_type(self.page2, Gtk.AssistantPageType.SUMMARY)
|
||||||
|
|
||||||
|
self.connect("cancel", self.destroy_and_quit)
|
||||||
|
self.connect("close", self.destroy_and_quit)
|
||||||
|
self.show_all()
|
||||||
|
load_css()
|
||||||
|
|
||||||
|
def _advance_page(self, index: int) -> int:
|
||||||
|
cur_page = self.get_nth_page(index)
|
||||||
|
if cur_page == self.page1:
|
||||||
|
self.page1.uninstall()
|
||||||
|
return index + 1
|
||||||
|
|
||||||
|
def destroy_and_quit(self, widget: Self) -> None:
|
||||||
|
self.destroy()
|
||||||
|
Gtk.main_quit()
|
||||||
|
|
||||||
|
|
||||||
|
class Window(Gtk.Window):
|
||||||
|
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||||
|
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||||
|
self.assistant = Assistant(is_deck, paths)
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionPage(ScrolledWizardPage):
|
||||||
|
def __init__(self, pyapp: str | None):
|
||||||
|
super().__init__(heading=uninstall.pg2_heading, description=uninstall.pg2_blurb)
|
||||||
|
|
||||||
|
text = (
|
||||||
|
uninstall.standalone_uninstall
|
||||||
|
if pyapp is not None
|
||||||
|
else uninstall.system_uninstall
|
||||||
|
)
|
||||||
|
|
||||||
|
label = Gtk.Label(
|
||||||
|
label=text,
|
||||||
|
wrap_mode=Pango.WrapMode.WORD,
|
||||||
|
margin=10,
|
||||||
|
max_width_chars=80,
|
||||||
|
justify=Gtk.Justification.CENTER,
|
||||||
|
)
|
||||||
|
frame = Gtk.Frame()
|
||||||
|
frame.add(label)
|
||||||
|
self.add_start(frame)
|
||||||
|
|
||||||
|
|
||||||
|
class FileLabel(HBox):
|
||||||
|
def __init__(self, path: Path) -> None:
|
||||||
|
super().__init__(spacing=10)
|
||||||
|
|
||||||
|
label = Gtk.Label(label=uninstall.path_remove_prefix, halign=Gtk.Align.START)
|
||||||
|
textview = Gtk.TextView(
|
||||||
|
editable=False,
|
||||||
|
halign=Gtk.Align.START,
|
||||||
|
left_margin=10,
|
||||||
|
right_margin=10,
|
||||||
|
)
|
||||||
|
textview.set_buffer(Gtk.TextBuffer(text=str(path)))
|
||||||
|
self.extend([label, textview])
|
||||||
|
|
||||||
|
|
||||||
|
class CheckboxWithPath(CheckboxWithLabel):
|
||||||
|
def __init__(
|
||||||
|
self, label: str, details: str, path: Path, active: bool = True
|
||||||
|
) -> None:
|
||||||
|
super().__init__(text=label, blurb_text=details)
|
||||||
|
|
||||||
|
self.conf_path = path
|
||||||
|
self.set_active(active)
|
||||||
|
fl = FileLabel(path)
|
||||||
|
self.indent_below(fl)
|
||||||
|
|
||||||
|
def get_conf_path(self) -> Path:
|
||||||
|
return self.conf_path
|
||||||
|
|
||||||
|
|
||||||
|
class UninstallPage(ScrolledWizardPage):
|
||||||
|
def __init__(self, paths: dict[str, str], pyapp: str | None):
|
||||||
|
super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb)
|
||||||
|
|
||||||
|
self.pyapp = pyapp
|
||||||
|
|
||||||
|
config = Path(paths["XDG_CONFIG_HOME"])
|
||||||
|
state = Path(paths["XDG_STATE_HOME"])
|
||||||
|
self.share = Path(paths["XDG_DATA_HOME"])
|
||||||
|
shortcut = self.share.parent.joinpath("applications/dzgui.desktop")
|
||||||
|
desktop = config.parent.parent.joinpath("Desktop/dzgui.desktop")
|
||||||
|
self.steam_path = self.parse_steam_path(config, self.share)
|
||||||
|
|
||||||
|
self.boxes: list[CheckboxWithPath] = []
|
||||||
|
self.checks_area = Gtk.Box(
|
||||||
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
|
halign=Gtk.Align.CENTER,
|
||||||
|
spacing=20,
|
||||||
|
margin_top=20,
|
||||||
|
margin_bottom=20,
|
||||||
|
)
|
||||||
|
self.config_box = CheckboxWithPath(
|
||||||
|
uninstall.config_label, uninstall.config_details, config, active=False
|
||||||
|
)
|
||||||
|
self.desktop_box = CheckboxWithPath(
|
||||||
|
uninstall.desktop_label, uninstall.desktop_details, desktop
|
||||||
|
)
|
||||||
|
self.start_menu_box = CheckboxWithPath(
|
||||||
|
uninstall.start_menu_label, uninstall.start_menu_details, shortcut
|
||||||
|
)
|
||||||
|
self.state_box = CheckboxWithPath(
|
||||||
|
uninstall.state_label, uninstall.state_details, state
|
||||||
|
)
|
||||||
|
self.steam_box = CheckboxWithLabel(
|
||||||
|
uninstall.steam_shortcut, uninstall.steam_details
|
||||||
|
)
|
||||||
|
|
||||||
|
for box in (
|
||||||
|
self.config_box,
|
||||||
|
self.state_box,
|
||||||
|
self.desktop_box,
|
||||||
|
self.start_menu_box,
|
||||||
|
):
|
||||||
|
self.checks_area.add(box)
|
||||||
|
self.boxes.append(box)
|
||||||
|
|
||||||
|
self.checks_area.add(self.steam_box)
|
||||||
|
|
||||||
|
self.add_start(self.checks_area)
|
||||||
|
self.show_all()
|
||||||
|
|
||||||
|
def parse_steam_path(self, config: Path, steam: Path) -> Path | None:
|
||||||
|
try:
|
||||||
|
file = config.joinpath("config.json")
|
||||||
|
conf = read_json(file)
|
||||||
|
steam = conf["default_steam_path"]
|
||||||
|
self.steam_path = Path(steam)
|
||||||
|
return Path(steam)
|
||||||
|
except Exception as e:
|
||||||
|
print(format_exception(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def wipe_conf_file(self, box: CheckboxWithPath) -> None:
|
||||||
|
if box.get_active():
|
||||||
|
path = box.get_conf_path()
|
||||||
|
try:
|
||||||
|
if path.is_dir():
|
||||||
|
shutil.rmtree(path)
|
||||||
|
else:
|
||||||
|
path.unlink()
|
||||||
|
print(f"Deleted '{path}'")
|
||||||
|
except Exception as e:
|
||||||
|
print(format_exception(e))
|
||||||
|
|
||||||
|
def wipe_steam_shortcut(self) -> None:
|
||||||
|
if self.steam_path is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
shortcuts = Shortcuts(self.steam_path)
|
||||||
|
shortcuts.delete_shortcut(self.share)
|
||||||
|
shutil.rmtree(self.share)
|
||||||
|
except Exception as e:
|
||||||
|
print(format_exception(e))
|
||||||
|
|
||||||
|
def wipe_pyapp(self) -> None:
|
||||||
|
if self.pyapp is None:
|
||||||
|
return
|
||||||
|
subprocess.run([self.pyapp, "self", "remove"])
|
||||||
|
|
||||||
|
def uninstall(self) -> None:
|
||||||
|
for box in self.boxes:
|
||||||
|
self.wipe_conf_file(box)
|
||||||
|
if self.steam_box.get_active():
|
||||||
|
self.wipe_steam_shortcut()
|
||||||
|
self.wipe_pyapp()
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class UninstallWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||||
|
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||||
|
super().__init__()
|
||||||
|
config = Path(paths["XDG_CONFIG_HOME"])
|
||||||
|
if not config.is_dir() or not config.joinpath("config.json").is_file():
|
||||||
|
print(uninstall.not_installed)
|
||||||
|
return
|
||||||
|
GLib.set_prgname(APP_NAME)
|
||||||
|
self.win = Window(is_deck, paths)
|
||||||
|
Gtk.main()
|
||||||
+184
-128
@@ -1,4 +1,3 @@
|
|||||||
import os
|
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
@@ -6,29 +5,31 @@ from importlib import resources
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable, Self, TYPE_CHECKING
|
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.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,
|
||||||
APP_NAME_LOWER,
|
APP_NAME_LOWER,
|
||||||
HERO_PATH,
|
HERO_PATH,
|
||||||
|
FLATPAK_ROOT_PATH,
|
||||||
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 import freedesktop
|
||||||
from dzgui.config.query import lookup
|
from dzgui.init.proc import is_sandboxed, path_to_steam_cmd
|
||||||
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
|
||||||
from dzgui.util._json import write_json
|
from dzgui.util._json import write_json
|
||||||
|
from dzgui.util.flatpak import apply_overrides
|
||||||
from dzgui.util.open_links import open_link_by_url
|
from dzgui.util.open_links import open_link_by_url
|
||||||
from dzgui.util.css import add_class, load_css
|
from dzgui.util.css import add_class, load_css
|
||||||
from dzgui.views.components.buttons import WebButton
|
from dzgui.views.components.buttons import WebButton
|
||||||
from dzgui.views.components.entry import APIEntry
|
from dzgui.views.components.entry import APIEntry
|
||||||
from dzgui.views.components.misc import ClientCombo
|
from dzgui.views.mixins.colorscheme import ColorAwareApp
|
||||||
|
from dzgui.views.trees.tree_simple import SimpleTree
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -44,12 +45,19 @@ class PageNum(Enum):
|
|||||||
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
|
||||||
SHORTCUTS = 7
|
FLATPAK = 7
|
||||||
FINAL = 8
|
FINAL = 8
|
||||||
|
|
||||||
|
|
||||||
|
class OptionalPageMixin:
|
||||||
|
"""Marks optional pages as advanceable"""
|
||||||
|
|
||||||
|
def _on_map(self, page: "EnumeratedWizardPage") -> 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)
|
||||||
@@ -66,10 +74,9 @@ class Progress(Gtk.ProgressBar):
|
|||||||
|
|
||||||
|
|
||||||
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||||
def __init__(self, enum: PageNum, heading: str, description: str):
|
def __init__(self, heading: str, description: str):
|
||||||
super().__init__(overlay_scrolling=False)
|
super().__init__(overlay_scrolling=False)
|
||||||
|
|
||||||
self.enum = enum
|
|
||||||
self.page_type: Gtk.AssistantPageType
|
self.page_type: Gtk.AssistantPageType
|
||||||
self.title = heading
|
self.title = heading
|
||||||
self.heading = Heading(heading)
|
self.heading = Heading(heading)
|
||||||
@@ -82,7 +89,6 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
height=600,
|
height=600,
|
||||||
preserve_aspect_ratio=True,
|
preserve_aspect_ratio=True,
|
||||||
)
|
)
|
||||||
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
|
||||||
|
|
||||||
self.box = Gtk.Box(
|
self.box = Gtk.Box(
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
@@ -91,21 +97,13 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
margin_top=50,
|
margin_top=50,
|
||||||
spacing=20,
|
spacing=20,
|
||||||
)
|
)
|
||||||
self.add(self.box)
|
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
||||||
self.prog = Progress()
|
|
||||||
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
|
||||||
self.box.pack_start(image, expand=False, fill=True, padding=0)
|
self.box.pack_start(image, expand=False, fill=True, padding=0)
|
||||||
|
|
||||||
|
self.add(self.box)
|
||||||
self.box.pack_start(self.heading, expand=False, fill=True, padding=0)
|
self.box.pack_start(self.heading, expand=False, fill=True, padding=0)
|
||||||
self.box.pack_start(self.description, expand=False, fill=True, padding=0)
|
self.box.pack_start(self.description, expand=False, fill=True, padding=0)
|
||||||
|
|
||||||
self.connect("map", self._on_map)
|
|
||||||
|
|
||||||
def get_enum(self) -> PageNum:
|
|
||||||
return self.enum
|
|
||||||
|
|
||||||
def get_progress_bar(self) -> Progress:
|
|
||||||
return self.prog
|
|
||||||
|
|
||||||
def get_page_type(self) -> Gtk.AssistantPageType:
|
def get_page_type(self) -> Gtk.AssistantPageType:
|
||||||
return self.page_type
|
return self.page_type
|
||||||
|
|
||||||
@@ -124,8 +122,20 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
|||||||
def get_box(self) -> Gtk.Box:
|
def get_box(self) -> Gtk.Box:
|
||||||
return self.box
|
return self.box
|
||||||
|
|
||||||
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
|
||||||
pass
|
class EnumeratedWizardPage(ScrolledWizardPage):
|
||||||
|
def __init__(self, enum: PageNum, heading: str, description: str) -> None:
|
||||||
|
super().__init__(heading=heading, description=description)
|
||||||
|
|
||||||
|
self.enum = enum
|
||||||
|
self.prog = Progress()
|
||||||
|
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
||||||
|
|
||||||
|
def get_enum(self) -> PageNum:
|
||||||
|
return self.enum
|
||||||
|
|
||||||
|
def get_progress_bar(self) -> Progress:
|
||||||
|
return self.prog
|
||||||
|
|
||||||
|
|
||||||
class NotificationFrame(Gtk.Frame):
|
class NotificationFrame(Gtk.Frame):
|
||||||
@@ -137,7 +147,7 @@ class NotificationFrame(Gtk.Frame):
|
|||||||
margin_start=50,
|
margin_start=50,
|
||||||
margin_end=50,
|
margin_end=50,
|
||||||
)
|
)
|
||||||
wrapped = textwrap.fill(label, width=80)
|
wrapped = textwrap.fill(label, width=60)
|
||||||
self.label = Gtk.Label(
|
self.label = Gtk.Label(
|
||||||
label=wrapped,
|
label=wrapped,
|
||||||
justify=Gtk.Justification.CENTER,
|
justify=Gtk.Justification.CENTER,
|
||||||
@@ -151,11 +161,11 @@ class NotificationFrame(Gtk.Frame):
|
|||||||
add_class(self, "error-frame")
|
add_class(self, "error-frame")
|
||||||
|
|
||||||
def set_text(self, text: str) -> None:
|
def set_text(self, text: str) -> None:
|
||||||
wrapped = textwrap.fill(text, width=80)
|
wrapped = textwrap.fill(text, width=60)
|
||||||
self.label.set_markup(wrapped)
|
self.label.set_markup(wrapped)
|
||||||
|
|
||||||
|
|
||||||
class APIValidationPage(ScrolledWizardPage):
|
class APIValidationPage(EnumeratedWizardPage):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
|
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -215,23 +225,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__(
|
||||||
@@ -249,16 +242,27 @@ class SteamValidationPage(APIValidationPage):
|
|||||||
self.thread_man.set_cleanup_func(cleanup)
|
self.thread_man.set_cleanup_func(cleanup)
|
||||||
|
|
||||||
|
|
||||||
class IntroductionPage(ScrolledWizardPage):
|
class IntroductionPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.INTRO,
|
enum=PageNum.INTRO,
|
||||||
heading=f"Welcome to {APP_NAME}!",
|
heading=wizard.welcome.format(APP_NAME),
|
||||||
description=wizard.blurb_intro,
|
description=wizard.blurb_intro,
|
||||||
)
|
)
|
||||||
self.page_type = Gtk.AssistantPageType.INTRO
|
self.page_type = Gtk.AssistantPageType.INTRO
|
||||||
self.set_title(wizard.title_intro)
|
self.set_title(wizard.title_intro)
|
||||||
|
|
||||||
|
self.error_box = NotificationFrame(wizard.error_sandbox, error=True)
|
||||||
|
self.add_start(self.error_box)
|
||||||
|
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_map(self, page: "EnumeratedWizardPage") -> None:
|
||||||
|
self.error_box.hide()
|
||||||
|
if is_sandboxed():
|
||||||
|
self.error_box.show()
|
||||||
|
EMITTER.emit("step_pending")
|
||||||
|
|
||||||
|
|
||||||
class Heading(Gtk.Label):
|
class Heading(Gtk.Label):
|
||||||
def __init__(self, label: str):
|
def __init__(self, label: str):
|
||||||
@@ -304,7 +308,7 @@ class RadioFrame(Gtk.Frame):
|
|||||||
return self.button
|
return self.button
|
||||||
|
|
||||||
|
|
||||||
class ConfigMigrationPage(ScrolledWizardPage):
|
class ConfigMigrationPage(EnumeratedWizardPage):
|
||||||
def __init__(self, config: Path) -> None:
|
def __init__(self, config: Path) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.HAS_CONFIG,
|
enum=PageNum.HAS_CONFIG,
|
||||||
@@ -313,6 +317,7 @@ class ConfigMigrationPage(ScrolledWizardPage):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.migrated = False
|
self.migrated = False
|
||||||
|
self.migrated_conf: dict[str, Any] = {}
|
||||||
self.config = config
|
self.config = config
|
||||||
self.page_type = Gtk.AssistantPageType.INTRO
|
self.page_type = Gtk.AssistantPageType.INTRO
|
||||||
|
|
||||||
@@ -352,8 +357,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)
|
self.migrated_conf = 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)
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -364,7 +368,7 @@ class ConfigMigrationPage(ScrolledWizardPage):
|
|||||||
EMITTER.emit("config", True)
|
EMITTER.emit("config", True)
|
||||||
|
|
||||||
|
|
||||||
class PreferencesPage(ScrolledWizardPage):
|
class PreferencesPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.USER_PREFS,
|
enum=PageNum.USER_PREFS,
|
||||||
@@ -395,26 +399,17 @@ class PreferencesPage(ScrolledWizardPage):
|
|||||||
self.dist_box.add(self.radio_km)
|
self.dist_box.add(self.radio_km)
|
||||||
self.dist_box.add(self.radio_miles)
|
self.dist_box.add(self.radio_miles)
|
||||||
|
|
||||||
client_label = Gtk.Label(label=wizard.label_client, halign=Gtk.Align.START)
|
|
||||||
self.client_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
|
||||||
self.client_combo = ClientCombo()
|
|
||||||
self.client_box.add(client_label)
|
|
||||||
self.client_box.add(self.client_combo)
|
|
||||||
|
|
||||||
outer_box = Gtk.Box(
|
outer_box = Gtk.Box(
|
||||||
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER, spacing=10
|
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER, spacing=10
|
||||||
)
|
)
|
||||||
for el in self.name_box, self.client_box, self.dist_box:
|
for el in self.name_box, self.dist_box:
|
||||||
outer_box.add(el)
|
outer_box.add(el)
|
||||||
self.add_start(outer_box)
|
self.add_start(outer_box)
|
||||||
|
|
||||||
def get_prefs(self) -> tuple[str, bool, str]:
|
def get_prefs(self) -> tuple[str, bool]:
|
||||||
name = self.name_entry.get_text().strip()
|
name = self.name_entry.get_text().strip()
|
||||||
use_miles = self.radio_miles.get_active()
|
use_miles = self.radio_miles.get_active()
|
||||||
model = self.client_combo.get_model()
|
return name, use_miles
|
||||||
ind = self.client_combo.get_active()
|
|
||||||
client = model[ind][1]
|
|
||||||
return name, use_miles, client
|
|
||||||
|
|
||||||
def _on_entry_changed(self, entry: Gtk.Entry) -> None:
|
def _on_entry_changed(self, entry: Gtk.Entry) -> None:
|
||||||
text = entry.get_text()
|
text = entry.get_text()
|
||||||
@@ -424,7 +419,43 @@ class PreferencesPage(ScrolledWizardPage):
|
|||||||
EMITTER.emit("step_complete")
|
EMITTER.emit("step_complete")
|
||||||
|
|
||||||
|
|
||||||
class CompletionPage(ScrolledWizardPage):
|
class FlatpakOverridePage(EnumeratedWizardPage):
|
||||||
|
def __init__(self, xdg_paths: dict[str, str]) -> None:
|
||||||
|
super().__init__(
|
||||||
|
enum=PageNum.FLATPAK,
|
||||||
|
heading=wizard.heading_flatpak,
|
||||||
|
description=wizard.blurb_flatpak,
|
||||||
|
)
|
||||||
|
self.xdg_paths = xdg_paths
|
||||||
|
self.page_type = Gtk.AssistantPageType.INTRO
|
||||||
|
|
||||||
|
store = Gtk.ListStore(str)
|
||||||
|
for path in xdg_paths.values():
|
||||||
|
store.append((str(path),))
|
||||||
|
tree = SimpleTree(store, wizard.tree_column)
|
||||||
|
tree.set_halign(Gtk.Align.CENTER)
|
||||||
|
|
||||||
|
self.authorize_button = Gtk.Button(
|
||||||
|
label=wizard.button_flatpak, halign=Gtk.Align.CENTER
|
||||||
|
)
|
||||||
|
self.authorize_button.connect("clicked", self._on_authorize)
|
||||||
|
|
||||||
|
self.notice = NotificationFrame(wizard.overrides_applied)
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
for el in tree, self.authorize_button, self.notice:
|
||||||
|
self.add_start(el)
|
||||||
|
|
||||||
|
def _on_map(self, page: "EnumeratedWizardPage") -> None:
|
||||||
|
self.notice.hide()
|
||||||
|
|
||||||
|
def _on_authorize(self, button: Gtk.Button) -> None:
|
||||||
|
apply_overrides(self.xdg_paths)
|
||||||
|
self.notice.show()
|
||||||
|
EMITTER.emit("step_complete")
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.FINAL,
|
enum=PageNum.FINAL,
|
||||||
@@ -437,7 +468,7 @@ class CompletionPage(ScrolledWizardPage):
|
|||||||
|
|
||||||
|
|
||||||
class Assistant(Gtk.Assistant):
|
class Assistant(Gtk.Assistant):
|
||||||
def __init__(self, is_deck: bool, XDG: "Xdg"):
|
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if is_deck:
|
if is_deck:
|
||||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||||
@@ -446,19 +477,18 @@ class Assistant(Gtk.Assistant):
|
|||||||
self.set_default_size(1500, 900)
|
self.set_default_size(1500, 900)
|
||||||
|
|
||||||
self.config_path = XDG.config
|
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.setup_complete = False
|
||||||
|
|
||||||
self.page1 = IntroductionPage()
|
self.page_intro = IntroductionPage()
|
||||||
self.page2 = ConfigMigrationPage(XDG.config)
|
self.page_migration = ConfigMigrationPage(XDG.config)
|
||||||
self.page3 = SteamPathPage()
|
self.page_paths = SteamPathPage()
|
||||||
self.page4 = SteamValidationPage()
|
self.page_api = SteamValidationPage()
|
||||||
self.page5 = BMValidationPage()
|
self.page_prefs = PreferencesPage()
|
||||||
self.page6 = PreferencesPage()
|
self.page_shortcuts = ShortcutCreationPage(XDG.shortcut)
|
||||||
self.page7 = ShortcutCreationPage(XDG.shortcut)
|
self.page_flatpak = FlatpakOverridePage(xdg_paths)
|
||||||
self.page8 = CompletionPage()
|
self.page_completion = CompletionPage()
|
||||||
|
|
||||||
self.set_forward_page_func(self._advance_page)
|
self.set_forward_page_func(self._advance_page)
|
||||||
|
|
||||||
@@ -466,17 +496,19 @@ class Assistant(Gtk.Assistant):
|
|||||||
EMITTER.connect("step_pending", self._mark_page_incomplete)
|
EMITTER.connect("step_pending", self._mark_page_incomplete)
|
||||||
EMITTER.connect("config", self._set_config_state)
|
EMITTER.connect("config", self._set_config_state)
|
||||||
|
|
||||||
|
self.pages: dict[EnumeratedWizardPage, int] = {}
|
||||||
|
|
||||||
legacy_path = Path.home().joinpath(LEGACY_CONFIG_PATH)
|
legacy_path = Path.home().joinpath(LEGACY_CONFIG_PATH)
|
||||||
self.has_legacy_config = legacy_path.is_file()
|
self.has_legacy_config = legacy_path.is_file()
|
||||||
for page in (
|
for page in (
|
||||||
self.page1,
|
self.page_intro,
|
||||||
self.page2,
|
self.page_migration,
|
||||||
self.page3,
|
self.page_paths,
|
||||||
self.page4,
|
self.page_api,
|
||||||
self.page5,
|
self.page_prefs,
|
||||||
self.page6,
|
self.page_shortcuts,
|
||||||
self.page7,
|
self.page_flatpak,
|
||||||
self.page8,
|
self.page_completion,
|
||||||
):
|
):
|
||||||
# NOTE: skip config migration page if no legacy config file
|
# NOTE: skip config migration page if no legacy config file
|
||||||
if (
|
if (
|
||||||
@@ -484,10 +516,8 @@ class Assistant(Gtk.Assistant):
|
|||||||
and self.has_legacy_config is False
|
and self.has_legacy_config is False
|
||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
# NOTE: disabled for now on system-provided packages
|
ind = self._add_page(page, page.get_page_type())
|
||||||
if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None:
|
self.pages[page] = ind
|
||||||
continue
|
|
||||||
self._add_page(page, page.get_page_type())
|
|
||||||
|
|
||||||
self.connect("prepare", self._on_page_prepare)
|
self.connect("prepare", self._on_page_prepare)
|
||||||
self.connect("cancel", self.destroy_and_quit)
|
self.connect("cancel", self.destroy_and_quit)
|
||||||
@@ -495,41 +525,58 @@ class Assistant(Gtk.Assistant):
|
|||||||
self.show_all()
|
self.show_all()
|
||||||
load_css()
|
load_css()
|
||||||
|
|
||||||
def write_config(self) -> None:
|
def write_config(self, config: dict[str, Any]) -> None:
|
||||||
write_json(self.config_values, self.config_path)
|
write_json(config, self.config_path)
|
||||||
|
|
||||||
|
def get_completion_page(self) -> int:
|
||||||
|
return self.pages[self.page_completion]
|
||||||
|
|
||||||
|
def get_shortcut_page(self) -> int:
|
||||||
|
return self.pages[self.page_shortcuts]
|
||||||
|
|
||||||
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/isinstance
|
|
||||||
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():
|
||||||
steam_path = lookup(self.config_path, Preferences.DEFAULT)
|
sp = page.migrated_conf["default_steam_path"]
|
||||||
self.page7.set_steam_path(steam_path)
|
self.page_shortcuts.set_steam_path(sp)
|
||||||
return self.get_n_pages() - 2
|
self.config_values = self.page_migration.migrated_conf
|
||||||
case self.page3:
|
return self.get_shortcut_page()
|
||||||
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
case SteamPathPage():
|
||||||
case self.page4:
|
path = page.get_path_from_radio()
|
||||||
|
self.config_values["default_steam_path"] = path
|
||||||
|
self.config_values["client"] = path_to_steam_cmd(path)
|
||||||
|
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
|
name, use_miles = self.page_prefs.get_prefs()
|
||||||
case self.page6:
|
|
||||||
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.page_shortcuts.set_steam_path(
|
||||||
self.write_config()
|
self.config_values["default_steam_path"]
|
||||||
self.page7.set_steam_path(self.config_values["default_steam_path"])
|
)
|
||||||
case self.page7:
|
case ShortcutCreationPage():
|
||||||
self.page7.create_shortcuts()
|
page.create_shortcuts()
|
||||||
self.setup_complete = True
|
if not self.needs_flatpak_override():
|
||||||
|
return self.get_completion_page()
|
||||||
|
case FlatpakOverridePage():
|
||||||
|
pass
|
||||||
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 needs_flatpak_override(self) -> bool:
|
||||||
|
if (
|
||||||
|
self.page_shortcuts.is_steam_checkbox_active()
|
||||||
|
and FLATPAK_ROOT_PATH in self.config_values["default_steam_path"]
|
||||||
|
):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
def is_setup_complete(self) -> bool:
|
def is_setup_complete(self) -> bool:
|
||||||
return self.setup_complete
|
return self.setup_complete
|
||||||
|
|
||||||
@@ -551,16 +598,19 @@ class Assistant(Gtk.Assistant):
|
|||||||
return
|
return
|
||||||
self.set_page_complete(page, True)
|
self.set_page_complete(page, True)
|
||||||
|
|
||||||
def _add_page(self, page: ScrolledWizardPage, ptype: Gtk.AssistantPageType) -> None:
|
def _add_page(
|
||||||
self.append_page(page)
|
self, page: EnumeratedWizardPage, ptype: Gtk.AssistantPageType
|
||||||
|
) -> int:
|
||||||
|
ind = self.append_page(page)
|
||||||
self.set_page_type(page, ptype)
|
self.set_page_type(page, ptype)
|
||||||
self.set_page_title(page, page.get_title())
|
self.set_page_title(page, page.get_title())
|
||||||
self.set_page_complete(page, True)
|
self.set_page_complete(page, True)
|
||||||
|
return ind
|
||||||
|
|
||||||
def _set_config_state(self, emitter: "Emitter", state: bool) -> None:
|
def _set_config_state(self, emitter: "Emitter", state: bool) -> None:
|
||||||
self.config = state
|
self.config = state
|
||||||
|
|
||||||
def _on_page_prepare(self: Self, wizard: Self, page: ScrolledWizardPage) -> None:
|
def _on_page_prepare(self: Self, wizard: Self, page: EnumeratedWizardPage) -> None:
|
||||||
page_num = self.get_current_page() + 1
|
page_num = self.get_current_page() + 1
|
||||||
total = self.get_n_pages()
|
total = self.get_n_pages()
|
||||||
fraction = page_num / total
|
fraction = page_num / total
|
||||||
@@ -569,27 +619,33 @@ 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")
|
||||||
|
|
||||||
|
if page == self.page_completion:
|
||||||
|
self.write_config(self.config_values)
|
||||||
|
self.setup_complete = True
|
||||||
|
|
||||||
|
|
||||||
class CheckboxWithLabel(Gtk.Box):
|
class CheckboxWithLabel(Gtk.Box):
|
||||||
def __init__(self, text: str, blurb_text: str) -> None:
|
def __init__(self, text: str, blurb_text: str) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||||
|
|
||||||
|
self.indent = 20
|
||||||
self.button = Gtk.CheckButton(label=text)
|
self.button = Gtk.CheckButton(label=text)
|
||||||
self.button.set_active(True)
|
self.button.set_active(True)
|
||||||
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20)
|
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=self.indent)
|
||||||
|
|
||||||
wrapped = textwrap.fill(blurb_text, width=100)
|
wrapped = textwrap.fill(blurb_text, width=100)
|
||||||
label.set_markup(f"- {wrapped}")
|
label.set_markup(f"- {wrapped}")
|
||||||
|
|
||||||
for el in self.button, label:
|
for el in self.button, label:
|
||||||
self.add(el)
|
self.add(el)
|
||||||
|
|
||||||
|
def indent_below(self, widget: Gtk.Widget) -> None:
|
||||||
|
widget.set_margin_start(self.indent)
|
||||||
|
self.add(widget)
|
||||||
|
|
||||||
def get_checkbox(self) -> Gtk.CheckButton:
|
def get_checkbox(self) -> Gtk.CheckButton:
|
||||||
return self.button
|
return self.button
|
||||||
|
|
||||||
@@ -600,7 +656,7 @@ class CheckboxWithLabel(Gtk.Box):
|
|||||||
self.button.set_active(state)
|
self.button.set_active(state)
|
||||||
|
|
||||||
|
|
||||||
class ShortcutCreationPage(ScrolledWizardPage):
|
class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ignore
|
||||||
def __init__(self, shortcut: Path) -> None:
|
def __init__(self, shortcut: Path) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.SHORTCUTS,
|
enum=PageNum.SHORTCUTS,
|
||||||
@@ -643,12 +699,12 @@ class ShortcutCreationPage(ScrolledWizardPage):
|
|||||||
self.desktop_checkbox.set_active(state)
|
self.desktop_checkbox.set_active(state)
|
||||||
self.desktop_checkbox.set_sensitive(state)
|
self.desktop_checkbox.set_sensitive(state)
|
||||||
|
|
||||||
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
|
||||||
EMITTER.emit("step_complete")
|
|
||||||
|
|
||||||
def set_steam_path(self, path: Path) -> None:
|
def set_steam_path(self, path: Path) -> None:
|
||||||
self.steam_path = path
|
self.steam_path = path
|
||||||
|
|
||||||
|
def is_steam_checkbox_active(self) -> bool:
|
||||||
|
return self.steam_checkbox.get_active()
|
||||||
|
|
||||||
def create_shortcuts(self) -> None:
|
def create_shortcuts(self) -> None:
|
||||||
# NOTE: best-effort, permissive even on failure (page is already marked as complete)
|
# NOTE: best-effort, permissive even on failure (page is already marked as complete)
|
||||||
if self.steam_checkbox.get_active():
|
if self.steam_checkbox.get_active():
|
||||||
@@ -661,7 +717,7 @@ class ShortcutCreationPage(ScrolledWizardPage):
|
|||||||
freedesktop.write_desktop_shortcut(desktop_file)
|
freedesktop.write_desktop_shortcut(desktop_file)
|
||||||
|
|
||||||
|
|
||||||
class SteamPathPage(ScrolledWizardPage):
|
class SteamPathPage(EnumeratedWizardPage):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
enum=PageNum.USER_PREFS,
|
enum=PageNum.USER_PREFS,
|
||||||
@@ -716,11 +772,11 @@ class SteamPathPage(ScrolledWizardPage):
|
|||||||
self.err_box.set_visible(False)
|
self.err_box.set_visible(False)
|
||||||
|
|
||||||
|
|
||||||
class SetupWizard(Gtk.Application):
|
class SetupWizard(ColorAwareApp, Gtk.Application): # type: ignore
|
||||||
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
GLib.set_prgname(APP_NAME)
|
GLib.set_prgname(APP_NAME)
|
||||||
self.win = Window(is_deck, XDG)
|
self.win = Window(is_deck, XDG, xdg_paths)
|
||||||
Gtk.main()
|
Gtk.main()
|
||||||
|
|
||||||
def is_setup_complete(self) -> int:
|
def is_setup_complete(self) -> int:
|
||||||
@@ -728,9 +784,9 @@ class SetupWizard(Gtk.Application):
|
|||||||
|
|
||||||
|
|
||||||
class Window(Gtk.Window):
|
class Window(Gtk.Window):
|
||||||
def __init__(self, is_deck: bool, XDG: "Xdg") -> None:
|
def __init__(self, is_deck: bool, XDG: "Xdg", xdg_paths: dict[str, str]) -> None:
|
||||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||||
self.assistant = Assistant(is_deck, XDG)
|
self.assistant = Assistant(is_deck, XDG, xdg_paths)
|
||||||
|
|
||||||
|
|
||||||
class Emitter(GObject.GObject):
|
class Emitter(GObject.GObject):
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dzgui.const.constants import (
|
||||||
|
GIO_SETTINGS_KEY,
|
||||||
|
GIO_SETTINGS_INTERFACE,
|
||||||
|
GIO_SETTINGS_PROP,
|
||||||
|
GIO_SETTINGS_VAL,
|
||||||
|
)
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk, Gio # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class ColorAwareApp:
|
||||||
|
"""
|
||||||
|
Polls the local theme's color scheme settings.
|
||||||
|
Intended to be consumed by Gtk.Applications (or ephemeral widgets like EarlyAlertDialogs)
|
||||||
|
on initialization, prior to being realized. Applying scheme changes too late in the chain
|
||||||
|
may cause a blinking effect.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.interface_settings = Gio.Settings.new(GIO_SETTINGS_INTERFACE)
|
||||||
|
self.interface_settings.connect(
|
||||||
|
"changed::color-scheme", self._on_color_scheme_changed
|
||||||
|
)
|
||||||
|
self.default_settings = Gtk.Settings.get_default()
|
||||||
|
self._on_color_scheme_changed(self.interface_settings, GIO_SETTINGS_PROP)
|
||||||
|
|
||||||
|
def _on_color_scheme_changed(self, settings: Gio.Settings, prop: str) -> None:
|
||||||
|
state = self.interface_settings.get_string(prop) == GIO_SETTINGS_VAL
|
||||||
|
if self.default_settings is None:
|
||||||
|
return
|
||||||
|
self.default_settings.set_property(GIO_SETTINGS_KEY, state)
|
||||||
@@ -104,20 +104,11 @@ class ContextMixin(TreeView):
|
|||||||
return False
|
return False
|
||||||
menu = self.context_menu
|
menu = self.context_menu
|
||||||
children = menu.get_children()
|
children = menu.get_children()
|
||||||
sel = menu.get_selected_item()
|
|
||||||
for i, child in enumerate(children):
|
|
||||||
if sel is child:
|
|
||||||
ind = i
|
|
||||||
break
|
|
||||||
|
|
||||||
match event.keyval:
|
match event.keyval:
|
||||||
case Gdk.KEY_j:
|
case Gdk.KEY_j:
|
||||||
if ind + 1 > len(children) - 1:
|
|
||||||
return False
|
|
||||||
menu.emit("move-current", Gtk.MenuDirectionType.NEXT)
|
menu.emit("move-current", Gtk.MenuDirectionType.NEXT)
|
||||||
case Gdk.KEY_k:
|
case Gdk.KEY_k:
|
||||||
if ind - 1 < 0:
|
|
||||||
return False
|
|
||||||
menu.emit("move-current", Gtk.MenuDirectionType.PREV)
|
menu.emit("move-current", Gtk.MenuDirectionType.PREV)
|
||||||
case Gdk.KEY_g:
|
case Gdk.KEY_g:
|
||||||
menu.select_item(children[0])
|
menu.select_item(children[0])
|
||||||
|
|||||||
@@ -37,7 +37,11 @@ class CursorMixin:
|
|||||||
end = len(model) - 1
|
end = len(model) - 1
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
cur_row = self.get_focused_row_index() # type: ignore
|
|
||||||
|
try:
|
||||||
|
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,10 +1,12 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
import textwrap
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from importlib import resources
|
from importlib import resources
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH
|
||||||
|
from dzgui.strings import changelog as clog
|
||||||
from dzgui.util.strings import missing_changelog
|
from dzgui.util.strings import missing_changelog
|
||||||
from dzgui.util.format import format_pango
|
from dzgui.util.format import format_pango
|
||||||
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
|
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
|
||||||
@@ -31,18 +33,39 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
changelog = missing_changelog
|
changelog = missing_changelog
|
||||||
|
|
||||||
# TODO: should long text be wrapped?
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10)
|
self.box = Gtk.Box(
|
||||||
|
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
|
||||||
|
)
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
|
|
||||||
|
expand_all = Gtk.Button(
|
||||||
|
label=clog.expand_all, halign=Gtk.Align.START, margin_start=20
|
||||||
|
)
|
||||||
|
expand_all.connect("clicked", self._on_expand_all_clicked)
|
||||||
|
self.box.add(expand_all)
|
||||||
|
|
||||||
|
self.expanded = False
|
||||||
|
self.expanders: list[Gtk.Expander] = []
|
||||||
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)
|
changes = self._parse(changelog)
|
||||||
self._generate_nodes(changes)
|
self._generate_nodes(changes)
|
||||||
|
|
||||||
self.show_all()
|
self.show_all()
|
||||||
|
|
||||||
|
def _on_expand_all_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.expanded = not self.expanded
|
||||||
|
for expander in self.expanders:
|
||||||
|
# NOTE: simply setting set_expanded() does not trigger activate() signal,
|
||||||
|
# so margins are not applied
|
||||||
|
if expander.get_expanded() == self.expanded:
|
||||||
|
continue
|
||||||
|
expander.activate()
|
||||||
|
label = clog.collapse_all if self.expanded else clog.expand_all
|
||||||
|
button.set_label(label)
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.grab_focus()
|
self.grab_focus()
|
||||||
|
|
||||||
@@ -55,7 +78,6 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
return version, date
|
return version, date
|
||||||
raise ValueError("Changelog parse error")
|
raise ValueError("Changelog parse error")
|
||||||
|
|
||||||
|
|
||||||
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
|
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
|
||||||
for header, changes in releases:
|
for header, changes in releases:
|
||||||
for ind in (0, -1):
|
for ind in (0, -1):
|
||||||
@@ -64,8 +86,10 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
text = "\n".join(changes)
|
text = "\n".join(changes)
|
||||||
formatted = format_pango(text)
|
formatted = format_pango(text)
|
||||||
|
|
||||||
label = Gtk.Label(valign=Gtk.Align.START, margin=15, halign=Gtk.Align.START)
|
container = Gtk.Box(valign=Gtk.Align.START, halign=Gtk.Align.START)
|
||||||
|
label = Gtk.Label()
|
||||||
label.set_markup(formatted)
|
label.set_markup(formatted)
|
||||||
|
container.add(label)
|
||||||
|
|
||||||
version, date = self._parse_version(header)
|
version, date = self._parse_version(header)
|
||||||
button_text = f"{version} ({date})"
|
button_text = f"{version} ({date})"
|
||||||
@@ -73,11 +97,24 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
|
|
||||||
expander = Gtk.Expander()
|
expander = Gtk.Expander()
|
||||||
expander.set_label_widget(button)
|
expander.set_label_widget(button)
|
||||||
expander.add(label)
|
expander.add(container)
|
||||||
|
expander.connect("activate", self._on_expand, container)
|
||||||
self.box.add(expander)
|
self.box.add(expander)
|
||||||
|
self.expanders.append(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]]]:
|
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
||||||
|
|
||||||
release = ""
|
release = ""
|
||||||
releases: list[tuple[str, list[str]]] = []
|
releases: list[tuple[str, list[str]]] = []
|
||||||
release_notes: list[str] = []
|
release_notes: list[str] = []
|
||||||
@@ -91,5 +128,5 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
|||||||
release = ""
|
release = ""
|
||||||
release = line
|
release = line
|
||||||
continue
|
continue
|
||||||
release_notes.append(line.rstrip())
|
release_notes.append(textwrap.fill(line.rstrip(), width=120))
|
||||||
return releases
|
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()
|
||||||
@@ -9,7 +9,6 @@ from dzgui.const.constants import (
|
|||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
EDIT_DELETE,
|
EDIT_DELETE,
|
||||||
ERROR,
|
|
||||||
FOLDER,
|
FOLDER,
|
||||||
WARNING,
|
WARNING,
|
||||||
)
|
)
|
||||||
@@ -21,6 +20,7 @@ from dzgui.views.components.buttons import Icon, IconTextButton
|
|||||||
from dzgui.views.components.eventbox import InfoEventBox
|
from dzgui.views.components.eventbox import InfoEventBox
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||||
|
from dzgui.views.components.misc import ErrorPopover
|
||||||
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
||||||
|
|
||||||
|
|
||||||
@@ -41,26 +41,27 @@ 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")
|
||||||
|
|
||||||
|
|
||||||
class ErrorPopover(Gtk.Popover):
|
class OfflineErrorPopover(ErrorPopover):
|
||||||
def __init__(self) -> None:
|
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
||||||
super().__init__(position=Gtk.PositionType.RIGHT)
|
super().__init__(relative_to=relative_to, position=position)
|
||||||
|
|
||||||
self.hbox = HBox()
|
def set_error_label(self, error: FolderError, msg: str) -> None:
|
||||||
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
|
||||||
error_icon = Icon(ERROR, margin_start=10)
|
|
||||||
self.hbox.extend([error_icon, self.label])
|
|
||||||
self.add(self.hbox)
|
|
||||||
self.show_all()
|
|
||||||
self.popdown()
|
|
||||||
|
|
||||||
def set_label(self, error: FolderError, msg: str) -> None:
|
|
||||||
match error:
|
match error:
|
||||||
case FolderError.NO_VALID_MODS:
|
case FolderError.NO_VALID_MODS:
|
||||||
prefix = offline.no_mods
|
prefix = offline.no_mods
|
||||||
@@ -106,13 +107,10 @@ class FolderHBox(HBox):
|
|||||||
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
||||||
)
|
)
|
||||||
|
|
||||||
self.pop = ErrorPopover()
|
self.pop = OfflineErrorPopover(self.button, Gtk.PositionType.RIGHT)
|
||||||
self.pop.set_relative_to(self.button)
|
|
||||||
self.pop.connect("unmap", lambda _: self.grab_focus())
|
self.pop.connect("unmap", lambda _: self.grab_focus())
|
||||||
|
|
||||||
self.sidepop = ErrorPopover()
|
self.sidepop = OfflineErrorPopover(self.scrolled_label, Gtk.PositionType.BOTTOM)
|
||||||
self.sidepop.set_position(Gtk.PositionType.BOTTOM)
|
|
||||||
self.sidepop.set_relative_to(self.scrolled_label)
|
|
||||||
self.sidepop.connect("unmap", lambda _: self.grab_focus())
|
self.sidepop.connect("unmap", lambda _: self.grab_focus())
|
||||||
|
|
||||||
self.connect("map", self._on_map)
|
self.connect("map", self._on_map)
|
||||||
@@ -161,13 +159,13 @@ class FolderHBox(HBox):
|
|||||||
|
|
||||||
def present_error(self, error: FolderError, msg: str) -> None:
|
def present_error(self, error: FolderError, msg: str) -> None:
|
||||||
if error == FolderError.FOLDER_CHANGED:
|
if error == FolderError.FOLDER_CHANGED:
|
||||||
self.sidepop.set_label(error, msg)
|
self.sidepop.set_error_label(error, msg)
|
||||||
self.sidepop.popup()
|
self.sidepop.popup()
|
||||||
return
|
return
|
||||||
self.folder = ""
|
self.folder = ""
|
||||||
self.label.set_text("")
|
self.label.set_text("")
|
||||||
self.unset_button.hide()
|
self.unset_button.hide()
|
||||||
self.pop.set_label(error, msg)
|
self.pop.set_error_label(error, msg)
|
||||||
self.pop.popup()
|
self.pop.popup()
|
||||||
|
|
||||||
|
|
||||||
@@ -434,7 +432,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)
|
||||||
@@ -457,7 +456,6 @@ class OfflineLoader(Gtk.Box):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: share ConnectBox class with preconnect dialog?
|
|
||||||
self.button_box = HBox(spacing=5)
|
self.button_box = HBox(spacing=5)
|
||||||
self.button_box.set_halign(Gtk.Align.END)
|
self.button_box.set_halign(Gtk.Align.END)
|
||||||
self.button_box.set_margin_top(15)
|
self.button_box.set_margin_top(15)
|
||||||
@@ -511,3 +509,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()
|
||||||
+275
-262
@@ -1,5 +1,5 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import Callable, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api import pefile as PeFile
|
from dzgui.api import pefile as PeFile
|
||||||
|
|
||||||
@@ -9,22 +9,28 @@ from dzgui.const.constants import (
|
|||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
|
FLATPAK_RUN_CMD,
|
||||||
|
FLATPAK_SANDBOX,
|
||||||
|
FLATPAK_STEAM_PATH,
|
||||||
NO_EXPAND,
|
NO_EXPAND,
|
||||||
NO_FILL,
|
NO_FILL,
|
||||||
NO_PADDING,
|
NO_PADDING,
|
||||||
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 options
|
||||||
from dzgui.util import strings, css, open_links
|
from dzgui.util import strings, css, open_links
|
||||||
|
|
||||||
|
from dzgui.views.components.box import ShortHBox, 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 SpinnerButton, 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 ErrorPopover
|
||||||
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,74 +43,270 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.controllers.emitter import Emitter
|
from dzgui.controllers.emitter import Emitter
|
||||||
|
|
||||||
|
|
||||||
class ShortHBox(Gtk.Box):
|
class SubmitField(Gtk.Box):
|
||||||
def __init__(self, widget: Gtk.Widget) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
placeholder: str,
|
||||||
|
context: Preferences,
|
||||||
|
slow: bool = False,
|
||||||
|
private: bool = True,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||||
|
|
||||||
|
self.callback: Callable
|
||||||
|
self.controller = controller
|
||||||
|
self.context = context
|
||||||
|
|
||||||
|
self.old_text: str
|
||||||
|
self.placeholder = placeholder
|
||||||
|
|
||||||
|
self.entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
|
||||||
|
|
||||||
|
# TODO: audit for all applicable callbacks
|
||||||
|
if private:
|
||||||
|
self.entry.set_icon_from_icon_name(
|
||||||
|
Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL
|
||||||
|
)
|
||||||
|
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||||
|
self.entry.connect("icon-release", self._on_icon_release)
|
||||||
|
self.entry.set_visibility(False)
|
||||||
|
|
||||||
|
self.button: SpinnerButton | Gtk.Button
|
||||||
|
if slow:
|
||||||
|
self.button = SpinnerButton(label=options.save_button)
|
||||||
|
else:
|
||||||
|
self.button = Gtk.Button(label=options.save_button)
|
||||||
|
|
||||||
|
self.button.connect("clicked", self._on_save_clicked)
|
||||||
|
self.entry.connect("changed", self._on_text_typed)
|
||||||
|
self.entry.connect("activate", self._on_field_activated)
|
||||||
|
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
|
||||||
|
|
||||||
|
self.add(self.entry)
|
||||||
|
self.add(self.button)
|
||||||
|
|
||||||
|
def set_text(self, text: str) -> None:
|
||||||
|
self.old_text = text
|
||||||
|
self.entry.set_text(text)
|
||||||
|
if len(text) == 0:
|
||||||
|
self.button.set_sensitive(False)
|
||||||
|
|
||||||
|
def _is_valid_text(self, text: str) -> bool:
|
||||||
|
if text.isspace():
|
||||||
|
return False
|
||||||
|
if len(text) == 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if text == self.old_text:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _on_text_deleted(
|
||||||
|
self,
|
||||||
|
buffer: Gtk.EntryBuffer,
|
||||||
|
position: int,
|
||||||
|
chars: int,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
text = buffer.get_text()
|
||||||
|
state = self._is_valid_text(text)
|
||||||
|
self.button.set_sensitive(state)
|
||||||
|
|
||||||
|
def _on_text_typed(
|
||||||
|
self,
|
||||||
|
entry: Gtk.Entry,
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
text = entry.get_text()
|
||||||
|
state = self._is_valid_text(text)
|
||||||
|
self.button.set_sensitive(state)
|
||||||
|
|
||||||
|
def _on_icon_release(
|
||||||
|
self,
|
||||||
|
widget: Gtk.Entry,
|
||||||
|
icon_pos: Gtk.EntryIconPosition,
|
||||||
|
event: Gdk.Event,
|
||||||
|
) -> None:
|
||||||
|
visible = widget.get_visibility()
|
||||||
|
if visible:
|
||||||
|
icon, state = VIEW_REVEAL, False
|
||||||
|
else:
|
||||||
|
icon, state = VIEW_CONCEAL, True
|
||||||
|
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||||
|
widget.set_visibility(state)
|
||||||
|
|
||||||
|
def _on_field_activated(self, entry: Gtk.Entry) -> None:
|
||||||
|
text = entry.get_text()
|
||||||
|
if not self._is_valid_text(text):
|
||||||
|
return
|
||||||
|
self.save_option()
|
||||||
|
|
||||||
|
def set_callback(self, callback: Callable) -> None:
|
||||||
|
self.callback = callback
|
||||||
|
|
||||||
|
def save_option(self) -> None:
|
||||||
|
self.callback()
|
||||||
|
self.button.set_sensitive(False)
|
||||||
|
|
||||||
|
def _on_save_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.save_option()
|
||||||
|
|
||||||
|
|
||||||
|
class SteamSubmitField(SubmitField):
|
||||||
|
def __init__(self, controller: "Controller") -> None:
|
||||||
|
super().__init__(
|
||||||
|
controller,
|
||||||
|
options.steam_placeholder,
|
||||||
|
Preferences.STEAM,
|
||||||
|
slow=True,
|
||||||
|
private=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pop = ErrorPopover(
|
||||||
|
position=Gtk.PositionType.BOTTOM, relative_to=self.entry
|
||||||
|
)
|
||||||
|
self.pop.set_label(options.api_failed)
|
||||||
|
self.pop.show_all()
|
||||||
|
self.pop.popdown()
|
||||||
|
|
||||||
|
emitter = controller.get_emitter()
|
||||||
|
emitter.connect("api_change_failed", self._on_api_failure)
|
||||||
|
emitter.connect("api_change_successful", self._on_api_success)
|
||||||
|
|
||||||
|
self.set_callback(self.save_setting)
|
||||||
|
|
||||||
|
def save_setting(self) -> None:
|
||||||
|
self.entry.set_sensitive(False)
|
||||||
|
text = "".join(self.entry.get_text().split())
|
||||||
|
self.controller.update_steam_api_key(text)
|
||||||
|
|
||||||
|
def _on_api_success(self, emitter: "Emitter") -> None:
|
||||||
|
self.old_text = self.entry.get_text()
|
||||||
|
if isinstance(self.button, SpinnerButton):
|
||||||
|
self.button.stop_spinner()
|
||||||
|
self.entry.set_sensitive(True)
|
||||||
|
|
||||||
|
def _on_api_failure(self, emitter: "Emitter") -> None:
|
||||||
|
self.pop.popup()
|
||||||
|
if isinstance(self.button, SpinnerButton):
|
||||||
|
self.button.stop_spinner()
|
||||||
|
self.entry.set_sensitive(True)
|
||||||
|
|
||||||
|
def block_text_entry(self) -> None:
|
||||||
|
self.entry.set_position(-1)
|
||||||
|
self.entry.set_can_focus(False)
|
||||||
|
|
||||||
|
def unblock_text_entry(self) -> None:
|
||||||
|
self.entry.set_can_focus(True)
|
||||||
|
|
||||||
|
|
||||||
|
class NameSubmitField(SubmitField):
|
||||||
|
def __init__(self, controller: "Controller") -> None:
|
||||||
|
super().__init__(
|
||||||
|
controller,
|
||||||
|
options.name_placeholder,
|
||||||
|
Preferences.NAME,
|
||||||
|
private=False,
|
||||||
|
)
|
||||||
|
self.entry.set_width_chars(30) # type: ignore
|
||||||
|
self.set_callback(self.save_player_name)
|
||||||
|
|
||||||
|
def save_player_name(self) -> None:
|
||||||
|
value = self.entry.get_text().strip()
|
||||||
|
self.old_text = value
|
||||||
|
self.controller.update_config(self.context, value)
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
super().__init__(spacing=5, halign=Gtk.Align.START)
|
||||||
|
|
||||||
self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING)
|
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 Options(Gtk.Box):
|
class FlatpakToggleField(ToggleField):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
controller: "Controller",
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
controller,
|
||||||
|
first_option=options.flatpak_standard,
|
||||||
|
second_option=options.flatpak_container,
|
||||||
|
context=Preferences.CLIENT,
|
||||||
|
)
|
||||||
|
self.add(InfoEventBox(options.flatpak_info, self.controller))
|
||||||
|
self.controller = controller
|
||||||
|
self.radio1.connect("toggled", self._on_radio_toggled, Preferences.CLIENT)
|
||||||
|
|
||||||
|
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
||||||
|
cmd = FLATPAK_RUN_CMD if self.radio1.get_active() else FLATPAK_SANDBOX
|
||||||
|
self.controller.update_config(context, cmd)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.controller.register_widget("options", self)
|
self.controller.register_widget("options", self)
|
||||||
emitter = controller.get_emitter()
|
|
||||||
emitter.connect("api_change_failed", self._on_api_change_failed)
|
|
||||||
|
|
||||||
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.pop: Gtk.Popover
|
||||||
|
|
||||||
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.steam_box = SteamSubmitField(controller)
|
||||||
self.bm.connect("clicked", self._on_link_button_clicked, BM_API_SETUP)
|
|
||||||
|
|
||||||
self.steam_box = self._make_submit_field(
|
|
||||||
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 = NameSubmitField(controller)
|
||||||
strings.options.name_placeholder, Preferences.NAME
|
|
||||||
)
|
|
||||||
self.player_box.set_halign(Gtk.Align.START)
|
self.player_box.set_halign(Gtk.Align.START)
|
||||||
# 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.flatpak_toggle = FlatpakToggleField(self.controller)
|
||||||
|
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,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.client_combo = ClientCombo()
|
self.distance_toggle = ToggleField(
|
||||||
self.client_combo.connect("changed", self._on_client_changed)
|
self.controller, strings.options.km, strings.options.mi, Preferences.DIST
|
||||||
|
|
||||||
client_hbox = ShortHBox(self.client_combo)
|
|
||||||
|
|
||||||
self.distance_toggle = self.make_binary_radio(
|
|
||||||
strings.options.km, strings.options.mi, Preferences.DIST
|
|
||||||
)
|
)
|
||||||
|
|
||||||
combo_store = Gtk.ListStore(str, object)
|
combo_store = Gtk.ListStore(str, object)
|
||||||
@@ -126,12 +328,17 @@ class Options(Gtk.Box):
|
|||||||
self.start_tab_combo.connect("changed", self._on_start_tab_changed)
|
self.start_tab_combo.connect("changed", self._on_start_tab_changed)
|
||||||
|
|
||||||
pref_rows = [
|
pref_rows = [
|
||||||
[LeftLabel(strings.options.client), client_hbox],
|
|
||||||
[LeftLabel(strings.options.window_size), self.fullscreen_toggle],
|
[LeftLabel(strings.options.window_size), self.fullscreen_toggle],
|
||||||
[LeftLabel(strings.options.distance), self.distance_toggle],
|
[LeftLabel(strings.options.distance), self.distance_toggle],
|
||||||
[LeftLabel(options.start_tab), start_tab_hbox],
|
[LeftLabel(options.start_tab), start_tab_hbox],
|
||||||
[LeftLabel(strings.options.name), self.player_box],
|
[LeftLabel(strings.options.name), self.player_box],
|
||||||
]
|
]
|
||||||
|
default = self.controller.query_config(Preferences.DEFAULT)
|
||||||
|
if FLATPAK_STEAM_PATH in default:
|
||||||
|
pref_rows.insert(
|
||||||
|
0,
|
||||||
|
[LeftLabel(options.flatpak_label), self.flatpak_toggle],
|
||||||
|
)
|
||||||
|
|
||||||
self.dayz_version_label = Gtk.Label(label=strings.null)
|
self.dayz_version_label = Gtk.Label(label=strings.null)
|
||||||
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
|
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
|
||||||
@@ -144,18 +351,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 +371,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,23 +380,22 @@ 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")
|
||||||
|
|
||||||
def get_client_name(self) -> str:
|
box = VBox()
|
||||||
model = self.client_combo.get_model()
|
box.add(label)
|
||||||
ind = self.client_combo.get_active()
|
box.add(grid)
|
||||||
return str(model[ind][0])
|
self.add(box)
|
||||||
|
|
||||||
|
self.connect("key-press-event", self._on_keypress)
|
||||||
|
|
||||||
def block_text_entry(self) -> None:
|
def block_text_entry(self) -> None:
|
||||||
for entry in self.steam_entry, self.bm_entry:
|
self.steam_box.block_text_entry()
|
||||||
entry.set_position(-1)
|
|
||||||
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_box.unblock_text_entry()
|
||||||
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()
|
||||||
@@ -209,48 +403,6 @@ class Options(Gtk.Box):
|
|||||||
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
|
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
|
||||||
open_links.open_link_by_url(url)
|
open_links.open_link_by_url(url)
|
||||||
|
|
||||||
def _make_submit_field(
|
|
||||||
self,
|
|
||||||
placeholder: str,
|
|
||||||
context: Preferences,
|
|
||||||
private: bool = False,
|
|
||||||
) -> Gtk.Box:
|
|
||||||
|
|
||||||
entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
|
|
||||||
button = Gtk.Button(label="Save")
|
|
||||||
|
|
||||||
button.connect("clicked", self._on_save_clicked, entry, context)
|
|
||||||
entry.connect("insert-text", self._on_text_typed, context, button)
|
|
||||||
entry.connect("activate", self._on_field_activated, context, button)
|
|
||||||
entry.get_property("buffer").connect(
|
|
||||||
"deleted-text", self._on_text_deleted, context, button
|
|
||||||
)
|
|
||||||
|
|
||||||
if private:
|
|
||||||
entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL)
|
|
||||||
entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
|
||||||
entry.connect("icon-release", self._on_icon_release)
|
|
||||||
entry.set_visibility(False)
|
|
||||||
|
|
||||||
if context == Preferences.STEAM:
|
|
||||||
self.steam_entry = entry
|
|
||||||
else:
|
|
||||||
self.bm_entry = entry
|
|
||||||
|
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
|
||||||
box.add(entry)
|
|
||||||
box.add(button)
|
|
||||||
|
|
||||||
return box
|
|
||||||
|
|
||||||
def _on_field_activated(
|
|
||||||
self, entry: Gtk.Entry, context: Preferences, button: Gtk.Button
|
|
||||||
) -> None:
|
|
||||||
text = entry.get_text()
|
|
||||||
if not self._is_valid_text(text, context):
|
|
||||||
return
|
|
||||||
self._on_save_clicked(button, entry, context)
|
|
||||||
|
|
||||||
def _make_grid(self, rows: list) -> Gtk.Grid:
|
def _make_grid(self, rows: list) -> Gtk.Grid:
|
||||||
grid = Gtk.Grid(
|
grid = Gtk.Grid(
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
@@ -270,38 +422,6 @@ class Options(Gtk.Box):
|
|||||||
row += 1
|
row += 1
|
||||||
return grid
|
return grid
|
||||||
|
|
||||||
def _on_save_clicked(
|
|
||||||
self, button: Gtk.Button, entry: Gtk.Entry, enum: Preferences
|
|
||||||
) -> None:
|
|
||||||
old_text = self.controller.query_config(enum)
|
|
||||||
self.old_text = old_text
|
|
||||||
self.old_entry = entry
|
|
||||||
|
|
||||||
button.set_sensitive(False)
|
|
||||||
match enum:
|
|
||||||
case Preferences.NAME:
|
|
||||||
value = entry.get_text().strip()
|
|
||||||
self.controller.update_config(enum, value)
|
|
||||||
case Preferences.BM | Preferences.STEAM:
|
|
||||||
text = "".join(entry.get_text().split())
|
|
||||||
self.controller.update_api_key(enum, text)
|
|
||||||
|
|
||||||
def _on_api_change_failed(self, emitter: "Emitter") -> None:
|
|
||||||
self.old_entry.set_text(self.old_text)
|
|
||||||
# TODO: use popover
|
|
||||||
dialog = ExceptionDialog(self.controller, errors.api_validation_error)
|
|
||||||
dialog.run()
|
|
||||||
|
|
||||||
def restore_api_text(self, text: str, entry: Gtk.Entry) -> None:
|
|
||||||
entry.set_text(text)
|
|
||||||
|
|
||||||
def revert(self, mode: Preferences) -> None:
|
|
||||||
if mode == Preferences.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()
|
||||||
if _iter is None:
|
if _iter is None:
|
||||||
@@ -310,88 +430,12 @@ class Options(Gtk.Box):
|
|||||||
index = enum.value
|
index = enum.value
|
||||||
self.controller.update_config(Preferences.START_TAB, index)
|
self.controller.update_config(Preferences.START_TAB, index)
|
||||||
|
|
||||||
def _on_client_changed(self, combo: Gtk.ComboBoxText) -> None:
|
|
||||||
_iter = combo.get_active_iter()
|
|
||||||
if _iter is None:
|
|
||||||
raise ValueError(f"No active iterator set on {combo}")
|
|
||||||
real_cmd = combo.get_model()[_iter][1]
|
|
||||||
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:
|
|
||||||
if text.isspace():
|
|
||||||
return False
|
|
||||||
if len(text) == 0:
|
|
||||||
return False
|
|
||||||
|
|
||||||
match context:
|
|
||||||
case Preferences.NAME:
|
|
||||||
old = self.old_name
|
|
||||||
case Preferences.STEAM:
|
|
||||||
old = self.old_steam
|
|
||||||
case Preferences.BM:
|
|
||||||
old = self.old_bm
|
|
||||||
if text == old:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _on_text_deleted(
|
|
||||||
self,
|
|
||||||
buffer: Gtk.EntryBuffer,
|
|
||||||
position: int,
|
|
||||||
chars: int,
|
|
||||||
context: Preferences,
|
|
||||||
button: Gtk.Button,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
text = buffer.get_text()
|
|
||||||
state = self._is_valid_text(text, context)
|
|
||||||
button.set_sensitive(state)
|
|
||||||
|
|
||||||
def _on_text_typed(
|
|
||||||
self,
|
|
||||||
entry: Gtk.Entry,
|
|
||||||
text: str,
|
|
||||||
length: int,
|
|
||||||
pos: int,
|
|
||||||
context: Preferences,
|
|
||||||
button: Gtk.Button,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
buffer = entry.get_property("buffer")
|
|
||||||
text = buffer.get_text() + text
|
|
||||||
state = self._is_valid_text(text, context)
|
|
||||||
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.show_all()
|
||||||
dialog.run()
|
dialog.run()
|
||||||
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||||
|
|
||||||
@@ -401,37 +445,24 @@ 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.steam_box.set_text(steam)
|
||||||
self.old_bm = bm
|
self.player_box.set_text(name)
|
||||||
self.old_name = name
|
|
||||||
|
|
||||||
self.steam_entry.set_text(steam)
|
fs = config["fullscreen"]
|
||||||
self.bm_entry.set_text(bm)
|
miles = config["use_miles"]
|
||||||
p = self.player_box.get_children()[0]
|
|
||||||
if hasattr(p, "set_text"):
|
|
||||||
p.set_text(name)
|
|
||||||
|
|
||||||
# NOTE: suppress toggle signal until radios are built
|
if prefs.is_steam_deck:
|
||||||
self._suppress_toggles(True)
|
self.fullscreen_toggle.set_sensitive(False)
|
||||||
for el, conf_state in [
|
eb = InfoEventBox(options.fullscreen_eventbox, self.controller)
|
||||||
(self.fullscreen_toggle, config["fullscreen"]),
|
self.fullscreen_toggle.add(eb)
|
||||||
(self.distance_toggle, config["use_miles"]),
|
|
||||||
]:
|
|
||||||
el.get_children()[conf_state].set_active(True)
|
|
||||||
self._suppress_toggles(False)
|
|
||||||
|
|
||||||
# NOTE: disable buttons if no text is set
|
fs = True
|
||||||
for field in (
|
|
||||||
[name, self.player_box],
|
self.fullscreen_toggle.set_suboption_active(fs)
|
||||||
[steam, self.steam_box],
|
self.distance_toggle.set_suboption_active(miles)
|
||||||
[bm, self.bm_box],
|
|
||||||
):
|
|
||||||
if field[0] == "":
|
|
||||||
field[1].get_children()[1].set_sensitive(False)
|
|
||||||
|
|
||||||
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
||||||
if dayz_version is None:
|
if dayz_version is None:
|
||||||
@@ -444,10 +475,6 @@ class Options(Gtk.Box):
|
|||||||
self.dayz_version_label.set_text(dayz_version)
|
self.dayz_version_label.set_text(dayz_version)
|
||||||
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
self.dayz_exp_version_label.set_text(dayz_exp_version)
|
||||||
|
|
||||||
# TODO: bicolumn list store with no cell renderer on index 1, use raw command names
|
|
||||||
active_combo = query.get_client_index(config["client"])
|
|
||||||
self.client_combo.set_active(active_combo)
|
|
||||||
|
|
||||||
start_tab = self.controller.query_config(Preferences.START_TAB)
|
start_tab = self.controller.query_config(Preferences.START_TAB)
|
||||||
self.start_tab_combo.set_active(start_tab)
|
self.start_tab_combo.set_active(start_tab)
|
||||||
|
|
||||||
@@ -460,19 +487,5 @@ class Options(Gtk.Box):
|
|||||||
self, toggle.get_children()[0], "_on_radio_toggled", state
|
self, toggle.get_children()[0], "_on_radio_toggled", state
|
||||||
)
|
)
|
||||||
|
|
||||||
def _on_icon_release(
|
|
||||||
self,
|
|
||||||
widget: Gtk.Entry,
|
|
||||||
icon_pos: Gtk.EntryIconPosition,
|
|
||||||
event: Gdk.Event,
|
|
||||||
) -> None:
|
|
||||||
visible = widget.get_visibility()
|
|
||||||
if visible:
|
|
||||||
icon, state = VIEW_REVEAL, False
|
|
||||||
else:
|
|
||||||
icon, state = VIEW_CONCEAL, True
|
|
||||||
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
|
||||||
widget.set_visibility(state)
|
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
return
|
self.grab_focus()
|
||||||
@@ -11,6 +11,7 @@ from dzgui.util.keys import is_ctrl_mask
|
|||||||
from dzgui.util.localize import number
|
from dzgui.util.localize import number
|
||||||
from dzgui.strings.server_mods import checkmark
|
from dzgui.strings.server_mods import checkmark
|
||||||
from dzgui.strings import preconnect
|
from dzgui.strings import preconnect
|
||||||
|
from dzgui.views.dialogs.generic import DebugDialog
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||||
|
|
||||||
@@ -120,6 +121,12 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
spacing=5,
|
spacing=5,
|
||||||
)
|
)
|
||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||||
|
|
||||||
|
# NOTE: -d flag
|
||||||
|
if self.controller.get_prefs().is_debug:
|
||||||
|
debug = Gtk.Button(label=preconnect.debug, halign=Gtk.Align.START)
|
||||||
|
debug.connect("clicked", self._on_debug_clicked)
|
||||||
|
box.add(debug)
|
||||||
for button in self.back, self.ok, self.connect_last:
|
for button in self.back, self.ok, self.connect_last:
|
||||||
box.add(button)
|
box.add(button)
|
||||||
self.button_box.add(box)
|
self.button_box.add(box)
|
||||||
@@ -150,8 +157,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
self.tree_box.add(self.scrolled)
|
self.tree_box.add(self.scrolled)
|
||||||
self.tree_box.add(self.progress_box)
|
self.tree_box.add(self.progress_box)
|
||||||
|
|
||||||
# TODO: strings
|
self.mods_placeholder = Placeholder(preconnect.placeholder_no_mods)
|
||||||
self.mods_placeholder = Placeholder("This server has no mods.")
|
|
||||||
self.tree_box.add(self.mods_placeholder)
|
self.tree_box.add(self.mods_placeholder)
|
||||||
|
|
||||||
self.tree_frame = HeadingFrame.new_with_widget_and_label(
|
self.tree_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
@@ -161,8 +167,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
# TODO: abstract into components
|
# TODO: abstract into components
|
||||||
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
self.warning_tree = MaskedTree(WARNING)
|
self.warning_tree = MaskedTree(WARNING)
|
||||||
# TODO: strings
|
self.warning_placeholder = Placeholder(preconnect.placeholder_no_warnings)
|
||||||
self.warning_placeholder = Placeholder("No warnings.")
|
|
||||||
self.warning_box.add(self.warning_tree)
|
self.warning_box.add(self.warning_tree)
|
||||||
self.warning_box.add(self.warning_placeholder)
|
self.warning_box.add(self.warning_placeholder)
|
||||||
self.warning_frame = HeadingFrame.new_with_widget_and_label(
|
self.warning_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
@@ -171,8 +176,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
|
|
||||||
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
self.error_tree = MaskedTree(ERROR)
|
self.error_tree = MaskedTree(ERROR)
|
||||||
# TODO: strings
|
self.error_placeholder = Placeholder(preconnect.no_errors)
|
||||||
self.error_placeholder = Placeholder("No errors.")
|
|
||||||
self.error_box.add(self.error_tree)
|
self.error_box.add(self.error_tree)
|
||||||
self.error_box.add(self.error_placeholder)
|
self.error_box.add(self.error_placeholder)
|
||||||
self.error_frame = HeadingFrame.new_with_widget_and_label(
|
self.error_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
@@ -223,6 +227,12 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_load_to_menu()
|
self.controller.update_and_load_to_menu()
|
||||||
|
|
||||||
|
def _on_debug_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
args = self.controller.get_debug_args()
|
||||||
|
d = DebugDialog(self.controller, args)
|
||||||
|
d.show_all()
|
||||||
|
d.run()
|
||||||
|
|
||||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_connect()
|
self.controller.update_and_connect()
|
||||||
|
|
||||||
@@ -233,52 +243,48 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
warnings: list[str] = []
|
warnings: list[str] = []
|
||||||
errors: list[str] = []
|
errors: list[str] = []
|
||||||
|
|
||||||
resync_msg = (
|
resync_msg = preconnect.resync.format(prereqs.build)
|
||||||
f"If you recently installed {prereqs.build} or moved it to a different drive,\n"
|
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
)
|
|
||||||
|
|
||||||
"""Errors"""
|
"""Errors"""
|
||||||
|
if len(prereqs.invalid_mods) > 0:
|
||||||
|
pairs = [": ".join(sub) for sub in prereqs.invalid_mods]
|
||||||
|
lines = "\n".join(pairs)
|
||||||
|
msg = preconnect.invalid_mods.format(lines)
|
||||||
|
errors.append(msg)
|
||||||
if prereqs.binary_missing:
|
if prereqs.binary_missing:
|
||||||
errors.append(
|
msg = preconnect.version_missing.format(prereqs.build)
|
||||||
f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}"
|
msg += f"\n{resync_msg}"
|
||||||
)
|
errors.append(msg)
|
||||||
elif prereqs.local_version != prereqs.remote_version:
|
elif prereqs.local_version != prereqs.remote_version:
|
||||||
errors.append(
|
msg = preconnect.version_mismatch.format(
|
||||||
f"Local client version '{prereqs.local_version}' does not match remote version '{prereqs.remote_version}'.\n{resync_msg}"
|
prereqs.local_version, prereqs.remote_version
|
||||||
)
|
)
|
||||||
|
msg += f"\n{resync_msg}"
|
||||||
|
errors.append(msg)
|
||||||
if prereqs.required_space > prereqs.available_space:
|
if prereqs.required_space > prereqs.available_space:
|
||||||
required_pretty = number(prereqs.required_space)
|
required_pretty = number(prereqs.required_space)
|
||||||
available_pretty = number(prereqs.available_space)
|
available_pretty = number(prereqs.available_space)
|
||||||
errors.append(
|
msg = preconnect.not_enough_space.format(required_pretty, available_pretty)
|
||||||
f"Need to update {required_pretty} MiB of mods, but installation path only has {available_pretty} MiB."
|
errors.append(msg)
|
||||||
)
|
|
||||||
if len(prereqs.mods) > 0 and prereqs.game_mode:
|
if len(prereqs.mods) > 0 and prereqs.game_mode:
|
||||||
errors.append("Use Desktop Mode to download mods on Steam Deck")
|
errors.append(preconnect.use_desktop_mode)
|
||||||
|
|
||||||
if prereqs.steam_proc.is_running is False:
|
if prereqs.steam_proc.is_running is False:
|
||||||
client = prereqs.steam_proc.name
|
client = prereqs.steam_proc.name
|
||||||
errors.append(
|
msg = preconnect.steam_not_running.format(client)
|
||||||
f"'{client}' is set as the default Steam client, but it is either not installed or not running."
|
errors.append(msg)
|
||||||
)
|
|
||||||
|
|
||||||
"""Warnings"""
|
"""Warnings"""
|
||||||
if prereqs.passworded:
|
if prereqs.passworded:
|
||||||
warnings.append(
|
warnings.append(preconnect.protected_server)
|
||||||
"Protected: you will be prompted for a password when connecting to this server."
|
|
||||||
)
|
|
||||||
if prereqs.dayz_running is True:
|
if prereqs.dayz_running is True:
|
||||||
warnings.append(
|
warnings.append(preconnect.dayz_running)
|
||||||
"It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
if len(prereqs.mods) > 0 and prereqs.download_paused:
|
||||||
)
|
warnings.append(preconnect.downloads_paused)
|
||||||
|
|
||||||
allows_dl, running_app = prereqs.allows_downloads
|
allows_dl, running_app = prereqs.allows_downloads
|
||||||
if len(prereqs.mods) > 0 and allows_dl is False:
|
if len(prereqs.mods) > 0 and allows_dl is False:
|
||||||
msg = (
|
msg = preconnect.running_app.format(running_app)
|
||||||
f"The game '{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)
|
warnings.append(msg)
|
||||||
|
|
||||||
self.add_warnings(warnings)
|
self.add_warnings(warnings)
|
||||||
@@ -317,7 +323,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
|
|
||||||
if prereqs.required_space != 0:
|
if prereqs.required_space != 0:
|
||||||
pretty = number(prereqs.required_space)
|
pretty = number(prereqs.required_space)
|
||||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
suffix = preconnect.required_space.format(pretty)
|
||||||
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:
|
else:
|
||||||
|
|||||||
@@ -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])
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
self.handler_id: int
|
self.handler_id: int
|
||||||
self.queue: Queue = Queue()
|
self.queue: Queue = Queue()
|
||||||
|
|
||||||
self.seen_cache: list[str] = []
|
self.seen_cache: list[Record] = []
|
||||||
|
|
||||||
prefs = self.controller.get_prefs()
|
prefs = self.controller.get_prefs()
|
||||||
columns = prefs.paths.columns
|
columns = prefs.paths.columns
|
||||||
@@ -252,7 +252,6 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
case Gdk.KEY_c:
|
case Gdk.KEY_c:
|
||||||
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
||||||
case Gdk.KEY_r:
|
case Gdk.KEY_r:
|
||||||
# TODO: unimplemented, needs threading
|
|
||||||
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
||||||
else:
|
else:
|
||||||
match event.keyval:
|
match event.keyval:
|
||||||
@@ -295,6 +294,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
) -> None:
|
) -> None:
|
||||||
if self.loaded is False:
|
if self.loaded is False:
|
||||||
return
|
return
|
||||||
|
# NOTE: signal triggers twice on right-click events
|
||||||
|
# due to deselect-then-select behavior in GTK
|
||||||
|
sel = self.get_selection()
|
||||||
|
model, row = sel.get_selected()
|
||||||
|
if row is None:
|
||||||
|
return
|
||||||
self.start_distcalc()
|
self.start_distcalc()
|
||||||
|
|
||||||
def get_name(self) -> str:
|
def get_name(self) -> str:
|
||||||
@@ -335,16 +340,24 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
control = self.proxy_man.get_control()
|
control = self.proxy_man.get_control()
|
||||||
return model, control
|
return model, control
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def ping_server(
|
def ping_server(
|
||||||
|
self,
|
||||||
model: "FastInsertListStore",
|
model: "FastInsertListStore",
|
||||||
_iter: Gtk.TreeIter,
|
_iter: Gtk.TreeIter,
|
||||||
ip: str,
|
record: Record,
|
||||||
qport: int,
|
|
||||||
ping_column: int,
|
|
||||||
) -> None:
|
) -> None:
|
||||||
_ping = ping(ip, qport)
|
_ping = ping(record.ip, record.qport)
|
||||||
GLib.idle_add(lambda: model.set(_iter, ping_column, _ping))
|
GLib.idle_add(self._redraw, model, _iter, _ping)
|
||||||
|
|
||||||
|
def _redraw(
|
||||||
|
self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int
|
||||||
|
) -> None:
|
||||||
|
ping_column = 9
|
||||||
|
model.set(_iter, ping_column, _ping)
|
||||||
|
self.queue_draw()
|
||||||
|
|
||||||
|
def wipe_cache(self) -> None:
|
||||||
|
self.seen_cache = []
|
||||||
|
|
||||||
def _get_ping(
|
def _get_ping(
|
||||||
self,
|
self,
|
||||||
@@ -357,13 +370,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
|
|
||||||
addr_column = 7
|
addr_column = 7
|
||||||
qport_column = 8
|
qport_column = 8
|
||||||
ping_column = 9
|
|
||||||
|
|
||||||
addr = model.get_value(_iter, addr_column).split(":")
|
addr = model.get_value(_iter, addr_column).split(":")
|
||||||
ip = addr[0]
|
ip = addr[0]
|
||||||
gameport = addr[1]
|
gameport = int(addr[1])
|
||||||
qport = model.get_value(_iter, qport_column)
|
qport = model.get_value(_iter, qport_column)
|
||||||
record = f"{addr}:{gameport}:{qport}"
|
record = Record(ip, gameport, qport)
|
||||||
|
|
||||||
if record in self.seen_cache:
|
if record in self.seen_cache:
|
||||||
return
|
return
|
||||||
@@ -372,7 +384,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
thread = threading.Thread(
|
thread = threading.Thread(
|
||||||
daemon=True,
|
daemon=True,
|
||||||
target=self.ping_server,
|
target=self.ping_server,
|
||||||
args=(model, _iter, ip, qport, ping_column),
|
args=(model, _iter, record),
|
||||||
)
|
)
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
from typing import TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class SimpleTree(Gtk.ScrolledWindow):
|
||||||
|
"""Simple scrollable 1-column tree"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, model: Union[Gtk.ListStore, "FastInsertListStore"], title: str
|
||||||
|
) -> None:
|
||||||
|
super().__init__(
|
||||||
|
overlay_scrolling=False,
|
||||||
|
propagate_natural_width=True,
|
||||||
|
propagate_natural_height=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tree = Gtk.TreeView(model=model, enable_search=False)
|
||||||
|
renderer = Gtk.CellRendererText()
|
||||||
|
column = Gtk.TreeViewColumn(title, renderer, text=0)
|
||||||
|
column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
|
||||||
|
column.set_resizable(False)
|
||||||
|
column.set_sort_column_id(0)
|
||||||
|
self.tree.append_column(column)
|
||||||
|
|
||||||
|
self.add(self.tree)
|
||||||
|
|
||||||
|
def get_tree(self) -> Gtk.TreeView:
|
||||||
|
return self.tree
|
||||||
+6
-2
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b19"
|
version = "7.0.0b28"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -47,6 +47,7 @@ docs = [
|
|||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"black==25.1.0",
|
"black==25.1.0",
|
||||||
|
"hypothesis==6.167.1",
|
||||||
"pre-commit==4.4.0",
|
"pre-commit==4.4.0",
|
||||||
"pytest==8.4.2",
|
"pytest==8.4.2",
|
||||||
"pytest-cov==7.0.0",
|
"pytest-cov==7.0.0",
|
||||||
@@ -107,11 +108,14 @@ 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",
|
||||||
|
"flatpak: depends on local flatpak installation",
|
||||||
"mods: tests mod metadata/link creation",
|
"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",
|
||||||
|
"realconfig: depends on actual generated local config file",
|
||||||
"redact: log redaction mechanisms",
|
"redact: log redaction mechanisms",
|
||||||
"slow: long-running tests",
|
"slow: long-running tests",
|
||||||
"webtest: checks remote endpoints"
|
"webtest: depends on remote endpoint",
|
||||||
]
|
]
|
||||||
+1
-1
@@ -27,7 +27,7 @@ def fetch(url: str, destination: str) -> None:
|
|||||||
|
|
||||||
def get_cpython() -> None:
|
def get_cpython() -> None:
|
||||||
source = f"https://github.com/astral-sh/python-build-standalone/releases/download/{DATE}/"
|
source = f"https://github.com/astral-sh/python-build-standalone/releases/download/{DATE}/"
|
||||||
build = f"{CPYTHON_BUILD}%2B{DATE}-x86_64_v3-unknown-linux-gnu-install_only_stripped.tar.gz"
|
build = f"{CPYTHON_BUILD}%2B{DATE}-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
|
||||||
url = f"{source}{build}"
|
url = f"{source}{build}"
|
||||||
outfile = unquote(build)
|
outfile = unquote(build)
|
||||||
fetch(url, outfile)
|
fetch(url, outfile)
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def iterate(search_str: str) -> None:
|
||||||
|
# TODO: exclude files from .gitignore
|
||||||
|
print()
|
||||||
|
print(f"Searching for '{search_str}'")
|
||||||
|
print()
|
||||||
|
files = Path(".").rglob("*")
|
||||||
|
ignore = [
|
||||||
|
"scripts/",
|
||||||
|
".git",
|
||||||
|
"egg",
|
||||||
|
"CHANGELOG",
|
||||||
|
".mypy",
|
||||||
|
"build/",
|
||||||
|
"test.py",
|
||||||
|
"bugs",
|
||||||
|
"pyproject.toml",
|
||||||
|
]
|
||||||
|
for file in files:
|
||||||
|
if file.is_dir():
|
||||||
|
continue
|
||||||
|
if any(s in str(file) for s in ignore):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
text = file.read_text()
|
||||||
|
if search_str in text:
|
||||||
|
c = text.count(search_str)
|
||||||
|
if 0 < c < 2:
|
||||||
|
print(file, c)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
imports = ["Pango", "GObject", "GLib", "Gdk", "Gtk"]
|
||||||
|
for i in imports:
|
||||||
|
iterate(i)
|
||||||
+1
-1
@@ -8,6 +8,6 @@
|
|||||||
"WantsOfflineMode" "0"
|
"WantsOfflineMode" "0"
|
||||||
"SkipOfflineModeWarning" "0"
|
"SkipOfflineModeWarning" "0"
|
||||||
"AutoLogin" "1"
|
"AutoLogin" "1"
|
||||||
"Timestamp" "0"
|
"timestamp" "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"users"
|
||||||
|
{
|
||||||
|
"0"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER"
|
||||||
|
"PersonaName" "STEAMUSER"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AutoLogin" "1"
|
||||||
|
"timestamp" "1787904411"
|
||||||
|
}
|
||||||
|
"1"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER2"
|
||||||
|
"PersonaName" "STEAMUSER2"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AutoLogin" "1"
|
||||||
|
"timestamp" "1787904647"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
"users"
|
||||||
|
{
|
||||||
|
"0"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER"
|
||||||
|
"PersonaName" "STEAMUSER"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AutoLogin" "1"
|
||||||
|
"timestamp" "1797904411"
|
||||||
|
}
|
||||||
|
"1"
|
||||||
|
{
|
||||||
|
"AccountName" "STEAMUSER2"
|
||||||
|
"PersonaName" "STEAMUSER2"
|
||||||
|
"RememberPassword" "0"
|
||||||
|
"WantsOfflineMode" "0"
|
||||||
|
"SkipOfflineModeWarning" "0"
|
||||||
|
"AutoLogin" "1"
|
||||||
|
"timestamp" "1787904647"
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -9,6 +9,6 @@
|
|||||||
"SkipOfflineModeWarning" "0"
|
"SkipOfflineModeWarning" "0"
|
||||||
"AllowAutoLogin" "1"
|
"AllowAutoLogin" "1"
|
||||||
"MostRecent" "1"
|
"MostRecent" "1"
|
||||||
"Timestamp" "0"
|
"timestamp" "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
"SkipOfflineModeWarning" "0"
|
"SkipOfflineModeWarning" "0"
|
||||||
"AllowAutoLogin" "1"
|
"AllowAutoLogin" "1"
|
||||||
"MostRecent" "0"
|
"MostRecent" "0"
|
||||||
"Timestamp" "0"
|
"timestamp" "0"
|
||||||
}
|
}
|
||||||
"1"
|
"1"
|
||||||
{
|
{
|
||||||
@@ -20,6 +20,6 @@
|
|||||||
"SkipOfflineModeWarning" "0"
|
"SkipOfflineModeWarning" "0"
|
||||||
"AllowAutoLogin" "1"
|
"AllowAutoLogin" "1"
|
||||||
"MostRecent" "1"
|
"MostRecent" "1"
|
||||||
"Timestamp" "0"
|
"timestamp" "10"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+26
-13
@@ -1,20 +1,33 @@
|
|||||||
|
import re
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from importlib import resources
|
from pathlib import Path
|
||||||
from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def changelog():
|
def changelog(request) -> None:
|
||||||
path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH)
|
root = request.config.rootpath
|
||||||
return path
|
changelog = Path(root).joinpath("CHANGELOG.md").read_text()
|
||||||
|
return changelog
|
||||||
|
|
||||||
|
|
||||||
def test_headings(changelog):
|
def count_hash(line: str) -> int:
|
||||||
with open(changelog, "r") as f:
|
cnt = 0
|
||||||
lines = f.readlines()
|
for c in line:
|
||||||
for line in lines:
|
if c == "#":
|
||||||
if line.startswith("#"):
|
cnt += 1
|
||||||
pass
|
return cnt
|
||||||
# TODO: use regex
|
|
||||||
pass
|
|
||||||
|
def test_changelog_prefix(changelog) -> None:
|
||||||
|
r = r".*(\[.*\]).*"
|
||||||
|
lines = changelog.splitlines()
|
||||||
|
sort = sorted(lines)
|
||||||
|
match = [line for line in sort if line.startswith("#")]
|
||||||
|
for m in match:
|
||||||
|
if "Changelog" in m:
|
||||||
|
assert count_hash(m) == 1
|
||||||
|
elif re.match(r, m) is not None:
|
||||||
|
assert count_hash(m) == 2
|
||||||
|
else:
|
||||||
|
assert count_hash(m) == 3
|
||||||
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user