Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ae5880890 | ||
|
|
afbced228d | ||
|
|
f5d7bf713d | ||
|
|
c1630adee1 | ||
|
|
88f6b84bea | ||
|
|
134dcb6f0b | ||
|
|
cc0354c881 | ||
|
|
f2d21be347 | ||
|
|
f65fa153cc | ||
|
|
8aa7328d8f | ||
|
|
bb04854cd7 | ||
|
|
5014b2a467 | ||
|
|
d020295aa3 | ||
|
|
1879347831 | ||
|
|
5a1c098af7 | ||
|
|
3d346dfb07 | ||
|
|
cb03db6032 | ||
|
|
c69022f74c | ||
|
|
7f0bccad64 | ||
|
|
d6b5c7bbb4 | ||
|
|
f0a07cbbba | ||
|
|
30fa762780 | ||
|
|
807a530550 | ||
|
|
f092cfdf4a | ||
|
|
d5c597ab03 | ||
|
|
06c85fdfd8 | ||
|
|
0c4026daf8 | ||
|
|
e03c6905c6 | ||
|
|
8fb3462f35 | ||
|
|
bfe9b9d147 | ||
|
|
9f36569fd4 | ||
|
|
da18b41136 | ||
|
|
f092207c61 | ||
|
|
c94e4e73db | ||
|
|
8da264686a | ||
|
|
598973104c | ||
|
|
15d4b71350 | ||
|
|
16d3df618a | ||
|
|
4e5fadf928 | ||
|
|
3dc37561be | ||
|
|
13786591b3 | ||
|
|
9eb1342877 | ||
|
|
3f29484895 | ||
|
|
22dd8fc67f | ||
|
|
fa2f86b03e | ||
|
|
c8fe6ec056 | ||
|
|
6901e5456e | ||
|
|
49298d9943 | ||
|
|
72e3d95808 | ||
|
|
a42c9d4389 | ||
|
|
cc708c64b2 | ||
|
|
03fa707305 | ||
|
|
2479cbd41b | ||
|
|
370edbe01b | ||
|
|
0c41654645 | ||
|
|
903f8fc31d | ||
|
|
6b583f68d6 | ||
|
|
3b3ccd0de6 | ||
|
|
70aacaa866 | ||
|
|
7582ab4d3b | ||
|
|
225a2a148e | ||
|
|
9117655ebd | ||
|
|
fdf92dca40 | ||
|
|
a97766855d | ||
|
|
3097701e98 | ||
|
|
895e08d228 | ||
|
|
08a6f698a5 | ||
|
|
78be8c93b7 | ||
|
|
8a2889d15d | ||
|
|
1a33f71716 | ||
|
|
ae13dc9a89 | ||
|
|
d5706f969c | ||
|
|
790e34b20c | ||
|
|
35230ae03f | ||
|
|
596d3a59ac | ||
|
|
e7cef215c6 | ||
|
|
12daf041ff | ||
|
|
90ef6cbdbb | ||
|
|
2cc26b6573 | ||
|
|
0ec1c157f4 | ||
|
|
0cf6439538 | ||
|
|
e3c1ef59ee | ||
|
|
78144e9127 | ||
|
|
33b8dfba70 | ||
|
|
abf0060cac | ||
|
|
95daa9163b | ||
|
|
ff068e5396 | ||
|
|
80680b86b0 | ||
|
|
a4ac357c8f | ||
|
|
728b8b8242 | ||
|
|
3b51392d39 | ||
|
|
159e2da355 | ||
|
|
b83a333cea | ||
|
|
46117ecdda | ||
|
|
30119fec46 | ||
|
|
ac01818549 | ||
|
|
d486712a4c | ||
|
|
d116702145 | ||
|
|
5bee3614bd | ||
|
|
c73b07991f | ||
|
|
199cd07982 | ||
|
|
e3d62c6749 | ||
|
|
5134bf02fb | ||
|
|
f97be9352d | ||
|
|
b092a809f4 | ||
|
|
2cc7710807 | ||
|
|
5fb10d9758 | ||
|
|
100cb43afc | ||
|
|
7e2a585812 | ||
|
|
c1181435b3 | ||
|
|
22a7814ce4 | ||
|
|
b030da5c65 | ||
|
|
3c0acdd068 | ||
|
|
25ae2052e4 | ||
|
|
1b1777117f | ||
|
|
3e6bd95285 | ||
|
|
f4be9a31e9 | ||
|
|
a6282e4401 | ||
|
|
a9ae5f59b5 | ||
|
|
cdec81071d | ||
|
|
97b5c0e6ee | ||
|
|
67c19fb90d | ||
|
|
d9d3f747bb | ||
|
|
1440871819 | ||
|
|
761988d8c2 | ||
|
|
97ee2b7307 | ||
|
|
ff6edb6181 | ||
|
|
4c22263fb5 | ||
|
|
ab5637ae3b | ||
|
|
682532a867 | ||
|
|
d577ba0970 | ||
|
|
c2993ad289 | ||
|
|
edb87893f9 | ||
|
|
519e6c354d | ||
|
|
e3d1d62b4d | ||
|
|
1dcfdda657 | ||
|
|
019847dcf9 | ||
|
|
c2cfe2b4d9 | ||
|
|
01b3babdf5 | ||
|
|
141f96fb9f | ||
|
|
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 | ||
|
|
b47cb482f5 | ||
|
|
152999d728 | ||
|
|
cdcb3b794f | ||
|
|
cd23f403c9 | ||
|
|
a40bef8f82 | ||
|
|
a4f9a67da1 | ||
|
|
5c07a77b63 | ||
|
|
91773c933c | ||
|
|
6a30ed44fc | ||
|
|
bbaec35a5f | ||
|
|
c46477f0a7 | ||
|
|
f47446a2e1 | ||
|
|
cdb311cb1f | ||
|
|
11cdcb62b2 | ||
|
|
1913f69e7b | ||
|
|
8c2cea0fdc | ||
|
|
8c29b9b35a | ||
|
|
d8239f1c34 | ||
|
|
8c3ec4512a | ||
|
|
1a18c8ff02 | ||
|
|
92c23ec609 | ||
|
|
e9d4054e76 | ||
|
|
1c9e026f70 | ||
|
|
eb076b7dcd | ||
|
|
8629e02670 | ||
|
|
3ba38f30ad | ||
|
|
7c37fd8b6e | ||
|
|
1a065123bb | ||
|
|
2586306323 | ||
|
|
1ccb99d935 | ||
|
|
054fe968d1 | ||
|
|
e664f68624 | ||
|
|
2e41856348 | ||
|
|
27d444ea6f | ||
|
|
26a2362172 | ||
|
|
50cf347164 | ||
|
|
dd69bc88b1 | ||
|
|
5c84b5ae0a | ||
|
|
6fdf5547dc | ||
|
|
fc1311579c | ||
|
|
da993db2e2 | ||
|
|
207d8fd407 | ||
|
|
72b10ad543 | ||
|
|
35680b1373 | ||
|
|
5121e6d005 | ||
|
|
2f61cc7ff7 | ||
|
|
8733094267 | ||
|
|
9d14b7f219 | ||
|
|
0084af4959 | ||
|
|
caf071f997 | ||
|
|
e92df8c062 | ||
|
|
494311e8df |
No files matched your search
@@ -1,7 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Added
|
## [7.0.0] Unreleased
|
||||||
|
### 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
|
||||||
@@ -23,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
|
||||||
@@ -41,11 +45,26 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
|
- Generate Steam shortcuts and cover art
|
||||||
|
- Warn user if background downloads are disabled
|
||||||
|
- Dynamic copy button
|
||||||
|
- Update map count (-u flag)
|
||||||
|
- Filter button tooltips
|
||||||
|
- Expand/collapse changelog versions
|
||||||
|
- Support local light/dark mode changes on system theme
|
||||||
|
- Server time calculator
|
||||||
|
- Add "Unmodded" filter to servers
|
||||||
|
- List timestamp of last DayZ client update
|
||||||
|
- Validate mods via Mod page to pull in updates
|
||||||
|
- Support for Nasdara (Badlands) DLC
|
||||||
|
|
||||||
## 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
|
||||||
@@ -58,59 +77,69 @@
|
|||||||
- 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
|
||||||
|
- Use SQLite DB for geolocation records
|
||||||
|
|
||||||
## 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
|
||||||
|
- Steam Flatpak cannot run DZGUI as a non-Steam game
|
||||||
|
- Collisions between native Steam and Steam Flatpak clients
|
||||||
|
|
||||||
## Unreleased
|
### Unreleased
|
||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
## Developer-facing
|
### Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
## Changed
|
### Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
## Fixed
|
### Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
## Fixed
|
### Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
## Changed
|
### Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
## Added
|
### Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@@ -136,7 +165,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@@ -175,7 +204,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@@ -187,50 +216,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
### Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
## Added
|
### Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
## Changed
|
### Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
## Fixed
|
### Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
## Added
|
### Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
## Fixed
|
### Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
## Changed
|
### Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
## Dropped
|
### Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
## Changed
|
### Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ If you are a developer, you can skip to the end.
|
|||||||
# Requesting help
|
# Requesting help
|
||||||
|
|
||||||
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
If you encounter a problem with DZGUI, you can submit tickets on the GitHub
|
||||||
(issue tracker)[https://github.com/aclist/dztui/issues] under the
|
[issue tracker](https://github.com/aclist/dztui/issues) under the
|
||||||
`troubleshooting` tag.
|
`troubleshooting` tag.
|
||||||
|
|
||||||
# How can I help the project?
|
# How can I help the project?
|
||||||
@@ -24,7 +24,7 @@ accordingly.
|
|||||||
|
|
||||||
## Submitting a ticket
|
## Submitting a ticket
|
||||||
|
|
||||||
Navigate to the GitHub (issue tracker)[https://github.com/aclist/dztui/issues].
|
Navigate to the GitHub [issue tracker](https://github.com/aclist/dztui/issues).
|
||||||
From there, follow the onscreen prompts. You will be asked questions such as:
|
From there, follow the onscreen prompts. You will be asked questions such as:
|
||||||
|
|
||||||
- What version are you using?
|
- What version are you using?
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# History of the project
|
||||||
|
This file collects notable milestones and events related to DZGUI, mainly around DZGUI as a framework and the history of development.
|
||||||
|
A chronological summary of features can be found in CHANGELOG.md.
|
||||||
|
|
||||||
|
## 2021
|
||||||
|
### 2021-12-03: DayZ adds support for Battleye over Proton
|
||||||
|
DayZ was among a handful of games with whitelisted anti-cheat support under Valve's emerging WINE-based Proton compatibility layer.
|
||||||
|
While adding anti-cheat compatibility at a technical level, this change did not actually allow the client to connect to servers running complex modlists.
|
||||||
|
|
||||||
|
Development of the precursor to DZGUI began at around this time.
|
||||||
|
|
||||||
|
### 2021-12-12: initial version of DZTUI is created
|
||||||
|
DZTUI, a Bash script and terminal user interface, was the precursor to DZGUI.
|
||||||
|
This was an initial attempt at bootstrapping the Steam client to handle mods correctly when launching DayZ.
|
||||||
|
Due to user interest, features continued to be added. This early tool is archived under the 'dztui' branch.
|
||||||
|
|
||||||
|
## 2022
|
||||||
|
### 2022-05-09: addition of zenity GUI and project name change (DZGUI)
|
||||||
|
With the introduction of the Steam Deck, more users wanted a graphical user interface
|
||||||
|
to interact with DZTUI. Being that DZTUI was purely a Bash script, a method of
|
||||||
|
drawing menus was needed, so the "zenity" package was used to create prefabricated dialogs.
|
||||||
|
While this met the minimum requirement to show prompts and selectable options, no rich customization was possible.
|
||||||
|
|
||||||
|
The introduction of zenity as a dependency marked the shift from "DZTUI" to "DZGUI".
|
||||||
|
|
||||||
|
Throughout 2022, the limits of zenity for DZGUI's purposes were tested, particularly in the context of SteamOS.
|
||||||
|
This led to the discovery of a number of bugs in zenity, GTK, and SteamOS, which were reported up to their respective channels.
|
||||||
|
While this was improving the SteamOS user experience, it started to become clear that a fully custom UI was needed.
|
||||||
|
|
||||||
|
This barebones version of DZGUI sufficed for some time, with the Bash script and some standalone Python modules doing the heavy lifting,
|
||||||
|
and this period served to surface a number of bugs and interesting interactions with the Steam client.
|
||||||
|
|
||||||
|
## 2023
|
||||||
|
### 2023-11-23: Python 3.10 dependency
|
||||||
|
Prior to this time, DZGUI open-endedly allowed you to use any Python version for its modules, without any significant handling for
|
||||||
|
the idiosyncrasies of different versions. A minimum requirement of Python 3.10 was thus instituted to normalize expected behavior.
|
||||||
|
|
||||||
|
Throughout 2023, various features related to server browsing were added, such as filters and more granular search parameters.
|
||||||
|
|
||||||
|
There were still a lot of unknowns at this time, so things were being resolved through trial and error.
|
||||||
|
|
||||||
|
## 2024
|
||||||
|
### 2024-01-31: DZGUI 5 API introduces GTK UI
|
||||||
|
Using zenity as a simple GUI had exposed some bugs and limitations in that package, particularly when trying to push huge lists of server records
|
||||||
|
into tables and sorting them in a performant manner.
|
||||||
|
|
||||||
|
Around this time it became clear that a more robust UI framework was needed. While keeping the original Bash script and Python modules for
|
||||||
|
background processing, PyGObject (GTK) was added to the project in order to ship a custom UI that could be designed from the ground up.
|
||||||
|
|
||||||
|
However, it should be noted that at this point, the DZGUI logic itself was still a (increasingly large) Bash script to which a GTK UI had been stitched on.
|
||||||
|
These were totally separate components, with UI actions on the GTK side triggering calls to Bash functions in an overly complex helper file.
|
||||||
|
The UI was basically a thin client around the old Bash logic, but this was still a far cry from an integrated application with proper signals and dedicated components.
|
||||||
|
|
||||||
|
Effectively, DZGUI 5 through 6 were a stepping stone that set the groundwork to go from DZGUI 4 (pure Bash script) to
|
||||||
|
DZGUI 7 (standalone Python application). Today, little of DZGUI 5's original logic remains outside of the basic UI grid.
|
||||||
|
|
||||||
|
### 2024-10-15: Frostline DLC released, widespread parsing problems
|
||||||
|
DayZ's first DLC introduced a new format for server responses that packed extra metadata into the A2S_RULES format.
|
||||||
|
Upon its release, all third-party DayZ launchers and server listing web sites were unable to parse the mod information being returned from Frostline servers.
|
||||||
|
|
||||||
|
For a period of two weeks, this remained unsolved and various attempts were made to understand the updated specification.
|
||||||
|
|
||||||
|
### 2024-10-30: DZGUI hotfix adds support for Frostline servers
|
||||||
|
DZGUI was the first third-party tool to fix parsing for Frostline servers and restore functionality.
|
||||||
|
This was a small milestone, because it meant DZGUI was evolving from a "dumb" launcher to a featureful tool
|
||||||
|
touching upon and improving all aspects of the end-to-end DayZ experience.
|
||||||
|
|
||||||
|
Particularly around this time, the sheer diversity of needs of modders, server operators, and players started to become clear.
|
||||||
|
Users had highly specific and diverse use cases for setting up their game environment, and requested a number of features to support that on
|
||||||
|
the DZGUI side.
|
||||||
|
|
||||||
|
## 2025
|
||||||
|
### 2025-01-17: DZGUI 6 API
|
||||||
|
With DZGUI 6, the difficult decision to enforce a Python 3.13 dependency was made.
|
||||||
|
While the previous dependency was Python 3.10+, it was deemed better to develop against and target identical versions.
|
||||||
|
|
||||||
|
However, due to the vagaries of managing Python environments on end-user systems, this change added unnecessary complexity and frustration.
|
||||||
|
Users had to properly set up virtual environments, install the relevant dependencies, and create a wrapper script to run this version, whereas
|
||||||
|
previous versions had essentially been point and click.
|
||||||
|
|
||||||
|
### 2025-10-23: DZGUI 7 API and the "Ship of Theseus"
|
||||||
|
It had become clear that the situation above was untenable and the legacy Bash script was unmaintainable given the project's enlarged scope.
|
||||||
|
The lack of a robust testing or typehinting framework made it difficult to diagnose rare bugs.
|
||||||
|
|
||||||
|
Shipping a standalone, turnkey application with zero or minimal dependencies was the most urgent priority.
|
||||||
|
DZGUI was still carrying around small bits and pieces of the old Bash script logic, so the decision was made to
|
||||||
|
refactor the application into a pure Python + GTK tool. This would effectively be a total rewrite of all logic,
|
||||||
|
only preserving the most minimal skeleton of the original UI layout as a reference.
|
||||||
|
|
||||||
|
Over the course of a year, a wholesale "Ship of Theseus" rewrite took place.
|
||||||
|
Nearly every component of DZGUI 6, both API and UI side, was taken apart and replaced.
|
||||||
|
|
||||||
|
A soft alpha and beta phase were used to iterate on this concept further. DZGUI 7 was increasingly recommended to users
|
||||||
|
as the preferred choice. This allowed for keeping the established userbase on the (relatively) stable DZGUI 6 spec, while building up
|
||||||
|
the new spec and making it available to more users as features were elaborated.
|
||||||
|
|
||||||
|
Through late 2025, several LLM-generated applications that recycled significant parts of DZGUI's code without the applicable GPL license were found.
|
||||||
|
The maintainers of those projects were advised to excise the offending segments or apply the necessary GPL licenses.
|
||||||
|
|
||||||
|
## 2026
|
||||||
|
### 2026-05-17: Switch to standalone releases
|
||||||
|
The DZGUI 7 beta starts shipping standalone glibc builds with an integrated setup wizard and installer.
|
||||||
|
|
||||||
|
DZGUI 6 had relied on "over the air" updates delivered via either a "stable" or "testing" branch
|
||||||
|
(archived respectively under the "dzgui" and "testing" branches). Effectively, the script would bootstrap itself and
|
||||||
|
fetch raw files directly from the repository. This was prone to problems because updates were monolithic, not giving
|
||||||
|
an easy escape hatch if the user needed to rollback in the event of a regression.
|
||||||
|
|
||||||
|
This was changed to atomic, tagged releases. In addition to updating directly through DZGUI, these prebuilt releases can be
|
||||||
|
downloaded manually as needed, and rollbacks are available for all prior versions under the DZGUI 7 specification.
|
||||||
|
|
||||||
|
### 2026-10-27: 500 tickets closed
|
||||||
|
The milestone of 500 tickets/PRs being closed was reached.
|
||||||
|
|
||||||
|
2026 was by far DZGUI's most intensive year by commits, perhaps suggesting the scope of the rewrite:
|
||||||
|
|
||||||
|
- 2021: 16
|
||||||
|
- 2022: 434
|
||||||
|
- 2023: 322
|
||||||
|
- 2024: 347
|
||||||
|
- 2025: 426
|
||||||
|
- 2026: 1300
|
||||||
|
|
||||||
|
DZGUI 7 is scheduled to release as a stable version within the year.
|
||||||
@@ -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
|
||||||
|
|
||||||
@@ -27,6 +35,10 @@ Geolocation records from [DB-IP](https://db-ip.com) under [CC 4.0 license](https
|
|||||||
Executable versions of DZGUI published as release binaries ship with various runtime dependencies and the Python interpreter built in.
|
Executable versions of DZGUI published as release binaries ship with various runtime dependencies and the Python interpreter built in.
|
||||||
Users wishing to review the licenses to these components can inspect the `LICENSE` file located in the release tarball.
|
Users wishing to review the licenses to these components can inspect the `LICENSE` file located in the release tarball.
|
||||||
|
|
||||||
|
Steam and the Steam logo are trademarks and/or registered trademarks of Valve Corporation in the U.S. and/or other countries.
|
||||||
|
|
||||||
|
DayZ is a trademark and brand of Bohemia Interactive a.s.
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
DZGUI is beta-quality software and is provided as-is.
|
DZGUI is beta-quality software and is provided as-is.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from collections.abc import Iterator
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any, TYPE_CHECKING
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import APP_NAME
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from collections.abc import Iterator
|
||||||
|
|
||||||
|
|
||||||
class ACF:
|
class ACF:
|
||||||
def __init__(self, file: Path) -> None:
|
def __init__(self, file: Path) -> None:
|
||||||
@@ -42,7 +45,7 @@ class ACF:
|
|||||||
def dequote(cls, s: str) -> str:
|
def dequote(cls, s: str) -> str:
|
||||||
return s.rstrip('"').lstrip('"')
|
return s.rstrip('"').lstrip('"')
|
||||||
|
|
||||||
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
|
def parse(self, lines: "Iterator[list[str]]") -> dict[str, str]:
|
||||||
acf: dict[str, Any] = {}
|
acf: dict[str, Any] = {}
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
@@ -69,6 +72,28 @@ class ACF:
|
|||||||
except StopIteration:
|
except StopIteration:
|
||||||
return acf
|
return acf
|
||||||
|
|
||||||
|
def search_key(self, key: str) -> str:
|
||||||
|
return str(self.dict["AppState"][key])
|
||||||
|
|
||||||
|
def date_to_str(self, epoch: int) -> str | None:
|
||||||
|
if epoch == 0:
|
||||||
|
return None
|
||||||
|
return datetime.fromtimestamp(epoch).isoformat(" ")
|
||||||
|
|
||||||
|
def get_date_by_key(self, key: str) -> str | None:
|
||||||
|
try:
|
||||||
|
stamp = int(self.search_key(key))
|
||||||
|
return self.date_to_str(stamp)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_last_updated(self) -> str | None:
|
||||||
|
return self.get_date_by_key("LastUpdated")
|
||||||
|
|
||||||
|
def get_last_played(self) -> str | None:
|
||||||
|
return self.get_date_by_key("LastPlayed")
|
||||||
|
|
||||||
def get_allows_downloads(self) -> int | None:
|
def get_allows_downloads(self) -> int | None:
|
||||||
try:
|
try:
|
||||||
# TODO: consider coercing strs to ints on initial import
|
# TODO: consider coercing strs to ints on initial import
|
||||||
@@ -86,8 +111,8 @@ class WorkshopACF(ACF):
|
|||||||
def __init__(self, file: Path) -> None:
|
def __init__(self, file: Path) -> None:
|
||||||
super().__init__(file)
|
super().__init__(file)
|
||||||
|
|
||||||
# TODO: break into workshop parser and generic acf parser
|
def unpack(self, d: dict[str, Any], lines: list[Any] = []) -> str:
|
||||||
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
self.indent: int
|
||||||
t1 = "AppWorkshop"
|
t1 = "AppWorkshop"
|
||||||
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -24,14 +24,6 @@ from typing import Any
|
|||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ModMeta:
|
|
||||||
protocol: str
|
|
||||||
published_id: str
|
|
||||||
name: str
|
|
||||||
timestamp: str
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_mod_ids(steam_path: Path) -> list[int]:
|
def get_local_mod_ids(steam_path: Path) -> list[int]:
|
||||||
workshop_path = get_local_mod_path(steam_path)
|
workshop_path = get_local_mod_path(steam_path)
|
||||||
mods = get_local_mods(workshop_path)
|
mods = get_local_mods(workshop_path)
|
||||||
@@ -114,10 +106,6 @@ def get_delimited_mods(steam_path: Path) -> list[Any]:
|
|||||||
return parse_mods(mods)
|
return parse_mods(mods)
|
||||||
|
|
||||||
|
|
||||||
def get_missing_mods(local: list, remote: list) -> list:
|
|
||||||
return [mod for mod in remote if mod not in local]
|
|
||||||
|
|
||||||
|
|
||||||
def _hash(uid: str, use_custom: bool = False) -> str:
|
def _hash(uid: str, use_custom: bool = False) -> str:
|
||||||
md5 = hashlib.md5()
|
md5 = hashlib.md5()
|
||||||
md5.update(uid.encode("ascii"))
|
md5.update(uid.encode("ascii"))
|
||||||
@@ -152,7 +140,7 @@ def remove_stale_signatures(
|
|||||||
|
|
||||||
|
|
||||||
def find_stale_mods(config: Path) -> list[int]:
|
def find_stale_mods(config: Path) -> list[int]:
|
||||||
def push_record(rec: str) -> list:
|
def push_record(rec: str) -> list[int]:
|
||||||
record = fqip_to_record(rec)
|
record = fqip_to_record(rec)
|
||||||
if record is None:
|
if record is None:
|
||||||
return []
|
return []
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -115,7 +111,7 @@ def get_netmask() -> str:
|
|||||||
|
|
||||||
|
|
||||||
# TODO: rename
|
# TODO: rename
|
||||||
def test_ip(suffix: int, port: int, event: threading.Event) -> dict | None:
|
def test_ip(suffix: int, port: int, event: threading.Event) -> dict[str, Any] | None:
|
||||||
if event.is_set():
|
if event.is_set():
|
||||||
return None
|
return None
|
||||||
netmask = get_netmask()
|
netmask = get_netmask()
|
||||||
@@ -143,7 +139,9 @@ def sanitize(name: str) -> str:
|
|||||||
return name
|
return name
|
||||||
|
|
||||||
|
|
||||||
def parse_json(json: list) -> list:
|
def parse_json(
|
||||||
|
json: list[dict[str, Any]],
|
||||||
|
) -> list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]]:
|
||||||
"""
|
"""
|
||||||
Server metadata is underspecified and server operators
|
Server metadata is underspecified and server operators
|
||||||
tend to insert random garbage in the headers. In case
|
tend to insert random garbage in the headers. In case
|
||||||
@@ -284,43 +282,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 +343,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 +382,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 +479,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)
|
||||||
@@ -518,14 +509,14 @@ def record_to_fqip(record: Record) -> str:
|
|||||||
return f"{record.ip}:{record.gameport}:{record.qport}"
|
return f"{record.ip}:{record.gameport}:{record.qport}"
|
||||||
|
|
||||||
|
|
||||||
def response_to_fqip(res: dict) -> str:
|
def response_to_fqip(res: dict[str, Any]) -> str:
|
||||||
ip = res["addr"].split(":")[0]
|
ip = res["addr"].split(":")[0]
|
||||||
gameport = res["gameport"]
|
gameport = res["gameport"]
|
||||||
qport = res["addr"].split(":")[1]
|
qport = res["addr"].split(":")[1]
|
||||||
return f"{ip}:{gameport}:{qport}"
|
return f"{ip}:{gameport}:{qport}"
|
||||||
|
|
||||||
|
|
||||||
def response_to_record(res: dict) -> Record:
|
def response_to_record(res: dict[str, Any]) -> Record:
|
||||||
ip = res["addr"].split(":")[0]
|
ip = res["addr"].split(":")[0]
|
||||||
gameport = res["gameport"]
|
gameport = res["gameport"]
|
||||||
qport = res["addr"].split(":")[1]
|
qport = res["addr"].split(":")[1]
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
import binascii
|
||||||
|
import ctypes
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
import vdf # type: ignore
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from dzgui.api.steam import find_user_id_32
|
||||||
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
||||||
|
from dzgui.util.dirs import copy_dzgui_to_xdg_data, make_parents
|
||||||
|
from dzgui.util.strings import unknown
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class ShortcutMetadata:
|
||||||
|
appname: str
|
||||||
|
appid: int
|
||||||
|
exe_path: str
|
||||||
|
start_dir: str
|
||||||
|
icon: str
|
||||||
|
|
||||||
|
|
||||||
|
class Shortcuts:
|
||||||
|
def __init__(self, steam_path: Path) -> None:
|
||||||
|
self.user_config_path: Path
|
||||||
|
self.shortcuts_path = self.find_shortcuts_path(steam_path)
|
||||||
|
self._load_shortcuts(self.shortcuts_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_bpid(cls, uid: str) -> int:
|
||||||
|
u = uid.encode()
|
||||||
|
encoded = binascii.crc32(u)
|
||||||
|
return encoded | 0x80000000
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_signed_appid(cls, appid: int) -> int:
|
||||||
|
return ctypes.c_int(appid).value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def gen_exe_uid(cls, appname: str, exe: Path) -> str:
|
||||||
|
wrapped_exe = f'"{exe}"'
|
||||||
|
return appname + wrapped_exe
|
||||||
|
|
||||||
|
def find_appname_by_unsigned_id(self, appid: int) -> str:
|
||||||
|
# NOTE: bitmask signed int back to 32-bit CRC
|
||||||
|
# varying client versions treat case sensitivity differently
|
||||||
|
try:
|
||||||
|
for key in self.shortcuts["shortcuts"].keys():
|
||||||
|
if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid:
|
||||||
|
try:
|
||||||
|
return str(self.shortcuts["shortcuts"][key]["appname"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
try:
|
||||||
|
return str(self.shortcuts["shortcuts"][key]["AppName"])
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return unknown
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(e)
|
||||||
|
return unknown
|
||||||
|
|
||||||
|
def get_shortcuts(self) -> Any:
|
||||||
|
return self.shortcuts
|
||||||
|
|
||||||
|
def _load_shortcuts(self, path: Path) -> None:
|
||||||
|
try:
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
shortcuts = vdf.binary_load(f)
|
||||||
|
self.shortcuts = shortcuts
|
||||||
|
logger.debug("Loaded shortcuts file")
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.shortcuts = {"shortcuts": {}}
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def find_shortcuts_path(self, steam_path: Path) -> Path:
|
||||||
|
uid = find_user_id_32(steam_path)
|
||||||
|
self.user_config_path = steam_path.joinpath(f"userdata/{uid}/config")
|
||||||
|
return self.user_config_path.joinpath("shortcuts.vdf")
|
||||||
|
|
||||||
|
def add_shortcut(
|
||||||
|
self, appname: str, start_dir: Path, exe_path: Path, icon: Path
|
||||||
|
) -> None:
|
||||||
|
uid = self.gen_exe_uid(appname, exe_path)
|
||||||
|
self.bpid = self.gen_bpid(uid)
|
||||||
|
self.signed_appid = self.gen_signed_appid(self.bpid)
|
||||||
|
|
||||||
|
meta = ShortcutMetadata(
|
||||||
|
appname, self.signed_appid, str(exe_path), str(start_dir), str(icon)
|
||||||
|
)
|
||||||
|
entry = self._create_entry(meta)
|
||||||
|
self._insert_at_last_index(entry)
|
||||||
|
|
||||||
|
def add_grid_images(self, images: Path) -> None:
|
||||||
|
"""
|
||||||
|
{BPID}_hero.png: hero (splash)
|
||||||
|
{BPID}p.ping: portrait boxart in library
|
||||||
|
{BPID}_logo.png: logo overlay on top of hero
|
||||||
|
{BPID}.png: legacy Big Picture/landscape image
|
||||||
|
"""
|
||||||
|
|
||||||
|
grid_path = self.user_config_path.joinpath("grid")
|
||||||
|
for img in ("_hero", "p", "_logo"):
|
||||||
|
filename = str(self.bpid) + img
|
||||||
|
dest = grid_path.joinpath(filename).with_suffix(".png")
|
||||||
|
make_parents(dest)
|
||||||
|
b = images.joinpath(img).with_suffix(".png").read_bytes()
|
||||||
|
dest.write_bytes(b)
|
||||||
|
|
||||||
|
# NOTE: legacy Big Picture header
|
||||||
|
b = images.joinpath("_hero.png").read_bytes()
|
||||||
|
dest = grid_path.joinpath(str(self.bpid)).with_suffix(".png")
|
||||||
|
dest.write_bytes(b)
|
||||||
|
|
||||||
|
def _insert_at_last_index(self, entry: dict[str, Any]) -> None:
|
||||||
|
try:
|
||||||
|
last = list(self.shortcuts["shortcuts"].keys())[-1]
|
||||||
|
n = int(last) + 1
|
||||||
|
except Exception:
|
||||||
|
n = 0
|
||||||
|
self.shortcuts["shortcuts"][str(n)] = entry
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _create_entry(cls, meta: ShortcutMetadata) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game
|
||||||
|
https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts
|
||||||
|
|
||||||
|
Key case is not internally consistent and varies between client versions
|
||||||
|
Most keys use Pascal case, but some do not
|
||||||
|
|
||||||
|
appid: signed int CRC
|
||||||
|
Exe: absolute path to the executable, must be wrapped in literal quotes
|
||||||
|
StartDir: directory the executable starts in, generally the parent
|
||||||
|
"""
|
||||||
|
|
||||||
|
NEW_ENTRY: dict[str, Any] = {}
|
||||||
|
NEW_ENTRY["appid"] = meta.appid
|
||||||
|
NEW_ENTRY["AppName"] = meta.appname
|
||||||
|
NEW_ENTRY["Exe"] = f'"{meta.exe_path}"'
|
||||||
|
NEW_ENTRY["StartDir"] = f"{meta.start_dir}"
|
||||||
|
NEW_ENTRY["icon"] = meta.icon
|
||||||
|
NEW_ENTRY["ShortcutPath"] = ""
|
||||||
|
NEW_ENTRY["LaunchOptions"] = ""
|
||||||
|
NEW_ENTRY["IsHidden"] = 0
|
||||||
|
NEW_ENTRY["AllowDesktopConfig"] = 1
|
||||||
|
NEW_ENTRY["AllowOverlay"] = 1
|
||||||
|
NEW_ENTRY["OpenVR"] = 0
|
||||||
|
NEW_ENTRY["Devkit"] = 0
|
||||||
|
NEW_ENTRY["DevkitGameID"] = ""
|
||||||
|
NEW_ENTRY["DevkitOverrideAppID"] = 0
|
||||||
|
NEW_ENTRY["LastPlayTime"] = 0
|
||||||
|
NEW_ENTRY["FlatpakAppID"] = ""
|
||||||
|
|
||||||
|
NEW_ENTRY["sortas"] = ""
|
||||||
|
NEW_ENTRY["tags"] = {}
|
||||||
|
return NEW_ENTRY
|
||||||
|
|
||||||
|
def 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:
|
||||||
|
if self.shortcuts_path.is_file():
|
||||||
|
try:
|
||||||
|
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||||
|
shutil.copy(self.shortcuts_path, backup)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
try:
|
||||||
|
with open(self.shortcuts_path, "wb") as f:
|
||||||
|
vdf.binary_dump(self.shortcuts, f)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def add_steam_shortcut(steam_path: Path, exe_path: Path) -> None:
|
||||||
|
try:
|
||||||
|
start_dir = exe_path.parent
|
||||||
|
|
||||||
|
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
||||||
|
images = Path(str(traversable))
|
||||||
|
icon = images.joinpath("icon.png")
|
||||||
|
|
||||||
|
shortcuts = Shortcuts(Path(steam_path))
|
||||||
|
shortcuts.add_shortcut(APP_NAME, start_dir, exe_path, icon)
|
||||||
|
shortcuts.save_shortcuts()
|
||||||
|
shortcuts.add_grid_images(images)
|
||||||
|
|
||||||
|
copy_dzgui_to_xdg_data(exe_path)
|
||||||
|
logger.debug("Finished creating Steam shortcut")
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
@@ -1,60 +1,69 @@
|
|||||||
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.const.endpoints import VALIDATE
|
||||||
|
from dzgui.init.prereqs import has_any_client
|
||||||
|
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
||||||
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_STEAM_PATH,
|
FLATPAK_STEAM_PATH,
|
||||||
LIBRARYFOLDERS_PATH,
|
LIBRARYFOLDERS_PATH,
|
||||||
UBUNTU_STEAM_PATH,
|
|
||||||
REQUEST_TIMEOUT,
|
REQUEST_TIMEOUT,
|
||||||
|
UBUNTU_STEAM_PATH,
|
||||||
VDF_PATH,
|
VDF_PATH,
|
||||||
)
|
)
|
||||||
from dzgui.const.endpoints import (
|
from dzgui.const.endpoints import (
|
||||||
APP_DETAILS,
|
STEAM_APP_DETAILS,
|
||||||
SUB_ENDPOINT,
|
|
||||||
STEAM_PUBLISHED_FILES,
|
STEAM_PUBLISHED_FILES,
|
||||||
UNSUB_ENDPOINT,
|
STEAM_SUB,
|
||||||
|
STEAM_UNSUB,
|
||||||
)
|
)
|
||||||
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
|
||||||
from dzgui.util.strings import unknown
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
class AppNotInstalledError(Exception):
|
class AppNotInstalledError(Exception):
|
||||||
"""App not present in user's libraryfolders"""
|
"""App not present in user's libraryfolders"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class AppMovedError(Exception):
|
class AppMovedError(Exception):
|
||||||
"""VDF points to a nonexistent location on disk"""
|
"""VDF points to a nonexistent location on disk"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class VDFLoadError(Exception):
|
class VDFLoadError(Exception):
|
||||||
"""Malformed VDF or JSON conversion"""
|
"""Malformed VDF or JSON conversion"""
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_steam_paths() -> list[tuple[Path, str]]:
|
def get_steam_paths() -> list[tuple[Path, str]]:
|
||||||
paths = []
|
paths = []
|
||||||
if has_steam_client():
|
if has_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:
|
||||||
@@ -75,6 +84,10 @@ 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_flatpak_client() is False:
|
||||||
|
continue
|
||||||
|
if path is not FLATPAK_PATH and has_steam_client() 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
|
||||||
@@ -137,6 +150,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"])
|
||||||
@@ -145,6 +161,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:
|
||||||
@@ -169,6 +204,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),
|
||||||
@@ -204,20 +240,49 @@ def launch_offline(
|
|||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
|
def find_loginusers(path: Path) -> Path:
|
||||||
|
return path / "config" / "loginusers.vdf"
|
||||||
|
|
||||||
|
|
||||||
def find_user_id(path: Path) -> str | None:
|
def find_user_id(path: Path) -> str | None:
|
||||||
resolved_path = path / "config" / "loginusers.vdf"
|
resolved_path = find_loginusers(path)
|
||||||
try:
|
try:
|
||||||
with open(resolved_path, "r") as f:
|
with open(resolved_path, "r") as f:
|
||||||
v = vdf.load(f)
|
v = vdf.load(f)
|
||||||
for user in v["users"]:
|
users = v["users"]
|
||||||
if v["users"][user]["MostRecent"] == "1":
|
# NOTE: beta client: /package/beta
|
||||||
return str(user)
|
last_user = str(next(iter(users)))
|
||||||
return None
|
if len(users) == 1:
|
||||||
|
return last_user
|
||||||
|
last_stamp = int(users[last_user]["timestamp"])
|
||||||
|
for user in users:
|
||||||
|
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.warn(e)
|
logger.debug(e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def find_user_id_32(path: Path) -> int:
|
||||||
|
uid = find_user_id(path)
|
||||||
|
if uid is None:
|
||||||
|
raise ValueError("Failed to parse a valid Steam32 ID")
|
||||||
|
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,
|
||||||
@@ -229,36 +294,32 @@ 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:
|
||||||
update_workshop(key, mod, SUB_ENDPOINT)
|
update_workshop(key, mod, STEAM_SUB)
|
||||||
|
|
||||||
|
|
||||||
def unsubscribe(key: str, mod: int) -> None:
|
def unsubscribe(key: str, mod: int) -> None:
|
||||||
update_workshop(key, mod, UNSUB_ENDPOINT)
|
update_workshop(key, mod, STEAM_UNSUB)
|
||||||
|
|
||||||
|
|
||||||
def gen_shortcut() -> None:
|
def validate_mod(client: str, mod: str) -> None:
|
||||||
# TODO:
|
|
||||||
"""
|
|
||||||
during setup, prompt user to select matching user id from loginusers
|
|
||||||
show user account name, select outer steam id
|
|
||||||
steam/userdata/<steamid_32>/config/shortcuts.vdf
|
|
||||||
"""
|
|
||||||
# STEAMID_MAGIC = 76561197960265728
|
|
||||||
# STEAMID_64 - STEAMID_MAGIC = STEAMID32
|
|
||||||
# or get right-most 32 bits
|
|
||||||
# STEAMID_64 & 0xFFFFFFFF
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@deprecated("Use subscribe()")
|
|
||||||
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
subprocess.Popen([*client_args, "+workshop_download_item", str(APPID_DAYZ), mod])
|
||||||
|
|
||||||
|
|
||||||
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
|
@deprecated("Cf. https://github.com/ValveSoftware/steam-for-linux/issues/9672")
|
||||||
@@ -328,10 +389,33 @@ def get_running_app() -> int | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
def get_last_updated(path: Path, appid: int) -> str | None:
|
||||||
root_path = get_app_path(path, appid)
|
root_path = get_app_path(path, appid)
|
||||||
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||||
flag = ACF(acf).get_allows_downloads()
|
try:
|
||||||
|
return ACF(acf).get_last_updated()
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_played(path: Path, appid: int) -> str | None:
|
||||||
|
root_path = get_app_path(path, appid)
|
||||||
|
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||||
|
try:
|
||||||
|
return ACF(acf).get_last_played()
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
||||||
|
try:
|
||||||
|
root_path = get_app_path(path, appid)
|
||||||
|
acf = root_path.joinpath(f"steamapps/appmanifest_{appid}.acf")
|
||||||
|
flag = ACF(acf).get_allows_downloads()
|
||||||
|
except AppNotInstalledError:
|
||||||
|
flag = 0
|
||||||
match flag:
|
match flag:
|
||||||
# NOTE: adheres to global client setting
|
# NOTE: adheres to global client setting
|
||||||
case 0:
|
case 0:
|
||||||
@@ -346,9 +430,11 @@ def get_app_allows_downloads(path: Path, appid: int) -> bool:
|
|||||||
case _:
|
case _:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_config(path: Path) -> Path:
|
def get_config(path: Path) -> Path:
|
||||||
return path.joinpath("config/config.vdf")
|
return path.joinpath("config/config.vdf")
|
||||||
|
|
||||||
|
|
||||||
def get_client_allows_downloads(path: Path) -> bool:
|
def get_client_allows_downloads(path: Path) -> bool:
|
||||||
config = get_config(path)
|
config = get_config(path)
|
||||||
try:
|
try:
|
||||||
@@ -371,14 +457,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:
|
||||||
@@ -408,13 +510,20 @@ def get_app_path(folders_path: Path, appid: int) -> Path:
|
|||||||
return Path(app_path)
|
return Path(app_path)
|
||||||
|
|
||||||
|
|
||||||
def get_app_name(appid: int) -> str:
|
def get_app_name(appid: int) -> str | None:
|
||||||
payload = {"appids": [appid]}
|
payload = {"appids": [appid]}
|
||||||
res = requests.get(APP_DETAILS, params=payload)
|
res = requests.get(STEAM_APP_DETAILS, params=payload)
|
||||||
if res.status_code != 200:
|
if res.status_code != 200:
|
||||||
return unknown
|
return None
|
||||||
try:
|
try:
|
||||||
return str(res.json()[str(appid)]["data"]["name"])
|
return str(res.json()[str(appid)]["data"]["name"])
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(e)
|
logger.debug(e)
|
||||||
return unknown
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Currently unused")
|
||||||
|
def validate_dayz(client: str) -> None:
|
||||||
|
client_args = concat_bash_args(client)
|
||||||
|
uri = VALIDATE + str(APPID_DAYZ)
|
||||||
|
subprocess.Popen([*client_args, uri])
|
||||||
@@ -1,10 +1,18 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import (
|
||||||
|
APP_NAME,
|
||||||
|
APP_NAME_LOWER,
|
||||||
|
FLATPAK_ROOT_PATH,
|
||||||
|
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
|
||||||
@@ -14,13 +22,13 @@ from dzgui.init.migrate import (
|
|||||||
copy_state_files,
|
copy_state_files,
|
||||||
migrate_cols_file,
|
migrate_cols_file,
|
||||||
)
|
)
|
||||||
from dzgui.init.flock import lock_acquire
|
from dzgui.init.prereqs import has_flatpak_client, has_steam_client
|
||||||
from dzgui.init.prereqs import has_steam_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.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
|
||||||
|
|
||||||
@@ -34,10 +42,7 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
# TODO: profile load time
|
# TODO: profile load time
|
||||||
def make_parents(path: "Path") -> None:
|
|
||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
|
|
||||||
|
|
||||||
def setup_logger(log_path: "Path") -> None:
|
def setup_logger(log_path: "Path") -> None:
|
||||||
@@ -98,9 +103,24 @@ def copy_bare_configs(config: "Path", resolution: "Path") -> tuple[bool, bool]:
|
|||||||
return (config_changed, state_changed)
|
return (config_changed, state_changed)
|
||||||
|
|
||||||
|
|
||||||
def load_gui(version: str, is_debug: bool) -> None:
|
def load_steam_icon() -> None:
|
||||||
lock = lock_acquire() # noqa
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk # noqa
|
||||||
|
|
||||||
|
try:
|
||||||
|
traversable = resources.files(APP_NAME_LOWER).joinpath(ICONS_PATH)
|
||||||
|
icons = Path(str(traversable))
|
||||||
|
|
||||||
|
theme = Gtk.IconTheme.get_default()
|
||||||
|
paths = theme.get_search_path()
|
||||||
|
theme.set_search_path(paths + [str(icons)])
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def build_gui(version: str, is_debug: bool) -> App | None:
|
||||||
set_locale()
|
set_locale()
|
||||||
# NOTE: consider aborting this check if steam deck
|
# NOTE: consider aborting this check if steam deck
|
||||||
xdg_paths = get_xdg_paths()
|
xdg_paths = get_xdg_paths()
|
||||||
@@ -121,30 +141,32 @@ 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
|
||||||
SetupWizard(_is_steam_deck, XDG.config)
|
wizard = SetupWizard(_is_steam_deck, XDG, xdg_paths)
|
||||||
|
|
||||||
# NOTE: implies that setup wizard failed or was closed
|
if not wizard.is_setup_complete():
|
||||||
if has_new_config(XDG.config) is False:
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
setup_logger(XDG.debug)
|
setup_logger(XDG.debug)
|
||||||
with open(XDG.debug, "w") as f:
|
with open(XDG.debug, "w") as f:
|
||||||
f.truncate(0)
|
f.truncate(0)
|
||||||
|
|
||||||
if _is_steam_deck is False:
|
if _is_steam_deck is False:
|
||||||
# TODO: sudo escalation dialog
|
if FLATPAK_ROOT_PATH in lookup(XDG.config, Preferences.DEFAULT):
|
||||||
# count = get_map_count()
|
has_client = has_flatpak_client()
|
||||||
# TODO: move into module
|
else:
|
||||||
if has_steam_client() is False:
|
has_client = has_steam_client()
|
||||||
|
if has_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,
|
||||||
@@ -156,4 +178,6 @@ 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)
|
load_steam_icon()
|
||||||
|
app = App(prefs)
|
||||||
|
return app
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -1,18 +1,43 @@
|
|||||||
|
import stat
|
||||||
import textwrap
|
import textwrap
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
|
||||||
|
|
||||||
|
|
||||||
|
def get_share_path(exe_path: Path) -> Path:
|
||||||
|
return exe_path.parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def write_desktop_file(exe_path: Path) -> Path:
|
||||||
|
icon = find_icon_resource()
|
||||||
|
|
||||||
def write_desktop_file(share_path: Path) -> None:
|
|
||||||
template = f"""\
|
template = f"""\
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Version=1.0
|
Version=1.0
|
||||||
Type=Application
|
Type=Application
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Exec={share_path}/launch.sh
|
Exec={exe_path}
|
||||||
Name=DZGUI
|
Name=DZGUI
|
||||||
Comment=dzgui
|
Comment=DayZ server browser and mod manager
|
||||||
Icon={share_path}/dzgui
|
Icon={icon}
|
||||||
Categories=Game"""
|
Categories=Game"""
|
||||||
|
|
||||||
file = share_path / "dzgui.desktop"
|
copy_dzgui_to_xdg_data(exe_path)
|
||||||
file.write_text(textwrap.dedent(template))
|
|
||||||
|
share_path = get_share_path(exe_path)
|
||||||
|
desktop_file = share_path.joinpath("applications/dzgui.desktop")
|
||||||
|
make_parents(desktop_file)
|
||||||
|
desktop_file.write_text(textwrap.dedent(template))
|
||||||
|
desktop_file.chmod(desktop_file.stat().st_mode | stat.S_IEXEC)
|
||||||
|
return desktop_file
|
||||||
|
|
||||||
|
|
||||||
|
def write_desktop_shortcut(desktop_file: Path) -> None:
|
||||||
|
# NOTE: necessarily depends on the above (UI blocks creation without XDG entry first)
|
||||||
|
link = Path.home().joinpath("Desktop/dzgui.desktop")
|
||||||
|
make_parents(link)
|
||||||
|
if link.exists():
|
||||||
|
link.unlink()
|
||||||
|
link.symlink_to(desktop_file)
|
||||||
@@ -3,6 +3,9 @@ import logging
|
|||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
import shutil
|
import shutil
|
||||||
|
import socket
|
||||||
|
import sqlite3
|
||||||
|
import time
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
@@ -22,15 +25,16 @@ def find_download_url(url: str) -> str | None:
|
|||||||
return str(urls[0])
|
return str(urls[0])
|
||||||
|
|
||||||
|
|
||||||
def get_ipdb(ips_path: Path) -> None:
|
def get_ipdb(db_path: Path) -> None:
|
||||||
try:
|
try:
|
||||||
url = find_download_url(DB_IP)
|
url = find_download_url(DB_IP)
|
||||||
if url is None:
|
if url is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
ips_path = db_path.with_suffix(".csv")
|
||||||
|
month_file = db_path.parent / ".month"
|
||||||
date = find_date(url)
|
date = find_date(url)
|
||||||
month_file = ips_path.parent / ".month"
|
if db_path.exists() and month_file.exists():
|
||||||
if ips_path.exists() and month_file.exists():
|
|
||||||
old_date = month_file.read_text().rstrip("\n")
|
old_date = month_file.read_text().rstrip("\n")
|
||||||
if old_date == date:
|
if old_date == date:
|
||||||
logger.info(f"IP DB date matches: {date}")
|
logger.info(f"IP DB date matches: {date}")
|
||||||
@@ -45,6 +49,9 @@ def get_ipdb(ips_path: Path) -> None:
|
|||||||
logger.info("Stripping IPv6 records")
|
logger.info("Stripping IPv6 records")
|
||||||
strip_ipv6(ips_path)
|
strip_ipv6(ips_path)
|
||||||
|
|
||||||
|
create_table(db_path, ips_path)
|
||||||
|
ips_path.unlink()
|
||||||
|
|
||||||
with open(month_file, "w") as f:
|
with open(month_file, "w") as f:
|
||||||
f.write(date)
|
f.write(date)
|
||||||
logger.info(f"Wrote {date} to {month_file}")
|
logger.info(f"Wrote {date} to {month_file}")
|
||||||
@@ -108,3 +115,95 @@ def strip_ipv6(path: Path) -> None:
|
|||||||
out.write(merged)
|
out.write(merged)
|
||||||
path.unlink()
|
path.unlink()
|
||||||
alt_path.rename(path)
|
alt_path.rename(path)
|
||||||
|
|
||||||
|
|
||||||
|
def ip_to_int(ip: str) -> int:
|
||||||
|
return int.from_bytes(socket.inet_aton(ip), "big")
|
||||||
|
|
||||||
|
|
||||||
|
def marshal_rows(ip_file: Path) -> list[tuple[int, int, str, str]]:
|
||||||
|
YIELD = 0.003
|
||||||
|
SLEEP = 0.001
|
||||||
|
with open(ip_file, "r") as f:
|
||||||
|
rows: list[tuple[int, int, str, str]] = []
|
||||||
|
# NOTE: yield to GIL periodically to reduce load on spinner
|
||||||
|
last_yield = time.monotonic()
|
||||||
|
for row in f:
|
||||||
|
start, end, lat, lon = row.rstrip("\n").split(",")
|
||||||
|
int_start = ip_to_int(start)
|
||||||
|
int_end = ip_to_int(end)
|
||||||
|
rows.append((int_start, int_end, lat, lon))
|
||||||
|
|
||||||
|
now = time.monotonic()
|
||||||
|
if (now - last_yield) >= YIELD:
|
||||||
|
time.sleep(SLEEP)
|
||||||
|
last_yield = time.monotonic()
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def create_table(db_file: Path, ip_file: Path) -> None:
|
||||||
|
create = """
|
||||||
|
CREATE TABLE IF NOT EXISTS ips (
|
||||||
|
start_range INTEGER PRIMARY KEY,
|
||||||
|
end_range INTEGER,
|
||||||
|
latitude REAL,
|
||||||
|
longitude REAL
|
||||||
|
) WITHOUT ROWID;
|
||||||
|
"""
|
||||||
|
insert = """
|
||||||
|
INSERT INTO ips
|
||||||
|
(start_range, end_range, latitude, longitude)
|
||||||
|
values(?, ?, ?, ?)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if db_file.exists():
|
||||||
|
db_file.unlink()
|
||||||
|
|
||||||
|
conn = sqlite3.connect(":memory:")
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
try:
|
||||||
|
c.execute(create)
|
||||||
|
rows = marshal_rows(ip_file)
|
||||||
|
c.executemany(insert, rows)
|
||||||
|
logger.info(f"Inserted {len(rows)} IP records into database")
|
||||||
|
conn.commit()
|
||||||
|
with sqlite3.connect(db_file) as dest:
|
||||||
|
conn.backup(dest)
|
||||||
|
logger.info(f"Serialized IP records to database: '{db_file}'")
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def read_from_table(db_path: Path, ip: str) -> tuple[float, float]:
|
||||||
|
if not db_path.exists():
|
||||||
|
raise FileNotFoundError(f"No file found at the location '{db_path}")
|
||||||
|
|
||||||
|
conn = sqlite3.connect(db_path)
|
||||||
|
c = conn.cursor()
|
||||||
|
|
||||||
|
int_ip = ip_to_int(ip)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT end_range, latitude, longitude
|
||||||
|
FROM ips
|
||||||
|
WHERE start_range <= ?
|
||||||
|
ORDER BY start_range DESC
|
||||||
|
LIMIT 1
|
||||||
|
"""
|
||||||
|
index = """
|
||||||
|
CREATE INDEX IF NOT EXISTS start_index
|
||||||
|
ON ips(start_range);
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
c.execute(index)
|
||||||
|
conn.commit()
|
||||||
|
c.execute(query, (int_ip,))
|
||||||
|
row = c.fetchone()
|
||||||
|
end, lat, lon = row
|
||||||
|
conn.close()
|
||||||
|
return lat, lon
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
raise e
|
||||||
@@ -4,7 +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):
|
||||||
@@ -40,23 +39,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.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
|
|
||||||
@@ -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
|
||||||
@@ -16,8 +22,9 @@ class Xdg:
|
|||||||
version: Path
|
version: Path
|
||||||
system: Path
|
system: Path
|
||||||
debug: Path
|
debug: Path
|
||||||
ips: Path
|
db: Path
|
||||||
filters: Path
|
filters: Path
|
||||||
|
shortcut: Path
|
||||||
|
|
||||||
|
|
||||||
def is_writeable(path_str: str) -> bool:
|
def is_writeable(path_str: str) -> bool:
|
||||||
@@ -44,7 +51,7 @@ def is_writeable(path_str: str) -> bool:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_xdg_paths() -> dict:
|
def get_xdg_paths() -> dict[str, Path]:
|
||||||
home = Path.home()
|
home = Path.home()
|
||||||
xdg_paths = {
|
xdg_paths = {
|
||||||
"XDG_CONFIG_HOME": home / ".config",
|
"XDG_CONFIG_HOME": home / ".config",
|
||||||
@@ -52,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:
|
||||||
@@ -64,9 +73,10 @@ def get_xdg_paths() -> dict:
|
|||||||
return resolved_paths
|
return resolved_paths
|
||||||
|
|
||||||
|
|
||||||
def parse_filepaths(xdg: dict) -> Xdg:
|
def parse_filepaths(xdg: dict[str, Path]) -> Xdg:
|
||||||
config = xdg["XDG_CONFIG_HOME"]
|
config = xdg["XDG_CONFIG_HOME"]
|
||||||
state = xdg["XDG_STATE_HOME"]
|
state = xdg["XDG_STATE_HOME"]
|
||||||
|
share = xdg["XDG_DATA_HOME"]
|
||||||
|
|
||||||
config = config / "config.json"
|
config = config / "config.json"
|
||||||
|
|
||||||
@@ -78,9 +88,11 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
resolution = state / "dzg.res.json"
|
resolution = state / "dzg.res.json"
|
||||||
history = state / "dzg.history"
|
history = state / "dzg.history"
|
||||||
versions = state / "dzg.versions"
|
versions = state / "dzg.versions"
|
||||||
ips = state / "ips.csv"
|
db = state / "ips.db"
|
||||||
filters = state / "dzg.filters.json"
|
filters = state / "dzg.filters.json"
|
||||||
|
|
||||||
|
shortcut = share / APP_NAME_LOWER
|
||||||
|
|
||||||
return Xdg(
|
return Xdg(
|
||||||
config,
|
config,
|
||||||
columns,
|
columns,
|
||||||
@@ -90,6 +102,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
versions,
|
versions,
|
||||||
system,
|
system,
|
||||||
debug,
|
debug,
|
||||||
ips,
|
db,
|
||||||
filters
|
filters,
|
||||||
|
shortcut,
|
||||||
)
|
)
|
||||||
@@ -2,7 +2,6 @@
|
|||||||
Generic defaults used when initializing a config file from scratch
|
Generic defaults used when initializing a config file from scratch
|
||||||
"""
|
"""
|
||||||
config_boilerplate = {
|
config_boilerplate = {
|
||||||
"bm_api": "",
|
|
||||||
"fav_server": "",
|
"fav_server": "",
|
||||||
"fav_label": "",
|
"fav_label": "",
|
||||||
"name": "",
|
"name": "",
|
||||||
|
|||||||
@@ -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,38 +31,14 @@ 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
|
|
||||||
CARET_DOWN = "go-down-symbolic"
|
|
||||||
CARET_UP = "go-up-symbolic"
|
|
||||||
CLIPBOARD = "edit-copy-symbolic"
|
|
||||||
FOLDER = "folder-symbolic"
|
|
||||||
EDIT_DELETE = "edit-delete-symbolic"
|
|
||||||
ERROR = "dialog-error-symbolic"
|
|
||||||
HELP_BUBBLE = "help-about-symbolic"
|
|
||||||
INPUT_KEYBOARD = "input-keyboard-symbolic"
|
|
||||||
LIST_ADD = "list-add-symbolic"
|
|
||||||
REFRESH_ICON = "view-refresh-symbolic"
|
|
||||||
SEARCH_ICON = "system-search-symbolic"
|
|
||||||
STEAM_ICON = "steam_tray_mono"
|
|
||||||
VIEW_CONCEAL = "view-conceal-symbolic"
|
|
||||||
VIEW_REVEAL = "view-reveal-symbolic"
|
|
||||||
WARNING = "dialog-warning-symbolic"
|
|
||||||
WEB_BROWSER = "web-browser-symbolic"
|
|
||||||
|
|
||||||
SEPARATOR = "SEPARATOR"
|
SEPARATOR = "SEPARATOR"
|
||||||
|
|
||||||
# TODO: drop
|
|
||||||
NO_PADDING = 0
|
|
||||||
NO_EXPAND = False
|
|
||||||
NO_FILL = False
|
|
||||||
EXPAND = True
|
|
||||||
FILL = True
|
|
||||||
|
|
||||||
SCROLL_INCREMENT = 50
|
SCROLL_INCREMENT = 50
|
||||||
WINDOW_DEFAULT_X = 1400
|
WINDOW_DEFAULT_X = 1400
|
||||||
WINDOW_DEFAULT_Y = 800
|
WINDOW_DEFAULT_Y = 800
|
||||||
@@ -72,11 +50,15 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv"
|
|||||||
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
||||||
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
||||||
CHANGELOG_PATH = "data/CHANGELOG.md"
|
CHANGELOG_PATH = "data/CHANGELOG.md"
|
||||||
HERO_PATH = "data/images/hero.png"
|
IMAGES_PATH = "data/images"
|
||||||
|
ICONS_PATH = "data/images/gtk"
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
@@ -85,5 +67,11 @@ LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
|
|||||||
TMP_PATH = "/tmp"
|
TMP_PATH = "/tmp"
|
||||||
TMP_TARBALL = "/tmp/dzgui.tar.gz"
|
TMP_TARBALL = "/tmp/dzgui.tar.gz"
|
||||||
TMP_EXE = "/tmp/dzgui/dzgui"
|
TMP_EXE = "/tmp/dzgui/dzgui"
|
||||||
|
LOCK_FILE = "/tmp/dzgui.flock"
|
||||||
|
|
||||||
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"
|
||||||
@@ -3,11 +3,10 @@ STEAM_PUBLISHED_FILES = (
|
|||||||
"https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1"
|
"https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1"
|
||||||
)
|
)
|
||||||
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
|
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
|
||||||
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
STEAM_SUB = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
||||||
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
STEAM_UNSUB = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
||||||
APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
|
STEAM_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"
|
||||||
@@ -19,6 +18,7 @@ COORDS_API = "http://ip-api.com/json"
|
|||||||
# steam URIs
|
# steam URIs
|
||||||
WORKSHOP = "steam://url/CommunityFilePage/"
|
WORKSHOP = "steam://url/CommunityFilePage/"
|
||||||
OPEN_URL = "steam://openurl/"
|
OPEN_URL = "steam://openurl/"
|
||||||
|
VALIDATE = "steam://validate/"
|
||||||
|
|
||||||
# user facing
|
# user facing
|
||||||
GITHUB_PAGES = "https://aclist.github.io/dzgui/index"
|
GITHUB_PAGES = "https://aclist.github.io/dzgui/index"
|
||||||
@@ -27,5 +27,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"
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from typing import Any, Self
|
|
||||||
|
|
||||||
from dzgui.util import strings
|
from dzgui.util import strings
|
||||||
|
from dzgui.strings import rows
|
||||||
|
|
||||||
|
|
||||||
class Port(Enum):
|
class Port(Enum):
|
||||||
@@ -46,136 +48,61 @@ class FilterMode(Enum):
|
|||||||
TOGGLE_ON = 3
|
TOGGLE_ON = 3
|
||||||
|
|
||||||
|
|
||||||
class EnumWithAttrs(Enum):
|
@dataclass(slots=True, frozen=True)
|
||||||
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
|
class ModButtonMeta:
|
||||||
value = len(cls.__members__) + 1
|
label: str
|
||||||
obj = object.__new__(cls)
|
tooltip: str
|
||||||
obj._value_ = value
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, d: dict):
|
|
||||||
self.dict = d
|
|
||||||
|
|
||||||
|
|
||||||
class Preferences(EnumWithAttrs):
|
@dataclass(slots=True, frozen=True)
|
||||||
STEAM = {
|
class ButtonTypeMeta:
|
||||||
"key": "steam_api",
|
label: str
|
||||||
}
|
tooltip: str
|
||||||
BM = {
|
opens: NotebookPage | None
|
||||||
"key": "bm_api",
|
|
||||||
}
|
|
||||||
CLIENT = {
|
|
||||||
"key": "client",
|
|
||||||
}
|
|
||||||
WINDOW = {
|
|
||||||
"key": "fullscreen",
|
|
||||||
}
|
|
||||||
DIST = {
|
|
||||||
"key": "use_miles",
|
|
||||||
}
|
|
||||||
NAME = {
|
|
||||||
"key": "name",
|
|
||||||
}
|
|
||||||
INSTALL = {
|
|
||||||
"key": "auto_install",
|
|
||||||
}
|
|
||||||
DEBUG = {
|
|
||||||
"key": "debug",
|
|
||||||
}
|
|
||||||
DEFAULT = {
|
|
||||||
"key": "default_steam_path",
|
|
||||||
}
|
|
||||||
FAV_LBL = {
|
|
||||||
"key": "fav_label",
|
|
||||||
}
|
|
||||||
FAV_SRV = {
|
|
||||||
"key": "fav_server",
|
|
||||||
}
|
|
||||||
IP_LIST = {
|
|
||||||
"key": "ip_list",
|
|
||||||
}
|
|
||||||
BRANCH = {
|
|
||||||
"key": "branch",
|
|
||||||
}
|
|
||||||
START_TAB = {
|
|
||||||
"key": "start_tab",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class NotebookPage(EnumWithAttrs):
|
@dataclass(slots=True, frozen=True)
|
||||||
OFFLINE = {"crumbs": strings.crumbs.offline, "statusbar": True}
|
class ConfigKey:
|
||||||
CHANGELOG = {"crumbs": strings.crumbs.changelog, "statusbar": True}
|
key: str
|
||||||
DEVELOPERS = {"crumbs": strings.crumbs.developers, "statusbar": False}
|
|
||||||
HELP = {"crumbs": strings.crumbs._help, "statusbar": True}
|
|
||||||
KEYS = {"crumbs": strings.crumbs.keys, "statusbar": True}
|
|
||||||
LOG = {"crumbs": strings.crumbs.log, "statusbar": True}
|
|
||||||
MODS = {"crumbs": strings.crumbs.mods, "statusbar": True}
|
|
||||||
OPTIONS = {"crumbs": strings.crumbs.options, "statusbar": False}
|
|
||||||
SERVERS = {"crumbs": strings.crumbs.servers, "statusbar": True}
|
|
||||||
THANKS = {"crumbs": strings.crumbs.thanks, "statusbar": True}
|
|
||||||
CONNECTION = {"crumbs": "Connect", "statusbar": True}
|
|
||||||
|
|
||||||
|
|
||||||
class RowType(EnumWithAttrs):
|
@dataclass(slots=True, frozen=True)
|
||||||
# TODO: strings
|
class NotebookMeta:
|
||||||
CHANGELOG = {
|
crumbs: str
|
||||||
"label": "View changelog",
|
has_statusbar: bool
|
||||||
"tooltip": "Opens the DZGUI changelog",
|
|
||||||
}
|
|
||||||
SHOW_LOG = {
|
|
||||||
"label": "Show debug log",
|
|
||||||
"tooltip": "Read the DZGUI log generated since startup",
|
|
||||||
}
|
|
||||||
SEPARATOR = {
|
|
||||||
"label": "SEPARATOR",
|
|
||||||
"tooltip": "",
|
|
||||||
}
|
|
||||||
DUMP_LOG = {
|
|
||||||
"label": "Output system info to log file",
|
|
||||||
"tooltip": "Dump diagnostic data for troubleshooting",
|
|
||||||
}
|
|
||||||
DOCS = {
|
|
||||||
"label": "Documentation/help files (GitHub) ⧉",
|
|
||||||
"tooltip": "Open the DZGUI documentation in a browser",
|
|
||||||
}
|
|
||||||
DOCS_FALLBACK = {
|
|
||||||
"label": "Documentation/help files (Codeberg mirror) ⧉",
|
|
||||||
"tooltip": "Open the DZGUI documentation in a browser",
|
|
||||||
}
|
|
||||||
BUGS = {
|
|
||||||
"label": "Report a bug (GitHub) ⧉",
|
|
||||||
"tooltip": "Open the DZGUI issue tracker in a browser",
|
|
||||||
}
|
|
||||||
FORUM = {
|
|
||||||
"label": "DZGUI Subreddit ⧉",
|
|
||||||
"tooltip": "Open the DZGUI discussion forum in a browser",
|
|
||||||
}
|
|
||||||
SPONSOR = {
|
|
||||||
"label": "Sponsor (GitHub) ⧉",
|
|
||||||
"tooltip": "Sponsor development of the DZGUI project",
|
|
||||||
}
|
|
||||||
THANKS = {
|
|
||||||
"label": "Special thanks",
|
|
||||||
"tooltip": "A list of contributors, testers, and sponsors",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: rename to ContextMenuItem
|
@dataclass(slots=True, frozen=True)
|
||||||
class ContextMenu(EnumWithAttrs):
|
class RowMeta:
|
||||||
ADD_NOTE = {"label": strings.add_note}
|
label: str
|
||||||
ADD_SERVER = {"label": strings.add}
|
tooltip: str
|
||||||
CONNECT = {"label": strings.connect}
|
|
||||||
COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
|
|
||||||
COPY_SERVER_IP = {"label": strings.copy_ip}
|
@dataclass(slots=True, frozen=True)
|
||||||
COPY_SERVER_NAME = {"label": strings.copy_name}
|
class ContextMenuMeta:
|
||||||
UNSUB_MOD = {"label": strings.unsub_mod}
|
label: str
|
||||||
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
|
||||||
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
|
||||||
REMOVE_HISTORY = {"label": strings.remove_history}
|
class ContextMenuItem(Enum):
|
||||||
REMOVE_SERVER = {"label": strings.remove}
|
ADD_NOTE = ContextMenuMeta(strings.add_note)
|
||||||
SET_FAV = {"label": strings.add_fav}
|
ADD_SERVER = ContextMenuMeta(strings.add)
|
||||||
SHOW_DETAILS = {"label": strings.show_details}
|
CONNECT = ContextMenuMeta(strings.connect)
|
||||||
SHOW_MODS = {"label": strings.show_mods}
|
COPY_LOG_CLIPBOARD = ContextMenuMeta(strings.copy_log)
|
||||||
|
COPY_SERVER_IP = ContextMenuMeta(strings.copy_ip)
|
||||||
|
COPY_SERVER_NAME = ContextMenuMeta(strings.copy_name)
|
||||||
|
UNSUB_MOD = ContextMenuMeta(strings.unsub_mod)
|
||||||
|
VALIDATE_MOD = ContextMenuMeta(strings.validate_mod)
|
||||||
|
OPEN_WORKSHOP = ContextMenuMeta(strings.open_workshop)
|
||||||
|
REFRESH_PLAYERS = ContextMenuMeta(strings.refresh_players)
|
||||||
|
REMOVE_HISTORY = ContextMenuMeta(strings.remove_history)
|
||||||
|
REMOVE_SERVER = ContextMenuMeta(strings.remove)
|
||||||
|
SET_FAV = ContextMenuMeta(strings.add_fav)
|
||||||
|
SHOW_DETAILS = ContextMenuMeta(strings.show_details)
|
||||||
|
SHOW_MODS = ContextMenuMeta(strings.show_mods)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> str:
|
||||||
|
return self.value.label
|
||||||
|
|
||||||
|
|
||||||
class ContextMenuGroup(Enum):
|
class ContextMenuGroup(Enum):
|
||||||
@@ -183,109 +110,193 @@ class ContextMenuGroup(Enum):
|
|||||||
Groupings of context menu items
|
Groupings of context menu items
|
||||||
"""
|
"""
|
||||||
|
|
||||||
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
SERVER_MOD = (ContextMenuItem.OPEN_WORKSHOP,)
|
||||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
|
MOD = (
|
||||||
|
ContextMenuItem.OPEN_WORKSHOP,
|
||||||
|
ContextMenuItem.UNSUB_MOD,
|
||||||
|
ContextMenuItem.VALIDATE_MOD,
|
||||||
|
)
|
||||||
MOD_OFFLINE = (None,)
|
MOD_OFFLINE = (None,)
|
||||||
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
LOG = (ContextMenuItem.COPY_LOG_CLIPBOARD,)
|
||||||
SERVER_BROWSER = (
|
SERVER_BROWSER = (
|
||||||
ContextMenu.CONNECT,
|
ContextMenuItem.CONNECT,
|
||||||
ContextMenu.ADD_SERVER,
|
ContextMenuItem.ADD_SERVER,
|
||||||
ContextMenu.SET_FAV,
|
ContextMenuItem.SET_FAV,
|
||||||
ContextMenu.COPY_SERVER_NAME,
|
ContextMenuItem.COPY_SERVER_NAME,
|
||||||
ContextMenu.COPY_SERVER_IP,
|
ContextMenuItem.COPY_SERVER_IP,
|
||||||
ContextMenu.ADD_NOTE,
|
ContextMenuItem.ADD_NOTE,
|
||||||
ContextMenu.SHOW_MODS,
|
ContextMenuItem.SHOW_MODS,
|
||||||
ContextMenu.SHOW_DETAILS,
|
ContextMenuItem.SHOW_DETAILS,
|
||||||
ContextMenu.REFRESH_PLAYERS,
|
ContextMenuItem.REFRESH_PLAYERS,
|
||||||
)
|
)
|
||||||
SCAN_LAN = (
|
SCAN_LAN = (
|
||||||
ContextMenu.CONNECT,
|
ContextMenuItem.CONNECT,
|
||||||
ContextMenu.SET_FAV,
|
ContextMenuItem.SET_FAV,
|
||||||
ContextMenu.COPY_SERVER_NAME,
|
ContextMenuItem.COPY_SERVER_NAME,
|
||||||
ContextMenu.COPY_SERVER_IP,
|
ContextMenuItem.COPY_SERVER_IP,
|
||||||
ContextMenu.ADD_NOTE,
|
ContextMenuItem.ADD_NOTE,
|
||||||
ContextMenu.SHOW_MODS,
|
ContextMenuItem.SHOW_MODS,
|
||||||
ContextMenu.SHOW_DETAILS,
|
ContextMenuItem.SHOW_DETAILS,
|
||||||
ContextMenu.REFRESH_PLAYERS,
|
ContextMenuItem.REFRESH_PLAYERS,
|
||||||
)
|
)
|
||||||
SAVED = (
|
SAVED = (
|
||||||
ContextMenu.CONNECT,
|
ContextMenuItem.CONNECT,
|
||||||
ContextMenu.SET_FAV,
|
ContextMenuItem.SET_FAV,
|
||||||
ContextMenu.REMOVE_SERVER,
|
ContextMenuItem.REMOVE_SERVER,
|
||||||
ContextMenu.COPY_SERVER_NAME,
|
ContextMenuItem.COPY_SERVER_NAME,
|
||||||
ContextMenu.COPY_SERVER_IP,
|
ContextMenuItem.COPY_SERVER_IP,
|
||||||
ContextMenu.ADD_NOTE,
|
ContextMenuItem.ADD_NOTE,
|
||||||
ContextMenu.SHOW_MODS,
|
ContextMenuItem.SHOW_MODS,
|
||||||
ContextMenu.SHOW_DETAILS,
|
ContextMenuItem.SHOW_DETAILS,
|
||||||
ContextMenu.REFRESH_PLAYERS,
|
ContextMenuItem.REFRESH_PLAYERS,
|
||||||
)
|
)
|
||||||
RECENT = (
|
RECENT = (
|
||||||
ContextMenu.CONNECT,
|
ContextMenuItem.CONNECT,
|
||||||
ContextMenu.ADD_SERVER,
|
ContextMenuItem.ADD_SERVER,
|
||||||
ContextMenu.SET_FAV,
|
ContextMenuItem.SET_FAV,
|
||||||
ContextMenu.REMOVE_HISTORY,
|
ContextMenuItem.REMOVE_HISTORY,
|
||||||
ContextMenu.COPY_SERVER_NAME,
|
ContextMenuItem.COPY_SERVER_NAME,
|
||||||
ContextMenu.COPY_SERVER_IP,
|
ContextMenuItem.COPY_SERVER_IP,
|
||||||
ContextMenu.ADD_NOTE,
|
ContextMenuItem.ADD_NOTE,
|
||||||
ContextMenu.SHOW_MODS,
|
ContextMenuItem.SHOW_MODS,
|
||||||
ContextMenu.SHOW_DETAILS,
|
ContextMenuItem.SHOW_DETAILS,
|
||||||
ContextMenu.REFRESH_PLAYERS,
|
ContextMenuItem.REFRESH_PLAYERS,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ModButton(EnumWithAttrs):
|
class Preferences(Enum):
|
||||||
SELECT_ALL = {
|
STEAM = ConfigKey("steam_api")
|
||||||
"label": strings.mod_panel.select_all,
|
CLIENT = ConfigKey("client")
|
||||||
"tooltip": strings.mod_panel.select_all_tooltip,
|
WINDOW = ConfigKey("fullscreen")
|
||||||
}
|
DIST = ConfigKey("use_miles")
|
||||||
UNSELECT_ALL = {
|
NAME = ConfigKey("name")
|
||||||
"label": strings.mod_panel.unselect_all,
|
DEFAULT = ConfigKey("default_steam_path")
|
||||||
"tooltip": strings.mod_panel.unselect_all_tooltip,
|
FAV_LBL = ConfigKey("fav_label")
|
||||||
}
|
FAV_SRV = ConfigKey("fav_server")
|
||||||
HIGHLIGHT_STALE = {
|
IP_LIST = ConfigKey("ip_list")
|
||||||
"label": strings.mod_panel.highlight_stale,
|
START_TAB = ConfigKey("start_tab")
|
||||||
"tooltip": strings.mod_panel.highlight_stale_tooltip,
|
|
||||||
}
|
@property
|
||||||
UNHIGHLIGHT_STALE = {
|
def key(self) -> str:
|
||||||
"label": strings.mod_panel.unhighlight_stale,
|
return self.value.key
|
||||||
"tooltip": strings.mod_panel.unhighlight_stale_tooltip,
|
|
||||||
}
|
|
||||||
UNSUB_SELECTED = {
|
|
||||||
"label": strings.mod_panel.unsub_selected,
|
|
||||||
"tooltip": strings.mod_panel.unsub_selected_tooltip,
|
|
||||||
}
|
|
||||||
SELECT_STALE = {
|
|
||||||
"label": strings.mod_panel.select_stale,
|
|
||||||
"tooltip": strings.mod_panel.select_stale_tooltip,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class ButtonType(EnumWithAttrs):
|
class NotebookPage(Enum):
|
||||||
SERVERS = {
|
OFFLINE = NotebookMeta(strings.crumbs.offline, True)
|
||||||
"label": strings.buttons.servers_label,
|
CHANGELOG = NotebookMeta(strings.crumbs.changelog, True)
|
||||||
"tooltip": strings.buttons.servers_tooltip,
|
DEVELOPERS = NotebookMeta(strings.crumbs.developers, False)
|
||||||
"opens": NotebookPage.SERVERS,
|
HELP = NotebookMeta(strings.crumbs._help, True)
|
||||||
}
|
KEYS = NotebookMeta(strings.crumbs.keys, True)
|
||||||
MODS = {
|
LOG = NotebookMeta(strings.crumbs.log, True)
|
||||||
"label": strings.buttons.mods_label,
|
MODS = NotebookMeta(strings.crumbs.mods, True)
|
||||||
"tooltip": strings.buttons.mods_tooltip,
|
OPTIONS = NotebookMeta(strings.crumbs.options, False)
|
||||||
"opens": NotebookPage.MODS,
|
SERVERS = NotebookMeta(strings.crumbs.servers, True)
|
||||||
}
|
THANKS = NotebookMeta(strings.crumbs.thanks, True)
|
||||||
OPTIONS = {
|
CONNECTION = NotebookMeta(strings.crumbs.connect, True)
|
||||||
"label": strings.buttons.options_label,
|
|
||||||
"tooltip": strings.buttons.options_tooltip,
|
@property
|
||||||
"opens": NotebookPage.OPTIONS,
|
def crumbs(self) -> str:
|
||||||
}
|
return self.value.crumbs
|
||||||
HELP = {
|
|
||||||
"label": strings.buttons.help_label,
|
@property
|
||||||
"tooltip": strings.buttons.help_tooltip,
|
def has_statusbar(self) -> bool:
|
||||||
"opens": NotebookPage.HELP,
|
return self.value.has_statusbar
|
||||||
}
|
|
||||||
EXIT = {
|
|
||||||
"label": strings.buttons.exit_label,
|
class RowType(Enum):
|
||||||
"tooltip": strings.buttons.exit_tooltip,
|
CHANGELOG = RowMeta(rows.changelog_label, rows.changelog_status)
|
||||||
"opens": None,
|
SHOW_LOG = RowMeta(rows.log_label, rows.log_status)
|
||||||
}
|
SEPARATOR = RowMeta(rows.separator_label, rows.separator_status)
|
||||||
|
DUMP_LOG = RowMeta(rows.system_log_label, rows.system_log_status)
|
||||||
|
DOCS = RowMeta(rows.docs_label, rows.docs_status)
|
||||||
|
DOCS_FALLBACK = RowMeta(rows.docs_fallback_label, rows.docs_fallback_status)
|
||||||
|
BUGS = RowMeta(rows.bugs_label, rows.bugs_status)
|
||||||
|
FORUM = RowMeta(rows.forum_label, rows.forum_status)
|
||||||
|
SPONSOR = RowMeta(rows.sponsor_label, rows.sponsor_status)
|
||||||
|
THANKS = RowMeta(rows.thanks_label, rows.thanks_status)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> str:
|
||||||
|
return self.value.label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tooltip(self) -> str:
|
||||||
|
return self.value.tooltip
|
||||||
|
|
||||||
|
|
||||||
|
class ModButton(Enum):
|
||||||
|
SELECT_ALL = ModButtonMeta(
|
||||||
|
strings.mod_panel.select_all, strings.mod_panel.select_all_tooltip
|
||||||
|
)
|
||||||
|
UNSELECT_ALL = ModButtonMeta(
|
||||||
|
strings.mod_panel.unselect_all, strings.mod_panel.unselect_all_tooltip
|
||||||
|
)
|
||||||
|
HIGHLIGHT_STALE = ModButtonMeta(
|
||||||
|
strings.mod_panel.highlight_stale, strings.mod_panel.highlight_stale_tooltip
|
||||||
|
)
|
||||||
|
UNHIGHLIGHT_STALE = ModButtonMeta(
|
||||||
|
strings.mod_panel.unhighlight_stale, strings.mod_panel.unhighlight_stale_tooltip
|
||||||
|
)
|
||||||
|
UNSUB_SELECTED = ModButtonMeta(
|
||||||
|
strings.mod_panel.unsub_selected,
|
||||||
|
strings.mod_panel.unsub_selected_tooltip,
|
||||||
|
)
|
||||||
|
SELECT_STALE = ModButtonMeta(
|
||||||
|
strings.mod_panel.select_stale,
|
||||||
|
strings.mod_panel.select_stale_tooltip,
|
||||||
|
)
|
||||||
|
VALIDATE = ModButtonMeta(
|
||||||
|
strings.mod_panel.validate,
|
||||||
|
strings.mod_panel.validate_tooltip,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> str:
|
||||||
|
return self.value.label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tooltip(self) -> str:
|
||||||
|
return self.value.tooltip
|
||||||
|
|
||||||
|
|
||||||
|
class ButtonType(Enum):
|
||||||
|
SERVERS = ButtonTypeMeta(
|
||||||
|
strings.buttons.servers_label,
|
||||||
|
strings.buttons.servers_tooltip,
|
||||||
|
NotebookPage.SERVERS,
|
||||||
|
)
|
||||||
|
MODS = ButtonTypeMeta(
|
||||||
|
strings.buttons.mods_label,
|
||||||
|
strings.buttons.mods_tooltip,
|
||||||
|
NotebookPage.MODS,
|
||||||
|
)
|
||||||
|
OPTIONS = ButtonTypeMeta(
|
||||||
|
strings.buttons.options_label,
|
||||||
|
strings.buttons.options_tooltip,
|
||||||
|
NotebookPage.OPTIONS,
|
||||||
|
)
|
||||||
|
HELP = ButtonTypeMeta(
|
||||||
|
strings.buttons.help_label,
|
||||||
|
strings.buttons.help_tooltip,
|
||||||
|
NotebookPage.HELP,
|
||||||
|
)
|
||||||
|
EXIT = ButtonTypeMeta(
|
||||||
|
strings.buttons.exit_label,
|
||||||
|
strings.buttons.exit_tooltip,
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def label(self) -> str:
|
||||||
|
return self.value.label
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tooltip(self) -> str:
|
||||||
|
return self.value.tooltip
|
||||||
|
|
||||||
|
@property
|
||||||
|
def opens(self) -> NotebookPage | None:
|
||||||
|
return self.value.opens
|
||||||
|
|
||||||
|
|
||||||
HELP_MENU_ROWS = (
|
HELP_MENU_ROWS = (
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
ICON_CALCULATOR = "accessories-calculator-symbolic"
|
||||||
|
ICON_CARET_DOWN = "go-down-symbolic"
|
||||||
|
ICON_CARET_UP = "go-up-symbolic"
|
||||||
|
ICON_CLIPBOARD = "edit-copy-symbolic"
|
||||||
|
ICON_CLOSE = "window-close-symbolic"
|
||||||
|
ICON_DAY = "weather-clear-symbolic"
|
||||||
|
ICON_EDIT_DELETE = "edit-delete-symbolic"
|
||||||
|
ICON_ERROR = "dialog-error-symbolic"
|
||||||
|
ICON_FOLDER = "folder-symbolic"
|
||||||
|
ICON_HELP_BUBBLE = "help-about-symbolic"
|
||||||
|
ICON_INPUT_KEYBOARD = "input-keyboard-symbolic"
|
||||||
|
ICON_LIST_ADD = "list-add-symbolic"
|
||||||
|
ICON_NIGHT = "weather-clear-night-symbolic"
|
||||||
|
ICON_REFRESH = "view-refresh-symbolic"
|
||||||
|
ICON_SEARCH = "system-search-symbolic"
|
||||||
|
ICON_STEAM = "steam-tray-symbolic"
|
||||||
|
ICON_VIEW_CONCEAL = "view-conceal-symbolic"
|
||||||
|
ICON_VIEW_REVEAL = "view-reveal-symbolic"
|
||||||
|
ICON_WARNING = "dialog-warning-symbolic"
|
||||||
|
ICON_WEB_BROWSER = "web-browser-symbolic"
|
||||||
@@ -3,8 +3,8 @@ from typing import TYPE_CHECKING
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository.Gtk import ListStore # noqa E402
|
from gi.repository.Gtk import ListStore
|
||||||
from gi.repository import Gdk, GObject # noqa E402
|
from gi.repository import GObject
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.const.enum import NotebookPage, ServerTab
|
from dzgui.const.enum import NotebookPage, ServerTab
|
||||||
@@ -111,6 +111,10 @@ class Emitter(GObject.GObject):
|
|||||||
def check_toggled(self, label: str, state: bool) -> None:
|
def check_toggled(self, label: str, state: bool) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
|
def servers_timed_out(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
def servers_loaded(self, tag: "ServerTab") -> None:
|
def servers_loaded(self, tag: "ServerTab") -> None:
|
||||||
pass
|
pass
|
||||||
@@ -145,6 +149,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
|
||||||
|
|||||||
@@ -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,
|
ContextMenuItem,
|
||||||
|
FilterMode,
|
||||||
|
NotebookPage,
|
||||||
|
Preferences,
|
||||||
|
ServerTab,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dzgui.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
@@ -31,13 +32,12 @@ 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, Gdk, GLib, GObject # noqa E402
|
from gi.repository import Gtk, Gdk, GLib, GObject
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
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
|
||||||
@@ -77,7 +77,7 @@ class AppNavigation:
|
|||||||
|
|
||||||
class Controller(GObject.GObject):
|
class Controller(GObject.GObject):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.dist_cache: dict[str, "Haversine"] = {}
|
self.dist_cache: dict[str, "Haversine" | None] = {}
|
||||||
self.mediator = AppNavigation()
|
self.mediator = AppNavigation()
|
||||||
|
|
||||||
self.prefs: UserPrefs
|
self.prefs: UserPrefs
|
||||||
@@ -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()
|
||||||
@@ -103,6 +102,16 @@ class Controller(GObject.GObject):
|
|||||||
except AttributeError:
|
except AttributeError:
|
||||||
logger.critical(f"{attr} is not a valid AppNavigation attribute.")
|
logger.critical(f"{attr} is not a valid AppNavigation attribute.")
|
||||||
|
|
||||||
|
def validate_mods(
|
||||||
|
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
|
||||||
|
) -> None:
|
||||||
|
if treeview is None:
|
||||||
|
view = self.mediator.modtreeview
|
||||||
|
else:
|
||||||
|
view = treeview
|
||||||
|
mod_man = view.get_mod_man()
|
||||||
|
mod_man.validate_mods()
|
||||||
|
|
||||||
def get_prefs(self) -> UserPrefs:
|
def get_prefs(self) -> UserPrefs:
|
||||||
return self.prefs
|
return self.prefs
|
||||||
|
|
||||||
@@ -115,9 +124,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:
|
||||||
@@ -190,7 +196,9 @@ class Controller(GObject.GObject):
|
|||||||
def show_developers_page(self) -> None:
|
def show_developers_page(self) -> None:
|
||||||
self.open_page(NotebookPage.DEVELOPERS)
|
self.open_page(NotebookPage.DEVELOPERS)
|
||||||
|
|
||||||
def open_page(self, page: NotebookPage) -> None:
|
def open_page(self, page: NotebookPage | None) -> None:
|
||||||
|
if page is None:
|
||||||
|
return
|
||||||
self.mediator.grid.notebook.set_page_by_enum(page)
|
self.mediator.grid.notebook.set_page_by_enum(page)
|
||||||
|
|
||||||
def open_page_by_button(self, button: "ContextualButton") -> None:
|
def open_page_by_button(self, button: "ContextualButton") -> None:
|
||||||
@@ -256,7 +264,7 @@ class Controller(GObject.GObject):
|
|||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
def menu_action(self, action: ContextMenu, tree: "ServerTreeView") -> None:
|
def menu_action(self, action: ContextMenuItem, tree: "ServerTreeView") -> None:
|
||||||
context_man = ContextMenuManager(tree, self)
|
context_man = ContextMenuManager(tree, self)
|
||||||
context_man.process(action)
|
context_man.process(action)
|
||||||
|
|
||||||
@@ -267,7 +275,8 @@ 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.run()
|
dialog.show_all()
|
||||||
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
def select_colorized(self) -> None:
|
def select_colorized(self) -> None:
|
||||||
mod_man = self.mediator.modtreeview.get_mod_man()
|
mod_man = self.mediator.modtreeview.get_mod_man()
|
||||||
@@ -300,10 +309,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.run()
|
dialog.show_all()
|
||||||
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
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)
|
||||||
@@ -347,22 +357,21 @@ class Controller(GObject.GObject):
|
|||||||
filter_man.set_filter(filt, state)
|
filter_man.set_filter(filt, state)
|
||||||
self.mediator.filters.set_filters(filters)
|
self.mediator.filters.set_filters(filters)
|
||||||
|
|
||||||
def get_dist_cache(self) -> dict[str, "Haversine"]:
|
def get_dist_cache(self) -> dict[str, Union["Haversine", None]]:
|
||||||
return self.dist_cache
|
return self.dist_cache
|
||||||
|
|
||||||
def set_dist_cache(self, addr: str, haversine: "Haversine") -> None:
|
def set_dist_cache(self, addr: str, haversine: Union["Haversine", None]) -> None:
|
||||||
self.dist_cache[addr] = haversine
|
self.dist_cache[addr] = haversine
|
||||||
|
|
||||||
def get_map_store(self) -> Gtk.ListStore:
|
def get_map_store(self) -> Gtk.ListStore:
|
||||||
filter_man = self.get_filter_man()
|
filter_man = self.get_filter_man()
|
||||||
return filter_man.get_map_store()
|
return filter_man.get_map_store()
|
||||||
|
|
||||||
def get_enabled_filters(self) -> dict:
|
def get_enabled_filters(self) -> dict[str, bool]:
|
||||||
filter_man = self.get_filter_man()
|
filter_man = self.get_filter_man()
|
||||||
return filter_man.get_filters()
|
return filter_man.get_filters()
|
||||||
|
|
||||||
# FIXME: wipe maps store when changing tabs if model is none
|
# FIXME: wipe maps store when changing tabs if model is none
|
||||||
|
|
||||||
def has_server_model(self) -> bool:
|
def has_server_model(self) -> bool:
|
||||||
treeview = self.get_active_treeview()
|
treeview = self.get_active_treeview()
|
||||||
proxy_man = treeview.get_proxy_man()
|
proxy_man = treeview.get_proxy_man()
|
||||||
@@ -402,6 +411,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 +425,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 +449,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 +471,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 +488,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 +509,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 +537,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 +552,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)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Added
|
## [7.0.0] Unreleased
|
||||||
|
### 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
|
||||||
@@ -23,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
|
||||||
@@ -41,11 +45,22 @@
|
|||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
- Play offline (load mods directly)
|
- Play offline (load mods directly)
|
||||||
|
- Generate Steam shortcuts and cover art
|
||||||
|
- Warn user if background downloads are disabled
|
||||||
|
- Dynamic copy button
|
||||||
|
- 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
|
||||||
@@ -58,59 +73,66 @@
|
|||||||
- 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
|
||||||
- Local documentation
|
- Local documentation
|
||||||
- Raw debug command in context menu
|
- Raw debug command in context menu
|
||||||
|
|
||||||
## Developer-facing
|
### Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
|
|
||||||
## [6.0.5] 2026-05-17
|
## [6.0.5] 2026-05-17
|
||||||
## Changed
|
### Changed
|
||||||
- Update symlink hash logic to align with DZGUI 7 beta
|
- Update symlink hash logic to align with DZGUI 7 beta
|
||||||
- Update A2S helper to latest commit
|
- Update A2S helper to latest commit
|
||||||
|
|
||||||
## [6.0.4] 2026-04-13
|
## [6.0.4] 2026-04-13
|
||||||
## Fixed
|
### Fixed
|
||||||
- Update A2S module checksums to support servers with missing description field
|
- Update A2S module checksums to support servers with missing description field
|
||||||
|
|
||||||
## [6.0.3] 2026-04-12
|
## [6.0.3] 2026-04-12
|
||||||
## Fixed
|
### Fixed
|
||||||
- Use GLibUnix namespace where available when setting up SIGINT callback
|
- Use GLibUnix namespace where available when setting up SIGINT callback
|
||||||
|
|
||||||
## [6.0.2] 2026-01-26
|
## [6.0.2] 2026-01-26
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
- Raise error correctly from module
|
- Raise error correctly from module
|
||||||
- Do not spawn dialogs from inside of thread when fetching prereqs
|
- Do not spawn dialogs from inside of thread when fetching prereqs
|
||||||
## Changed
|
### Changed
|
||||||
- Optimize time complexity of startup checks
|
- Optimize time complexity of startup checks
|
||||||
- Optimize coordinate calculation by using local records first
|
- Optimize coordinate calculation by using local records first
|
||||||
- Rename some internal functions
|
- Rename some internal functions
|
||||||
- Wrap entire startup process in dialog
|
- Wrap entire startup process in dialog
|
||||||
|
|
||||||
## [6.0.1] 2026-01-16
|
## [6.0.1] 2026-01-16
|
||||||
## Fixed
|
### Fixed
|
||||||
- Explicitly use Python 3.13 when calling subprocesses
|
- Explicitly use Python 3.13 when calling subprocesses
|
||||||
|
|
||||||
## [6.0.0] 2026-01-14
|
## [6.0.0] 2026-01-14
|
||||||
## Added
|
### Added
|
||||||
- Support DayZ Experimental
|
- Support DayZ Experimental
|
||||||
- Ping display on server tables
|
- Ping display on server tables
|
||||||
- Save descriptive text notes on a per-server basis
|
- Save descriptive text notes on a per-server basis
|
||||||
@@ -136,7 +158,7 @@
|
|||||||
- Commandline usage help text (GaryBlackbourne)
|
- Commandline usage help text (GaryBlackbourne)
|
||||||
- More descriptive help dialogs when initial dependencies are missing
|
- More descriptive help dialogs when initial dependencies are missing
|
||||||
|
|
||||||
## Fixed
|
### Fixed
|
||||||
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
- Script failing to start when remote endpoints are unavailable (GaryBlackbourne)
|
||||||
- Key stickiness when quickly navigating through entries in tables
|
- Key stickiness when quickly navigating through entries in tables
|
||||||
- Servers returning malformed A2S_INFO blocking server browser from loading
|
- Servers returning malformed A2S_INFO blocking server browser from loading
|
||||||
@@ -175,7 +197,7 @@
|
|||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## Changed
|
### Changed
|
||||||
- Require Python 3.13
|
- Require Python 3.13
|
||||||
- Reduced global API cooldown from 60s to 30s
|
- Reduced global API cooldown from 60s to 30s
|
||||||
- Clarify dialog messages when DayZ path could not be found
|
- Clarify dialog messages when DayZ path could not be found
|
||||||
@@ -187,50 +209,50 @@
|
|||||||
- Rewrote distance calculation module (GaryBlackbourne)
|
- Rewrote distance calculation module (GaryBlackbourne)
|
||||||
- Changed preferred client setting from radio toggle to combobox
|
- Changed preferred client setting from radio toggle to combobox
|
||||||
|
|
||||||
## Dropped
|
### Dropped
|
||||||
- Ping readout in statusbar
|
- Ping readout in statusbar
|
||||||
- Extraneous information from right statusbar
|
- Extraneous information from right statusbar
|
||||||
|
|
||||||
## [5.8.3] 2026-01-04
|
## [5.8.3] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Normalize checksum numbers and dates
|
- Normalize checksum numbers and dates
|
||||||
|
|
||||||
## [5.8.2] 2026-01-04
|
## [5.8.2] 2026-01-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Fix regression in symlink routine introduced by previous hotfix
|
- Fix regression in symlink routine introduced by previous hotfix
|
||||||
|
|
||||||
## [5.8.1] 2025-12-27
|
## [5.8.1] 2025-12-27
|
||||||
## Fixed
|
### Fixed
|
||||||
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
- Path discovery during first-time setup when parsing filepaths with whitespaces
|
||||||
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
- First-time setup dialog continuously triggering when DayZ install path had whitespaces in it
|
||||||
|
|
||||||
## [5.8.0] 2025-07-06
|
## [5.8.0] 2025-07-06
|
||||||
## Added
|
### Added
|
||||||
- Filter servers by official/unofficial status
|
- Filter servers by official/unofficial status
|
||||||
- Automatically fetch geolocation records
|
- Automatically fetch geolocation records
|
||||||
## Changed
|
### Changed
|
||||||
- Updated internal versioning of helper files
|
- Updated internal versioning of helper files
|
||||||
## Fixed
|
### Fixed
|
||||||
- Corrected erroneous 2024 date in prior changelog entries
|
- Corrected erroneous 2024 date in prior changelog entries
|
||||||
|
|
||||||
## [5.7.0] 2025-04-17
|
## [5.7.0] 2025-04-17
|
||||||
## Added
|
### Added
|
||||||
- Save application dimensions when quitting and restore on subsequent boot
|
- Save application dimensions when quitting and restore on subsequent boot
|
||||||
## Fixed
|
### Fixed
|
||||||
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
- Issues with the window exceeding the bounds of the screen when displaying some table contexts
|
||||||
## Changed
|
### Changed
|
||||||
- More direct server ping query method that should return marginally more accurate times
|
- More direct server ping query method that should return marginally more accurate times
|
||||||
|
|
||||||
## [5.6.7] 2025-04-04
|
## [5.6.7] 2025-04-04
|
||||||
## Dropped
|
### Dropped
|
||||||
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
- Removed extraneous pre-boot API checks that could cause error messages to be printed if the user had not set up an API key yet
|
||||||
|
|
||||||
## [5.6.6] 2025-03-16
|
## [5.6.6] 2025-03-16
|
||||||
## Changed
|
### Changed
|
||||||
- Update IP database records for 2025-03
|
- Update IP database records for 2025-03
|
||||||
|
|
||||||
## [5.6.5] 2025-03-04
|
## [5.6.5] 2025-03-04
|
||||||
## Fixed
|
### Fixed
|
||||||
- Livonia server results being dropped from batch queries
|
- Livonia server results being dropped from batch queries
|
||||||
|
|
||||||
## [5.6.4] 2025-02-10
|
## [5.6.4] 2025-02-10
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 81 B After Width: | Height: | Size: 81 B |
|
Before Width: | Height: | Size: 130 KiB After Width: | Height: | Size: 361 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free 7.3.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path d="M568 320C568 457 456.8 568 319.6 568C205.8 568 110 491.7 80.6 387.6L175.8 426.9C182.2 459 210.7 483.3 244.7 483.3C283.9 483.3 316.6 450.9 314.9 409.8L399.4 349.6C451.5 350.9 495.2 308.7 495.2 256.1C495.2 204.5 453.2 162.6 401.5 162.6C349.8 162.6 307.8 204.6 307.8 256.1L307.8 257.3L248.6 343C233.1 342.1 217.9 346.4 205.1 355.1L72 300.1C82.2 172.4 189.1 72 319.6 72C456.8 72 568 183 568 320zM227.7 448.3L197.2 435.7C202.8 447.3 212.5 456.5 224.4 461.5C251.3 472.7 282.2 459.9 293.4 433.1C298.8 420.1 298.9 405.8 293.5 392.8C288.1 379.8 278 369.6 265 364.2C252.1 358.8 238.3 359 226.1 363.6L257.6 376.6C277.4 384.8 286.8 407.5 278.5 427.3C270.2 447.2 247.5 456.5 227.7 448.3zM401.5 193.8C435.9 193.8 463.8 221.7 463.8 256.1C463.8 290.5 435.9 318.4 401.5 318.4C367.1 318.4 339.2 290.5 339.2 256.1C339.2 221.7 367.1 193.8 401.5 193.8zM401.6 302.8C427.4 302.8 448.4 281.8 448.4 256C448.4 230.2 427.4 209.2 401.6 209.2C375.8 209.2 354.8 230.2 354.8 256C354.8 281.8 375.8 302.8 401.6 302.8z"/></svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
@@ -10,10 +10,10 @@ if TYPE_CHECKING:
|
|||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def get_local_coords(path: "Path") -> Coords | None:
|
def get_local_coords(db_path: "Path") -> Coords | None:
|
||||||
try:
|
try:
|
||||||
my_ip = get_local_ip()
|
my_ip = get_local_ip()
|
||||||
return get_coords(path, my_ip)
|
return get_coords(db_path, my_ip)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(e)
|
logger.warn(e)
|
||||||
return None
|
return None
|
||||||
@@ -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)
|
||||||
@@ -5,8 +5,9 @@ import sys
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TextIO
|
from typing import TextIO
|
||||||
|
|
||||||
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
from dzgui.const.constants import LOCK_FILE
|
||||||
from dzgui.util.strings import cannot_acquire_lock
|
from dzgui.util.strings import cannot_acquire_lock
|
||||||
|
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
||||||
|
|
||||||
|
|
||||||
def lock_release(lock: TextIO) -> None:
|
def lock_release(lock: TextIO) -> None:
|
||||||
@@ -15,7 +16,7 @@ def lock_release(lock: TextIO) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def lock_acquire() -> TextIO:
|
def lock_acquire() -> TextIO:
|
||||||
lockfile = Path("/tmp/dzgui.flock")
|
lockfile = Path(LOCK_FILE)
|
||||||
lock = open(lockfile, "w")
|
lock = open(lockfile, "w")
|
||||||
try:
|
try:
|
||||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -3,23 +3,39 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, FLATPAK_CMD, STEAM_CMD
|
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, FLATPAK_CMD, STEAM_CMD
|
||||||
|
from dzgui.init.proc import is_sandboxed
|
||||||
|
|
||||||
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):
|
if is_sandboxed():
|
||||||
return True
|
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()
|
||||||
@@ -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
|
||||||
@@ -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()
|
||||||
|
|
||||||
@@ -39,6 +42,12 @@ def main() -> None:
|
|||||||
test_libgi_missing()
|
test_libgi_missing()
|
||||||
|
|
||||||
# NOTE: only import ligbirepository modules after the above check
|
# NOTE: only import ligbirepository modules after the above check
|
||||||
from dzgui.app_init import load_gui
|
from dzgui.app_init import build_gui
|
||||||
|
from dzgui.init.flock import lock_acquire
|
||||||
|
|
||||||
load_gui(version, args.debug)
|
lock = lock_acquire() # noqa
|
||||||
|
|
||||||
|
app = build_gui(version, args.debug)
|
||||||
|
if app is None:
|
||||||
|
sys.exit(0)
|
||||||
|
app.run(None)
|
||||||
@@ -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
|
||||||
@@ -55,8 +47,11 @@ class ConfigManager:
|
|||||||
self.update_config(Preferences.IP_LIST, ips)
|
self.update_config(Preferences.IP_LIST, ips)
|
||||||
|
|
||||||
def update_history_file(self, fqip: str) -> None:
|
def update_history_file(self, fqip: str) -> None:
|
||||||
with open(self.prefs.paths.history, "r") as f:
|
try:
|
||||||
ips = [line.rstrip() for line in f.readlines()]
|
with open(self.prefs.paths.history, "r") as f:
|
||||||
|
ips = [line.rstrip() for line in f.readlines()]
|
||||||
|
except FileNotFoundError:
|
||||||
|
ips = []
|
||||||
|
|
||||||
seen = set()
|
seen = set()
|
||||||
ips.append(fqip)
|
ips.append(fqip)
|
||||||
@@ -95,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"))
|
||||||
@@ -130,16 +124,7 @@ class ConfigManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def enum_to_key(self, enum: Preferences) -> str:
|
def enum_to_key(self, enum: Preferences) -> str:
|
||||||
return str(enum.dict["key"])
|
return str(enum.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:
|
||||||
@@ -159,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)
|
||||||
@@ -169,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.run()
|
dialog.show_all()
|
||||||
raise e
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
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()
|
||||||
@@ -197,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()
|
||||||
@@ -209,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:
|
||||||
|
|||||||
@@ -9,9 +9,12 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
import dzgui.api.pefile as PeFile
|
import dzgui.api.pefile as PeFile
|
||||||
import dzgui.api.servers as Servers
|
import dzgui.api.servers as Servers
|
||||||
|
from dzgui.api.shortcuts import Shortcuts
|
||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
|
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,
|
||||||
@@ -44,18 +47,14 @@ from dzgui.strings.dialogs import (
|
|||||||
waiting_for_mods,
|
waiting_for_mods,
|
||||||
waiting_for_directories,
|
waiting_for_directories,
|
||||||
)
|
)
|
||||||
from dzgui.strings.server_mods import checkmark, resync
|
from dzgui.strings import kb
|
||||||
from dzgui.util.format import format_mib
|
from dzgui.strings.server_mods import checkmark, resync, invalid_mod
|
||||||
|
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
|
||||||
from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog
|
from dzgui.views.dialogs.servers import ServerDetailsDialog, ServerModDialog
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
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
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -81,10 +80,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]
|
||||||
|
|
||||||
|
|
||||||
@@ -102,11 +103,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)
|
||||||
@@ -170,18 +166,24 @@ 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:
|
||||||
allows_dl = get_app_allows_downloads(steam_path, running_app)
|
allows_dl = get_app_allows_downloads(steam_path, running_app)
|
||||||
running_app_name = get_app_name(running_app)
|
running_app_name = get_app_name(running_app)
|
||||||
|
# NOTE: for out-of-range appid, assume NSG.
|
||||||
|
# dzgui.api.steam.get_app_name() will return None
|
||||||
|
if running_app_name is None:
|
||||||
|
s = Shortcuts(steam_path)
|
||||||
|
# NOTE: returns "Unknown" if unparseable
|
||||||
|
running_app_name = s.find_appname_by_unsigned_id(running_app)
|
||||||
allows_downloads = (allows_dl, running_app_name)
|
allows_downloads = (allows_dl, running_app_name)
|
||||||
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
|
||||||
@@ -190,6 +192,10 @@ class ConnectionManager:
|
|||||||
|
|
||||||
is_last = self.is_last_server()
|
is_last = self.is_last_server()
|
||||||
|
|
||||||
|
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,
|
||||||
@@ -201,10 +207,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,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -225,7 +233,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
|
||||||
)
|
)
|
||||||
@@ -260,6 +268,10 @@ 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] == "":
|
||||||
|
mod[2] = invalid_mod
|
||||||
|
|
||||||
return alpha_mods, missing_mods
|
return alpha_mods, missing_mods
|
||||||
|
|
||||||
@@ -279,15 +291,29 @@ class ConnectionManager:
|
|||||||
|
|
||||||
def _present_modlist_dialog(self, mods: list[list[str]]) -> None:
|
def _present_modlist_dialog(self, mods: list[list[str]]) -> None:
|
||||||
dialog = ServerModDialog(self.controller, mods)
|
dialog = ServerModDialog(self.controller, mods)
|
||||||
dialog.run()
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
def _present_details_dialog(self, details: Servers.Details) -> None:
|
def _present_details_dialog(self, details: Servers.Details) -> None:
|
||||||
dialog = ServerDetailsDialog(self.controller, details)
|
dialog = ServerDetailsDialog(self.controller, details)
|
||||||
dialog.run()
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
|
def _workshop_timeout(self, msg: str) -> None:
|
||||||
|
dialog = ExceptionDialog(self.controller, msg)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
def _server_timeout(self) -> None:
|
def _server_timeout(self) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
dialog.run()
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
|
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}"
|
||||||
@@ -327,8 +353,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,9 +1,8 @@
|
|||||||
import gi
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME
|
from dzgui.const.constants import APP_NAME
|
||||||
from dzgui.const.enum import ContextMenu, Preferences
|
from dzgui.const.enum import ContextMenuItem, Preferences
|
||||||
from dzgui.util.clip import copy_clipboard
|
from dzgui.util.clip import copy_clipboard
|
||||||
from dzgui.util.open_links import open_workshop_page
|
from dzgui.util.open_links import open_workshop_page
|
||||||
|
|
||||||
@@ -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:
|
||||||
@@ -39,7 +34,7 @@ class ContextMenuManager:
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.treeview = treeview
|
self.treeview = treeview
|
||||||
|
|
||||||
def process(self, action: ContextMenu) -> None:
|
def process(self, action: ContextMenuItem) -> None:
|
||||||
if isinstance(self.treeview, ServerTreeView):
|
if isinstance(self.treeview, ServerTreeView):
|
||||||
record = self.treeview.get_record()
|
record = self.treeview.get_record()
|
||||||
if record is None:
|
if record is None:
|
||||||
@@ -47,14 +42,14 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
match action:
|
match action:
|
||||||
# UNTHREADED
|
# UNTHREADED
|
||||||
case ContextMenu.ADD_NOTE:
|
case ContextMenuItem.ADD_NOTE:
|
||||||
dialog = NoteDialog(self.controller)
|
dialog = NoteDialog(self.controller)
|
||||||
dialog.run()
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
case ContextMenu.COPY_SERVER_IP:
|
case ContextMenuItem.COPY_SERVER_IP:
|
||||||
self.copy_server_ip()
|
self.copy_server_ip()
|
||||||
case ContextMenu.COPY_SERVER_NAME:
|
case ContextMenuItem.COPY_SERVER_NAME:
|
||||||
self.copy_server_name()
|
self.copy_server_name()
|
||||||
case ContextMenu.SET_FAV:
|
case ContextMenuItem.SET_FAV:
|
||||||
name = self.treeview.get_value_at_index(0)
|
name = self.treeview.get_value_at_index(0)
|
||||||
# NOTE: fully qualified ip
|
# NOTE: fully qualified ip
|
||||||
fqip = self.treeview.get_record_string()
|
fqip = self.treeview.get_record_string()
|
||||||
@@ -63,36 +58,40 @@ class ContextMenuManager:
|
|||||||
self.controller.set_fav(name, fqip, simple)
|
self.controller.set_fav(name, fqip, simple)
|
||||||
|
|
||||||
# THREADED
|
# THREADED
|
||||||
case ContextMenu.ADD_SERVER:
|
case ContextMenuItem.ADD_SERVER:
|
||||||
self.controller.add_by_record(record)
|
self.controller.add_by_record(record)
|
||||||
case ContextMenu.CONNECT:
|
case ContextMenuItem.CONNECT:
|
||||||
self.controller.connect_by_record(record)
|
self.controller.connect_by_record(record)
|
||||||
case ContextMenu.REFRESH_PLAYERS:
|
case ContextMenuItem.REFRESH_PLAYERS:
|
||||||
self.controller.refresh_players(record)
|
self.controller.refresh_players(record)
|
||||||
case ContextMenu.REMOVE_HISTORY:
|
case ContextMenuItem.REMOVE_HISTORY:
|
||||||
self.controller.remove_from_history(record)
|
self.controller.remove_from_history(record)
|
||||||
case ContextMenu.REMOVE_SERVER:
|
case ContextMenuItem.REMOVE_SERVER:
|
||||||
self.controller.remove_by_record(record)
|
self.controller.remove_by_record(record)
|
||||||
case ContextMenu.SHOW_DETAILS:
|
case ContextMenuItem.SHOW_DETAILS:
|
||||||
self.controller.get_details(record)
|
self.controller.get_details(record)
|
||||||
case ContextMenu.SHOW_MODS:
|
case ContextMenuItem.SHOW_MODS:
|
||||||
self.controller.get_modlist(record)
|
self.controller.get_modlist(record)
|
||||||
|
|
||||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||||
match action:
|
match action:
|
||||||
case ContextMenu.UNSUB_MOD:
|
case ContextMenuItem.OPEN_WORKSHOP:
|
||||||
self.controller.unsub_mods(self.treeview)
|
|
||||||
case ContextMenu.OPEN_WORKSHOP:
|
|
||||||
self.open_mod_page()
|
self.open_mod_page()
|
||||||
|
case ContextMenuItem.UNSUB_MOD:
|
||||||
|
self.controller.unsub_mods(self.treeview)
|
||||||
|
case ContextMenuItem.VALIDATE_MOD:
|
||||||
|
self.controller.validate_mods(self.treeview)
|
||||||
|
case _:
|
||||||
|
pass
|
||||||
|
|
||||||
if isinstance(self.treeview, LogTreeView):
|
if isinstance(self.treeview, LogTreeView):
|
||||||
match action:
|
match action:
|
||||||
case ContextMenu.COPY_LOG_CLIPBOARD:
|
case ContextMenuItem.COPY_LOG_CLIPBOARD:
|
||||||
self.copy_log()
|
self.copy_log()
|
||||||
|
|
||||||
if isinstance(self.treeview, ServerModTreeView):
|
if isinstance(self.treeview, ServerModTreeView):
|
||||||
match action:
|
match action:
|
||||||
case ContextMenu.OPEN_WORKSHOP:
|
case ContextMenuItem.OPEN_WORKSHOP:
|
||||||
self.open_mod_page()
|
self.open_mod_page()
|
||||||
|
|
||||||
def copy_server_ip(self) -> None:
|
def copy_server_ip(self) -> None:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from typing import TYPE_CHECKING
|
|||||||
|
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
from dzgui.util import strings
|
from dzgui.util import strings
|
||||||
from dzgui.util.strings import all_maps
|
from dzgui.strings.filters import all_maps
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.model.model_factory import FastInsertListStore
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
@@ -21,21 +21,41 @@ 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 = ""
|
||||||
self.active_map = (0, all_maps)
|
self.active_map = (0, all_maps)
|
||||||
self.prior_map = all_maps
|
self.prior_map = all_maps
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -54,11 +74,11 @@ class FilterManager:
|
|||||||
def set_active_keyword(self, word: str) -> None:
|
def set_active_keyword(self, word: str) -> None:
|
||||||
self.active_keyword = word
|
self.active_keyword = word
|
||||||
|
|
||||||
def get_default_filters(self) -> dict:
|
def get_default_filters(self) -> dict[str, bool]:
|
||||||
"""Deep copy of defaults"""
|
"""Deep copy of defaults"""
|
||||||
return dict(self.default_filters)
|
return dict(self.default_filters)
|
||||||
|
|
||||||
def get_filters(self) -> dict:
|
def get_filters(self) -> dict[str, bool]:
|
||||||
return self.enabled_filters
|
return self.enabled_filters
|
||||||
|
|
||||||
def set_filter(self, label: str, state: bool) -> None:
|
def set_filter(self, label: str, state: bool) -> None:
|
||||||
@@ -85,16 +105,11 @@ class FilterManager:
|
|||||||
|
|
||||||
self.reinit_map_store()
|
self.reinit_map_store()
|
||||||
maps.sort()
|
maps.sort()
|
||||||
for m in maps:
|
clean = [m for m in maps if m != all_maps]
|
||||||
if m == "All maps":
|
for m in clean:
|
||||||
continue
|
|
||||||
self.append_map([m])
|
self.append_map([m])
|
||||||
|
|
||||||
# TODO: currently unused
|
def get_all_filters(self) -> tuple[str, ...]:
|
||||||
def get_unique_maps(self) -> list[str]:
|
|
||||||
return [row[0] for row in self.map_store if row[0] != "All maps"]
|
|
||||||
|
|
||||||
def get_all_filters(self) -> tuple:
|
|
||||||
map_name = self.get_active_map_name()
|
map_name = self.get_active_map_name()
|
||||||
enabled = self.get_filters()
|
enabled = self.get_filters()
|
||||||
kw = self.get_active_keyword()
|
kw = self.get_active_keyword()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import time
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api.steam import unsubscribe
|
from dzgui.api.steam import unsubscribe, validate_mod
|
||||||
from dzgui.api.mods import (
|
from dzgui.api.mods import (
|
||||||
get_delimited_mods,
|
get_delimited_mods,
|
||||||
find_stale_mods,
|
find_stale_mods,
|
||||||
@@ -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
|
||||||
@@ -32,7 +30,7 @@ import dzgui.api.pefile as PeFile
|
|||||||
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
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -87,7 +85,7 @@ class ModManager:
|
|||||||
total_mods = len(self.store)
|
total_mods = len(self.store)
|
||||||
self.emitter.emit("mods_updated", msg, total_mods)
|
self.emitter.emit("mods_updated", msg, total_mods)
|
||||||
|
|
||||||
def unsub_mods(self) -> None:
|
def get_selected_mods(self) -> list[tuple[str, Gtk.TreeIter]]:
|
||||||
sel = self.treeview.get_selection()
|
sel = self.treeview.get_selection()
|
||||||
model, pathlist = sel.get_selected_rows()
|
model, pathlist = sel.get_selected_rows()
|
||||||
# NOTE: reverse when multiple selection
|
# NOTE: reverse when multiple selection
|
||||||
@@ -98,6 +96,15 @@ class ModManager:
|
|||||||
continue
|
continue
|
||||||
mod, _iter = res
|
mod, _iter = res
|
||||||
mods.append((mod, _iter))
|
mods.append((mod, _iter))
|
||||||
|
return mods
|
||||||
|
|
||||||
|
def validate_mods(self) -> None:
|
||||||
|
mods = self.get_selected_mods()
|
||||||
|
self.thread_man.set_job_count(len(mods))
|
||||||
|
self.validate_all_mods(mods)
|
||||||
|
|
||||||
|
def unsub_mods(self) -> None:
|
||||||
|
mods = self.get_selected_mods()
|
||||||
self.thread_man.set_job_count(len(mods))
|
self.thread_man.set_job_count(len(mods))
|
||||||
self.unsub_all_mods(mods)
|
self.unsub_all_mods(mods)
|
||||||
|
|
||||||
@@ -111,6 +118,21 @@ class ModManager:
|
|||||||
mod = model.get_value(tree_iter, 2)
|
mod = model.get_value(tree_iter, 2)
|
||||||
return mod, tree_iter
|
return mod, tree_iter
|
||||||
|
|
||||||
|
def validate_atomic_mod(self, mod: str) -> None:
|
||||||
|
# NOTE: propagation to dayz exp will happen during
|
||||||
|
# subsequent symlink phase on connect
|
||||||
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
|
validate_mod(client, mod)
|
||||||
|
|
||||||
|
@call_on_thread(dialogs.validating_mods)
|
||||||
|
def validate_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||||
|
for mod, _iter in mods:
|
||||||
|
self.validate_atomic_mod(mod)
|
||||||
|
self.thread_man.increment_dialog()
|
||||||
|
time.sleep(API_RATE_LIMIT)
|
||||||
|
func = StoredFunc(lambda: None)
|
||||||
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
@call_on_thread(dialogs.deleting_mods)
|
@call_on_thread(dialogs.deleting_mods)
|
||||||
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||||
for mod, _iter in mods:
|
for mod, _iter in mods:
|
||||||
@@ -124,7 +146,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)
|
||||||
@@ -195,7 +221,7 @@ class ModManager:
|
|||||||
total_size += mod[3]
|
total_size += mod[3]
|
||||||
return total_mods, total_size
|
return total_mods, total_size
|
||||||
|
|
||||||
def _on_stale_mods_found(self, stale_mods: list) -> None:
|
def _on_stale_mods_found(self, stale_mods: list[int]) -> None:
|
||||||
"""Manipulates attached ListStore in the main event loop"""
|
"""Manipulates attached ListStore in the main event loop"""
|
||||||
model = self.treeview.get_model()
|
model = self.treeview.get_model()
|
||||||
if model is None:
|
if model is None:
|
||||||
@@ -213,10 +239,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:
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class OfflineManager:
|
|||||||
is_valid = is_mission(folder)
|
is_valid = is_mission(folder)
|
||||||
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
||||||
|
|
||||||
def find_custom_mods(self, callback: Callable) -> None:
|
def find_custom_mods(self, callback: Callable[[], None]) -> None:
|
||||||
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
||||||
window = self.controller.get_window()
|
window = self.controller.get_window()
|
||||||
window.set_sensitive(False)
|
window.set_sensitive(False)
|
||||||
|
|||||||
@@ -15,15 +15,15 @@ if TYPE_CHECKING:
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, GLib # noqa E402
|
from gi.repository import GLib
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
def call_on_thread(
|
def call_on_thread(
|
||||||
dialog_str: str, show_dialog: bool = True, show_cancel: bool = False
|
dialog_str: str, show_dialog: bool = True, show_cancel: bool = False
|
||||||
) -> Callable:
|
) -> Callable[..., Any]:
|
||||||
def decorator(func: Callable) -> Callable:
|
def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
|
||||||
@wraps(func)
|
@wraps(func)
|
||||||
def wrapper(*args: Any, **kwargs: Any) -> None:
|
def wrapper(*args: Any, **kwargs: Any) -> None:
|
||||||
self = args[0]
|
self = args[0]
|
||||||
@@ -42,7 +42,7 @@ def call_on_thread(
|
|||||||
|
|
||||||
|
|
||||||
class StoredFunc:
|
class StoredFunc:
|
||||||
def __init__(self, func: Callable, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
|
||||||
sig = inspect.signature(func)
|
sig = inspect.signature(func)
|
||||||
self.func = func
|
self.func = func
|
||||||
self.bindings = sig.bind(*args, **kwargs)
|
self.bindings = sig.bind(*args, **kwargs)
|
||||||
|
|||||||
@@ -15,11 +15,6 @@ from dzgui.strings import dialogs
|
|||||||
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, QuitDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog, QuitDialog
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk, GLib # noqa E402
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
|
|
||||||
@@ -60,8 +55,9 @@ class UpdateManager:
|
|||||||
def _on_update_success(self) -> None:
|
def _on_update_success(self) -> None:
|
||||||
msg = dialogs.update_success
|
msg = dialogs.update_success
|
||||||
dialog = QuitDialog(self.controller, msg)
|
dialog = QuitDialog(self.controller, msg)
|
||||||
dialog.run()
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
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.run()
|
dialog.show_all()
|
||||||
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
@@ -7,11 +7,11 @@ from dzgui.util.strings import delimiter
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository.Gtk import ListStore, TreeIter # noqa E402
|
from gi.repository.Gtk import ListStore, TreeIter
|
||||||
from gi.repository import GObject # noqa E402
|
from gi.repository import GObject
|
||||||
|
|
||||||
GTYPE_TO_PYTHON = {
|
GTYPE_TO_PYTHON = {
|
||||||
GObject.type_from_name(GObject.type_name(ptype)): ptype
|
GObject.type_from_name(GObject.type_name(ptype)): ptype # type: ignore[no-untyped-call]
|
||||||
for ptype in (int, float, str, bool, object)
|
for ptype in (int, float, str, bool, object)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class ModelFactory:
|
|||||||
|
|
||||||
def make_help_store(self) -> FastInsertListStore:
|
def make_help_store(self) -> FastInsertListStore:
|
||||||
store = self.new_model_from_class(MenuCols)
|
store = self.new_model_from_class(MenuCols)
|
||||||
rows = [[row.dict["label"], row] for row in HELP_MENU_ROWS]
|
rows = [[row.label, row] for row in HELP_MENU_ROWS]
|
||||||
store.extend(rows)
|
store.extend(rows)
|
||||||
return store
|
return store
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
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
|
||||||
from dzgui.util import strings
|
from dzgui.util import strings
|
||||||
|
from dzgui.strings import filters as str_filters
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.managers.filter import FilterManager
|
from dzgui.managers.filter import FilterManager
|
||||||
@@ -13,6 +14,25 @@ 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])"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(slots=True, frozen=True)
|
||||||
|
class ServerRow:
|
||||||
|
name: str
|
||||||
|
mapname: str
|
||||||
|
view: str
|
||||||
|
time: str
|
||||||
|
players: int
|
||||||
|
max_players: int
|
||||||
|
queue: int
|
||||||
|
ip: str
|
||||||
|
qport: int
|
||||||
|
ping: str
|
||||||
|
provider: str
|
||||||
|
modded: bool
|
||||||
|
|
||||||
|
|
||||||
class ProxyModelManager:
|
class ProxyModelManager:
|
||||||
"""
|
"""
|
||||||
@@ -28,7 +48,7 @@ class ProxyModelManager:
|
|||||||
|
|
||||||
def __init__(self, filter_man: "FilterManager") -> None:
|
def __init__(self, filter_man: "FilterManager") -> None:
|
||||||
self.filter_cache: dict[
|
self.filter_cache: dict[
|
||||||
tuple[str],
|
tuple[str, ...],
|
||||||
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]],
|
list[tuple[str, str, str, str, int, int, int, str, int, int, str, bool]],
|
||||||
] = {}
|
] = {}
|
||||||
|
|
||||||
@@ -49,9 +69,6 @@ class ProxyModelManager:
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def append_row(self, row: list) -> None:
|
|
||||||
self.proxy_model.append(row)
|
|
||||||
|
|
||||||
def append_row_to_control(
|
def append_row_to_control(
|
||||||
self, row: tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
|
self, row: tuple[str, str, str, str, int, int, int, str, int, int, str, bool]
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -65,7 +82,6 @@ class ProxyModelManager:
|
|||||||
self.proxy_model[treeiter][4] = playercount.players
|
self.proxy_model[treeiter][4] = playercount.players
|
||||||
self.proxy_model[treeiter][6] = playercount.queue
|
self.proxy_model[treeiter][6] = playercount.queue
|
||||||
|
|
||||||
# TODO: use dataclass for record rows
|
|
||||||
def append_row_to_history(
|
def append_row_to_history(
|
||||||
self,
|
self,
|
||||||
history: tuple[str, str, str, str, int, int, int, str, int, int, str, bool],
|
history: tuple[str, str, str, str, int, int, int, str, int, int, str, bool],
|
||||||
@@ -86,8 +102,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)
|
||||||
|
|
||||||
@@ -173,7 +189,7 @@ class ProxyModelManager:
|
|||||||
rows = self.filtered
|
rows = self.filtered
|
||||||
sel_map = self.filter_man.get_active_map_name()
|
sel_map = self.filter_man.get_active_map_name()
|
||||||
|
|
||||||
if sel_map == strings.all_maps:
|
if sel_map == str_filters.all_maps:
|
||||||
return rows
|
return rows
|
||||||
|
|
||||||
rows = [row for row in rows if row[1] == sel_map]
|
rows = [row for row in rows if row[1] == sel_map]
|
||||||
@@ -232,20 +248,20 @@ 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:
|
||||||
"""Effectively applies all filters"""
|
"""Effectively applies all filters"""
|
||||||
self.set_filtered(self.control_model)
|
self.set_filtered(self.control_model)
|
||||||
self.set_filtered(self.filter_map(filters))
|
self.set_filtered(self.filter_map(filters))
|
||||||
@@ -256,13 +272,9 @@ class ProxyModelManager:
|
|||||||
self.set_filtered(self.filter_toggle_off(filters, f))
|
self.set_filtered(self.filter_toggle_off(filters, f))
|
||||||
return self.filtered
|
return self.filtered
|
||||||
|
|
||||||
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 +310,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)
|
||||||
@@ -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
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.api.servers import A2SInfo, Record
|
from dzgui.api.servers import A2SInfo, Record
|
||||||
@@ -92,6 +92,7 @@ class ServerModelManager:
|
|||||||
job = Servers.query_api
|
job = Servers.query_api
|
||||||
params = Servers.params
|
params = Servers.params
|
||||||
servers = []
|
servers = []
|
||||||
|
|
||||||
with ThreadPoolExecutor() as executor:
|
with ThreadPoolExecutor() as executor:
|
||||||
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
|
futures = [executor.submit(job, key, APPID_DAYZ, param) for param in params]
|
||||||
for future in as_completed(futures):
|
for future in as_completed(futures):
|
||||||
@@ -175,44 +176,68 @@ 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)
|
self.preserve_on_fail = True
|
||||||
self._parse_single_record(res)
|
try:
|
||||||
|
res = Servers.query_by_ip(addr)
|
||||||
|
self._parse_single_record(res)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
self.thread_man.set_cleanup_func(
|
||||||
|
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def add_by_record(self, record: Servers.Record) -> None:
|
def add_by_record(self, record: Servers.Record) -> None:
|
||||||
|
self.preserve_on_fail = True
|
||||||
# NOTE: rationale: a raw Record as shown in server browser may resolve to a different IP
|
# NOTE: rationale: a raw Record as shown in server browser may resolve to a different IP
|
||||||
res = Servers.query_by_record(record)
|
try:
|
||||||
self._parse_single_record(res)
|
res = Servers.query_by_record(record)
|
||||||
|
self._parse_single_record(res)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
self.thread_man.set_cleanup_func(
|
||||||
|
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
@call_on_thread(dialog.querying)
|
@call_on_thread(dialog.querying)
|
||||||
def remove_by_record(self, record: Servers.Record) -> None:
|
def remove_by_record(self, record: Servers.Record) -> None:
|
||||||
res = Servers.query_by_record(record)
|
self.preserve_on_fail = True
|
||||||
self._parse_single_record(res, delete=True)
|
try:
|
||||||
|
res = Servers.query_by_record(record)
|
||||||
|
self._parse_single_record(res, delete=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
self.thread_man.set_cleanup_func(
|
||||||
|
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
def update_history(
|
def update_history(
|
||||||
self,
|
self,
|
||||||
@@ -241,7 +266,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 +287,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
|
||||||
@@ -286,7 +304,6 @@ class ServerModelManager:
|
|||||||
proxy_man.update_playercount(self.playercount)
|
proxy_man.update_playercount(self.playercount)
|
||||||
|
|
||||||
def _parse_single_record(self, response: "A2SInfo", delete: bool = False) -> None:
|
def _parse_single_record(self, response: "A2SInfo", delete: bool = False) -> None:
|
||||||
self.preserve_on_fail = True
|
|
||||||
try:
|
try:
|
||||||
row = response.as_row()
|
row = response.as_row()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -397,6 +414,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,12 +431,15 @@ 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.run()
|
dialog.show_all()
|
||||||
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
||||||
# TODO: disable map, keyword, and filter widgets if model is None
|
# TODO: disable map, keyword, and filter widgets if model is None
|
||||||
# signal driven (servers_empty, servers_failed_to_load)
|
# signal driven (servers_empty, servers_failed_to_load)
|
||||||
|
|
||||||
|
self.emitter.emit("servers_timed_out")
|
||||||
|
|
||||||
if self.preserve_on_fail is False:
|
if self.preserve_on_fail is False:
|
||||||
self.tv.set_model(None)
|
self.tv.set_model(None)
|
||||||
filter_man = self.tv.get_filter_man()
|
filter_man = self.tv.get_filter_man()
|
||||||
@@ -426,15 +448,17 @@ class ServerModelManager:
|
|||||||
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
|
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
|
||||||
# customize statusbar and dialog accordingly
|
# customize statusbar and dialog accordingly
|
||||||
if show_dialog:
|
if show_dialog:
|
||||||
dialog = ExceptionDialog(self.controller, api_warn_msg)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
dialog.run()
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run() # type: ignore[no-untyped-call]
|
||||||
|
|
||||||
def _push_data(self, data: list[Any]) -> None:
|
def _push_data(self, data: list[Any]) -> None:
|
||||||
self._get_proxy_man().push(data)
|
self._get_proxy_man().push(data)
|
||||||
self._sort_unique_maps(data)
|
self._sort_unique_maps(data)
|
||||||
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
|
self.thread_man.set_cleanup_func(StoredFunc(self._cleanup_on_success))
|
||||||
|
|
||||||
def _sort_unique_maps(self, data: list) -> None:
|
def _sort_unique_maps(self, data: list[Any]) -> None:
|
||||||
u_maps = set([row[1] for row in data])
|
u_maps = set([row[1] for row in data])
|
||||||
self._set_new_maps(sorted(u_maps))
|
self._set_new_maps(sorted(u_maps))
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,16 @@
|
|||||||
|
clipboard_button = "Copy to clipboard"
|
||||||
|
clipboard_ip_button = "Copy IP to clipboard"
|
||||||
|
|
||||||
|
dock_button_collapse = "Collapse the connection panel"
|
||||||
|
dock_button_expand = "Expand the connection panel"
|
||||||
|
|
||||||
workshop = "Steam Workshop"
|
workshop = "Steam Workshop"
|
||||||
workshop_tooltip = "Open your Workshop subscriptions"
|
workshop_tooltip = "Open your Workshop subscriptions"
|
||||||
|
|
||||||
|
version_notifier = "Copied!"
|
||||||
|
version_tooltip = "Click to copy to clipboard"
|
||||||
|
|
||||||
|
update_button = "Updates available"
|
||||||
|
|
||||||
|
unset_button_label = "Unset"
|
||||||
|
unset_button_tooltip = "Unset this server as favorite"
|
||||||
@@ -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"
|
||||||
@@ -9,6 +9,7 @@ update_success = "Updated successfully. Please exit and relaunch."
|
|||||||
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
|
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
|
||||||
fetching_mods = "Fetching mod metadata"
|
fetching_mods = "Fetching mod metadata"
|
||||||
deleting_mods = "Unsubscribing mods"
|
deleting_mods = "Unsubscribing mods"
|
||||||
|
validating_mods = "Validating mods"
|
||||||
scanning_mods = "Scanning mods"
|
scanning_mods = "Scanning mods"
|
||||||
parsing_mods = "Parsing mods"
|
parsing_mods = "Parsing mods"
|
||||||
|
|
||||||
@@ -20,3 +21,13 @@ 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"
|
||||||
|
|
||||||
|
caught_cancel_signal = "Caught cancel signal, cleaning up"
|
||||||
@@ -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,4 @@
|
|||||||
|
all_maps = "All maps"
|
||||||
|
filters_heading = "Filters"
|
||||||
|
keyword_placeholder = "Filter by keyword"
|
||||||
|
map_placeholder = "Filter by map"
|
||||||
@@ -1 +1,2 @@
|
|||||||
|
tab_changed_label = "*"
|
||||||
selectable_tree = "Ctrl-click to select multiple; Shift-click to select a range."
|
selectable_tree = "Ctrl-click to select multiple; Shift-click to select a range."
|
||||||
@@ -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"
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
dialog_title = "Add a short note/reminder"
|
||||||
|
dialog_subtitle = "Limit 30 chars"
|
||||||
|
delete_button = "Delete note"
|
||||||
|
ok_button = "Update note"
|
||||||
|
current_note = "Current note: '{0}'"
|
||||||
@@ -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,28 @@
|
|||||||
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_placeholder = "Enter API key"
|
||||||
|
api_failed = "API validation failed"
|
||||||
|
api_validate = "Validate"
|
||||||
|
|
||||||
|
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,29 @@
|
|||||||
|
changelog_label = "View changelog"
|
||||||
|
changelog_status = "Opens the DZGUI changelog"
|
||||||
|
|
||||||
|
log_label = "Show debug log"
|
||||||
|
log_status = "Read the DZGUI log generated since startup"
|
||||||
|
|
||||||
|
separator_label = "SEPARATOR"
|
||||||
|
separator_status = ""
|
||||||
|
|
||||||
|
system_log_label = "Output system info to log file"
|
||||||
|
system_log_status = "Dump diagnostic data for troubleshooting"
|
||||||
|
|
||||||
|
docs_label = "Documentation/help files (GitHub) ⧉"
|
||||||
|
docs_status = "Open the DZGUI documentation in a browser"
|
||||||
|
|
||||||
|
docs_fallback_label = "Documentation/help files (Codeberg mirror) ⧉"
|
||||||
|
docs_fallback_status = "Open the DZGUI documentation in a browser"
|
||||||
|
|
||||||
|
bugs_label = "Report a bug (GitHub) ⧉"
|
||||||
|
bugs_status = "Open the DZGUI issue tracker in a browser"
|
||||||
|
|
||||||
|
forum_label = "DZGUI Subreddit ⧉"
|
||||||
|
forum_status = "Open the DZGUI discussion forum in a browser"
|
||||||
|
|
||||||
|
sponsor_label = "Sponsor (GitHub) ⧉"
|
||||||
|
sponsor_status = "Sponsor development of the DZGUI project"
|
||||||
|
|
||||||
|
thanks_label = "Special thanks"
|
||||||
|
thanks_status = "A list of contributors, testers, and sponsors"
|
||||||
@@ -3,4 +3,5 @@ mod_id = "ID"
|
|||||||
up_to_date = "Up to date"
|
up_to_date = "Up to date"
|
||||||
modlist = "Modlist"
|
modlist = "Modlist"
|
||||||
checkmark = "✓"
|
checkmark = "✓"
|
||||||
resync="⟳"
|
resync = "⟳"
|
||||||
|
invalid_mod = "Invalid mod"
|
||||||
@@ -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."
|
||||||
|
)
|
||||||
@@ -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"
|
||||||
@@ -17,7 +19,7 @@ desc_flatpak_path = (
|
|||||||
desc_ubuntu_path = "This is the default Steam path on <b>Ubuntu-based systems</b>."
|
desc_ubuntu_path = "This is the default Steam path on <b>Ubuntu-based systems</b>."
|
||||||
desc_debian_path = "This is the default Steam path on <b>Debian-based systems</b>."
|
desc_debian_path = "This is the default Steam path on <b>Debian-based systems</b>."
|
||||||
no_valid_paths = (
|
no_valid_paths = (
|
||||||
"No valid Steam paths found on system. Please install Steam to continue."
|
"No valid Steam path or Steam command found on system. Please install Steam to continue."
|
||||||
)
|
)
|
||||||
button_scan = "Scan for Steam"
|
button_scan = "Scan for Steam"
|
||||||
|
|
||||||
@@ -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"
|
||||||
@@ -64,6 +61,37 @@ radio_mi = "mi (miles)"
|
|||||||
label_dist = "Distance display"
|
label_dist = "Distance display"
|
||||||
label_client = "Steam client"
|
label_client = "Steam client"
|
||||||
|
|
||||||
|
### Shortcuts page
|
||||||
|
heading_shortcuts = "Create shortcuts"
|
||||||
|
blurb_shortcuts = (
|
||||||
|
"You can optionally create shortcuts to facilitate launching DZGUI faster."
|
||||||
|
)
|
||||||
|
button_shortcuts = "Create shortcuts"
|
||||||
|
checkbox_steam_shortcut = (
|
||||||
|
"Add DZGUI to Steam library",
|
||||||
|
(
|
||||||
|
"This creates a standalone copy of DZGUI and adds it to Steam. "
|
||||||
|
"You <b>must restart Steam</b> for the shortcut to appear. This will not replace or update existing shortcuts. "
|
||||||
|
"If you update DZGUI when it is launched via Steam, updates will be applied to the Steam version. "
|
||||||
|
"Not available if you selected the Flatpak client as your Steam default."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
checkbox_start_menu = (
|
||||||
|
"Add a start menu shortcut",
|
||||||
|
"Depends on local window manager settings. Will work on Steam Deck.",
|
||||||
|
)
|
||||||
|
checkbox_desktop_shortcut = ("Add a desktop shortcut", "Depends on the option above.")
|
||||||
|
|
||||||
|
### 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"
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
gi.require_version("Gdk", "3.0")
|
from gi.repository import Gtk, Gdk
|
||||||
from gi.repository import Gtk, Gdk # noqa E402
|
|
||||||
|
|
||||||
|
|
||||||
def copy_clipboard(text: str) -> None:
|
def copy_clipboard(text: str) -> None:
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from dzgui.const.constants import APP_NAME_LOWER, CSS_PATH
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk # noqa E402
|
from gi.repository import Gtk, Gdk
|
||||||
|
|
||||||
|
|
||||||
def add_class(widget: Gtk.Widget, label: str) -> None:
|
def add_class(widget: Gtk.Widget, label: str) -> None:
|
||||||
|
|||||||
@@ -41,10 +41,10 @@ def print_mods(mods: list[int]) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def write_diagnostic(config: Path, outfile: Path) -> None:
|
def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||||
# TODO: test availability on other distros
|
|
||||||
date = datetime.now().isoformat()
|
date = datetime.now().isoformat()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
# FIXME: ID_LIKE not available on CachyOS
|
||||||
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
distro = platform.freedesktop_os_release()["ID_LIKE"]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warn(e)
|
logger.warn(e)
|
||||||
@@ -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}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
from importlib import resources
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, IMAGES_PATH
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def make_parents(path: "Path") -> None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_dzgui_to_xdg_data(exe_path: Path) -> None:
|
||||||
|
dzgui = os.getenv("PYAPP")
|
||||||
|
if dzgui is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
make_parents(exe_path)
|
||||||
|
shutil.copy(dzgui, exe_path)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def find_icon_resource() -> "Path":
|
||||||
|
traversable = resources.files(APP_NAME_LOWER).joinpath(IMAGES_PATH)
|
||||||
|
images = Path(str(traversable))
|
||||||
|
icon = images.joinpath("icon.png")
|
||||||
|
return icon
|
||||||
@@ -7,11 +7,6 @@ from dzgui.const.constants import APP_NAME
|
|||||||
from dzgui.util.ip import get_coords
|
from dzgui.util.ip import get_coords
|
||||||
from dzgui.util.localize import number
|
from dzgui.util.localize import number
|
||||||
|
|
||||||
import gi
|
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
|
||||||
from gi.repository import Gtk # noqa E402
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from dzgui.const.enum import ServerTab
|
from dzgui.const.enum import ServerTab
|
||||||
@@ -59,7 +54,7 @@ class CalcDist:
|
|||||||
self,
|
self,
|
||||||
addr: str,
|
addr: str,
|
||||||
enum: "ServerTab",
|
enum: "ServerTab",
|
||||||
result_queue: "Queue",
|
result_queue: "Queue[tuple[str, Haversine | None, ServerTab]]",
|
||||||
controller: "Controller",
|
controller: "Controller",
|
||||||
cache: dict[str, Haversine],
|
cache: dict[str, Haversine],
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -72,7 +67,7 @@ class CalcDist:
|
|||||||
self.ip = self.addr
|
self.ip = self.addr
|
||||||
|
|
||||||
dist = self.compare(self.ip)
|
dist = self.compare(self.ip)
|
||||||
self.result_queue.put([self.addr, dist, self.enum])
|
self.result_queue.put((self.addr, dist, self.enum))
|
||||||
|
|
||||||
def compare(self, remote_ip: str) -> Haversine | None:
|
def compare(self, remote_ip: str) -> Haversine | None:
|
||||||
prefs = self.controller.get_prefs()
|
prefs = self.controller.get_prefs()
|
||||||
@@ -80,10 +75,10 @@ class CalcDist:
|
|||||||
if local is None:
|
if local is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
remote = get_coords(prefs.paths.ips, remote_ip)
|
remote = get_coords(prefs.paths.db, remote_ip)
|
||||||
except Exception:
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
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,39 @@
|
|||||||
|
import logging
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
from dzgui.const.constants import APP_NAME, FLATPAK_APPID, GTK_APP_ID, TMP_PATH
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def apply_overrides(xdg: dict[str, "Path"]) -> 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)
|
||||||
|
|
||||||
|
paths: list[str] = []
|
||||||
|
paths.extend([str(path) for path in xdg.values()])
|
||||||
|
paths.append(TMP_PATH)
|
||||||
|
|
||||||
|
for path in paths:
|
||||||
|
args = [
|
||||||
|
"flatpak",
|
||||||
|
"--user",
|
||||||
|
"override",
|
||||||
|
f"--filesystem={path}",
|
||||||
|
FLATPAK_APPID,
|
||||||
|
]
|
||||||
|
subprocess.run([*args], capture_output=True)
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import re
|
import re
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from dzgui.util.localize import number
|
from dzgui.util.localize import number
|
||||||
from dzgui.strings.generic import selectable_tree
|
from dzgui.strings.generic import selectable_tree
|
||||||
@@ -7,7 +8,7 @@ from dzgui.util.strings import no_mods, no_servers, workshop
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
def format_pango(text: str) -> str:
|
def format_pango(text: str) -> str:
|
||||||
@@ -60,7 +61,7 @@ def format_server_mods(mods: int) -> str:
|
|||||||
return f"Found {mods:n} {plural}. {workshop}"
|
return f"Found {mods:n} {plural}. {workshop}"
|
||||||
|
|
||||||
|
|
||||||
def format_player_count(model: Gtk.TreeModel | None, control: list) -> str:
|
def format_player_count(model: Gtk.TreeModel | None, control: list[Any]) -> str:
|
||||||
players = 0
|
players = 0
|
||||||
hits: int
|
hits: int
|
||||||
status: str
|
status: str
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import a2s
|
import a2s
|
||||||
|
from ipaddress import ip_address
|
||||||
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.config.ipdb import read_from_table
|
||||||
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:
|
||||||
@@ -30,58 +31,25 @@ class ReservedIPError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def ip2list(ip: str) -> list:
|
def ip2list(ip: str) -> list[int]:
|
||||||
return [int(digit) for digit in ip.split(".")]
|
return [int(digit) for digit in ip.split(".")]
|
||||||
|
|
||||||
|
|
||||||
# TODO: tests
|
def is_usable(ip: str) -> bool:
|
||||||
def is_reserved(digits: list) -> bool:
|
address = ip_address(ip)
|
||||||
reserved = [0, 10, 127]
|
if address.is_reserved or address.is_private:
|
||||||
|
return False
|
||||||
if digits[0] in reserved:
|
return True
|
||||||
return True
|
|
||||||
if digits[0] == 192 and digits[1] == 168:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def get_coords(ips: "Path", ip: str) -> Coords:
|
def get_coords(db_path: "Path", ip: str) -> Coords:
|
||||||
split = ip2list(ip)
|
if not is_usable(ip):
|
||||||
|
|
||||||
if is_reserved(split):
|
|
||||||
raise ReservedIPError(f"{ip} is a reserved IP address.")
|
raise ReservedIPError(f"{ip} is a reserved IP address.")
|
||||||
|
try:
|
||||||
prefix = f"^{split[0]}.{split[1]}."
|
lat, lon = read_from_table(db_path, ip)
|
||||||
|
return Coords(lat, lon)
|
||||||
if shutil.which("rg") is not None:
|
except Exception:
|
||||||
args = ["/usr/bin/rg", prefix, str(ips)]
|
raise GeolocationError("No matching records found")
|
||||||
else:
|
|
||||||
args = ["/usr/bin/grep", "-E", prefix, str(ips)]
|
|
||||||
proc = subprocess.run(args, capture_output=True, text=True)
|
|
||||||
|
|
||||||
if proc.returncode != 0:
|
|
||||||
raise GeolocationError("Failed to split records")
|
|
||||||
|
|
||||||
ip_list = proc.stdout.splitlines()
|
|
||||||
|
|
||||||
for address in ip_list:
|
|
||||||
fields = address.split(",")
|
|
||||||
upper = fields[1]
|
|
||||||
|
|
||||||
upper_digits = ip2list(upper)
|
|
||||||
if ip == upper:
|
|
||||||
return Coords(float(fields[-2]), float(fields[-1]))
|
|
||||||
|
|
||||||
if split[2] > upper_digits[2]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if split[2] == upper_digits[2]:
|
|
||||||
if split[3] > upper_digits[3]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if split[2] <= upper_digits[2]:
|
|
||||||
return Coords(float(fields[-2]), float(fields[-1]))
|
|
||||||
raise GeolocationError("No matching records found")
|
|
||||||
|
|
||||||
|
|
||||||
def get_local_ip() -> str:
|
def get_local_ip() -> str:
|
||||||
@@ -127,10 +95,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
|
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gdk # noqa E402
|
from gi.repository import Gdk
|
||||||
|
|
||||||
|
|
||||||
def is_navkey(key: int) -> bool:
|
def is_navkey(key: int) -> bool:
|
||||||
nav_keys = [
|
nav_keys = [
|
||||||
@@ -18,6 +20,7 @@ def is_navkey(key: int) -> bool:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def is_ctrl_mask(event: Gdk.EventKey) -> bool:
|
def is_ctrl_mask(event: Gdk.EventKey) -> bool:
|
||||||
if event.state is Gdk.ModifierType.CONTROL_MASK:
|
if event.state is Gdk.ModifierType.CONTROL_MASK:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
def set_surrounding_margins(widget: Gtk.Widget, margin: int) -> None:
|
def set_surrounding_margins(widget: Gtk.Widget, margin: int) -> None:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
from typing import cast, TYPE_CHECKING
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gdk, GLib
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from dzgui.controllers.mc import Controller
|
||||||
|
from dzgui.views.base import OuterWindow
|
||||||
|
from dzgui.views.mixins.context_mixin import ContextMixin
|
||||||
|
from gi.repository import GdkPixbuf
|
||||||
|
|
||||||
|
# TODO: randomize/mock PII distance values
|
||||||
|
|
||||||
|
|
||||||
|
class IdleScreenshot:
|
||||||
|
def __init__(
|
||||||
|
self, controller: "Controller", window: "OuterWindow", scrot_path: Path
|
||||||
|
) -> None:
|
||||||
|
self.window = window
|
||||||
|
self.controller = controller
|
||||||
|
self.scrot_path = scrot_path
|
||||||
|
|
||||||
|
clock = window.get_frame_clock()
|
||||||
|
if clock is None:
|
||||||
|
raise TypeError("Widget not realized, FrameClock not initialized")
|
||||||
|
clock.connect("after-paint", self.callback)
|
||||||
|
|
||||||
|
def callback(self, clock: Gdk.FrameClock) -> None:
|
||||||
|
GLib.timeout_add(1000, self.check_idle)
|
||||||
|
|
||||||
|
def check_idle(self) -> bool:
|
||||||
|
tv = self.controller.get_active_treeview()
|
||||||
|
loaded = tv.is_loaded()
|
||||||
|
if loaded is True:
|
||||||
|
self.emit_key_press(tv)
|
||||||
|
GLib.timeout_add(1000, self.save_and_quit)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
def emit_key_press(self, widget: "ContextMixin") -> None:
|
||||||
|
window = widget.get_parent_window()
|
||||||
|
if window is None:
|
||||||
|
raise TypeError(f"Failed to find a valid parent window for {widget}")
|
||||||
|
event = Gdk.EventKey.new(Gdk.EventType.KEY_PRESS)
|
||||||
|
# NOTE: GTK introspection returns Gdk.Event type
|
||||||
|
event_key = cast(Gdk.EventKey, event)
|
||||||
|
event_key.keyval = Gdk.KEY_l
|
||||||
|
event_key.state = Gdk.ModifierType.CONTROL_MASK
|
||||||
|
event_key.window = window
|
||||||
|
event_key.time = Gdk.CURRENT_TIME
|
||||||
|
event_key.send_event = True
|
||||||
|
# TODO: suppress errors from synthetic key events
|
||||||
|
widget.present_menu(widget, event)
|
||||||
|
|
||||||
|
def save_and_quit(self) -> None:
|
||||||
|
self.save_pixbuf()
|
||||||
|
self.controller.save_res_and_quit()
|
||||||
|
|
||||||
|
def save_pixbuf(self) -> None:
|
||||||
|
pixbuf = self.capture_application()
|
||||||
|
if pixbuf:
|
||||||
|
pixbuf.savev(str(self.scrot_path), "png", [], [])
|
||||||
|
|
||||||
|
def capture_application(self) -> "GdkPixbuf.Pixbuf":
|
||||||
|
# NOTE: composited context menu over X11 window
|
||||||
|
root = Gdk.get_default_root_window()
|
||||||
|
|
||||||
|
xwindow = self.window.get_window()
|
||||||
|
if xwindow is None:
|
||||||
|
raise ValueError("Failed to get a valid X11 window")
|
||||||
|
index, x, y = xwindow.get_origin()
|
||||||
|
width, height = self.window.get_size()
|
||||||
|
|
||||||
|
application = Gdk.pixbuf_get_from_window(root, x, y, width, height)
|
||||||
|
if application is None:
|
||||||
|
raise ValueError("Failed to get a valid GdkPixbuf")
|
||||||
|
return application
|
||||||
@@ -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"
|
||||||
@@ -53,6 +31,7 @@ show_details = "Show server details"
|
|||||||
refresh_players = "Refresh player count"
|
refresh_players = "Refresh player count"
|
||||||
open_workshop = "Open in Steam Workshop"
|
open_workshop = "Open in Steam Workshop"
|
||||||
unsub_mod = "Unsubscribe mod"
|
unsub_mod = "Unsubscribe mod"
|
||||||
|
validate_mod = "Validate mod"
|
||||||
copy_name = "Copy name to clipboard"
|
copy_name = "Copy name to clipboard"
|
||||||
copy_ip = "Copy IP to clipboard"
|
copy_ip = "Copy IP to clipboard"
|
||||||
copy_log = "Copy record(s) to clipboard"
|
copy_log = "Copy record(s) to clipboard"
|
||||||
@@ -80,7 +59,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 +78,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."
|
||||||
)
|
)
|
||||||
@@ -143,6 +118,7 @@ servers = {
|
|||||||
"0": "toggle filter 10",
|
"0": "toggle filter 10",
|
||||||
"Minus": "toggle filter 11",
|
"Minus": "toggle filter 11",
|
||||||
"Backslash": "toggle filter 12",
|
"Backslash": "toggle filter 12",
|
||||||
|
"Apostrophe": "toggle filter 13",
|
||||||
}
|
}
|
||||||
vim = {
|
vim = {
|
||||||
"j": "Move down a row/scroll up",
|
"j": "Move down a row/scroll up",
|
||||||
@@ -182,9 +158,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
|
|
||||||
all_maps = "All maps"
|
|
||||||
|
|
||||||
# state
|
# state
|
||||||
unknown = "Unknown"
|
unknown = "Unknown"
|
||||||
@@ -192,11 +166,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"
|
||||||
@@ -224,6 +199,8 @@ class ModPanelStrings:
|
|||||||
clear_highlights: str
|
clear_highlights: str
|
||||||
select_stale: str
|
select_stale: str
|
||||||
select_stale_tooltip: str
|
select_stale_tooltip: str
|
||||||
|
validate: str
|
||||||
|
validate_tooltip: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
@@ -273,6 +250,7 @@ class Crumbs:
|
|||||||
developers: str
|
developers: str
|
||||||
default: str
|
default: str
|
||||||
offline: str
|
offline: str
|
||||||
|
connect: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True, frozen=True)
|
@dataclass(slots=True, frozen=True)
|
||||||
@@ -286,11 +264,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 +286,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
|
||||||
@@ -324,7 +299,7 @@ class Options:
|
|||||||
init = Init(
|
init = Init(
|
||||||
is_steam_running="Is Steam running? DZGUI must be run on top of Steam.",
|
is_steam_running="Is Steam running? DZGUI must be run on top of Steam.",
|
||||||
is_dayz_running="Is DayZ already running? DZGUI cannot launch DayZ if another process is using it.",
|
is_dayz_running="Is DayZ already running? DZGUI cannot launch DayZ if another process is using it.",
|
||||||
requires_steam="DZGUI requires that Steam or Flatpak Steam be installed on the system.",
|
requires_steam="Your chosen client (Steam/Flatpak Steam) must be installed on the system.",
|
||||||
)
|
)
|
||||||
|
|
||||||
mod_panel = ModPanelStrings(
|
mod_panel = ModPanelStrings(
|
||||||
@@ -333,8 +308,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",
|
||||||
@@ -346,6 +321,8 @@ mod_panel = ModPanelStrings(
|
|||||||
clear_highlights="Clears highlights and reverts the table to a default state",
|
clear_highlights="Clears highlights and reverts the table to a default state",
|
||||||
select_stale="Select stale",
|
select_stale="Select stale",
|
||||||
select_stale_tooltip="Only selects highlighted stale mods",
|
select_stale_tooltip="Only selects highlighted stale mods",
|
||||||
|
validate="Validate selected",
|
||||||
|
validate_tooltip="Validates selected mods and updates them if necessary",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -378,11 +355,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 +385,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",
|
||||||
@@ -464,6 +438,7 @@ crumbs = Crumbs(
|
|||||||
developers="Options > Developers",
|
developers="Options > Developers",
|
||||||
default="Servers > ",
|
default="Servers > ",
|
||||||
offline="Mods > Play offline",
|
offline="Mods > Play offline",
|
||||||
|
connect="Connect",
|
||||||
)
|
)
|
||||||
|
|
||||||
no_mods = "No local mods found."
|
no_mods = "No local mods found."
|
||||||
@@ -476,13 +451,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 +511,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"
|
||||||
|
|
||||||
|
|||||||
@@ -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}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import logging
|
import logging
|
||||||
import signal
|
import signal
|
||||||
import warnings
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Literal
|
from typing import Any, Literal, Self, TYPE_CHECKING
|
||||||
|
|
||||||
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?
|
||||||
@@ -34,27 +38,28 @@ import gi
|
|||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
gi.require_version("GLibUnix", "2.0")
|
gi.require_version("GLibUnix", "2.0")
|
||||||
from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore # noqa E402
|
from gi.repository import Gtk, GLib, GLibUnix, Gdk # type: ignore
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
|
|
||||||
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: "App") -> 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,13 +71,22 @@ 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
|
||||||
MainController.populate_model(MainController.get_active_treeview())
|
MainController.populate_model(MainController.get_active_treeview())
|
||||||
|
|
||||||
|
try:
|
||||||
|
from dzgui.util.save_screenshot import IdleScreenshot
|
||||||
|
|
||||||
|
scrot = app.get_automation_param("screenshot_path")
|
||||||
|
self.fullscreen()
|
||||||
|
IdleScreenshot(MainController, self, scrot)
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
def _on_delete_event(self, window: "OuterWindow", event: Gdk.EventKey) -> None:
|
def _on_delete_event(self, window: "OuterWindow", event: Gdk.EventKey) -> None:
|
||||||
self.halt_proc_and_quit()
|
self.halt_proc_and_quit()
|
||||||
|
|
||||||
@@ -276,13 +290,26 @@ 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)
|
||||||
|
|
||||||
|
self.automation_params: dict[str, Any] = {}
|
||||||
|
|
||||||
|
def get_automation_param(self, key: str) -> Any:
|
||||||
|
return self.automation_params[key]
|
||||||
|
|
||||||
|
def set_automation_param(self, key: str, value: Any) -> None:
|
||||||
|
self.automation_params[key] = value
|
||||||
|
|
||||||
|
def _on_activate(self, app: Self) -> None:
|
||||||
|
self.win = OuterWindow(app)
|
||||||
|
|
||||||
accel = Gtk.AccelGroup()
|
accel = Gtk.AccelGroup()
|
||||||
accel.connect(
|
accel.connect(
|
||||||
@@ -293,9 +320,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"
|
||||||
@@ -326,13 +350,3 @@ class App(Gtk.Application):
|
|||||||
|
|
||||||
|
|
||||||
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,13 +1,13 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import APP_NAME, NO_EXPAND, NO_FILL, NO_PADDING
|
from dzgui.const.constants import APP_NAME
|
||||||
from dzgui.const.enum import ButtonType
|
from dzgui.const.enum import ButtonType
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk # noqa E402
|
from gi.repository import Gtk, Gdk
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
@@ -19,7 +19,11 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
class ContextualButton(Gtk.Button):
|
class ContextualButton(Gtk.Button):
|
||||||
def __init__(
|
def __init__(
|
||||||
self, label: str, opens: "NotebookPage", tooltip: str, context: ButtonType
|
self,
|
||||||
|
label: str,
|
||||||
|
opens: "NotebookPage | None",
|
||||||
|
tooltip: str,
|
||||||
|
context: ButtonType,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
label=label,
|
label=label,
|
||||||
@@ -51,9 +55,9 @@ class ButtonBox(Gtk.Box):
|
|||||||
|
|
||||||
for side_button in ButtonType:
|
for side_button in ButtonType:
|
||||||
button = ContextualButton(
|
button = ContextualButton(
|
||||||
label=side_button.dict["label"],
|
label=side_button.label,
|
||||||
opens=side_button.dict["opens"],
|
opens=side_button.opens,
|
||||||
tooltip=side_button.dict["tooltip"],
|
tooltip=side_button.tooltip,
|
||||||
context=side_button,
|
context=side_button,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -63,7 +67,7 @@ class ButtonBox(Gtk.Box):
|
|||||||
|
|
||||||
self.buttons.append(button)
|
self.buttons.append(button)
|
||||||
button.connect("clicked", self._on_selection_button_clicked)
|
button.connect("clicked", self._on_selection_button_clicked)
|
||||||
self.pack_start(button, NO_EXPAND, NO_FILL, NO_PADDING)
|
self.pack_start(button, expand=False, fill=False, padding=0)
|
||||||
|
|
||||||
def _focus_first_button(self, emitter: "Emitter") -> None:
|
def _focus_first_button(self, emitter: "Emitter") -> None:
|
||||||
self.buttons[0].grab_focus()
|
self.buttons[0].grab_focus()
|
||||||
|
|||||||
@@ -1,27 +1,25 @@
|
|||||||
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.strings import buttons
|
from dzgui.strings import buttons
|
||||||
from dzgui.util.strings import (
|
from dzgui.util.strings import (
|
||||||
alert_button_tooltip,
|
|
||||||
atomic_buttons,
|
atomic_buttons,
|
||||||
connect_panel,
|
|
||||||
)
|
)
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.icons import (
|
||||||
CLIPBOARD,
|
ICON_CLIPBOARD,
|
||||||
INPUT_KEYBOARD,
|
ICON_CLOSE,
|
||||||
LIST_ADD,
|
ICON_INPUT_KEYBOARD,
|
||||||
REFRESH_ICON,
|
ICON_LIST_ADD,
|
||||||
STEAM_ICON,
|
ICON_REFRESH,
|
||||||
WARNING,
|
ICON_STEAM,
|
||||||
WEB_BROWSER,
|
ICON_WEB_BROWSER,
|
||||||
)
|
)
|
||||||
|
from dzgui.strings import connect_panel
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk, GLib # noqa E402
|
from gi.repository import Gtk, GLib
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
@@ -63,8 +61,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) # type: ignore[no-untyped-call]
|
||||||
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):
|
||||||
@@ -91,36 +98,47 @@ class LargeIconTextButton(IconButton):
|
|||||||
|
|
||||||
|
|
||||||
class ClipboardButton(IconTextButton):
|
class ClipboardButton(IconTextButton):
|
||||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
def __init__(self, func: Callable[[], str]) -> None:
|
||||||
super().__init__(CLIPBOARD, atomic_buttons.copy)
|
super().__init__(ICON_CLIPBOARD, atomic_buttons.copy)
|
||||||
self.controller = controller
|
|
||||||
self.connect("clicked", self._on_button_clicked, func)
|
self.connect("clicked", self._on_button_clicked, func)
|
||||||
|
|
||||||
self.set_tooltip_text("Copy to clipboard")
|
self.set_tooltip_text(buttons.clipboard_button)
|
||||||
|
|
||||||
def _on_button_clicked(self, button: Self, func: Callable) -> None:
|
def _on_button_clicked(self, button: Self, func: Callable[[], str]) -> 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
|
|
||||||
class CopyIpButton(ClipboardButton):
|
class CopyIpButton(ClipboardButton):
|
||||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
def __init__(self, func: Callable[[], str]) -> None:
|
||||||
super().__init__(controller, func)
|
super().__init__(func)
|
||||||
|
|
||||||
self.set_tooltip_text("Copy IP to clipboard")
|
self.set_tooltip_text(buttons.clipboard_ip_button)
|
||||||
|
|
||||||
|
|
||||||
|
class CloseButton(IconTextButton):
|
||||||
|
def __init__(self, label: str) -> None:
|
||||||
|
super().__init__(icon=ICON_CLOSE, label=label)
|
||||||
|
|
||||||
|
self.set_tooltip_text(buttons.unset_button_tooltip)
|
||||||
|
|
||||||
|
|
||||||
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=ICON_WEB_BROWSER, label=label)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class RefreshButton(IconTextButton):
|
class RefreshButton(IconTextButton):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
super().__init__(
|
super().__init__(
|
||||||
icon=REFRESH_ICON,
|
icon=ICON_REFRESH,
|
||||||
label=atomic_buttons.refresh,
|
label=atomic_buttons.refresh,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -137,18 +155,24 @@ class RefreshButton(IconTextButton):
|
|||||||
self.set_tooltip_text(atomic_buttons.refresh_tooltip)
|
self.set_tooltip_text(atomic_buttons.refresh_tooltip)
|
||||||
|
|
||||||
self.connect("clicked", self._on_refresh_clicked)
|
self.connect("clicked", self._on_refresh_clicked)
|
||||||
emitter.connect("servers_loaded", self.start_decrement)
|
emitter.connect("servers_loaded", self._on_load)
|
||||||
|
emitter.connect("servers_timed_out", self._on_timeout)
|
||||||
|
|
||||||
|
def _on_timeout(self, emitter: "Emitter") -> None:
|
||||||
|
self.is_clicked = True
|
||||||
|
self.start_decrement()
|
||||||
|
|
||||||
|
def _on_load(self, emitter: "Emitter", tab: "ServerTab") -> None:
|
||||||
|
self.start_decrement()
|
||||||
|
|
||||||
def _on_refresh_clicked(self, button: Self) -> None:
|
def _on_refresh_clicked(self, button: Self) -> None:
|
||||||
"""Spawned in a thread"""
|
"""Spawned in a thread"""
|
||||||
if self.controller.get_active_treeview().is_loaded() is False:
|
|
||||||
return
|
|
||||||
self.is_clicked = True
|
self.is_clicked = True
|
||||||
self.controller.refresh_tree()
|
self.controller.refresh_tree()
|
||||||
# TODO: get server tab enum
|
# TODO: get server tab enum
|
||||||
# if LAN tab, reload existing entries in place
|
# if LAN tab, reload existing entries in place
|
||||||
|
|
||||||
def start_decrement(self, emitter: "Emitter", tab: "ServerTab") -> None:
|
def start_decrement(self) -> None:
|
||||||
if self.is_clicked is False:
|
if self.is_clicked is False:
|
||||||
return
|
return
|
||||||
if self.loading:
|
if self.loading:
|
||||||
@@ -179,7 +203,7 @@ class RefreshButton(IconTextButton):
|
|||||||
|
|
||||||
class KeysButton(IconTextButton):
|
class KeysButton(IconTextButton):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
super().__init__(icon=INPUT_KEYBOARD, label=atomic_buttons.keys)
|
super().__init__(icon=ICON_INPUT_KEYBOARD, label=atomic_buttons.keys)
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.set_margin_top(10)
|
self.set_margin_top(10)
|
||||||
@@ -196,7 +220,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=ICON_STEAM, label=connect_panel.connect_button)
|
||||||
self.set_tooltip_text(connect_panel.connect_tooltip)
|
self.set_tooltip_text(connect_panel.connect_tooltip)
|
||||||
|
|
||||||
|
|
||||||
@@ -216,28 +240,40 @@ 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=ICON_LIST_ADD, label=connect_panel.add_button)
|
||||||
self.set_tooltip_text(connect_panel.add_tooltip)
|
self.set_tooltip_text(connect_panel.add_tooltip)
|
||||||
|
|
||||||
|
|
||||||
class LoggerAlertsButton(IconTextButton):
|
class SpinnerButton(Gtk.Button):
|
||||||
def __init__(self, warnings: int, errors: int) -> None:
|
def __init__(self, label: str) -> None:
|
||||||
warnings_text = ""
|
super().__init__()
|
||||||
errors_text = ""
|
|
||||||
separator = ""
|
self.text = label
|
||||||
if warnings > 0:
|
self.label = Gtk.Label(label=label, halign=Gtk.Align.CENTER)
|
||||||
warnings_plural = pluralize("warnings", warnings)
|
|
||||||
warnings_text = f"{warnings} {warnings_plural}"
|
self.spinner = Gtk.Spinner()
|
||||||
if errors > 0:
|
self.spinner.set_halign(Gtk.Align.END)
|
||||||
errors_plural = pluralize("errors", errors)
|
self.spinner.set_sensitive(False)
|
||||||
errors_text = f" {errors} {errors_plural}"
|
|
||||||
if warnings + errors > 1:
|
grid = Gtk.Grid(column_spacing=10)
|
||||||
separator = ","
|
for el in self.label, self.spinner:
|
||||||
concat = warnings_text + separator + errors_text
|
grid.add(el)
|
||||||
super().__init__(
|
|
||||||
icon=WARNING,
|
self.add(grid)
|
||||||
label=f"Loaded with{concat}",
|
|
||||||
)
|
self.connect("clicked", self._on_button_clicked)
|
||||||
self.set_halign(Gtk.Align.END)
|
self.connect("map", self._on_map)
|
||||||
self.set_hexpand(True)
|
|
||||||
self.set_tooltip_text(alert_button_tooltip)
|
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
|
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
from typing import Self, TYPE_CHECKING
|
from typing import Self, TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.constants import CARET_DOWN, CARET_UP, UDP_PORT
|
from dzgui.const.icons import ICON_CARET_DOWN, ICON_CARET_UP
|
||||||
|
from dzgui.const.constants import UDP_PORT
|
||||||
from dzgui.model.servers import ServerModelManager
|
from dzgui.model.servers import ServerModelManager
|
||||||
from dzgui.strings import connect_panel
|
from dzgui.strings import connect_panel
|
||||||
|
from dzgui.strings.buttons import (
|
||||||
|
dock_button_collapse,
|
||||||
|
dock_button_expand,
|
||||||
|
unset_button_label,
|
||||||
|
)
|
||||||
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,
|
||||||
@@ -17,7 +24,7 @@ from dzgui.views.components.labels import BoldLabel
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk # noqa E402
|
from gi.repository import Gtk, Gdk
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -29,27 +36,24 @@ ROWS = 1
|
|||||||
|
|
||||||
class DockButton(IconButton):
|
class DockButton(IconButton):
|
||||||
def __init__(self, child: Gtk.Widget) -> None:
|
def __init__(self, child: Gtk.Widget) -> None:
|
||||||
super().__init__(CARET_DOWN)
|
super().__init__(ICON_CARET_DOWN)
|
||||||
|
|
||||||
self.expanded = True
|
self.expanded = True
|
||||||
self.child = child
|
self.child = child
|
||||||
self.connect("clicked", self._on_click)
|
self.connect("clicked", self._on_click)
|
||||||
|
|
||||||
self.collapse_text = "Collapse the connection panel"
|
|
||||||
self.expand_text = "Expand the connection panel"
|
|
||||||
|
|
||||||
self.set_margin_top(10)
|
self.set_margin_top(10)
|
||||||
self.set_tooltip_text(self.collapse_text)
|
self.set_tooltip_text(dock_button_collapse)
|
||||||
|
|
||||||
def collapse(self) -> None:
|
def collapse(self) -> None:
|
||||||
self.set_image(Icon(CARET_UP))
|
self.set_image(Icon(ICON_CARET_UP))
|
||||||
self.child.hide()
|
self.child.hide()
|
||||||
self.set_tooltip_text(self.expand_text)
|
self.set_tooltip_text(dock_button_expand)
|
||||||
|
|
||||||
def expand(self) -> None:
|
def expand(self) -> None:
|
||||||
self.set_image(Icon(CARET_DOWN))
|
self.set_image(Icon(ICON_CARET_DOWN))
|
||||||
self.child.show()
|
self.child.show()
|
||||||
self.set_tooltip_text(self.collapse_text)
|
self.set_tooltip_text(dock_button_collapse)
|
||||||
|
|
||||||
def _on_click(self, button: Self) -> None:
|
def _on_click(self, button: Self) -> None:
|
||||||
if self.expanded:
|
if self.expanded:
|
||||||
@@ -80,7 +84,6 @@ class LanPanel(Gtk.Frame):
|
|||||||
self.scan = Gtk.Button(label=connect_panel.lan_scan_button)
|
self.scan = Gtk.Button(label=connect_panel.lan_scan_button)
|
||||||
self.scan.connect("clicked", self._on_scan_clicked)
|
self.scan.connect("clicked", self._on_scan_clicked)
|
||||||
|
|
||||||
# TODO: strings
|
|
||||||
self.early_abort = Gtk.CheckButton(label=connect_panel.lan_checkbox)
|
self.early_abort = Gtk.CheckButton(label=connect_panel.lan_checkbox)
|
||||||
self.early_abort.set_active(True)
|
self.early_abort.set_active(True)
|
||||||
self.early_abort.set_tooltip_text(connect_panel.lan_abort_tooltip)
|
self.early_abort.set_tooltip_text(connect_panel.lan_abort_tooltip)
|
||||||
@@ -186,18 +189,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.get_fav_ip)
|
||||||
|
|
||||||
|
self.unset_button = CloseButton(unset_button_label)
|
||||||
|
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 +217,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:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from dzgui.util.format import embolden
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa E402
|
from gi.repository import Gtk
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -43,7 +43,7 @@ class Breadcrumbs(Gtk.Label):
|
|||||||
label = page.get_current_tab_text()
|
label = page.get_current_tab_text()
|
||||||
self.set_server_crumbs(label)
|
self.set_server_crumbs(label)
|
||||||
else:
|
else:
|
||||||
crumbs = enum.dict["crumbs"]
|
crumbs = enum.crumbs
|
||||||
self.set_crumbs(crumbs)
|
self.set_crumbs(crumbs)
|
||||||
|
|
||||||
def set_server_crumbs(self, suffix: str) -> None:
|
def set_server_crumbs(self, suffix: str) -> None:
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
from typing import Any, Callable, TYPE_CHECKING
|
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.icons import ICON_VIEW_CONCEAL, ICON_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.options import api_placeholder, api_validate
|
||||||
from dzgui.strings.errors import api_popover
|
from dzgui.strings.errors import api_popover
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk, GObject # noqa E402
|
from gi.repository import Gtk, Gdk, GObject
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -26,22 +27,19 @@ 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):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
controller: "Controller",
|
controller: "Controller",
|
||||||
func: Callable,
|
func: Callable[[str], bool],
|
||||||
placeholder_text: str = "",
|
placeholder_text: str = "",
|
||||||
tooltip_text: str = "",
|
tooltip_text: str = "",
|
||||||
) -> None:
|
) -> None:
|
||||||
@@ -49,7 +47,6 @@ class ValidatedEntry(Gtk.Entry):
|
|||||||
hexpand=True, placeholder_text=placeholder_text, tooltip_text=tooltip_text
|
hexpand=True, placeholder_text=placeholder_text, tooltip_text=tooltip_text
|
||||||
)
|
)
|
||||||
|
|
||||||
self.func: Callable
|
|
||||||
self.set_validation_func(func)
|
self.set_validation_func(func)
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
|
|
||||||
@@ -75,7 +72,7 @@ class ValidatedEntry(Gtk.Entry):
|
|||||||
self.emit("string_validated", False)
|
self.emit("string_validated", False)
|
||||||
remove_class(self, self.classname)
|
remove_class(self, self.classname)
|
||||||
|
|
||||||
def set_validation_func(self, func: Callable) -> None:
|
def set_validation_func(self, func: Callable[[str], bool]) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def insert_icon(self) -> None:
|
def insert_icon(self) -> None:
|
||||||
@@ -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
|
||||||
@@ -149,20 +141,19 @@ class APIEntry(Gtk.Box):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||||
|
|
||||||
self.func: Callable | None = None
|
self.func: Callable[[str], None] | None = None
|
||||||
|
|
||||||
# TODO: strings
|
|
||||||
self.entry = Gtk.Entry(
|
self.entry = Gtk.Entry(
|
||||||
width_chars=60, hexpand=True, placeholder_text="Enter API key"
|
width_chars=60, hexpand=True, placeholder_text=api_placeholder
|
||||||
)
|
)
|
||||||
self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL)
|
self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, ICON_VIEW_REVEAL)
|
||||||
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||||
self.entry.set_visibility(False)
|
self.entry.set_visibility(False)
|
||||||
self.entry.connect("changed", self._on_text_changed)
|
self.entry.connect("changed", self._on_text_changed)
|
||||||
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")
|
self.submit = Gtk.Button(label=api_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)
|
||||||
|
|
||||||
@@ -213,9 +204,9 @@ class APIEntry(Gtk.Box):
|
|||||||
) -> None:
|
) -> None:
|
||||||
visible = widget.get_visibility()
|
visible = widget.get_visibility()
|
||||||
if visible:
|
if visible:
|
||||||
icon, state = VIEW_REVEAL, False
|
icon, state = ICON_VIEW_REVEAL, False
|
||||||
else:
|
else:
|
||||||
icon, state = VIEW_CONCEAL, True
|
icon, state = ICON_VIEW_CONCEAL, True
|
||||||
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||||
widget.set_visibility(state)
|
widget.set_visibility(state)
|
||||||
|
|
||||||
@@ -229,7 +220,7 @@ class APIEntry(Gtk.Box):
|
|||||||
res = self.func(text)
|
res = self.func(text)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def set_validation_func(self, func: Callable | None) -> None:
|
def set_validation_func(self, func: Callable[[str], None] | None) -> None:
|
||||||
self.func = func
|
self.func = func
|
||||||
|
|
||||||
def disable_button(self) -> None:
|
def disable_button(self) -> None:
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.const.enum import NotebookPage
|
from dzgui.const.enum import NotebookPage
|
||||||
from dzgui.const.constants import HELP_BUBBLE
|
from dzgui.const.icons import ICON_HELP_BUBBLE
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk # noqa E402
|
from gi.repository import Gtk, Gdk
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -20,7 +20,7 @@ class InfoEventBox(Gtk.EventBox):
|
|||||||
self.text = text
|
self.text = text
|
||||||
|
|
||||||
self.icon = Gtk.Image.new_from_icon_name(
|
self.icon = Gtk.Image.new_from_icon_name(
|
||||||
HELP_BUBBLE, Gtk.IconSize.LARGE_TOOLBAR
|
ICON_HELP_BUBBLE, Gtk.IconSize.LARGE_TOOLBAR
|
||||||
)
|
)
|
||||||
self.icon.set_opacity(0.8)
|
self.icon.set_opacity(0.8)
|
||||||
box = Gtk.Box()
|
box = Gtk.Box()
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -3,23 +3,22 @@ from typing import Literal, TYPE_CHECKING
|
|||||||
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
EXPAND,
|
|
||||||
NO_EXPAND,
|
|
||||||
NO_FILL,
|
|
||||||
NO_PADDING,
|
|
||||||
SEARCH_ICON,
|
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import FilterMode
|
from dzgui.const.icons import ICON_SEARCH
|
||||||
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.strings.filters import (
|
||||||
|
all_maps,
|
||||||
|
filters_heading,
|
||||||
|
keyword_placeholder,
|
||||||
|
map_placeholder,
|
||||||
|
)
|
||||||
from dzgui.views.components.maps_combo import MapsCombo
|
from dzgui.views.components.maps_combo import MapsCombo
|
||||||
from dzgui.views.components.labels import BoldLabel
|
from dzgui.views.components.labels import BoldLabel
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk, Pango, GLib # noqa E402
|
from gi.repository import Gtk, Gdk, Pango, GLib
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
@@ -29,21 +28,36 @@ 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, bool],
|
||||||
|
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 +65,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(
|
||||||
@@ -85,14 +97,13 @@ class ButtonGrid(Gtk.Grid):
|
|||||||
logger.info(f"User toggled button '{label}' to {state}")
|
logger.info(f"User toggled button '{label}' to {state}")
|
||||||
self.emitter.emit("check_toggled", label, state)
|
self.emitter.emit("check_toggled", label, state)
|
||||||
|
|
||||||
def get_checkboxes(self) -> list:
|
def get_checkboxes(self) -> list[Gtk.CheckButton]:
|
||||||
return self.checks
|
return self.checks
|
||||||
|
|
||||||
|
|
||||||
class KeywordEntry(Gtk.Entry):
|
class KeywordEntry(Gtk.Entry):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: "Controller") -> None:
|
||||||
# TODO :strings
|
super().__init__(placeholder_text=keyword_placeholder)
|
||||||
super().__init__(placeholder_text="Filter by keyword")
|
|
||||||
|
|
||||||
self.keyword = ""
|
self.keyword = ""
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
@@ -101,7 +112,7 @@ class KeywordEntry(Gtk.Entry):
|
|||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
self.connect("icon-release", lambda *args: self.activate())
|
self.connect("icon-release", lambda *args: self.activate())
|
||||||
|
|
||||||
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, SEARCH_ICON)
|
self.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, ICON_SEARCH)
|
||||||
self.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
self.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||||
|
|
||||||
self.emitter.connect("load_maps", self._on_maps_loaded)
|
self.emitter.connect("load_maps", self._on_maps_loaded)
|
||||||
@@ -138,10 +149,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,14 +170,14 @@ 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
|
self.filters_label = BoldLabel(filters_heading)
|
||||||
self.filters_label = BoldLabel("Filters")
|
|
||||||
|
|
||||||
self.emitter.connect(
|
self.emitter.connect(
|
||||||
"request_keyword_focus", lambda _: self.keyword_entry.grab_focus()
|
"request_keyword_focus", lambda _: self.keyword_entry.grab_focus()
|
||||||
@@ -190,13 +197,12 @@ class FilterPanel(Gtk.Box):
|
|||||||
|
|
||||||
self.maps_entry: Gtk.Entry = self.maps_combo.get_child() # type: ignore
|
self.maps_entry: Gtk.Entry = self.maps_combo.get_child() # type: ignore
|
||||||
self.maps_entry.set_completion(completion)
|
self.maps_entry.set_completion(completion)
|
||||||
# TODO: strings
|
self.maps_entry.set_placeholder_text(map_placeholder)
|
||||||
self.maps_entry.set_placeholder_text("Filter by map")
|
|
||||||
self.maps_entry.connect("changed", self._on_map_completion, True)
|
self.maps_entry.connect("changed", self._on_map_completion, True)
|
||||||
self.maps_entry.connect("key-press-event", self._on_map_entry_keypress)
|
self.maps_entry.connect("key-press-event", self._on_map_entry_keypress)
|
||||||
self.maps_entry.connect("activate", self._on_maps_activated)
|
self.maps_entry.connect("activate", self._on_maps_activated)
|
||||||
|
|
||||||
self.maps_combo.pack_start(renderer_text, EXPAND)
|
self.maps_combo.pack_start(renderer_text, expand=True)
|
||||||
self.maps_combo.connect("changed", self._on_map_changed)
|
self.maps_combo.connect("changed", self._on_map_changed)
|
||||||
self.maps_combo.connect("key-press-event", self._on_combo_keypress)
|
self.maps_combo.connect("key-press-event", self._on_combo_keypress)
|
||||||
|
|
||||||
@@ -206,7 +212,7 @@ class FilterPanel(Gtk.Box):
|
|||||||
self.maps_combo,
|
self.maps_combo,
|
||||||
self.button_grid,
|
self.button_grid,
|
||||||
):
|
):
|
||||||
self.pack_start(el, NO_EXPAND, NO_FILL, NO_PADDING)
|
self.pack_start(el, expand=False, fill=False, padding=0)
|
||||||
|
|
||||||
def _on_maps_activated(self, entry: Gtk.Entry) -> None:
|
def _on_maps_activated(self, entry: Gtk.Entry) -> None:
|
||||||
text = entry.get_text()
|
text = entry.get_text()
|
||||||
@@ -300,6 +306,7 @@ class FilterPanel(Gtk.Box):
|
|||||||
Gdk.KEY_0: 9,
|
Gdk.KEY_0: 9,
|
||||||
Gdk.KEY_minus: 10,
|
Gdk.KEY_minus: 10,
|
||||||
Gdk.KEY_backslash: 11,
|
Gdk.KEY_backslash: 11,
|
||||||
|
Gdk.KEY_apostrophe: 12,
|
||||||
}
|
}
|
||||||
if keyval not in mappings:
|
if keyval not in mappings:
|
||||||
return False
|
return False
|
||||||
@@ -308,6 +315,8 @@ class FilterPanel(Gtk.Box):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def toggle_check(self, digit: int) -> None:
|
def toggle_check(self, digit: int) -> None:
|
||||||
|
if not self.button_grid.is_sensitive():
|
||||||
|
return
|
||||||
checks = self.button_grid.get_checkboxes()
|
checks = self.button_grid.get_checkboxes()
|
||||||
check = checks[digit]
|
check = checks[digit]
|
||||||
state = check.get_active()
|
state = check.get_active()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
class Icon(Gtk.Image):
|
class Icon(Gtk.Image):
|
||||||
@@ -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)
|
||||||