mirror of
https://github.com/aclist/dztui.git
synced 2026-10-25 12:12:31 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
+17
-1
@@ -3,6 +3,8 @@
|
||||
## [7.0.0] Unreleased
|
||||
### Added
|
||||
- Setup wizard
|
||||
- Uninstall wizard
|
||||
- Descriptive details panel to certain error dialogs
|
||||
- Changelog text wrapping and formatting
|
||||
- Changelog ships with source
|
||||
- Documentation ships with source
|
||||
@@ -24,6 +26,7 @@
|
||||
- Set favorite server from tables
|
||||
- Detailed/copyable trace in critical error dialogs
|
||||
- Visual icons
|
||||
- Unset favorite server button
|
||||
- Integrated server notebook
|
||||
- Propagate width changes to all tables
|
||||
- Remember tree position in menus
|
||||
@@ -44,11 +47,18 @@
|
||||
- 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
|
||||
|
||||
### Changed
|
||||
- Conform to PEP 440 versioning for beta versions
|
||||
- Enforce fullscreen mode on Steam Deck
|
||||
- Reduce padding on keys button
|
||||
- Boldface breadcrumbs
|
||||
- Changed some dialog error messages from popups to popovers
|
||||
- Changed how filter buttons are packed in sidebar
|
||||
- Bold labels inside frames
|
||||
- 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
|
||||
@@ -61,18 +71,24 @@
|
||||
- Reduce size of geolocation DB on disk (~100MB)
|
||||
- Enable LAN page Empty/Full filters on first run of app
|
||||
- Propagate subscribed mods to Steam client
|
||||
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||
|
||||
### Dropped
|
||||
- Debug mode
|
||||
- Battlemetrics API support
|
||||
- Unused metadata from system log files
|
||||
- Branch switching
|
||||
- Manual mod install mode (describe rationale)
|
||||
- Force update mods
|
||||
|
||||
### Fixed
|
||||
- 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
|
||||
- Rare segfaults when changing maps (threading)
|
||||
- Moved dialogs out of threads
|
||||
- History table not being in lock-step with state file
|
||||
|
||||
### Unreleased
|
||||
- Load offline mods
|
||||
|
||||
@@ -67,3 +67,7 @@ It is recommended to follow
|
||||
integrates well with tooling and helps the project review your code.
|
||||
|
||||
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.
|
||||
@@ -8,7 +8,7 @@ runtime environment are built-in.
|
||||
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.
|
||||
- From a terminal, run the command ``./dzgui`` or double click on the extracted file in
|
||||
a file explorer.
|
||||
|
||||
+37
-7
@@ -3,17 +3,22 @@ Knowledge Base
|
||||
|
||||
.. _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,
|
||||
unresponsiveness, or a timeout.
|
||||
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 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::
|
||||
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.
|
||||
|
||||
You are responsible for the creation, storage, management, and revocation of your Web API key.
|
||||
|
||||
DZG-008: Periodically getting dropped from servers while connected
|
||||
-------------------------------------------------------------------
|
||||
In some cases, DayZ opens a large number of connections while connected to servers.
|
||||
|
||||
If your network does not have enough headroom or has settings departing from defaults, this may lead to getting dropped from servers,
|
||||
unresponsiveness, or a timeout.
|
||||
|
||||
If you are on Wi-Fi, try switching to a wired connection and see if the problem resolves itself. Consumer Wi-Fi routers
|
||||
tend to have less headroom for simultaneous connections than their wired counterparts.
|
||||
|
||||
DZG-009: Floating dialogs appear maximized on tiling window managers
|
||||
-------------------------------------------------------------------
|
||||
|
||||
The main DZGUI window and its child dialogs are expected to be rendered as floating by your window manager.
|
||||
DZGUI sends window manager hints to this effect, but tiling window managers designed to bisect the screen into quadrants (e.g., i3 window manager)
|
||||
may try to always launch applications in fullscreen.
|
||||
|
||||
To resolve this, set specific exclusions or window hints in your WM's configuration file.
|
||||
For example, for i3, add the following to your `XDG_CONFIG_HOME/i3/config` file (defaults to `$HOME/.config/i3/config`):
|
||||
|
||||
.. code:: console
|
||||
|
||||
for_window [instance="DZGUI"] floating enable, move position center
|
||||
for_window [instance="DZGUI - Dialog"] floating enable, move position center
|
||||
+4
-14
@@ -4,7 +4,7 @@ Setup
|
||||
DayZ license
|
||||
-----------------
|
||||
|
||||
Prepare a Steam account with a DayZ license (i.e., own the game). From Steam's right-click menu for the game options, under ``Compatibility``, enable a Proton version ≥ ``6.8`` (or use Proton
|
||||
Prepare a Steam account with a DayZ license (i.e., own the game). From Steam's right-click menu for the game options, under ``Compatibility``, enable a Proton version ≥ ``6.8`` (or use Proton
|
||||
Experimental for the latest version).
|
||||
|
||||
As of this writing, any recent version of Proton should work, and it is encouraged to use the most recent one.
|
||||
@@ -33,20 +33,10 @@ Once configured, you can insert this key in the app when launching it for the fi
|
||||
|
||||
Battlemetrics API key
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
**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).
|
||||
|
||||
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``.
|
||||
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.
|
||||
|
||||
Next steps
|
||||
--------------
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
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.
|
||||
|
||||
First, launch Steam in the **Large** (default) view.
|
||||
@@ -74,7 +78,7 @@ The final result will create box art looking like the above.
|
||||
:scale: 100%
|
||||
:alt: Adding a non-Steam game
|
||||
|
||||
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
|
||||
Next, right-click the DZGUI entry in your Library and select **Properties** to open the properties dialog. Next to the **Shortcut** field, you will see a small square box which represents the game’s
|
||||
icon. Click this to open a file explorer, navigate to ``$HOME/.local/share/dzgui``, and select ``icon.png``. This will add a small icon to the list view.
|
||||
|
||||
Finally, after you launch DZGUI for the first time, you should quit the application and return to the Library view. Select the **Recent Games** dropdown on the right-hand side.
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
import logging
|
||||
import requests
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.const.endpoints import BM_SERVERS
|
||||
|
||||
logger = logging.getLogger(APP_NAME)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from dzgui.api.servers import Record
|
||||
|
||||
|
||||
def get_attributes(key: str, uid: int) -> Any:
|
||||
# TODO: handle if key is not set
|
||||
# TODO: tests for malformed IDs/values
|
||||
|
||||
hdr = {"Authorization": "Bearer " + key}
|
||||
payload: dict[str, str] = {
|
||||
"filter[game]": "dayz",
|
||||
"sort": "-players",
|
||||
"filter[ids][whitelist]": str(uid),
|
||||
}
|
||||
res = requests.get(BM_SERVERS, params=payload, headers=hdr)
|
||||
res.raise_for_status()
|
||||
j = res.json()["data"][0]["attributes"]
|
||||
return j
|
||||
|
||||
|
||||
def map_id_to_record(key: str, uid: int) -> Optional["Record"]:
|
||||
from dzgui.api.servers import Record
|
||||
|
||||
try:
|
||||
record = get_attributes(key, uid)
|
||||
ip = record["ip"]
|
||||
port = int(record["port"])
|
||||
qport = int(record["portQuery"])
|
||||
return Record(ip, port, qport)
|
||||
except Exception:
|
||||
return None
|
||||
@@ -41,21 +41,6 @@ def test_ipdb() -> bool:
|
||||
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:
|
||||
if res.status_code == 200:
|
||||
return True
|
||||
|
||||
@@ -11,7 +11,6 @@ import threading
|
||||
from dataclasses import dataclass
|
||||
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.endpoints import STEAM_SERVERS
|
||||
from dzgui.util.format import format_exception
|
||||
@@ -478,16 +477,6 @@ def query_by_ip(addr: str) -> A2SInfo:
|
||||
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:
|
||||
try:
|
||||
info = a2s.info((record.ip, record.qport), 3.0)
|
||||
|
||||
+29
-13
@@ -49,9 +49,21 @@ class Shortcuts:
|
||||
|
||||
def find_appname_by_unsigned_id(self, appid: int) -> str:
|
||||
# NOTE: bitmask signed int back to 32-bit CRC
|
||||
for key in self.shortcuts["shortcuts"].keys():
|
||||
if self.shortcuts["shortcuts"][key]["appid"] & 0xFFFFFFFF == appid:
|
||||
return str(self.shortcuts["shortcuts"][key]["appname"])
|
||||
# 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:
|
||||
@@ -119,22 +131,18 @@ class Shortcuts:
|
||||
https://developer.valvesoftware.com/wiki/Add_Non-Steam_Game
|
||||
https://developer.valvesoftware.com/wiki/Steam_Library_Shortcuts
|
||||
|
||||
Wiki variously lists keys with title case and lowercase, but keys actually do
|
||||
not seem to be case-sensitive. Some entries generated by Steam do not match the wiki
|
||||
|
||||
Keys are entered into the dictionary in a linear insertion order
|
||||
|
||||
# TODO: try to replicate key case sensitivity as it is created by Steam with generic shortcut
|
||||
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
|
||||
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["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"] = ""
|
||||
@@ -142,7 +150,7 @@ class Shortcuts:
|
||||
NEW_ENTRY["IsHidden"] = 0
|
||||
NEW_ENTRY["AllowDesktopConfig"] = 1
|
||||
NEW_ENTRY["AllowOverlay"] = 1
|
||||
NEW_ENTRY["openvr"] = 0
|
||||
NEW_ENTRY["OpenVR"] = 0
|
||||
NEW_ENTRY["Devkit"] = 0
|
||||
NEW_ENTRY["DevkitGameID"] = ""
|
||||
NEW_ENTRY["DevkitOverrideAppID"] = 0
|
||||
@@ -152,6 +160,14 @@ class Shortcuts:
|
||||
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:
|
||||
try:
|
||||
backup = self.shortcuts_path.with_suffix(".vdf.bak")
|
||||
|
||||
+32
-4
@@ -16,6 +16,7 @@ from dzgui.const.constants import (
|
||||
APPID_DAYZ_EXP,
|
||||
APP_NAME,
|
||||
DAYZ_BINARY,
|
||||
DAYZ_LAUNCHER,
|
||||
DEBIAN_STEAM_PATH,
|
||||
DEFAULT_STEAM_PATH,
|
||||
FLATPAK_STEAM_PATH,
|
||||
@@ -139,6 +140,9 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
||||
j = r.json()
|
||||
rows = j["response"]["publishedfiledetails"]
|
||||
for row in rows:
|
||||
# NOTE: not a valid published file
|
||||
if row["result"] == 9:
|
||||
continue
|
||||
title = str(row["title"])
|
||||
_id = str(row["publishedfileid"])
|
||||
time = int(row["time_updated"])
|
||||
@@ -147,6 +151,22 @@ def get_remote_signatures(mods: list[str]) -> list[tuple[str, str, int, int]]:
|
||||
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,
|
||||
# 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:
|
||||
@@ -206,17 +226,25 @@ def launch_offline(
|
||||
return proc.returncode
|
||||
|
||||
|
||||
def find_loginusers(path: Path) -> Path:
|
||||
return path / "config" / "loginusers.vdf"
|
||||
|
||||
|
||||
def find_user_id(path: Path) -> str | None:
|
||||
resolved_path = path / "config" / "loginusers.vdf"
|
||||
resolved_path = find_loginusers(path)
|
||||
try:
|
||||
with open(resolved_path, "r") as f:
|
||||
v = vdf.load(f)
|
||||
# NOTE: beta client
|
||||
# /package/beta
|
||||
if len(v["users"]) == 1:
|
||||
return str(list(v["users"].keys())[0])
|
||||
for user in v["users"]:
|
||||
if v["users"][user]["MostRecent"] == "1":
|
||||
return str(user)
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warn(e)
|
||||
logger.debug(e)
|
||||
return None
|
||||
|
||||
|
||||
@@ -371,14 +399,14 @@ def get_client_allows_downloads(path: Path) -> bool:
|
||||
def is_dayz_running() -> bool:
|
||||
"""Subprocesses spawned from Steam will not show up in regular process tree"""
|
||||
procs = []
|
||||
substring = DAYZ_BINARY
|
||||
substring = [DAYZ_BINARY, DAYZ_LAUNCHER]
|
||||
for proc in psutil.process_iter():
|
||||
try:
|
||||
procs.append(proc.cmdline())
|
||||
except Exception as e:
|
||||
logger.warning(e)
|
||||
continue
|
||||
return any(substring in item for sublist in procs for item in sublist)
|
||||
return any(s in item for sublist in procs for item in sublist for s in substring)
|
||||
|
||||
|
||||
def get_app_path(folders_path: Path, appid: int) -> Path:
|
||||
|
||||
+2
-1
@@ -18,10 +18,10 @@ from dzgui.init.flock import lock_acquire
|
||||
from dzgui.init.prereqs import has_steam_client
|
||||
from dzgui.strings import boot
|
||||
|
||||
# from dzgui.util.map_count import get_map_count
|
||||
from dzgui.util.deck import is_steam_deck, is_game_mode
|
||||
from dzgui.util.dirs import make_parents
|
||||
from dzgui.util.localize import set_locale
|
||||
from dzgui.util.map_count import test_map_count
|
||||
from dzgui.util.redact import RedactionFilter, REDACTION_PATTERNS
|
||||
from dzgui.util.strings import init
|
||||
|
||||
@@ -119,6 +119,7 @@ def load_gui(version: str, is_debug: bool) -> None:
|
||||
del os.environ["GTK_IM_MODULE"]
|
||||
|
||||
if has_new_config(XDG.config) is False:
|
||||
test_map_count()
|
||||
migrate_cols_file(XDG.columns)
|
||||
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
||||
# TODO: add logging inside wizard
|
||||
|
||||
@@ -42,6 +42,7 @@ def rc2json(file: Path) -> str:
|
||||
|
||||
toggles = ["fullscreen"]
|
||||
deprecated = [
|
||||
"api_key",
|
||||
"staging_dir",
|
||||
"src_path",
|
||||
"steam_path",
|
||||
@@ -66,8 +67,6 @@ def rc2json(file: Path) -> str:
|
||||
value = str2bool(ntok)
|
||||
elif tok == "preferred_client":
|
||||
tok = "client"
|
||||
elif tok == "api_key":
|
||||
tok = "bm_api"
|
||||
elif tok == "ip_list":
|
||||
while True:
|
||||
ntok = lex.get_token()
|
||||
|
||||
@@ -3,7 +3,6 @@ import textwrap
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.util.dirs import copy_dzgui_to_xdg_data, find_icon_resource, make_parents
|
||||
|
||||
|
||||
@@ -21,7 +20,7 @@ def write_desktop_file(exe_path: Path) -> Path:
|
||||
Terminal=false
|
||||
Exec={exe_path}
|
||||
Name=DZGUI
|
||||
Comment=dzgui
|
||||
Comment=DayZ server browser and mod manager
|
||||
Icon={icon}
|
||||
Categories=Game"""
|
||||
|
||||
@@ -37,7 +36,8 @@ def write_desktop_file(exe_path: Path) -> Path:
|
||||
|
||||
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(f"Desktop/{APP_NAME}.desktop")
|
||||
link = Path.home().joinpath("Desktop/dzgui.desktop")
|
||||
make_parents(link)
|
||||
if link.exists():
|
||||
link.unlink()
|
||||
link.symlink_to(desktop_file)
|
||||
@@ -2,7 +2,6 @@
|
||||
Generic defaults used when initializing a config file from scratch
|
||||
"""
|
||||
config_boilerplate = {
|
||||
"bm_api": "",
|
||||
"fav_server": "",
|
||||
"fav_label": "",
|
||||
"name": "",
|
||||
|
||||
@@ -12,6 +12,7 @@ APPNAME_DAYZ = "DayZ"
|
||||
APPNAME_DAYZ_EXP = "DayZ Exp"
|
||||
APPNAME_DAYZ_EXP_HUMAN = "DayZ Experimental"
|
||||
DAYZ_BINARY = "DayZ_x64.exe"
|
||||
DAYZ_LAUNCHER = "DayZLauncher.exe"
|
||||
|
||||
LIBRARYFOLDERS_PATH = "steamapps/libraryfolders.vdf"
|
||||
WORKSHOP_PATH = "steamapps/workshop/content/" + str(APPID_DAYZ)
|
||||
@@ -38,6 +39,7 @@ HEX_ORANGE = "#FFAC1C"
|
||||
CARET_DOWN = "go-down-symbolic"
|
||||
CARET_UP = "go-up-symbolic"
|
||||
CLIPBOARD = "edit-copy-symbolic"
|
||||
CLOSE = "window-close-symbolic"
|
||||
FOLDER = "folder-symbolic"
|
||||
EDIT_DELETE = "edit-delete-symbolic"
|
||||
ERROR = "dialog-error-symbolic"
|
||||
|
||||
@@ -7,7 +7,6 @@ SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
||||
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
||||
APP_DETAILS = "https://store.steampowered.com/api/appdetails?"
|
||||
|
||||
BM_SERVERS = "https://api.battlemetrics.com/servers?"
|
||||
GITHUB = "https://github.com/aclist"
|
||||
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
||||
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
|
||||
@@ -27,5 +26,3 @@ GITHUB_ISSUES = "https://github.com/aclist/dzgui/issues"
|
||||
FORUM = "https://old.reddit.com/r/dzgui"
|
||||
SPONSORS = "https://github.com/sponsors/aclist"
|
||||
STEAM_API_SETUP = "https://steamcommunity.com/dev/apikey"
|
||||
BM_API_SETUP = "https://www.battlemetrics.com/developers"
|
||||
BM_BROWSE = "https://www.battlemetrics.com/servers/dayz"
|
||||
@@ -61,9 +61,6 @@ class Preferences(EnumWithAttrs):
|
||||
STEAM = {
|
||||
"key": "steam_api",
|
||||
}
|
||||
BM = {
|
||||
"key": "bm_api",
|
||||
}
|
||||
CLIENT = {
|
||||
"key": "client",
|
||||
}
|
||||
@@ -76,12 +73,6 @@ class Preferences(EnumWithAttrs):
|
||||
NAME = {
|
||||
"key": "name",
|
||||
}
|
||||
INSTALL = {
|
||||
"key": "auto_install",
|
||||
}
|
||||
DEBUG = {
|
||||
"key": "debug",
|
||||
}
|
||||
DEFAULT = {
|
||||
"key": "default_steam_path",
|
||||
}
|
||||
@@ -94,9 +85,6 @@ class Preferences(EnumWithAttrs):
|
||||
IP_LIST = {
|
||||
"key": "ip_list",
|
||||
}
|
||||
BRANCH = {
|
||||
"key": "branch",
|
||||
}
|
||||
START_TAB = {
|
||||
"key": "start_tab",
|
||||
}
|
||||
|
||||
@@ -145,6 +145,10 @@ class Emitter(GObject.GObject):
|
||||
def api_change_failed(self) -> None:
|
||||
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=())
|
||||
def already_saved_server(self) -> None:
|
||||
pass
|
||||
|
||||
+18
-15
@@ -88,7 +88,6 @@ class Controller(GObject.GObject):
|
||||
|
||||
# NOTE: suppress requests until entire UI is loaded
|
||||
self.loaded = False
|
||||
self.pending_jobs = 1
|
||||
|
||||
self.exit_event = threading.Event()
|
||||
self.cancel_event = threading.Event()
|
||||
@@ -115,9 +114,6 @@ class Controller(GObject.GObject):
|
||||
def query_config(self, key: Preferences) -> Any:
|
||||
return self.config_man.lookup(key)
|
||||
|
||||
def is_auto_install(self) -> bool:
|
||||
return bool(self.query_config(Preferences.INSTALL))
|
||||
|
||||
def suppress_signal(
|
||||
self, owner: Any, child: Any, func_name: str, state: bool
|
||||
) -> None:
|
||||
@@ -267,6 +263,7 @@ class Controller(GObject.GObject):
|
||||
self.open_page(NotebookPage.LOG)
|
||||
except Exception as e:
|
||||
dialog = ExceptionDialog(self, str(e))
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
|
||||
def select_colorized(self) -> None:
|
||||
@@ -300,10 +297,11 @@ class Controller(GObject.GObject):
|
||||
write_diagnostic(self.prefs.paths.config, file)
|
||||
except Exception as e:
|
||||
dialog = ExceptionDialog(self, str(e))
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
|
||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
||||
self.config_man.update_api_key(key, text)
|
||||
def update_steam_api_key(self, text: str) -> None:
|
||||
self.config_man.update_steam_api_key(text)
|
||||
|
||||
def set_resolution(self, window: "OuterWindow") -> None:
|
||||
self.config_man.set_resolution(window)
|
||||
@@ -402,6 +400,9 @@ class Controller(GObject.GObject):
|
||||
def get_menu(self) -> "MenuTreeView":
|
||||
return self.mediator.menu
|
||||
|
||||
def unset_fav(self) -> None:
|
||||
self.config_man.unset_fav()
|
||||
|
||||
def has_favorites(self) -> bool:
|
||||
favs = self.config_man.get_favorites()
|
||||
if len(favs) < 1:
|
||||
@@ -413,7 +414,7 @@ class Controller(GObject.GObject):
|
||||
|
||||
def add_by_str(self, addr: str) -> None:
|
||||
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:
|
||||
saved_tree = self.get_servers().get_saved()
|
||||
@@ -437,14 +438,8 @@ class Controller(GObject.GObject):
|
||||
ServerModelManager(self, tv).remove_from_history(record)
|
||||
|
||||
def connect_by_str(self, addr: str) -> None:
|
||||
if addr.isdigit():
|
||||
config_man = self.get_config_man()
|
||||
key = config_man.lookup(Preferences.BM)
|
||||
self.connection_man = ConnectionManager(self)
|
||||
self.connection_man.connect_by_id(int(addr), key)
|
||||
else:
|
||||
self.connection_man = ConnectionManager(self)
|
||||
self.connection_man.connect_by_ip(addr)
|
||||
self.connection_man = ConnectionManager(self)
|
||||
self.connection_man.connect_by_ip(addr)
|
||||
|
||||
def connect_by_record(self, record: "Record") -> None:
|
||||
self.connection_man = ConnectionManager(self)
|
||||
@@ -465,6 +460,9 @@ class Controller(GObject.GObject):
|
||||
open_workshop_page(mod, cmd)
|
||||
|
||||
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()
|
||||
if len(note) > 0:
|
||||
return True
|
||||
@@ -479,11 +477,13 @@ class Controller(GObject.GObject):
|
||||
return self.notes_man.get_note(record)
|
||||
|
||||
def add_note(self, note: str) -> None:
|
||||
# TODO: record should be cached upon creation of dialog
|
||||
tv = self.get_active_treeview()
|
||||
record = tv.get_record_string()
|
||||
self.notes_man.add_note(record, note)
|
||||
|
||||
def delete_note(self) -> None:
|
||||
# TODO: record should be cached upon creation of dialog
|
||||
tv = self.get_active_treeview()
|
||||
record = tv.get_record_string()
|
||||
self.notes_man.delete_note(record)
|
||||
@@ -525,6 +525,9 @@ class Controller(GObject.GObject):
|
||||
ind = self.config_man.get_start_tab()
|
||||
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:
|
||||
self.connection_man.update_and_connect(menu_only=True)
|
||||
|
||||
|
||||
+17
-1
@@ -3,6 +3,8 @@
|
||||
## [7.0.0] Unreleased
|
||||
### Added
|
||||
- Setup wizard
|
||||
- Uninstall wizard
|
||||
- Descriptive details panel to certain error dialogs
|
||||
- Changelog text wrapping and formatting
|
||||
- Changelog ships with source
|
||||
- Documentation ships with source
|
||||
@@ -24,6 +26,7 @@
|
||||
- Set favorite server from tables
|
||||
- Detailed/copyable trace in critical error dialogs
|
||||
- Visual icons
|
||||
- Unset favorite server button
|
||||
- Integrated server notebook
|
||||
- Propagate width changes to all tables
|
||||
- Remember tree position in menus
|
||||
@@ -44,11 +47,18 @@
|
||||
- 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
|
||||
|
||||
### Changed
|
||||
- Conform to PEP 440 versioning for beta versions
|
||||
- Enforce fullscreen mode on Steam Deck
|
||||
- Reduce padding on keys button
|
||||
- Boldface breadcrumbs
|
||||
- Changed some dialog error messages from popups to popovers
|
||||
- Changed how filter buttons are packed in sidebar
|
||||
- Bold labels inside frames
|
||||
- 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
|
||||
@@ -61,18 +71,24 @@
|
||||
- Reduce size of geolocation DB on disk (~100MB)
|
||||
- Enable LAN page Empty/Full filters on first run of app
|
||||
- Propagate subscribed mods to Steam client
|
||||
- Make j/k keys in context menus follow same wraparound logic as down arrow/up arrow
|
||||
|
||||
### Dropped
|
||||
- Debug mode
|
||||
- Battlemetrics API support
|
||||
- Unused metadata from system log files
|
||||
- Branch switching
|
||||
- Manual mod install mode (describe rationale)
|
||||
- Force update mods
|
||||
|
||||
### Fixed
|
||||
- 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
|
||||
- Rare segfaults when changing maps (threading)
|
||||
- Moved dialogs out of threads
|
||||
- History table not being in lock-step with state file
|
||||
|
||||
### Unreleased
|
||||
- Load offline mods
|
||||
|
||||
+16
-13
@@ -5,33 +5,36 @@ import warnings
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.init.libgi import test_libgi_missing
|
||||
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.views.dialogs.uninstall import UninstallWizard
|
||||
from dzgui.config.xdg import get_xdg_paths
|
||||
|
||||
parser = argparse.ArgumentParser(description=flags.description)
|
||||
parser.add_argument("-v", "--version", action="store_true", help=flags.version)
|
||||
parser.add_argument("-u", "--uninstall", action="store_true", help=flags.uninstall)
|
||||
parser.add_argument("-d", "--debug", action="store_true", help=flags.debug)
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.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()
|
||||
|
||||
|
||||
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:
|
||||
if args.version is True:
|
||||
print(get_version())
|
||||
sys.exit(0)
|
||||
if args.uninstall is True:
|
||||
uninstall()
|
||||
paths = get_xdg_paths()
|
||||
_is_steam_deck = is_steam_deck()
|
||||
UninstallWizard(_is_steam_deck, paths)
|
||||
sys.exit(0)
|
||||
if args.debug is True:
|
||||
warnings.filterwarnings("default", category=DeprecationWarning)
|
||||
if args.map is True:
|
||||
set_map_count()
|
||||
sys.exit(0)
|
||||
|
||||
version = get_version()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ from dzgui.const.constants import (
|
||||
WINDOW_DEFAULT_X,
|
||||
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.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
@@ -98,14 +98,13 @@ class ConfigManager:
|
||||
logger.critical(e)
|
||||
raise e
|
||||
|
||||
@call_on_thread(dialogs.checking_api)
|
||||
def update_api_key(self, key: Preferences, text: str) -> None:
|
||||
if key is Preferences.STEAM:
|
||||
res = test_steam_api(text)
|
||||
else:
|
||||
res = test_bm_api(text)
|
||||
@call_on_thread(dialogs.checking_api, show_dialog=False)
|
||||
def update_steam_api_key(self, text: str) -> None:
|
||||
res = test_steam_api(text)
|
||||
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:
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(lambda: self.emitter.emit("api_change_failed"))
|
||||
@@ -162,6 +161,13 @@ class ConfigManager:
|
||||
except Exception:
|
||||
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:
|
||||
try:
|
||||
real_key = self.enum_to_key(key)
|
||||
@@ -172,8 +178,8 @@ class ConfigManager:
|
||||
logger.critical(e)
|
||||
trace = traceback.format_exc()
|
||||
dialog = ExceptionDialog(self.controller, trace)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
raise e
|
||||
|
||||
def save_res_and_quit(self, tv: "ServerTreeView", window: "OuterWindow") -> None:
|
||||
columns = tv.get_columns()
|
||||
|
||||
@@ -13,6 +13,7 @@ from dzgui.api.shortcuts import Shortcuts
|
||||
|
||||
from dzgui.api.steam import (
|
||||
connect,
|
||||
connect_debug,
|
||||
get_app_allows_downloads,
|
||||
get_app_name,
|
||||
get_needs_update,
|
||||
@@ -45,6 +46,7 @@ from dzgui.strings.dialogs import (
|
||||
waiting_for_mods,
|
||||
waiting_for_directories,
|
||||
)
|
||||
from dzgui.strings import kb
|
||||
from dzgui.strings.server_mods import checkmark, resync
|
||||
from dzgui.util.format import format_mib
|
||||
from dzgui.util.strings import dialog, server_timeout
|
||||
@@ -86,6 +88,7 @@ class Prerequisites:
|
||||
mods: list[list[str]]
|
||||
game_mode: bool
|
||||
is_last_server: bool
|
||||
invalid_mods: list[tuple[str, str]]
|
||||
allows_downloads: tuple[bool, str]
|
||||
|
||||
|
||||
@@ -103,11 +106,6 @@ class ConnectionManager:
|
||||
self.remote_mod_ids: list[str] = []
|
||||
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)
|
||||
def connect_by_ip(self, addr: str) -> None:
|
||||
res = Servers.query_by_ip(addr)
|
||||
@@ -186,7 +184,6 @@ class ConnectionManager:
|
||||
else:
|
||||
allows_downloads = (True, "")
|
||||
|
||||
|
||||
client_name = self.controller.get_steam_client_name()
|
||||
client = self.controller.query_config(Preferences.CLIENT)
|
||||
running = is_steam_running(client)
|
||||
@@ -197,6 +194,11 @@ class ConnectionManager:
|
||||
|
||||
is_last = self.is_last_server()
|
||||
|
||||
# TODO: strings
|
||||
invalid_mods = [
|
||||
(mod[0], mod[1]) for mod in remote_mods if mod[2] == "Invalid mod"
|
||||
]
|
||||
|
||||
prereqs = Prerequisites(
|
||||
name=info.server_name,
|
||||
appid=info.game_id,
|
||||
@@ -212,6 +214,7 @@ class ConnectionManager:
|
||||
mods=remote_mods,
|
||||
game_mode=game_mode,
|
||||
is_last_server=is_last,
|
||||
invalid_mods=invalid_mods,
|
||||
allows_downloads=allows_downloads,
|
||||
)
|
||||
|
||||
@@ -267,6 +270,11 @@ class ConnectionManager:
|
||||
for mod in alpha_mods:
|
||||
if any(mod[1] in tuple for tuple in missing_mods):
|
||||
mod[2] = resync
|
||||
for mod in alpha_mods:
|
||||
# NOTE: if the mod is neither synched or out of date, it is a malformed mod
|
||||
if mod[2] == "":
|
||||
# TODO: strings
|
||||
mod[2] = "Invalid mod"
|
||||
|
||||
return alpha_mods, missing_mods
|
||||
|
||||
@@ -294,8 +302,17 @@ class ConnectionManager:
|
||||
|
||||
def _server_timeout(self) -> None:
|
||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||
dialog.set_details_buffer(kb.DZG_006)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
|
||||
def get_debug_args(self) -> str:
|
||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||
playername = self.controller.query_config(Preferences.NAME)
|
||||
return connect_debug(
|
||||
self.client, addr, self.appid, playername, self.remote_mod_ids
|
||||
)
|
||||
|
||||
def _connect_steam(self, menu_only: bool) -> None:
|
||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||
playername = self.controller.query_config(Preferences.NAME)
|
||||
|
||||
@@ -21,14 +21,30 @@ class FilterManager:
|
||||
strings.filter_3pp: True,
|
||||
strings.filter_night: True,
|
||||
strings.filter_full: show_full,
|
||||
strings.filter_lowpop: True,
|
||||
strings.filter_nonascii: False,
|
||||
strings.filter_duplicate: False,
|
||||
strings.filter_official: True,
|
||||
strings.filter_nonascii: False,
|
||||
strings.filter_lowpop: True,
|
||||
strings.filter_unofficial: True,
|
||||
strings.filter_duplicate: False,
|
||||
strings.filter_modded: True,
|
||||
}
|
||||
|
||||
# TODO: strings
|
||||
self.tooltips = {
|
||||
strings.filter_1pp: "First-person perspective",
|
||||
strings.filter_day: "In-game time between 0700 and 1659",
|
||||
strings.filter_empty: "Servers with no players",
|
||||
strings.filter_3pp: "Third-person perspective",
|
||||
strings.filter_night: "In-game time between 1700 and 0659",
|
||||
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",
|
||||
}
|
||||
|
||||
self.active_keyword = ""
|
||||
self.active_map = (0, all_maps)
|
||||
self.prior_map = all_maps
|
||||
@@ -36,6 +52,9 @@ class FilterManager:
|
||||
self.filters: list
|
||||
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:
|
||||
self.prior_map = name
|
||||
|
||||
@@ -86,6 +105,7 @@ class FilterManager:
|
||||
self.reinit_map_store()
|
||||
maps.sort()
|
||||
for m in maps:
|
||||
# TODO: strings
|
||||
if m == "All maps":
|
||||
continue
|
||||
self.append_map([m])
|
||||
|
||||
@@ -22,9 +22,7 @@ from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManage
|
||||
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
||||
from dzgui.strings import dialogs
|
||||
from dzgui.util.format import format_mods
|
||||
from dzgui.util.strings import server_timeout
|
||||
from dzgui.util.symlink import rebuild_symlinks
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
|
||||
|
||||
import dzgui.api.pefile as PeFile
|
||||
@@ -213,10 +211,6 @@ class ModManager:
|
||||
func = StoredFunc(self._on_stale_mods_found, stale)
|
||||
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:
|
||||
model = self.treeview.get_model()
|
||||
if model is None:
|
||||
|
||||
@@ -64,4 +64,5 @@ class UpdateManager:
|
||||
|
||||
def _on_update_failure(self, msg: str) -> None:
|
||||
dialog = ExceptionDialog(self.controller, msg)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
@@ -1,7 +1,6 @@
|
||||
import re
|
||||
|
||||
from typing import Any, TYPE_CHECKING, Union
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.const.enum import FilterMode
|
||||
from dzgui.model.model_factory import ModelFactory
|
||||
@@ -13,6 +12,9 @@ if TYPE_CHECKING:
|
||||
from dzgui.api.servers import Record
|
||||
from dzgui.model.servers import NewPlayerCount
|
||||
|
||||
DAY_REG = r"([0][7-9]|[1][0-6])"
|
||||
NIGHT_REG = r"([0][0-6]|[1][7-9]|[2][0-3])"
|
||||
|
||||
|
||||
class ProxyModelManager:
|
||||
"""
|
||||
@@ -86,8 +88,8 @@ class ProxyModelManager:
|
||||
if found is False:
|
||||
self.control_model.append(history)
|
||||
|
||||
# if len(self.control_model) == 11:
|
||||
# del self.control_model[0]
|
||||
if len(self.control_model) == 11:
|
||||
del self.control_model[0]
|
||||
|
||||
self.filter(FilterMode.INITIAL, skip_cache=True)
|
||||
|
||||
@@ -232,11 +234,9 @@ class ProxyModelManager:
|
||||
final.append(row)
|
||||
rows = final
|
||||
case strings.filter_day:
|
||||
reg = r"([0][0-9]|[1][0-6])"
|
||||
rows = [row for row in rows if not re.match(reg, row[3])]
|
||||
rows = [row for row in rows if not re.match(DAY_REG, row[3])]
|
||||
case strings.filter_night:
|
||||
reg = r"([0][0-4]|[1][8]|[2][0-3])"
|
||||
rows = [row for row in rows if not re.match(reg, row[3])]
|
||||
rows = [row for row in rows if not re.match(NIGHT_REG, row[3])]
|
||||
case strings.filter_nonascii:
|
||||
rows = [row for row in rows if row[0].isascii()]
|
||||
case strings.filter_lowpop:
|
||||
@@ -259,7 +259,7 @@ class ProxyModelManager:
|
||||
def set_cache(self, filters: tuple[str], filtered_rows: list[tuple]) -> None:
|
||||
self.filter_cache[filters] = filtered_rows
|
||||
|
||||
@deprecated("Currently unused")
|
||||
# @deprecated("Currently unused")
|
||||
# def convert_model_to_list(self, model: "FastInsertListStore") -> list:
|
||||
# return [[el for el in row] for row in model]
|
||||
|
||||
|
||||
+24
-28
@@ -14,8 +14,8 @@ from dzgui.const.constants import (
|
||||
)
|
||||
from dzgui.const.enum import FilterMode, Preferences, ServerTab
|
||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||
from dzgui.strings import dialogs
|
||||
from dzgui.util.strings import api_warn_msg, dialog
|
||||
from dzgui.strings import dialogs, kb
|
||||
from dzgui.util.strings import server_timeout, dialog
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
|
||||
import gi
|
||||
@@ -175,29 +175,29 @@ class ServerModelManager:
|
||||
# TODO: wrap except
|
||||
if self.controller.get_exit_event().is_set():
|
||||
return
|
||||
res = future.result(timeout=API_TIMEOUT)
|
||||
self.thread_man.increment_dialog()
|
||||
# NOTE: failing entries are culled
|
||||
if res is None:
|
||||
# TODO: log which servers failed
|
||||
continue
|
||||
servers.append(res)
|
||||
if len(servers) == 0:
|
||||
self.thread_man.set_cleanup_func(
|
||||
StoredFunc(self._cleanup_on_failure), destroy_first=True
|
||||
)
|
||||
return
|
||||
|
||||
"""
|
||||
Collects futures in linear order after completion
|
||||
for lock-step representation of the state file
|
||||
"""
|
||||
for future in futures:
|
||||
res = future.result(timeout=API_TIMEOUT)
|
||||
# NOTE: failing entries are culled
|
||||
if res is None:
|
||||
# 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)
|
||||
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)
|
||||
def add_by_ip(self, addr: str) -> None:
|
||||
res = Servers.query_by_ip(addr)
|
||||
@@ -241,7 +241,6 @@ class ServerModelManager:
|
||||
|
||||
self.first_iteration = False
|
||||
self.emitter.emit("load_maps", store)
|
||||
# self.emitter.emit("servers_loaded_init")
|
||||
|
||||
# TODO: dataclass for record rows; check for other dict annotations
|
||||
def add_to_history(self, data: tuple[dict[str, Any], "Record"]) -> None:
|
||||
@@ -263,12 +262,6 @@ class ServerModelManager:
|
||||
proxy_man.remove_row_from_control(record)
|
||||
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)
|
||||
def update_playercount(
|
||||
self, treeiter: Gtk.TreeIter, record: Servers.Record
|
||||
@@ -412,6 +405,7 @@ class ServerModelManager:
|
||||
|
||||
if show_dialog:
|
||||
dialog = ExceptionDialog(self.controller, dialogs.load_error_lan)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
|
||||
def _cleanup_on_failure(self, show_dialog: bool = True) -> None:
|
||||
@@ -426,7 +420,9 @@ class ServerModelManager:
|
||||
# TODO: distinguish signals, e.g. "servers_failed_to_load", "servers_loaded_empty"
|
||||
# customize statusbar and dialog accordingly
|
||||
if show_dialog:
|
||||
dialog = ExceptionDialog(self.controller, api_warn_msg)
|
||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||
dialog.set_details_buffer(kb.DZG_006)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
|
||||
def _push_data(self, data: list[Any]) -> None:
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
collapse_all = "Collapse all"
|
||||
expand_all = "Expand all"
|
||||
@@ -4,11 +4,7 @@ add_popover = "This address is already in your Saved Servers"
|
||||
add_tooltip="Add to Saved Servers"
|
||||
connect_button="Connect"
|
||||
connect_tooltip="Connect to this server"
|
||||
connect_entry_tooltip=(
|
||||
"- 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)"
|
||||
connect_entry_placeholder="Enter IP:Query port (e.g., 192.168.1.1:27016)"
|
||||
|
||||
fav_heading="Favorite server"
|
||||
favs_empty="None set. Right click a server and select 'Set favorite' to set."
|
||||
@@ -19,6 +15,6 @@ lan_custom_button="Custom port"
|
||||
lan_scan_button="Scan"
|
||||
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_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_scan_tooltip="Scan for servers"
|
||||
@@ -20,3 +20,6 @@ ok = "OK"
|
||||
|
||||
mission_dialog = "Set a mission folder location"
|
||||
custom_mod_dialog = "Set location to a custom mods folder"
|
||||
|
||||
debug_heading = "Debug"
|
||||
debug_secondary = "Launch arguments"
|
||||
@@ -0,0 +1,13 @@
|
||||
DZG_006 = """The leading cause of specific servers periodically timing out is local network configuration.
|
||||
|
||||
Many third-party DayZ servers use a server rental/hosting provider with DDoS protection.
|
||||
|
||||
This can cause responses from servers to originate from a server other than the one originally queried.
|
||||
|
||||
Consumer-grade routers are likely to drop this traffic as invalid due to how they handle NAT (network address translation).
|
||||
|
||||
By contrast, wireless routers and enterprise-grade routers may be less likely to have this issue.
|
||||
|
||||
If you find that a specific server is unresponsive for you when it shouldn't be, add a port forwarding rule to your router's settings for the server's query port.
|
||||
|
||||
In addition, a uniform packet size is expected. If your router's MTU (maximum transmission unit) setting differs from the default of 1500, it could lead to incoming responses being discarded."""
|
||||
@@ -0,0 +1,16 @@
|
||||
exit_msg = (
|
||||
"System map count is not high enough to run DayZ. "
|
||||
"Please exit and run 'dzgui -m' to update map count."
|
||||
)
|
||||
failed_to_parse = (
|
||||
"Failed to parse system map count.\n"
|
||||
"This usually indicates that systemd is not installed."
|
||||
)
|
||||
failed_to_update = (
|
||||
"Failed to update system map count.\n"
|
||||
"Please report the issue upstream and provide the following traceback.\n\n"
|
||||
"{0}"
|
||||
)
|
||||
meets_minimum = "System map count of {0} already meets the minimum."
|
||||
prompt = "Updated map count will be written to the file {0}."
|
||||
user_exit = "User exit"
|
||||
@@ -1,4 +1,7 @@
|
||||
heading = "Play offline"
|
||||
heading_disclaimer = (
|
||||
"Not all mod combinations will function offline. Use at your own discretion."
|
||||
)
|
||||
|
||||
mission_button = "Select mission folder"
|
||||
mission_frame = "Mission"
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
developers = "Developers"
|
||||
start_tab = "Start tab"
|
||||
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"
|
||||
flatpak_combo = "Flatpak"
|
||||
flatpak_container_combo = "Flatpak (container)"
|
||||
|
||||
steam_placeholder = "Enter your Steam API key"
|
||||
name_placeholder = "Identifies you to other players"
|
||||
|
||||
server_combo = "Server browser"
|
||||
saved_combo = "Saved servers"
|
||||
recent_combo = "Recent"
|
||||
lan_combo = "LAN"
|
||||
|
||||
fullscreen_eventbox = "This option is not available on Steam Deck."
|
||||
save_button = "Save"
|
||||
|
||||
api_failed = "API validation failed"
|
||||
@@ -1,9 +1,8 @@
|
||||
update_mods = "Update mods and connect"
|
||||
connect = "Connect"
|
||||
connect_last = "Load to menu screen"
|
||||
connect_last_tooltip = (
|
||||
"Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
||||
)
|
||||
connect_last_tooltip = "Launch DayZ with these mods (if any) and\ngo to the menu screen without connecting."
|
||||
debug = "Debug"
|
||||
back = "Back"
|
||||
cancel = "Cancel"
|
||||
warnings = "Warnings"
|
||||
@@ -14,3 +13,38 @@ up_to_date = "All mods are up to date."
|
||||
all_updated = "All mods updated."
|
||||
"If you recently installed {build} or moved it to a different drive, "
|
||||
"restart Steam to allow these changes to synchronize, then try again."
|
||||
required_space = " Need to download {0} MiB of mod updates."
|
||||
|
||||
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."
|
||||
|
||||
|
||||
# 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."
|
||||
)
|
||||
@@ -0,0 +1,38 @@
|
||||
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, you can 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."
|
||||
)
|
||||
@@ -46,11 +46,6 @@ You must set up a Steam Web API key in order to browse the global server list.
|
||||
\nIf you don't have one already, it can be set up via the page below.
|
||||
\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
|
||||
heading_prefs = "User preferences"
|
||||
|
||||
@@ -54,8 +54,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||
cpu = get_cpu_model()
|
||||
version = get_version()
|
||||
|
||||
debug = lookup(config, Preferences.DEBUG)
|
||||
install = lookup(config, Preferences.INSTALL)
|
||||
default = lookup(config, Preferences.DEFAULT)
|
||||
|
||||
steam_path = Path(default)
|
||||
@@ -79,8 +77,6 @@ def write_diagnostic(config: Path, outfile: Path) -> None:
|
||||
Kernel: {kernel}
|
||||
CPU: {cpu}
|
||||
|
||||
Debug: {debug}
|
||||
Auto-install: {install}
|
||||
Steam path: {steam_redacted}
|
||||
Workshop path: {workshop_redacted}
|
||||
|
||||
|
||||
+59
-16
@@ -1,28 +1,71 @@
|
||||
import logging
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import traceback
|
||||
|
||||
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:
|
||||
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
|
||||
count = int(path.read_text())
|
||||
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:
|
||||
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"
|
||||
count = f"vm.max_map_count={MIN_COUNT}"
|
||||
with open(conf, "w") as f:
|
||||
f.write(count)
|
||||
# TODO: use concat_bash_args()
|
||||
subprocess.run(["/usr/bin/sudo", "sysctl", "-p", conf])
|
||||
value = f"vm.max_map_count={MIN_COUNT}"
|
||||
try:
|
||||
msg = map_count.prompt.format(conf)
|
||||
print(msg)
|
||||
with tempfile.NamedTemporaryFile(delete=False) as f:
|
||||
tmp = f.name
|
||||
Path(tmp).write_text(value)
|
||||
|
||||
mv_cmd = f"sudo mv {tmp} {conf}"
|
||||
reload_cmd = f"sudo sysctl -p {conf}"
|
||||
for cmd in mv_cmd, reload_cmd:
|
||||
args = concat_bash_args(cmd)
|
||||
subprocess.run([*args])
|
||||
except Exception as e:
|
||||
logger.debug(e)
|
||||
trace = traceback.format_exc()
|
||||
print(map_count.failed_to_update.format(trace))
|
||||
except KeyboardInterrupt:
|
||||
print(map_count.user_exit)
|
||||
sys.exit(0)
|
||||
@@ -4,8 +4,9 @@ from typing import Literal
|
||||
|
||||
api_filter = r"(.*&key=)([^&]*)(.*)"
|
||||
home_filter = r"(/home/)([^\s'\/]*)(.*)"
|
||||
user_filter = r"(.*Steam/userdata/)([^/]*)(.*)"
|
||||
REDACTED = r"\1REDACTED\3"
|
||||
REDACTION_PATTERNS = [api_filter, home_filter]
|
||||
REDACTION_PATTERNS = [api_filter, home_filter, user_filter]
|
||||
|
||||
|
||||
def redact_home(text: str) -> str:
|
||||
|
||||
+10
-96
@@ -1,27 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
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
|
||||
dialog_header = "DZGUI - Dialog"
|
||||
@@ -100,10 +79,7 @@ steam_missing = "Local Steam installation is not set, possibly malformed config
|
||||
build_corrupted = (
|
||||
"Steam settings or DayZ installation may be corrupted. Try restarting Steam."
|
||||
)
|
||||
api_warn_msg = """No servers returned. Please wait and try again.
|
||||
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_timeout = "Timed out when querying server(s). Check IP or try again later."
|
||||
server_error = (
|
||||
"Error while contacting server, possibly timed out. Please wait and try again."
|
||||
)
|
||||
@@ -286,11 +262,8 @@ class Thanks:
|
||||
class Options:
|
||||
header: str
|
||||
steam_web: str
|
||||
bm_web: str
|
||||
enter_steam: str
|
||||
enter_bm: str
|
||||
steam_placeholder: str
|
||||
bm_placeholder: str
|
||||
name_placeholder: str
|
||||
last_used: str
|
||||
always_fs: str
|
||||
@@ -311,7 +284,7 @@ class Options:
|
||||
force_eventbox: str
|
||||
self_update: str
|
||||
no_self_update: str
|
||||
api_keys: str
|
||||
api_key: str
|
||||
prefs: str
|
||||
mods: str
|
||||
version: str
|
||||
@@ -333,8 +306,8 @@ mod_panel = ModPanelStrings(
|
||||
unhighlight_stale_tooltip="Clears highlight from stale mods",
|
||||
highlight_stale="Highlight stale",
|
||||
highlight_stale_tooltip=(
|
||||
"Shows locally-installed mods which are not used by any server "
|
||||
"in your Saved Servers"
|
||||
"Shows locally-installed mods which are not \n"
|
||||
"used by any server in your Saved Servers"
|
||||
),
|
||||
unsub_selected="Unsubscribe selected",
|
||||
unsub_selected_tooltip="Unsubscribes from selected mods",
|
||||
@@ -378,11 +351,8 @@ thanks = Thanks(
|
||||
options = Options(
|
||||
header="Options",
|
||||
steam_web="Steam API page",
|
||||
bm_web="Battlemetrics API page",
|
||||
enter_steam="Enter your Steam API key",
|
||||
enter_bm="Enter your Battlemetrics API key",
|
||||
steam_placeholder="Steam API key",
|
||||
bm_placeholder="Battlemetrics API key",
|
||||
name_placeholder="Identifies you to other players in-game",
|
||||
last_used="Last used dimensions",
|
||||
always_fs="Always fullscreen",
|
||||
@@ -411,7 +381,7 @@ options = Options(
|
||||
"In-app updates are disabled when DZGUI is "
|
||||
"installed globally (e.g., via package manager)."
|
||||
),
|
||||
api_keys="API Keys",
|
||||
api_key="API Key",
|
||||
prefs="Preferences",
|
||||
mods="Mods",
|
||||
version="Version",
|
||||
@@ -476,13 +446,15 @@ class Flags:
|
||||
version: str
|
||||
uninstall: str
|
||||
debug: str
|
||||
map_count: str
|
||||
|
||||
|
||||
flags = Flags(
|
||||
description="DayZ server browser and mod manager",
|
||||
version="Print version information",
|
||||
uninstall="Uninstall data files (use prior to 'pip uninstall')",
|
||||
debug="Enables developer debugging features",
|
||||
version="print version information",
|
||||
uninstall="clean up state/config files",
|
||||
debug="enable developer debugging features",
|
||||
map_count="check and update system map count value",
|
||||
)
|
||||
|
||||
|
||||
@@ -534,64 +506,6 @@ server_labels = ServerLabels(
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class ConnectPanel:
|
||||
connect: str
|
||||
add: str
|
||||
add_con: str
|
||||
placeholder: str
|
||||
entry_tooltip: str
|
||||
add_tooltip: str
|
||||
connect_tooltip: str
|
||||
|
||||
|
||||
connect_panel = ConnectPanel(
|
||||
connect="Connect",
|
||||
add="Add",
|
||||
add_con="Add/connect",
|
||||
placeholder="Enter IP (IP:Query port) or Battlemetrics ID (numerical id)",
|
||||
entry_tooltip=(
|
||||
"- IP: format as IP:Query port\ne.g. 192.168.1.1:27016\n"
|
||||
"- Battlemetrics: numeric server ID\ne.g. 123456"
|
||||
),
|
||||
add_tooltip="Add to Saved Servers",
|
||||
connect_tooltip="Connect to this server",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class FavPanel:
|
||||
heading: str
|
||||
no_fav: str
|
||||
|
||||
|
||||
fav_panel = FavPanel(
|
||||
heading="Favorite server",
|
||||
no_fav="None set. Right click a server and select 'Set favorite' to set.",
|
||||
)
|
||||
|
||||
|
||||
@dataclass(slots=True, frozen=True)
|
||||
class LanPanel:
|
||||
heading: str
|
||||
default_button: str
|
||||
custom_button: str
|
||||
scan_button: str
|
||||
placeholder: str
|
||||
entry_tooltip: str
|
||||
scan_tooltip: str
|
||||
|
||||
|
||||
lan_panel = LanPanel(
|
||||
heading="LAN query port",
|
||||
default_button="Default port (27016)",
|
||||
custom_button="Custom port",
|
||||
scan_button="Scan",
|
||||
placeholder="Enter the query port (1-65535)",
|
||||
entry_tooltip="Specify the port to search for DayZ servers on the local network",
|
||||
scan_tooltip="Scan for servers",
|
||||
)
|
||||
|
||||
distance_suffix = "Distance: calculating..."
|
||||
dialog_error = "ERROR"
|
||||
|
||||
|
||||
+5
-2
@@ -54,7 +54,10 @@ class OuterWindow(Gtk.Window):
|
||||
MainController.register_widget("window", self)
|
||||
|
||||
# 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.connect("delete-event", self._on_delete_event)
|
||||
@@ -66,8 +69,8 @@ class OuterWindow(Gtk.Window):
|
||||
self.show_all()
|
||||
css.load_css()
|
||||
|
||||
MainController.open_page(NotebookPage.SERVERS)
|
||||
MainController.set_start_tab()
|
||||
MainController.open_page(NotebookPage.SERVERS)
|
||||
self.grid.hide_widgets_on_init()
|
||||
|
||||
MainController.loaded = True
|
||||
|
||||
@@ -23,3 +23,10 @@ class HBox(GenericBox):
|
||||
class VBox(GenericBox):
|
||||
def __init__(self, spacing: int = 0) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
||||
|
||||
class ShortHBox(HBox):
|
||||
def __init__(self, widget: Gtk.Widget) -> None:
|
||||
super().__init__(spacing=5)
|
||||
|
||||
self.set_halign(Gtk.Align.START)
|
||||
self.pack_start(widget, expand=False, fill=False, padding=0)
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Callable, Self, TYPE_CHECKING, Union
|
||||
from typing import Callable, Literal, Self, TYPE_CHECKING, Union
|
||||
|
||||
from dzgui.util.clip import copy_clipboard
|
||||
from dzgui.util.format import pluralize
|
||||
@@ -6,10 +6,10 @@ from dzgui.strings import buttons
|
||||
from dzgui.util.strings import (
|
||||
alert_button_tooltip,
|
||||
atomic_buttons,
|
||||
connect_panel,
|
||||
)
|
||||
from dzgui.const.constants import (
|
||||
CLIPBOARD,
|
||||
CLOSE,
|
||||
INPUT_KEYBOARD,
|
||||
LIST_ADD,
|
||||
REFRESH_ICON,
|
||||
@@ -17,6 +17,7 @@ from dzgui.const.constants import (
|
||||
WARNING,
|
||||
WEB_BROWSER,
|
||||
)
|
||||
from dzgui.strings import connect_panel
|
||||
|
||||
import gi
|
||||
|
||||
@@ -63,9 +64,17 @@ class IconButton(Gtk.Button):
|
||||
self.icon = Icon(icon, margin_start=margin_start, margin_end=margin_end)
|
||||
self.set_image(self.icon)
|
||||
self.set_image_position(position)
|
||||
# self.set_image_position(Gtk.PositionType.RIGHT)
|
||||
self.set_focus_on_click(False)
|
||||
|
||||
def swap_icon(self, icon: str) -> None:
|
||||
start = self.icon.get_margin_start()
|
||||
end = self.icon.get_margin_end()
|
||||
alt_icon = Icon(icon, margin_start=start, margin_end=end)
|
||||
self.set_image(alt_icon)
|
||||
|
||||
def revert_icon(self) -> None:
|
||||
self.set_image(self.icon)
|
||||
|
||||
|
||||
class IconTextButton(IconButton):
|
||||
def __init__(
|
||||
@@ -96,11 +105,18 @@ class ClipboardButton(IconTextButton):
|
||||
self.controller = controller
|
||||
self.connect("clicked", self._on_button_clicked, func)
|
||||
|
||||
# TODO: strings
|
||||
self.set_tooltip_text("Copy to clipboard")
|
||||
|
||||
def _on_button_clicked(self, button: Self, func: Callable) -> None:
|
||||
def revert() -> Literal[False]:
|
||||
self.revert_icon()
|
||||
return False
|
||||
|
||||
self.swap_icon("object-select-symbolic")
|
||||
data = func()
|
||||
copy_clipboard(data)
|
||||
GLib.timeout_add(600, revert)
|
||||
|
||||
|
||||
# TODO: determine when controller would be passed to this button or drop
|
||||
@@ -108,9 +124,18 @@ class CopyIpButton(ClipboardButton):
|
||||
def __init__(self, controller: Union["Controller", None], func: Callable) -> None:
|
||||
super().__init__(controller, func)
|
||||
|
||||
# TODO: strings
|
||||
self.set_tooltip_text("Copy IP to clipboard")
|
||||
|
||||
|
||||
class CloseButton(IconTextButton):
|
||||
def __init__(self, label: str) -> None:
|
||||
super().__init__(icon=CLOSE, label=label)
|
||||
|
||||
# TODO: strings
|
||||
self.set_tooltip_text("Unset this server as favorite")
|
||||
|
||||
|
||||
class WebButton(IconTextButton):
|
||||
def __init__(self, label: str) -> None:
|
||||
super().__init__(icon=WEB_BROWSER, label=label)
|
||||
@@ -196,7 +221,7 @@ class KeysButton(IconTextButton):
|
||||
|
||||
class SteamConnectButton(LargeIconTextButton):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(icon=STEAM_ICON, label=connect_panel.connect)
|
||||
super().__init__(icon=STEAM_ICON, label=connect_panel.connect_button)
|
||||
self.set_tooltip_text(connect_panel.connect_tooltip)
|
||||
|
||||
|
||||
@@ -216,7 +241,7 @@ class SteamWorkshopButton(SteamTextButton):
|
||||
|
||||
class AddButton(IconTextButton):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(icon=LIST_ADD, label=connect_panel.add)
|
||||
super().__init__(icon=LIST_ADD, label=connect_panel.add_button)
|
||||
self.set_tooltip_text(connect_panel.add_tooltip)
|
||||
|
||||
|
||||
@@ -241,3 +266,38 @@ class LoggerAlertsButton(IconTextButton):
|
||||
self.set_halign(Gtk.Align.END)
|
||||
self.set_hexpand(True)
|
||||
self.set_tooltip_text(alert_button_tooltip)
|
||||
|
||||
|
||||
class SpinnerButton(Gtk.Button):
|
||||
def __init__(self, label: str) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.text = label
|
||||
self.label = Gtk.Label(label=label, halign=Gtk.Align.CENTER)
|
||||
|
||||
self.spinner = Gtk.Spinner()
|
||||
self.spinner.set_halign(Gtk.Align.END)
|
||||
self.spinner.set_sensitive(False)
|
||||
|
||||
grid = Gtk.Grid(column_spacing=10)
|
||||
for el in self.label, self.spinner:
|
||||
grid.add(el)
|
||||
|
||||
self.add(grid)
|
||||
|
||||
self.connect("clicked", self._on_button_clicked)
|
||||
self.connect("map", self._on_map)
|
||||
|
||||
def _on_map(self, button: Self) -> None:
|
||||
self.spinner.set_visible(False)
|
||||
|
||||
def _on_button_clicked(self, button: Self) -> None:
|
||||
self.start_spinner()
|
||||
|
||||
def stop_spinner(self) -> None:
|
||||
self.spinner.set_visible(False)
|
||||
self.spinner.stop()
|
||||
|
||||
def start_spinner(self) -> None:
|
||||
self.spinner.set_visible(True)
|
||||
self.spinner.start()
|
||||
@@ -1,68 +0,0 @@
|
||||
from dzgui.strings import preconnect
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gdk, Gtk # noqa E402
|
||||
|
||||
|
||||
class ConnectBox(Gtk.Box):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
|
||||
self.back = Gtk.Button(
|
||||
label=preconnect.back, halign=Gtk.Align.END, hexpand=True
|
||||
)
|
||||
|
||||
# TODO: dynamically update this
|
||||
self.ok = Gtk.Button(label="Launch", halign=Gtk.Align.END)
|
||||
|
||||
for button in self.back, self.ok:
|
||||
self.add(button)
|
||||
|
||||
def get_back_button(self) -> Gtk.Button:
|
||||
return self.back
|
||||
|
||||
def get_ok_button(self) -> Gtk.Button:
|
||||
return self.ok
|
||||
|
||||
|
||||
class PreconnectBox(Gtk.Box):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
valign=Gtk.Align.END,
|
||||
vexpand=True,
|
||||
spacing=5,
|
||||
)
|
||||
|
||||
self.button_hbox = ConnectBox()
|
||||
self.ok = self.button_hbox.get_ok_button()
|
||||
self.ok.set_label(preconnect.update_mods)
|
||||
|
||||
self.connect_last = Gtk.Button(
|
||||
label=preconnect.connect_last,
|
||||
halign=Gtk.Align.END,
|
||||
tooltip_text=preconnect.connect_last_tooltip,
|
||||
)
|
||||
self.button_hbox.add(self.connect_last)
|
||||
|
||||
self.raise_window = Gtk.CheckButton(
|
||||
# TODO: strings
|
||||
label="Foreground DZGUI while downloading",
|
||||
halign=Gtk.Align.END,
|
||||
hexpand=True,
|
||||
valign=Gtk.Align.END,
|
||||
visible=False,
|
||||
has_tooltip=True,
|
||||
sensitive=False,
|
||||
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
|
||||
active=True,
|
||||
)
|
||||
self.add(self.button_hbox)
|
||||
self.add(self.raise_window)
|
||||
|
||||
def get_back_button(self) -> Gtk.Button:
|
||||
return self.button_hbox.get_back_button()
|
||||
|
||||
def get_ok_button(self) -> Gtk.Button:
|
||||
return self.ok
|
||||
@@ -6,6 +6,7 @@ from dzgui.strings import connect_panel
|
||||
from dzgui.util.keys import is_ctrl_mask
|
||||
from dzgui.views.components.buttons import (
|
||||
AddButton,
|
||||
CloseButton,
|
||||
CopyIpButton,
|
||||
Icon,
|
||||
IconButton,
|
||||
@@ -187,17 +188,27 @@ class FavPanel(Gtk.Frame):
|
||||
self.fav_button = SteamConnectButton()
|
||||
self.fav_button.connect("clicked", self._on_connect_clicked)
|
||||
self.copy_button = CopyIpButton(self.controller, self.get_fav_ip)
|
||||
|
||||
# TODO: strings
|
||||
self.unset_button = CloseButton("Unset")
|
||||
self.unset_button.connect("clicked", self._on_unset_clicked)
|
||||
|
||||
if favorite is None:
|
||||
self.toggle_buttons(False)
|
||||
|
||||
# NOTE: disable vscrollbar to prevent layout jumping behavior
|
||||
scrollable_label = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER)
|
||||
scrollable_label.add(self.fav_label)
|
||||
self.scrollable_label = Gtk.ScrolledWindow(
|
||||
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.attach(scrollable_label, 0, 0, 3, ROWS)
|
||||
grid.attach(self.scrollable_label, 0, 0, 3, ROWS)
|
||||
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(
|
||||
self.fav_button, self.copy_button, Gtk.PositionType.RIGHT, COLS, ROWS
|
||||
@@ -205,8 +216,16 @@ class FavPanel(Gtk.Frame):
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
def _on_connect_clicked(self, button: Gtk.Button) -> None:
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Any, Callable, TYPE_CHECKING
|
||||
from dzgui.api.servers import validate_ip
|
||||
from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL
|
||||
from dzgui.util.css import add_class, remove_class
|
||||
from dzgui.util.strings import connect_panel, lan_panel
|
||||
from dzgui.strings import connect_panel
|
||||
from dzgui.strings.errors import api_popover
|
||||
|
||||
import gi
|
||||
@@ -26,15 +26,12 @@ def validate_port(text: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def validate_ip_or_id(text: str) -> bool:
|
||||
def validate_ip_truthy(text: str) -> bool:
|
||||
try:
|
||||
validate_ip(text)
|
||||
return True
|
||||
except Exception:
|
||||
if text.isdigit():
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class ValidatedEntry(Gtk.Entry):
|
||||
@@ -124,12 +121,9 @@ class IpEntry(ValidatedEntry):
|
||||
def __init__(self, controller: "Controller") -> None:
|
||||
super().__init__(
|
||||
controller,
|
||||
func=validate_ip_or_id,
|
||||
placeholder_text=connect_panel.placeholder,
|
||||
tooltip_text=connect_panel.entry_tooltip,
|
||||
func=validate_ip_truthy,
|
||||
placeholder_text=connect_panel.connect_entry_placeholder,
|
||||
)
|
||||
self.set_placeholder_text(connect_panel.placeholder)
|
||||
self.set_tooltip_text(connect_panel.entry_tooltip)
|
||||
|
||||
|
||||
class PortEntry(ValidatedEntry):
|
||||
@@ -137,11 +131,9 @@ class PortEntry(ValidatedEntry):
|
||||
super().__init__(
|
||||
controller,
|
||||
func=validate_port,
|
||||
placeholder_text=connect_panel.placeholder,
|
||||
tooltip_text=connect_panel.entry_tooltip,
|
||||
placeholder_text=connect_panel.lan_entry_placeholder,
|
||||
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
|
||||
@@ -162,7 +154,8 @@ class APIEntry(Gtk.Box):
|
||||
self.entry.connect("icon-release", self._on_icon_release)
|
||||
self.entry.connect("activate", self._on_field_activated)
|
||||
|
||||
self.submit = Gtk.Button(label="Submit")
|
||||
# TODO: strings
|
||||
self.submit = Gtk.Button(label="Validate")
|
||||
self.submit.set_sensitive(False)
|
||||
self.submit.connect("clicked", self._on_submit)
|
||||
|
||||
|
||||
@@ -30,6 +30,9 @@ class InfoEventBox(Gtk.EventBox):
|
||||
self.connect("leave-notify-event", self._on_leave_tooltip)
|
||||
self.add(box)
|
||||
|
||||
def set_icon_yalign(self, offset: float) -> None:
|
||||
self.icon.set_property("yalign", offset)
|
||||
|
||||
def _on_enter_tooltip(
|
||||
self, eventbox: Gtk.EventBox, eventcrossing: Gdk.EventCrossing
|
||||
) -> None:
|
||||
|
||||
@@ -29,21 +29,33 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class ButtonGrid(Gtk.Grid):
|
||||
def __init__(self, controller: "Controller", defaults: dict) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
defaults: dict[str, str],
|
||||
tooltips: dict[str, str],
|
||||
) -> None:
|
||||
super().__init__(
|
||||
halign=Gtk.Align.CENTER, column_spacing=5, column_homogeneous=True
|
||||
)
|
||||
row = 1
|
||||
col = 0
|
||||
|
||||
self.controller = controller
|
||||
self.emitter = controller.get_emitter()
|
||||
|
||||
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():
|
||||
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()
|
||||
if label is not None:
|
||||
label.set_ellipsize(Pango.EllipsizeMode.END) # type: ignore
|
||||
@@ -51,14 +63,12 @@ class ButtonGrid(Gtk.Grid):
|
||||
if defaults[check]:
|
||||
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)
|
||||
flowbox.add(checkbox)
|
||||
self.checks.append(checkbox)
|
||||
|
||||
self.add(flowbox)
|
||||
|
||||
def block_toggles(self, state: bool) -> None:
|
||||
for check in self.checks:
|
||||
self.controller.suppress_signal(
|
||||
@@ -163,11 +173,12 @@ class FilterPanel(Gtk.Box):
|
||||
|
||||
filter_man = self.controller.get_filter_man()
|
||||
defaults = filter_man.get_default_filters()
|
||||
tooltips = filter_man.get_tooltips()
|
||||
self.map_store = filter_man.get_map_store()
|
||||
self.enabled_filters = defaults
|
||||
|
||||
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")
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
from dzgui.const.constants import ERROR, FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
||||
from dzgui.strings import options
|
||||
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
|
||||
from dzgui.views.components.box import HBox
|
||||
from dzgui.views.components.buttons import Icon
|
||||
|
||||
|
||||
import gi
|
||||
|
||||
@@ -33,3 +36,18 @@ class ClientCombo(Gtk.ComboBox):
|
||||
self.pack_start(renderer_text, True)
|
||||
self.add_attribute(renderer_text, "text", 0)
|
||||
self.set_active(0)
|
||||
|
||||
class ErrorPopover(Gtk.Popover):
|
||||
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
||||
super().__init__(relative_to=relative_to, position=position)
|
||||
|
||||
self.hbox = HBox()
|
||||
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
||||
error_icon = Icon(ERROR, margin_start=10)
|
||||
self.hbox.extend([error_icon, self.label])
|
||||
self.add(self.hbox)
|
||||
self.show_all()
|
||||
self.popdown()
|
||||
|
||||
def set_label(self, label: str) -> None:
|
||||
self.label.set_label(label)
|
||||
@@ -131,6 +131,7 @@ class RightPanel(Gtk.Box):
|
||||
return
|
||||
self.copying = True
|
||||
version = self.version_label.get_text()
|
||||
# TODO: strings
|
||||
self.version_label.set_text("Copied!")
|
||||
copy_clipboard(version)
|
||||
GLib.timeout_add_seconds(1, revert)
|
||||
@@ -41,7 +41,7 @@ class BootDialog(Gtk.Dialog):
|
||||
def __init__(self, parent: "BootWindow", xdg: "Xdg", version: str) -> None:
|
||||
super().__init__(
|
||||
title=dialog_header,
|
||||
parent=parent,
|
||||
transient_for=parent,
|
||||
modal=True,
|
||||
)
|
||||
|
||||
|
||||
@@ -10,21 +10,26 @@ gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
|
||||
|
||||
class EarlyAlertDialog(Gtk.MessageDialog):
|
||||
def __init__(self, string: str) -> None:
|
||||
class AbortDialog(Gtk.MessageDialog):
|
||||
def __init__(self, string: str, buttons: Gtk.ButtonsType) -> None:
|
||||
super().__init__(
|
||||
title=dialog_header,
|
||||
text=dialog_error,
|
||||
transient_for=None,
|
||||
buttons=Gtk.ButtonsType.OK,
|
||||
buttons=buttons,
|
||||
)
|
||||
|
||||
msg = textwrap.fill(string, 50)
|
||||
self.format_secondary_text(msg)
|
||||
|
||||
ma = self.get_message_area()
|
||||
label = ma.get_children()[1] # type: ignore
|
||||
label.set_justify(Gtk.Justification.CENTER)
|
||||
|
||||
aa = self.get_action_area()
|
||||
aa.set_margin_bottom(20)
|
||||
# self.action_area.set_margin_bottom(20)
|
||||
aa.set_layout(Gtk.ButtonBoxStyle.CENTER) # type: ignore
|
||||
|
||||
self.outer = self.get_content_area()
|
||||
self.outer.set_margin_start(30)
|
||||
self.outer.set_margin_end(30)
|
||||
@@ -32,26 +37,30 @@ class EarlyAlertDialog(Gtk.MessageDialog):
|
||||
self.set_default_size(250, 100)
|
||||
|
||||
abort = self.get_widget_for_response(Gtk.ResponseType.OK)
|
||||
ignore = self.get_widget_for_response(Gtk.ResponseType.CANCEL)
|
||||
if abort is not None and hasattr(abort, "set_label"):
|
||||
abort.set_label("Exit")
|
||||
if ignore is not None and hasattr(ignore, "set_label"):
|
||||
ignore.set_label("Ignore")
|
||||
|
||||
self.connect("response", self._on_response)
|
||||
|
||||
self.run()
|
||||
self.destroy()
|
||||
|
||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||
match response:
|
||||
case Gtk.ResponseType.OK:
|
||||
case Gtk.ResponseType.OK | Gtk.ResponseType.DELETE_EVENT:
|
||||
sys.exit(1)
|
||||
case Gtk.ResponseType.CANCEL:
|
||||
return
|
||||
|
||||
|
||||
class EarlyIgnoreDialog(EarlyAlertDialog):
|
||||
class EarlyAlertDialog(AbortDialog):
|
||||
def __init__(self, string: str) -> None:
|
||||
super().__init__(string=string)
|
||||
super().__init__(string=string, buttons=Gtk.ButtonsType.OK)
|
||||
|
||||
# TODO: reverse order
|
||||
self.add_button("Ignore", Gtk.ResponseType.CANCEL)
|
||||
|
||||
# TODO: if exit, sys.exit(1), else pass
|
||||
class EarlyIgnoreDialog(AbortDialog):
|
||||
def __init__(self, string: str) -> None:
|
||||
super().__init__(string=string, buttons=Gtk.ButtonsType.OK_CANCEL)
|
||||
@@ -13,7 +13,7 @@ class FilePicker(Gtk.FileChooserDialog):
|
||||
super().__init__(
|
||||
title=picker.title,
|
||||
action=Gtk.FileChooserAction.SAVE,
|
||||
parent=parent,
|
||||
transient_for=parent,
|
||||
resizable=True,
|
||||
)
|
||||
self.add_buttons("_Cancel", Gtk.ResponseType.CANCEL)
|
||||
|
||||
+144
-51
@@ -1,6 +1,7 @@
|
||||
from typing import Literal, Self, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import NO_EXPAND, NO_FILL, NO_PADDING, EXPAND, FILL
|
||||
from dzgui.strings import dialogs
|
||||
from dzgui.util import strings
|
||||
from dzgui.views.components.buttons import ClipboardButton
|
||||
|
||||
@@ -107,23 +108,26 @@ class WaitDialog(GenericDialog):
|
||||
self.cur_job = 1
|
||||
|
||||
self.cancel = Gtk.Button(label="Cancel", halign=Gtk.Align.CENTER)
|
||||
self.cancel.connect("clicked", lambda _: self.controller.set_cancel_event())
|
||||
self.cancel.connect("clicked", self._on_cancel_clicked)
|
||||
|
||||
self.connect("delete-event", lambda widget, event: True)
|
||||
content = self.get_content_area()
|
||||
spinner = Gtk.Spinner()
|
||||
self.prog = Gtk.ProgressBar()
|
||||
|
||||
content.pack_end(self.cancel, NO_EXPAND, NO_FILL, 0)
|
||||
content.pack_end(spinner, NO_EXPAND, NO_FILL, 0)
|
||||
spinner_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL, halign=Gtk.Align.CENTER
|
||||
)
|
||||
spinner = Gtk.Spinner()
|
||||
spinner_box.add(spinner)
|
||||
|
||||
if self.jobs > 1:
|
||||
content.pack_end(self.prog, NO_EXPAND, NO_FILL, 0)
|
||||
content.pack_start(self.prog, NO_EXPAND, NO_FILL, 0)
|
||||
else:
|
||||
content.pack_start(spinner_box, NO_EXPAND, NO_FILL, 0)
|
||||
spinner.start()
|
||||
|
||||
if show_cancel is False:
|
||||
self.connect("realize", lambda _: self.cancel.set_visible(False))
|
||||
if show_cancel is True:
|
||||
content.pack_start(self.cancel, NO_EXPAND, NO_FILL, 0)
|
||||
|
||||
def update_text(self, msg: str) -> None:
|
||||
self.format_secondary_text(msg)
|
||||
@@ -135,6 +139,11 @@ class WaitDialog(GenericDialog):
|
||||
self.prog.set_fraction(fraction)
|
||||
self.cur_job += 1
|
||||
|
||||
def _on_cancel_clicked(self, button: Gtk.Button) -> None:
|
||||
# TODO: strings
|
||||
GLib.idle_add(self.update_text, "Caught cancel signal, cleaning up")
|
||||
self.controller.set_cancel_event()
|
||||
|
||||
def show_cancel(self, state: bool) -> None:
|
||||
self.cancel.set_visible(state)
|
||||
|
||||
@@ -153,6 +162,7 @@ class QuitDialog(GenericDialog):
|
||||
self.add_button(strings.exit_app, Gtk.ResponseType.OK)
|
||||
|
||||
self.connect("response", self._on_response)
|
||||
# TODO: superfluous with the above
|
||||
self.connect("delete-event", self._on_response)
|
||||
|
||||
def _on_response(self, dialog: Self, response: Gtk.ResponseType) -> None:
|
||||
@@ -160,58 +170,30 @@ class QuitDialog(GenericDialog):
|
||||
self.controller.save_res_and_quit()
|
||||
|
||||
|
||||
class ExceptionDialog(GenericDialog):
|
||||
"""
|
||||
Error dialog with rich traceback.
|
||||
Usage:
|
||||
try:
|
||||
foo()
|
||||
except Exception:
|
||||
trace = traceback.format_exc()
|
||||
dialog = ExceptionDialog(Controller, trace)
|
||||
dialog.run()
|
||||
"""
|
||||
|
||||
def __init__(self, controller: "Controller", trace: str):
|
||||
class TextBufferDialog(GenericDialog):
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
mtype: Gtk.MessageType,
|
||||
heading: str,
|
||||
secondary: str,
|
||||
text: str,
|
||||
):
|
||||
super().__init__(
|
||||
controller=controller,
|
||||
text=strings.error_heading,
|
||||
mtype=Gtk.MessageType.ERROR,
|
||||
text=heading,
|
||||
mtype=mtype,
|
||||
buttons=Gtk.ButtonsType.NONE,
|
||||
secondary=strings.something_wrong,
|
||||
secondary=secondary,
|
||||
)
|
||||
|
||||
self.trace = trace
|
||||
# NOTE: box expands to end of content area
|
||||
scrollable = Gtk.ScrolledWindow(
|
||||
propagate_natural_height=True, max_content_height=500
|
||||
)
|
||||
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
||||
# TODO: wrap/truncate long messages
|
||||
textview = Gtk.TextView(
|
||||
wrap_mode=Gtk.WrapMode.WORD, editable=False, left_margin=10, right_margin=10
|
||||
)
|
||||
textview.set_buffer(Gtk.TextBuffer(text=self.trace))
|
||||
box.pack_start(textview, EXPAND, FILL, 10)
|
||||
scrollable.add(box)
|
||||
|
||||
content = self.get_content_area()
|
||||
content.set_spacing(0)
|
||||
# TODO: padding around top of content area when traceback is long
|
||||
content.add(scrollable)
|
||||
|
||||
copy_button = ClipboardButton(controller, self.get_trace)
|
||||
self.text = text
|
||||
copy_button = ClipboardButton(controller, self.get_text)
|
||||
self.add_action_widget(copy_button, Gtk.ResponseType.NONE)
|
||||
self.add_button("OK", Gtk.ResponseType.OK)
|
||||
|
||||
self.show_all()
|
||||
self.ok = self.get_widget_for_response(Gtk.ResponseType.OK)
|
||||
if self.ok is not None:
|
||||
self.ok.grab_focus()
|
||||
self.connect("response", self._on_response)
|
||||
|
||||
def get_trace(self) -> str:
|
||||
return self.trace
|
||||
def get_text(self) -> str:
|
||||
return self.text
|
||||
|
||||
def _on_response(
|
||||
self, dialog: Self, response: Gtk.ResponseType
|
||||
@@ -227,3 +209,114 @@ class ExceptionDialog(GenericDialog):
|
||||
return None
|
||||
case _:
|
||||
return None
|
||||
|
||||
|
||||
class DebugDialog(TextBufferDialog):
|
||||
def __init__(self, controller: "Controller", debug: str):
|
||||
super().__init__(
|
||||
controller=controller,
|
||||
mtype=Gtk.MessageType.INFO,
|
||||
heading=dialogs.debug_heading,
|
||||
secondary=dialogs.debug_secondary,
|
||||
text=debug,
|
||||
)
|
||||
scrollable = Gtk.ScrolledWindow(
|
||||
propagate_natural_height=False,
|
||||
min_content_height=200,
|
||||
max_content_height=200,
|
||||
margin_bottom=10,
|
||||
)
|
||||
textview = Gtk.TextView(
|
||||
wrap_mode=Gtk.WrapMode.CHAR, editable=False, left_margin=10, right_margin=10
|
||||
)
|
||||
textview.set_buffer(Gtk.TextBuffer(text=debug))
|
||||
scrollable.add(textview)
|
||||
content = self.get_content_area()
|
||||
content.set_spacing(0)
|
||||
content.add(scrollable)
|
||||
|
||||
|
||||
class ExceptionDialog(TextBufferDialog):
|
||||
"""
|
||||
Error dialog with rich traceback.
|
||||
Usage:
|
||||
try:
|
||||
foo()
|
||||
except Exception:
|
||||
trace = traceback.format_exc()
|
||||
dialog = ExceptionDialog(Controller, trace)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
"""
|
||||
|
||||
def __init__(self, controller: "Controller", trace: str):
|
||||
super().__init__(
|
||||
controller=controller,
|
||||
heading=strings.error_heading,
|
||||
secondary=strings.something_wrong,
|
||||
text=trace,
|
||||
mtype=Gtk.MessageType.ERROR,
|
||||
)
|
||||
|
||||
# NOTE: box expands to end of content area
|
||||
scrollable = Gtk.ScrolledWindow(
|
||||
propagate_natural_height=True, max_content_height=500
|
||||
)
|
||||
box = Gtk.Box(hexpand=True, vexpand=True, orientation=Gtk.Orientation.VERTICAL)
|
||||
textview = Gtk.TextView(
|
||||
wrap_mode=Gtk.WrapMode.WORD,
|
||||
editable=False,
|
||||
left_margin=10,
|
||||
right_margin=10,
|
||||
top_margin=15,
|
||||
bottom_margin=10,
|
||||
)
|
||||
textview.set_buffer(Gtk.TextBuffer(text=trace))
|
||||
box.pack_start(textview, EXPAND, FILL, 0)
|
||||
|
||||
self.error_details = Gtk.ScrolledWindow(
|
||||
overlay_scrolling=False,
|
||||
max_content_height=150,
|
||||
propagate_natural_height=False,
|
||||
)
|
||||
details_box = Gtk.Box(
|
||||
hexpand=True,
|
||||
vexpand=True,
|
||||
margin_right=5,
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
)
|
||||
|
||||
self.details_buffer = Gtk.TextBuffer()
|
||||
details_textview = Gtk.TextView(
|
||||
wrap_mode=Gtk.WrapMode.WORD_CHAR,
|
||||
editable=False,
|
||||
left_margin=10,
|
||||
right_margin=10,
|
||||
top_margin=15,
|
||||
)
|
||||
|
||||
details_textview.set_buffer(self.details_buffer)
|
||||
details_box.pack_start(details_textview, EXPAND, FILL, 10)
|
||||
self.error_details.add(details_box)
|
||||
|
||||
self.error_notebook = Gtk.Notebook(show_tabs=False, margin_bottom=15)
|
||||
self.error_notebook.append_page(box, Gtk.Label(label="Error"))
|
||||
self.error_notebook.append_page(self.error_details, Gtk.Label(label="Details"))
|
||||
self.error_notebook.connect("switch-page", self._on_page_changed)
|
||||
scrollable.add(self.error_notebook)
|
||||
|
||||
content = self.get_content_area()
|
||||
content.set_spacing(0)
|
||||
content.add(scrollable)
|
||||
|
||||
def _on_page_changed(
|
||||
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
|
||||
) -> None:
|
||||
if child == self.error_details:
|
||||
GLib.idle_add(self.error_details.set_propagate_natural_height, True)
|
||||
else:
|
||||
GLib.idle_add(self.error_details.set_propagate_natural_height, False)
|
||||
|
||||
def set_details_buffer(self, text: str) -> None:
|
||||
self.details_buffer.set_text(text)
|
||||
self.error_notebook.set_show_tabs(True)
|
||||
@@ -1,3 +1,4 @@
|
||||
import html
|
||||
from typing import Self, TYPE_CHECKING
|
||||
|
||||
from dzgui.const.constants import EXPAND, FILL
|
||||
@@ -65,6 +66,7 @@ class ServerDialog(GenericDialog):
|
||||
|
||||
class ServerDetailsDialog(ServerDialog):
|
||||
def __init__(self, controller: "Controller", details: "Details"):
|
||||
# TODO: server name should also be packed in details struct
|
||||
name = controller.get_server_name()
|
||||
super().__init__(controller, strings.server_details, name, menu=None)
|
||||
|
||||
@@ -109,7 +111,7 @@ class ServerDetailsDialog(ServerDialog):
|
||||
self.view.set_model(self.store)
|
||||
text = details.description
|
||||
text = format_hyperlinks(text)
|
||||
self.description.set_markup(text)
|
||||
self.description.set_markup(html.escape(text))
|
||||
|
||||
self.show_all()
|
||||
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Self
|
||||
|
||||
from dzgui.api.shortcuts import Shortcuts
|
||||
from dzgui.const.constants import APP_NAME
|
||||
from dzgui.strings import uninstall
|
||||
from dzgui.util._json import read_json
|
||||
from dzgui.util.css import load_css
|
||||
from dzgui.util.format import format_exception
|
||||
from dzgui.views.components.box import HBox
|
||||
from dzgui.views.dialogs.wizard import ScrolledWizardPage, CheckboxWithLabel
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk, GLib, Pango # noqa E402
|
||||
|
||||
|
||||
class Assistant(Gtk.Assistant):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]):
|
||||
super().__init__()
|
||||
if is_deck:
|
||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||
self.set_default_size(1085, 670)
|
||||
else:
|
||||
self.set_default_size(1500, 900)
|
||||
|
||||
self.set_forward_page_func(self._advance_page)
|
||||
|
||||
pyapp = os.getenv("PYAPP")
|
||||
|
||||
self.page1 = UninstallPage(paths, pyapp)
|
||||
self.page2 = CompletionPage(pyapp)
|
||||
|
||||
self.append_page(self.page1)
|
||||
self.set_page_type(self.page1, Gtk.AssistantPageType.INTRO)
|
||||
self.set_page_title(self.page1, uninstall.pg1_title)
|
||||
self.set_page_complete(self.page1, True)
|
||||
|
||||
self.append_page(self.page2)
|
||||
self.set_page_title(self.page2, uninstall.pg2_title)
|
||||
self.set_page_type(self.page2, Gtk.AssistantPageType.SUMMARY)
|
||||
|
||||
self.connect("cancel", self.destroy_and_quit)
|
||||
self.connect("close", self.destroy_and_quit)
|
||||
self.show_all()
|
||||
load_css()
|
||||
|
||||
def _advance_page(self, index: int) -> int:
|
||||
cur_page = self.get_nth_page(index)
|
||||
if cur_page == self.page1:
|
||||
self.page1.uninstall()
|
||||
return index + 1
|
||||
|
||||
def destroy_and_quit(self, widget: Self) -> None:
|
||||
self.destroy()
|
||||
Gtk.main_quit()
|
||||
|
||||
|
||||
class Window(Gtk.Window):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||
super().__init__(title=APP_NAME, icon_name=APP_NAME)
|
||||
self.assistant = Assistant(is_deck, paths)
|
||||
|
||||
|
||||
class CompletionPage(ScrolledWizardPage):
|
||||
def __init__(self, pyapp: str | None):
|
||||
super().__init__(heading=uninstall.pg2_heading, description=uninstall.pg2_blurb)
|
||||
|
||||
text = (
|
||||
uninstall.standalone_uninstall
|
||||
if pyapp is not None
|
||||
else uninstall.system_uninstall
|
||||
)
|
||||
|
||||
label = Gtk.Label(
|
||||
label=text,
|
||||
wrap_mode=Pango.WrapMode.WORD,
|
||||
margin=10,
|
||||
max_width_chars=80,
|
||||
justify=Gtk.Justification.CENTER,
|
||||
)
|
||||
frame = Gtk.Frame()
|
||||
frame.add(label)
|
||||
self.add_start(frame)
|
||||
|
||||
|
||||
class FileLabel(HBox):
|
||||
def __init__(self, path: Path) -> None:
|
||||
super().__init__(spacing=10)
|
||||
|
||||
label = Gtk.Label(label=uninstall.path_remove_prefix, halign=Gtk.Align.START)
|
||||
textview = Gtk.TextView(
|
||||
editable=False,
|
||||
halign=Gtk.Align.START,
|
||||
left_margin=10,
|
||||
right_margin=10,
|
||||
)
|
||||
textview.set_buffer(Gtk.TextBuffer(text=str(path)))
|
||||
self.extend([label, textview])
|
||||
|
||||
|
||||
class CheckboxWithPath(CheckboxWithLabel):
|
||||
def __init__(
|
||||
self, label: str, details: str, path: Path, active: bool = True
|
||||
) -> None:
|
||||
super().__init__(text=label, blurb_text=details)
|
||||
|
||||
self.conf_path = path
|
||||
self.set_active(active)
|
||||
fl = FileLabel(path)
|
||||
self.indent_below(fl)
|
||||
|
||||
def get_conf_path(self) -> Path:
|
||||
return self.conf_path
|
||||
|
||||
|
||||
class UninstallPage(ScrolledWizardPage):
|
||||
def __init__(self, paths: dict[str, str], pyapp: str | None):
|
||||
super().__init__(heading=uninstall.pg1_heading, description=uninstall.pg1_blurb)
|
||||
|
||||
self.pyapp = pyapp
|
||||
|
||||
config = Path(paths["XDG_CONFIG_HOME"])
|
||||
state = Path(paths["XDG_STATE_HOME"])
|
||||
self.share = Path(paths["XDG_DATA_HOME"])
|
||||
shortcut = self.share.parent.joinpath("applications/dzgui.desktop")
|
||||
desktop = config.parent.parent.joinpath("Desktop/dzgui.desktop")
|
||||
self.steam_path = self.parse_steam_path(config, self.share)
|
||||
|
||||
self.boxes: list[CheckboxWithPath] = []
|
||||
self.checks_area = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
halign=Gtk.Align.CENTER,
|
||||
spacing=20,
|
||||
margin_top=20,
|
||||
margin_bottom=20,
|
||||
)
|
||||
self.config_box = CheckboxWithPath(
|
||||
uninstall.config_label, uninstall.config_details, config, active=False
|
||||
)
|
||||
self.desktop_box = CheckboxWithPath(
|
||||
uninstall.desktop_label, uninstall.desktop_details, desktop
|
||||
)
|
||||
self.start_menu_box = CheckboxWithPath(
|
||||
uninstall.start_menu_label, uninstall.start_menu_details, shortcut
|
||||
)
|
||||
self.state_box = CheckboxWithPath(
|
||||
uninstall.state_label, uninstall.state_details, state
|
||||
)
|
||||
self.steam_box = CheckboxWithLabel(
|
||||
uninstall.steam_shortcut, uninstall.steam_details
|
||||
)
|
||||
|
||||
for box in (
|
||||
self.config_box,
|
||||
self.state_box,
|
||||
self.desktop_box,
|
||||
self.start_menu_box,
|
||||
):
|
||||
self.checks_area.add(box)
|
||||
self.boxes.append(box)
|
||||
|
||||
self.checks_area.add(self.steam_box)
|
||||
|
||||
self.add_start(self.checks_area)
|
||||
self.show_all()
|
||||
|
||||
def parse_steam_path(self, config: Path, steam: Path) -> Path | None:
|
||||
try:
|
||||
file = config.joinpath("config.json")
|
||||
conf = read_json(file)
|
||||
steam = conf["default_steam_path"]
|
||||
self.steam_path = Path(steam)
|
||||
return Path(steam)
|
||||
except Exception as e:
|
||||
print(format_exception(e))
|
||||
return None
|
||||
|
||||
def wipe_conf_file(self, box: CheckboxWithPath) -> None:
|
||||
if box.get_active():
|
||||
path = box.get_conf_path()
|
||||
try:
|
||||
if path.is_dir():
|
||||
shutil.rmtree(path)
|
||||
else:
|
||||
path.unlink()
|
||||
print(f"Deleted '{path}'")
|
||||
except Exception as e:
|
||||
print(format_exception(e))
|
||||
|
||||
def wipe_steam_shortcut(self) -> None:
|
||||
if self.steam_path is None:
|
||||
return
|
||||
try:
|
||||
shortcuts = Shortcuts(self.steam_path)
|
||||
shortcuts.delete_shortcut(self.share)
|
||||
shutil.rmtree(self.share)
|
||||
except Exception as e:
|
||||
print(format_exception(e))
|
||||
|
||||
def wipe_pyapp(self) -> None:
|
||||
if self.pyapp is None:
|
||||
return
|
||||
subprocess.run([self.pyapp, "self", "remove"])
|
||||
|
||||
def uninstall(self) -> None:
|
||||
for box in self.boxes:
|
||||
self.wipe_conf_file(box)
|
||||
if self.steam_box.get_active():
|
||||
self.wipe_steam_shortcut()
|
||||
self.wipe_pyapp()
|
||||
pass
|
||||
|
||||
|
||||
class UninstallWizard(Gtk.Application):
|
||||
def __init__(self, is_deck: bool, paths: dict[str, str]) -> None:
|
||||
super().__init__()
|
||||
config = Path(paths["XDG_CONFIG_HOME"])
|
||||
if not config.is_dir() or not config.joinpath("config.json").is_file():
|
||||
print(uninstall.not_installed)
|
||||
return
|
||||
GLib.set_prgname(APP_NAME)
|
||||
self.win = Window(is_deck, paths)
|
||||
Gtk.main()
|
||||
@@ -6,7 +6,7 @@ from importlib import resources
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Self, TYPE_CHECKING
|
||||
|
||||
from dzgui.api.probe import test_steam_api, test_bm_api
|
||||
from dzgui.api.probe import test_steam_api
|
||||
from dzgui.api.shortcuts import add_steam_shortcut
|
||||
from dzgui.api.steam import get_steam_paths
|
||||
from dzgui.const.constants import (
|
||||
@@ -16,7 +16,7 @@ from dzgui.const.constants import (
|
||||
LEGACY_CONFIG_PATH,
|
||||
)
|
||||
from dzgui.const.boilerplate import config_boilerplate
|
||||
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP
|
||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||
from dzgui.const.enum import Preferences
|
||||
from dzgui.config import freedesktop
|
||||
from dzgui.config.query import lookup
|
||||
@@ -44,10 +44,16 @@ class PageNum(Enum):
|
||||
HAS_CONFIG = 2
|
||||
STEAM_PATH = 3
|
||||
STEAM_API = 4
|
||||
BM_API = 5
|
||||
USER_PREFS = 6
|
||||
SHORTCUTS = 7
|
||||
FINAL = 8
|
||||
USER_PREFS = 5
|
||||
SHORTCUTS = 6
|
||||
FINAL = 7
|
||||
|
||||
|
||||
class OptionalPageMixin:
|
||||
"""Marks optional pages as advanceable"""
|
||||
|
||||
def _on_map(self, page: "EnumeratedWizardPage") -> None:
|
||||
EMITTER.emit("step_complete")
|
||||
|
||||
|
||||
class DescriptionArea(Gtk.Box):
|
||||
@@ -66,10 +72,9 @@ class Progress(Gtk.ProgressBar):
|
||||
|
||||
|
||||
class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||
def __init__(self, enum: PageNum, heading: str, description: str):
|
||||
def __init__(self, heading: str, description: str):
|
||||
super().__init__(overlay_scrolling=False)
|
||||
|
||||
self.enum = enum
|
||||
self.page_type: Gtk.AssistantPageType
|
||||
self.title = heading
|
||||
self.heading = Heading(heading)
|
||||
@@ -82,7 +87,6 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||
height=600,
|
||||
preserve_aspect_ratio=True,
|
||||
)
|
||||
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
|
||||
self.box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
@@ -91,21 +95,13 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||
margin_top=50,
|
||||
spacing=20,
|
||||
)
|
||||
self.add(self.box)
|
||||
self.prog = Progress()
|
||||
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
||||
image = Gtk.Image.new_from_pixbuf(pixbuf)
|
||||
self.box.pack_start(image, expand=False, fill=True, padding=0)
|
||||
|
||||
self.add(self.box)
|
||||
self.box.pack_start(self.heading, expand=False, fill=True, padding=0)
|
||||
self.box.pack_start(self.description, expand=False, fill=True, padding=0)
|
||||
|
||||
self.connect("map", self._on_map)
|
||||
|
||||
def get_enum(self) -> PageNum:
|
||||
return self.enum
|
||||
|
||||
def get_progress_bar(self) -> Progress:
|
||||
return self.prog
|
||||
|
||||
def get_page_type(self) -> Gtk.AssistantPageType:
|
||||
return self.page_type
|
||||
|
||||
@@ -124,8 +120,20 @@ class ScrolledWizardPage(Gtk.ScrolledWindow):
|
||||
def get_box(self) -> Gtk.Box:
|
||||
return self.box
|
||||
|
||||
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
||||
pass
|
||||
|
||||
class EnumeratedWizardPage(ScrolledWizardPage):
|
||||
def __init__(self, enum: PageNum, heading: str, description: str) -> None:
|
||||
super().__init__(heading=heading, description=description)
|
||||
|
||||
self.enum = enum
|
||||
self.prog = Progress()
|
||||
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
|
||||
|
||||
def get_enum(self) -> PageNum:
|
||||
return self.enum
|
||||
|
||||
def get_progress_bar(self) -> Progress:
|
||||
return self.prog
|
||||
|
||||
|
||||
class NotificationFrame(Gtk.Frame):
|
||||
@@ -155,7 +163,7 @@ class NotificationFrame(Gtk.Frame):
|
||||
self.label.set_markup(wrapped)
|
||||
|
||||
|
||||
class APIValidationPage(ScrolledWizardPage):
|
||||
class APIValidationPage(EnumeratedWizardPage):
|
||||
def __init__(
|
||||
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
|
||||
) -> None:
|
||||
@@ -215,23 +223,6 @@ class APIValidationPage(ScrolledWizardPage):
|
||||
self.spinner.stop()
|
||||
|
||||
|
||||
class BMValidationPage(APIValidationPage):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.BM_API,
|
||||
heading=wizard.heading_bm_api,
|
||||
description=wizard.blurb_bm_api,
|
||||
link=BM_API_SETUP,
|
||||
func=self._validate,
|
||||
)
|
||||
|
||||
@call_on_thread("", show_dialog=False)
|
||||
def _validate(self, key: str) -> None:
|
||||
is_valid = test_bm_api(key.strip())
|
||||
cleanup = StoredFunc(self._cleanup, is_valid, key)
|
||||
self.thread_man.set_cleanup_func(cleanup)
|
||||
|
||||
|
||||
class SteamValidationPage(APIValidationPage):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
@@ -249,7 +240,7 @@ class SteamValidationPage(APIValidationPage):
|
||||
self.thread_man.set_cleanup_func(cleanup)
|
||||
|
||||
|
||||
class IntroductionPage(ScrolledWizardPage):
|
||||
class IntroductionPage(EnumeratedWizardPage):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.INTRO,
|
||||
@@ -304,7 +295,7 @@ class RadioFrame(Gtk.Frame):
|
||||
return self.button
|
||||
|
||||
|
||||
class ConfigMigrationPage(ScrolledWizardPage):
|
||||
class ConfigMigrationPage(EnumeratedWizardPage):
|
||||
def __init__(self, config: Path) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.HAS_CONFIG,
|
||||
@@ -364,7 +355,7 @@ class ConfigMigrationPage(ScrolledWizardPage):
|
||||
EMITTER.emit("config", True)
|
||||
|
||||
|
||||
class PreferencesPage(ScrolledWizardPage):
|
||||
class PreferencesPage(EnumeratedWizardPage):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.USER_PREFS,
|
||||
@@ -424,7 +415,7 @@ class PreferencesPage(ScrolledWizardPage):
|
||||
EMITTER.emit("step_complete")
|
||||
|
||||
|
||||
class CompletionPage(ScrolledWizardPage):
|
||||
class CompletionPage(EnumeratedWizardPage):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.FINAL,
|
||||
@@ -440,10 +431,12 @@ class Assistant(Gtk.Assistant):
|
||||
def __init__(self, is_deck: bool, XDG: "Xdg"):
|
||||
super().__init__()
|
||||
if is_deck:
|
||||
self.fullscreen()
|
||||
# NOTE: deemed to be "safe" dimensions that exclude taskbar size
|
||||
self.set_default_size(1085, 670)
|
||||
else:
|
||||
self.set_default_size(1500, 900)
|
||||
|
||||
self.is_binary = False if os.getenv("PYAPP") is None else True
|
||||
self.config_path = XDG.config
|
||||
|
||||
self.config_values: dict[str, Any] = config_boilerplate
|
||||
@@ -454,10 +447,9 @@ class Assistant(Gtk.Assistant):
|
||||
self.page2 = ConfigMigrationPage(XDG.config)
|
||||
self.page3 = SteamPathPage()
|
||||
self.page4 = SteamValidationPage()
|
||||
self.page5 = BMValidationPage()
|
||||
self.page6 = PreferencesPage()
|
||||
self.page7 = ShortcutCreationPage(XDG.shortcut)
|
||||
self.page8 = CompletionPage()
|
||||
self.page5 = PreferencesPage()
|
||||
self.page6 = ShortcutCreationPage(XDG.shortcut)
|
||||
self.page7 = CompletionPage()
|
||||
|
||||
self.set_forward_page_func(self._advance_page)
|
||||
|
||||
@@ -475,7 +467,6 @@ class Assistant(Gtk.Assistant):
|
||||
self.page5,
|
||||
self.page6,
|
||||
self.page7,
|
||||
self.page8,
|
||||
):
|
||||
# NOTE: skip config migration page if no legacy config file
|
||||
if (
|
||||
@@ -484,7 +475,7 @@ class Assistant(Gtk.Assistant):
|
||||
):
|
||||
continue
|
||||
# NOTE: disabled for now on system-provided packages
|
||||
if isinstance(page, ShortcutCreationPage) and os.getenv("PYAPP") is None:
|
||||
if isinstance(page, ShortcutCreationPage) and not self.is_binary:
|
||||
continue
|
||||
self._add_page(page, page.get_page_type())
|
||||
|
||||
@@ -499,32 +490,31 @@ class Assistant(Gtk.Assistant):
|
||||
|
||||
def _advance_page(self, index: int) -> int:
|
||||
page = self.get_nth_page(index)
|
||||
# TODO: use enums/isinstance
|
||||
match page:
|
||||
case self.page1:
|
||||
case IntroductionPage():
|
||||
pass
|
||||
case self.page2:
|
||||
if self.page2.is_migrated():
|
||||
case ConfigMigrationPage():
|
||||
if page.is_migrated():
|
||||
steam_path = lookup(self.config_path, Preferences.DEFAULT)
|
||||
self.page7.set_steam_path(steam_path)
|
||||
return self.get_n_pages() - 2
|
||||
case self.page3:
|
||||
self.page6.set_steam_path(steam_path)
|
||||
offset = 1 if not self.is_binary else 2
|
||||
self.setup_complete = True
|
||||
return self.get_n_pages() - offset
|
||||
case SteamPathPage():
|
||||
self.config_values["default_steam_path"] = page.get_path_from_radio()
|
||||
case self.page4:
|
||||
case SteamValidationPage():
|
||||
self.config_values["steam_api"] = page.get_api_key()
|
||||
case self.page5:
|
||||
self.config_values["bm_api"] = page.get_api_key()
|
||||
# NOTE: collects config values before advancing to last page
|
||||
case self.page6:
|
||||
name, use_miles, client = self.page6.get_prefs()
|
||||
case PreferencesPage():
|
||||
# NOTE: collects config values before advancing to last page
|
||||
name, use_miles, client = self.page5.get_prefs()
|
||||
self.config_values["name"] = name
|
||||
self.config_values["use_miles"] = use_miles
|
||||
self.config_values["client"] = client
|
||||
self.write_config()
|
||||
self.page7.set_steam_path(self.config_values["default_steam_path"])
|
||||
case self.page7:
|
||||
self.page7.create_shortcuts()
|
||||
self.page6.set_steam_path(self.config_values["default_steam_path"])
|
||||
self.setup_complete = True
|
||||
case ShortcutCreationPage():
|
||||
page.create_shortcuts()
|
||||
case _:
|
||||
raise AttributeError("Trying to advance a non-canonical page")
|
||||
return index + 1
|
||||
@@ -550,7 +540,9 @@ class Assistant(Gtk.Assistant):
|
||||
return
|
||||
self.set_page_complete(page, True)
|
||||
|
||||
def _add_page(self, page: ScrolledWizardPage, ptype: Gtk.AssistantPageType) -> None:
|
||||
def _add_page(
|
||||
self, page: EnumeratedWizardPage, ptype: Gtk.AssistantPageType
|
||||
) -> None:
|
||||
self.append_page(page)
|
||||
self.set_page_type(page, ptype)
|
||||
self.set_page_title(page, page.get_title())
|
||||
@@ -559,7 +551,7 @@ class Assistant(Gtk.Assistant):
|
||||
def _set_config_state(self, emitter: "Emitter", state: bool) -> None:
|
||||
self.config = state
|
||||
|
||||
def _on_page_prepare(self: Self, wizard: Self, page: ScrolledWizardPage) -> None:
|
||||
def _on_page_prepare(self: Self, wizard: Self, page: EnumeratedWizardPage) -> None:
|
||||
page_num = self.get_current_page() + 1
|
||||
total = self.get_n_pages()
|
||||
fraction = page_num / total
|
||||
@@ -568,11 +560,7 @@ class Assistant(Gtk.Assistant):
|
||||
bar.set_fraction(fraction)
|
||||
bar.set_text(f"{page_num}/{total}")
|
||||
|
||||
# NOTE: disable forward action
|
||||
# TODO: use page enums
|
||||
if page == self.page5:
|
||||
return
|
||||
if page != self.page1:
|
||||
if not isinstance(page, IntroductionPage):
|
||||
EMITTER.emit("step_pending")
|
||||
|
||||
|
||||
@@ -580,15 +568,21 @@ class CheckboxWithLabel(Gtk.Box):
|
||||
def __init__(self, text: str, blurb_text: str) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=5)
|
||||
|
||||
self.indent = 20
|
||||
self.button = Gtk.CheckButton(label=text)
|
||||
self.button.set_active(True)
|
||||
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=20)
|
||||
label = Gtk.Label(label="", halign=Gtk.Align.START, margin_start=self.indent)
|
||||
|
||||
wrapped = textwrap.fill(blurb_text, width=100)
|
||||
label.set_markup(f"- {wrapped}")
|
||||
|
||||
for el in self.button, label:
|
||||
self.add(el)
|
||||
|
||||
def indent_below(self, widget: Gtk.Widget) -> None:
|
||||
widget.set_margin_start(self.indent)
|
||||
self.add(widget)
|
||||
|
||||
def get_checkbox(self) -> Gtk.CheckButton:
|
||||
return self.button
|
||||
|
||||
@@ -599,7 +593,7 @@ class CheckboxWithLabel(Gtk.Box):
|
||||
self.button.set_active(state)
|
||||
|
||||
|
||||
class ShortcutCreationPage(ScrolledWizardPage):
|
||||
class ShortcutCreationPage(OptionalPageMixin, EnumeratedWizardPage): # type: ignore
|
||||
def __init__(self, shortcut: Path) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.SHORTCUTS,
|
||||
@@ -642,9 +636,6 @@ class ShortcutCreationPage(ScrolledWizardPage):
|
||||
self.desktop_checkbox.set_active(state)
|
||||
self.desktop_checkbox.set_sensitive(state)
|
||||
|
||||
def _on_map(self, page: "ScrolledWizardPage") -> None:
|
||||
EMITTER.emit("step_complete")
|
||||
|
||||
def set_steam_path(self, path: Path) -> None:
|
||||
self.steam_path = path
|
||||
|
||||
@@ -660,7 +651,7 @@ class ShortcutCreationPage(ScrolledWizardPage):
|
||||
freedesktop.write_desktop_shortcut(desktop_file)
|
||||
|
||||
|
||||
class SteamPathPage(ScrolledWizardPage):
|
||||
class SteamPathPage(EnumeratedWizardPage):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(
|
||||
enum=PageNum.USER_PREFS,
|
||||
|
||||
@@ -104,20 +104,11 @@ class ContextMixin(TreeView):
|
||||
return False
|
||||
menu = self.context_menu
|
||||
children = menu.get_children()
|
||||
sel = menu.get_selected_item()
|
||||
for i, child in enumerate(children):
|
||||
if sel is child:
|
||||
ind = i
|
||||
break
|
||||
|
||||
match event.keyval:
|
||||
case Gdk.KEY_j:
|
||||
if ind + 1 > len(children) - 1:
|
||||
return False
|
||||
menu.emit("move-current", Gtk.MenuDirectionType.NEXT)
|
||||
case Gdk.KEY_k:
|
||||
if ind - 1 < 0:
|
||||
return False
|
||||
menu.emit("move-current", Gtk.MenuDirectionType.PREV)
|
||||
case Gdk.KEY_g:
|
||||
menu.select_item(children[0])
|
||||
|
||||
@@ -37,7 +37,11 @@ class CursorMixin:
|
||||
end = len(model) - 1
|
||||
else:
|
||||
return False
|
||||
cur_row = self.get_focused_row_index() # type: ignore
|
||||
|
||||
try:
|
||||
cur_row = self.get_focused_row_index() # type: ignore
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
if position == CursorPosition.DOWN:
|
||||
if cur_row == end:
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import logging
|
||||
import re
|
||||
import textwrap
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from importlib import resources
|
||||
|
||||
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, CHANGELOG_PATH
|
||||
from dzgui.strings import changelog as clog
|
||||
from dzgui.util.strings import missing_changelog
|
||||
from dzgui.util.format import format_pango
|
||||
from dzgui.views.mixins.help_menu_mixin import HelpMenuMixin
|
||||
@@ -31,18 +33,39 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
||||
logger.critical(e)
|
||||
changelog = missing_changelog
|
||||
|
||||
# TODO: should long text be wrapped?
|
||||
self.controller = controller
|
||||
self.box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10)
|
||||
self.box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.VERTICAL, spacing=5, margin_top=10
|
||||
)
|
||||
self.add(self.box)
|
||||
|
||||
expand_all = Gtk.Button(
|
||||
label=clog.expand_all, halign=Gtk.Align.START, margin_start=20
|
||||
)
|
||||
expand_all.connect("clicked", self._on_expand_all_clicked)
|
||||
self.box.add(expand_all)
|
||||
|
||||
self.expanded = False
|
||||
self.expanders: list[Gtk.Expander] = []
|
||||
self.connect("key-press-event", self._on_keypress)
|
||||
self.connect("key-press-event", self._on_esc_keypress)
|
||||
|
||||
changes = self._parse(changelog)
|
||||
self._generate_nodes(changes)
|
||||
|
||||
self.show_all()
|
||||
|
||||
def _on_expand_all_clicked(self, button: Gtk.Button) -> None:
|
||||
self.expanded = not self.expanded
|
||||
for expander in self.expanders:
|
||||
# NOTE: simply setting set_expanded() does not trigger activate() signal,
|
||||
# so margins are not applied
|
||||
if expander.get_expanded() == self.expanded:
|
||||
continue
|
||||
expander.activate()
|
||||
label = clog.collapse_all if self.expanded else clog.expand_all
|
||||
button.set_label(label)
|
||||
|
||||
def grab_content_area(self) -> None:
|
||||
self.grab_focus()
|
||||
|
||||
@@ -55,7 +78,6 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
||||
return version, date
|
||||
raise ValueError("Changelog parse error")
|
||||
|
||||
|
||||
def _generate_nodes(self, releases: list[tuple[str, list[str]]]) -> None:
|
||||
for header, changes in releases:
|
||||
for ind in (0, -1):
|
||||
@@ -64,8 +86,10 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
||||
text = "\n".join(changes)
|
||||
formatted = format_pango(text)
|
||||
|
||||
label = Gtk.Label(valign=Gtk.Align.START, margin=15, halign=Gtk.Align.START)
|
||||
container = Gtk.Box(valign=Gtk.Align.START, halign=Gtk.Align.START)
|
||||
label = Gtk.Label()
|
||||
label.set_markup(formatted)
|
||||
container.add(label)
|
||||
|
||||
version, date = self._parse_version(header)
|
||||
button_text = f"{version} ({date})"
|
||||
@@ -73,11 +97,24 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
||||
|
||||
expander = Gtk.Expander()
|
||||
expander.set_label_widget(button)
|
||||
expander.add(label)
|
||||
expander.add(container)
|
||||
expander.connect("activate", self._on_expand, container)
|
||||
self.box.add(expander)
|
||||
self.expanders.append(expander)
|
||||
|
||||
def _on_expand(self, expander: Gtk.Box, container: Gtk.Box) -> None:
|
||||
"""
|
||||
Wayland/some themes do not expect widgets to have a size allocation until expanded.
|
||||
Trying to set margins a priori may cause negative allocation,
|
||||
as the margins are subtracted from zero. In effect, the widget is not realized
|
||||
until it is expanded for the first time.
|
||||
"""
|
||||
container.set_margin_start(15)
|
||||
container.set_margin_top(15)
|
||||
container.set_margin_bottom(15)
|
||||
|
||||
def _parse(self, changelog: str) -> list[tuple[str, list[str]]]:
|
||||
|
||||
release = ""
|
||||
releases: list[tuple[str, list[str]]] = []
|
||||
release_notes: list[str] = []
|
||||
@@ -91,5 +128,5 @@ class Changelog(HelpMenuMixin, ScrollableMixin, Gtk.ScrolledWindow): # type: ig
|
||||
release = ""
|
||||
release = line
|
||||
continue
|
||||
release_notes.append(line.rstrip())
|
||||
release_notes.append(textwrap.fill(line.rstrip(), width=120))
|
||||
return releases
|
||||
@@ -103,4 +103,4 @@ class Keybindings(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
return grid
|
||||
|
||||
def grab_content_area(self) -> None:
|
||||
self.box.grab_focus()
|
||||
self.grab_focus()
|
||||
@@ -9,7 +9,6 @@ from dzgui.const.constants import (
|
||||
APPNAME_DAYZ,
|
||||
APPNAME_DAYZ_EXP_HUMAN,
|
||||
EDIT_DELETE,
|
||||
ERROR,
|
||||
FOLDER,
|
||||
WARNING,
|
||||
)
|
||||
@@ -21,6 +20,7 @@ from dzgui.views.components.buttons import Icon, IconTextButton
|
||||
from dzgui.views.components.eventbox import InfoEventBox
|
||||
from dzgui.views.components.frame import HeadingFrame
|
||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||
from dzgui.views.components.misc import ErrorPopover
|
||||
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
||||
|
||||
|
||||
@@ -41,26 +41,27 @@ class FolderError(Enum):
|
||||
FOLDER_CHANGED = 3
|
||||
|
||||
|
||||
class PageHeading(Gtk.Label):
|
||||
def __init__(self, label: str) -> None:
|
||||
super().__init__(label=label, halign=Gtk.Align.CENTER)
|
||||
class PageHeading(Gtk.Box):
|
||||
def __init__(self, label: str, controller: "Controller") -> None:
|
||||
super().__init__(
|
||||
halign=Gtk.Align.CENTER, orientation=Gtk.Orientation.HORIZONTAL
|
||||
)
|
||||
|
||||
self.label = Gtk.Label(label=label)
|
||||
self.eb = InfoEventBox(offline.heading_disclaimer, controller)
|
||||
self.eb.set_icon_yalign(0.7)
|
||||
|
||||
for el in self.label, self.eb:
|
||||
self.add(el)
|
||||
|
||||
css.add_class(self, "page-heading")
|
||||
|
||||
|
||||
class ErrorPopover(Gtk.Popover):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(position=Gtk.PositionType.RIGHT)
|
||||
class OfflineErrorPopover(ErrorPopover):
|
||||
def __init__(self, relative_to: Gtk.Widget, position: Gtk.PositionType) -> None:
|
||||
super().__init__(relative_to=relative_to, position=position)
|
||||
|
||||
self.hbox = HBox()
|
||||
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
||||
error_icon = Icon(ERROR, margin_start=10)
|
||||
self.hbox.extend([error_icon, self.label])
|
||||
self.add(self.hbox)
|
||||
self.show_all()
|
||||
self.popdown()
|
||||
|
||||
def set_label(self, error: FolderError, msg: str) -> None:
|
||||
def set_error_label(self, error: FolderError, msg: str) -> None:
|
||||
match error:
|
||||
case FolderError.NO_VALID_MODS:
|
||||
prefix = offline.no_mods
|
||||
@@ -106,13 +107,10 @@ class FolderHBox(HBox):
|
||||
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
||||
)
|
||||
|
||||
self.pop = ErrorPopover()
|
||||
self.pop.set_relative_to(self.button)
|
||||
self.pop = OfflineErrorPopover(self.button, Gtk.PositionType.RIGHT)
|
||||
self.pop.connect("unmap", lambda _: self.grab_focus())
|
||||
|
||||
self.sidepop = ErrorPopover()
|
||||
self.sidepop.set_position(Gtk.PositionType.BOTTOM)
|
||||
self.sidepop.set_relative_to(self.scrolled_label)
|
||||
self.sidepop = OfflineErrorPopover(self.scrolled_label, Gtk.PositionType.BOTTOM)
|
||||
self.sidepop.connect("unmap", lambda _: self.grab_focus())
|
||||
|
||||
self.connect("map", self._on_map)
|
||||
@@ -161,13 +159,13 @@ class FolderHBox(HBox):
|
||||
|
||||
def present_error(self, error: FolderError, msg: str) -> None:
|
||||
if error == FolderError.FOLDER_CHANGED:
|
||||
self.sidepop.set_label(error, msg)
|
||||
self.sidepop.set_error_label(error, msg)
|
||||
self.sidepop.popup()
|
||||
return
|
||||
self.folder = ""
|
||||
self.label.set_text("")
|
||||
self.unset_button.hide()
|
||||
self.pop.set_label(error, msg)
|
||||
self.pop.set_error_label(error, msg)
|
||||
self.pop.popup()
|
||||
|
||||
|
||||
@@ -434,7 +432,8 @@ class OfflineLoader(Gtk.Box):
|
||||
self.emitter = controller.get_emitter()
|
||||
self.offline_man = OfflineManager(controller)
|
||||
|
||||
self.add(PageHeading(offline.heading))
|
||||
heading = PageHeading(offline.heading, controller)
|
||||
self.add(heading)
|
||||
|
||||
self.local_frame = ModFrame(self, controller, offline.local_frame)
|
||||
self.custom_frame = CustomModFrame(self, controller, offline.custom_frame)
|
||||
@@ -457,7 +456,6 @@ class OfflineLoader(Gtk.Box):
|
||||
]
|
||||
)
|
||||
|
||||
# TODO: share ConnectBox class with preconnect dialog?
|
||||
self.button_box = HBox(spacing=5)
|
||||
self.button_box.set_halign(Gtk.Align.END)
|
||||
self.button_box.set_margin_top(15)
|
||||
@@ -511,3 +509,6 @@ class OfflineLoader(Gtk.Box):
|
||||
self.mission_frame.present_folder_changed(mission)
|
||||
return
|
||||
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
|
||||
|
||||
def grab_content_area(self) -> None:
|
||||
self.local_frame.tree.grab_focus()
|
||||
+253
-241
@@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Callable, TYPE_CHECKING
|
||||
|
||||
from dzgui.api import pefile as PeFile
|
||||
|
||||
@@ -15,16 +15,19 @@ from dzgui.const.constants import (
|
||||
VIEW_CONCEAL,
|
||||
VIEW_REVEAL,
|
||||
)
|
||||
from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
|
||||
from dzgui.const.endpoints import STEAM_API_SETUP
|
||||
from dzgui.const.enum import Preferences, ServerTab
|
||||
from dzgui.strings import errors, options
|
||||
from dzgui.strings import options
|
||||
from dzgui.util import strings, css, open_links
|
||||
|
||||
from dzgui.views.components.box import ShortHBox, VBox
|
||||
from dzgui.views.components.eventbox import InfoEventBox
|
||||
from dzgui.views.components.labels import LeftLabel
|
||||
from dzgui.views.components.buttons import WebButton
|
||||
from dzgui.views.components.buttons import SpinnerButton, WebButton
|
||||
from dzgui.views.components.frame import HeadingFrame
|
||||
from dzgui.views.components.misc import ClientCombo
|
||||
from dzgui.views.components.misc import ClientCombo, ErrorPopover
|
||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||
from dzgui.views.mixins.scrollable_mixin import ScrollableMixin
|
||||
|
||||
|
||||
import gi
|
||||
@@ -37,62 +40,246 @@ if TYPE_CHECKING:
|
||||
from dzgui.controllers.emitter import Emitter
|
||||
|
||||
|
||||
class ShortHBox(Gtk.Box):
|
||||
def __init__(self, widget: Gtk.Widget) -> None:
|
||||
class SubmitField(Gtk.Box):
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
placeholder: str,
|
||||
context: Preferences,
|
||||
slow: bool = False,
|
||||
private: bool = True,
|
||||
) -> None:
|
||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
|
||||
self.callback: Callable
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
|
||||
self.old_text: str
|
||||
self.placeholder = placeholder
|
||||
|
||||
self.entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
|
||||
|
||||
# TODO: audit for all applicable callbacks
|
||||
if private:
|
||||
self.entry.set_icon_from_icon_name(
|
||||
Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL
|
||||
)
|
||||
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||
self.entry.connect("icon-release", self._on_icon_release)
|
||||
self.entry.set_visibility(False)
|
||||
|
||||
self.button: SpinnerButton | Gtk.Button
|
||||
if slow:
|
||||
self.button = SpinnerButton(label=options.save_button)
|
||||
else:
|
||||
self.button = Gtk.Button(label=options.save_button)
|
||||
|
||||
self.button.connect("clicked", self._on_save_clicked)
|
||||
self.entry.connect("insert-text", self._on_text_typed)
|
||||
self.entry.connect("activate", self._on_field_activated)
|
||||
self.entry.get_property("buffer").connect("deleted-text", self._on_text_deleted)
|
||||
|
||||
self.add(self.entry)
|
||||
self.add(self.button)
|
||||
|
||||
def set_text(self, text: str) -> None:
|
||||
self.old_text = text
|
||||
self.entry.set_text(text)
|
||||
if len(text) == 0:
|
||||
self.button.set_sensitive(False)
|
||||
|
||||
def _is_valid_text(self, text: str) -> bool:
|
||||
if text.isspace():
|
||||
return False
|
||||
if len(text) == 0:
|
||||
return False
|
||||
|
||||
if text == self.old_text:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _on_text_deleted(
|
||||
self,
|
||||
buffer: Gtk.EntryBuffer,
|
||||
position: int,
|
||||
chars: int,
|
||||
) -> None:
|
||||
|
||||
text = buffer.get_text()
|
||||
state = self._is_valid_text(text)
|
||||
self.button.set_sensitive(state)
|
||||
|
||||
def _on_text_typed(
|
||||
self,
|
||||
entry: Gtk.Entry,
|
||||
text: str,
|
||||
length: int,
|
||||
pos: int,
|
||||
) -> None:
|
||||
|
||||
buffer = entry.get_property("buffer")
|
||||
text = buffer.get_text() + text
|
||||
state = self._is_valid_text(text)
|
||||
self.button.set_sensitive(state)
|
||||
|
||||
def _on_icon_release(
|
||||
self,
|
||||
widget: Gtk.Entry,
|
||||
icon_pos: Gtk.EntryIconPosition,
|
||||
event: Gdk.Event,
|
||||
) -> None:
|
||||
visible = widget.get_visibility()
|
||||
if visible:
|
||||
icon, state = VIEW_REVEAL, False
|
||||
else:
|
||||
icon, state = VIEW_CONCEAL, True
|
||||
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
widget.set_visibility(state)
|
||||
|
||||
def _on_field_activated(self, entry: Gtk.Entry) -> None:
|
||||
text = entry.get_text()
|
||||
if not self._is_valid_text(text):
|
||||
return
|
||||
self.save_option()
|
||||
|
||||
def set_callback(self, callback: Callable) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def save_option(self) -> None:
|
||||
self.callback()
|
||||
self.button.set_sensitive(False)
|
||||
|
||||
def _on_save_clicked(self, button: Gtk.Button) -> None:
|
||||
self.save_option()
|
||||
|
||||
|
||||
class SteamSubmitField(SubmitField):
|
||||
def __init__(self, controller: "Controller") -> None:
|
||||
super().__init__(
|
||||
controller,
|
||||
options.steam_placeholder,
|
||||
Preferences.STEAM,
|
||||
slow=True,
|
||||
private=True,
|
||||
)
|
||||
|
||||
self.pop = ErrorPopover(
|
||||
position=Gtk.PositionType.BOTTOM, relative_to=self.entry
|
||||
)
|
||||
self.pop.set_label(options.api_failed)
|
||||
self.pop.show_all()
|
||||
self.pop.popdown()
|
||||
|
||||
emitter = controller.get_emitter()
|
||||
emitter.connect("api_change_failed", self._on_api_failure)
|
||||
emitter.connect("api_change_successful", self._on_api_success)
|
||||
|
||||
self.set_callback(self.save_setting)
|
||||
|
||||
def save_setting(self) -> None:
|
||||
self.entry.set_sensitive(False)
|
||||
text = "".join(self.entry.get_text().split())
|
||||
self.controller.update_steam_api_key(text)
|
||||
|
||||
def _on_api_success(self, emitter: "Emitter") -> None:
|
||||
self.old_text = self.entry.get_text()
|
||||
if isinstance(self.button, SpinnerButton):
|
||||
self.button.stop_spinner()
|
||||
self.entry.set_sensitive(True)
|
||||
|
||||
def _on_api_failure(self, emitter: "Emitter") -> None:
|
||||
self.pop.popup()
|
||||
if isinstance(self.button, SpinnerButton):
|
||||
self.button.stop_spinner()
|
||||
self.entry.set_sensitive(True)
|
||||
|
||||
def block_text_entry(self) -> None:
|
||||
self.entry.set_position(-1)
|
||||
self.entry.set_can_focus(False)
|
||||
|
||||
def unblock_text_entry(self) -> None:
|
||||
self.entry.set_can_focus(True)
|
||||
|
||||
|
||||
class NameSubmitField(SubmitField):
|
||||
def __init__(self, controller: "Controller") -> None:
|
||||
super().__init__(
|
||||
controller,
|
||||
options.name_placeholder,
|
||||
Preferences.NAME,
|
||||
private=False,
|
||||
)
|
||||
self.entry.set_width_chars(30) # type: ignore
|
||||
self.set_callback(self.save_player_name)
|
||||
|
||||
def save_player_name(self) -> None:
|
||||
value = self.entry.get_text().strip()
|
||||
self.old_text = value
|
||||
self.controller.update_config(self.context, value)
|
||||
|
||||
|
||||
class ToggleField(Gtk.Box):
|
||||
def __init__(
|
||||
self,
|
||||
controller: "Controller",
|
||||
first_option: str,
|
||||
second_option: str,
|
||||
context: Preferences,
|
||||
) -> None:
|
||||
super().__init__(spacing=5, halign=Gtk.Align.START)
|
||||
|
||||
self.pack_start(widget, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
self.radio1 = Gtk.RadioButton.new_with_label(None, first_option)
|
||||
self.radio2 = Gtk.RadioButton.new_from_widget(self.radio1)
|
||||
self.radio2.set_label(second_option)
|
||||
self.pack_start(self.radio1, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||
self.pack_start(self.radio2, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||
|
||||
def set_suboption_active(self, state: bool) -> None:
|
||||
# NOTE: defer connection of signal until after state is set
|
||||
self.radio2.set_active(state)
|
||||
self.radio1.connect("toggled", self._on_radio_toggled, self.context)
|
||||
|
||||
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
||||
self.controller.toggle_config(context)
|
||||
|
||||
def set_sensitive(self, state: bool) -> None:
|
||||
for el in self.radio1, self.radio2:
|
||||
el.set_sensitive(state)
|
||||
|
||||
|
||||
class Options(Gtk.Box):
|
||||
class Options(ScrollableMixin, Gtk.ScrolledWindow): # type: ignore
|
||||
def __init__(self, controller: "Controller"):
|
||||
super().__init__(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
margin_start=10,
|
||||
margin_end=10,
|
||||
)
|
||||
|
||||
self.controller = controller
|
||||
self.controller.register_widget("options", self)
|
||||
emitter = controller.get_emitter()
|
||||
emitter.connect("api_change_failed", self._on_api_change_failed)
|
||||
|
||||
self.DEFAULT_WIDTH = 1
|
||||
self.DEFAULT_HEIGHT = 1
|
||||
|
||||
label = Gtk.Label(label=strings.options.header)
|
||||
label.set_halign(Gtk.Align.CENTER)
|
||||
css.add_class(label, "page-heading")
|
||||
self.add(label)
|
||||
|
||||
self.steam_entry: Gtk.Entry
|
||||
self.bm_entry: Gtk.Entry
|
||||
self.pop: Gtk.Popover
|
||||
|
||||
self.steam = WebButton(label=strings.options.steam_web)
|
||||
self.steam.connect("clicked", self._on_link_button_clicked, STEAM_API_SETUP)
|
||||
|
||||
self.bm = WebButton(label=strings.options.bm_web)
|
||||
self.bm.connect("clicked", self._on_link_button_clicked, BM_API_SETUP)
|
||||
|
||||
self.steam_box = self._make_submit_field(
|
||||
strings.options.enter_steam, Preferences.STEAM, True
|
||||
)
|
||||
self.bm_box = self._make_submit_field(
|
||||
strings.options.enter_bm, Preferences.BM, True
|
||||
)
|
||||
self.steam_box = SteamSubmitField(controller)
|
||||
api_rows = [
|
||||
[LeftLabel(strings.options.steam_placeholder), self.steam_box],
|
||||
[LeftLabel(strings.options.bm_placeholder), self.bm_box],
|
||||
[LeftLabel(strings.options.steam_placeholder), self.steam_box, self.steam],
|
||||
]
|
||||
|
||||
self.player_box = self._make_submit_field(
|
||||
strings.options.name_placeholder, Preferences.NAME
|
||||
)
|
||||
self.player_box = NameSubmitField(controller)
|
||||
self.player_box.set_halign(Gtk.Align.START)
|
||||
# TODO: make submit field a standalone class
|
||||
self.player_box.get_children()[0].set_width_chars(30) # type: ignore
|
||||
|
||||
self.fullscreen_toggle = self.make_binary_radio(
|
||||
self.fullscreen_toggle = ToggleField(
|
||||
self.controller,
|
||||
strings.options.last_used,
|
||||
strings.options.always_fs,
|
||||
Preferences.WINDOW,
|
||||
@@ -103,8 +290,8 @@ class Options(Gtk.Box):
|
||||
|
||||
client_hbox = ShortHBox(self.client_combo)
|
||||
|
||||
self.distance_toggle = self.make_binary_radio(
|
||||
strings.options.km, strings.options.mi, Preferences.DIST
|
||||
self.distance_toggle = ToggleField(
|
||||
self.controller, strings.options.km, strings.options.mi, Preferences.DIST
|
||||
)
|
||||
|
||||
combo_store = Gtk.ListStore(str, object)
|
||||
@@ -144,18 +331,6 @@ class Options(Gtk.Box):
|
||||
api_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
api_grid = self._make_grid(api_rows)
|
||||
api_box.add(api_grid)
|
||||
api_links_box = Gtk.Box(
|
||||
orientation=Gtk.Orientation.HORIZONTAL,
|
||||
margin_start=5,
|
||||
margin_end=5,
|
||||
margin_top=5,
|
||||
margin_bottom=10,
|
||||
homogeneous=True,
|
||||
spacing=10,
|
||||
)
|
||||
api_links_box.add(self.steam)
|
||||
api_links_box.add(self.bm)
|
||||
api_box.add(api_links_box)
|
||||
|
||||
prefs_grid = self._make_grid(pref_rows)
|
||||
version_grid = self._make_grid(version_rows)
|
||||
@@ -176,7 +351,7 @@ class Options(Gtk.Box):
|
||||
grid.attach(developers, 1, 0, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
||||
|
||||
for pair in [
|
||||
(api_box, strings.options.api_keys),
|
||||
(api_box, strings.options.api_key),
|
||||
(prefs_grid, strings.options.prefs),
|
||||
(version_grid, strings.options.version),
|
||||
]:
|
||||
@@ -185,9 +360,16 @@ class Options(Gtk.Box):
|
||||
grid.attach(frame, col, row, self.DEFAULT_WIDTH, self.DEFAULT_HEIGHT)
|
||||
row += 1
|
||||
|
||||
self.scrollable = Gtk.ScrolledWindow(vexpand=True)
|
||||
self.scrollable.add(grid)
|
||||
self.add(self.scrollable)
|
||||
label = Gtk.Label(label=strings.options.header)
|
||||
label.set_halign(Gtk.Align.CENTER)
|
||||
css.add_class(label, "page-heading")
|
||||
|
||||
box = VBox()
|
||||
box.add(label)
|
||||
box.add(grid)
|
||||
self.add(box)
|
||||
|
||||
self.connect("key-press-event", self._on_keypress)
|
||||
|
||||
def get_client_name(self) -> str:
|
||||
model = self.client_combo.get_model()
|
||||
@@ -195,13 +377,13 @@ class Options(Gtk.Box):
|
||||
return str(model[ind][0])
|
||||
|
||||
def block_text_entry(self) -> None:
|
||||
for entry in self.steam_entry, self.bm_entry:
|
||||
entry.set_position(-1)
|
||||
entry.set_can_focus(False)
|
||||
self.steam_box.block_text_entry()
|
||||
# self.steam_entry.set_position(-1)
|
||||
# self.steam_entry.set_can_focus(False)
|
||||
|
||||
def unblock_text_entry(self) -> None:
|
||||
for entry in self.steam_entry, self.bm_entry:
|
||||
entry.set_can_focus(True)
|
||||
self.steam_box.unblock_text_entry()
|
||||
# self.steam_entry.set_can_focus(True)
|
||||
|
||||
def _on_developers_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.show_developers_page()
|
||||
@@ -209,48 +391,6 @@ class Options(Gtk.Box):
|
||||
def _on_link_button_clicked(self, button: Gtk.Button, url: str) -> None:
|
||||
open_links.open_link_by_url(url)
|
||||
|
||||
def _make_submit_field(
|
||||
self,
|
||||
placeholder: str,
|
||||
context: Preferences,
|
||||
private: bool = False,
|
||||
) -> Gtk.Box:
|
||||
|
||||
entry = Gtk.Entry(placeholder_text=placeholder, hexpand=True)
|
||||
button = Gtk.Button(label="Save")
|
||||
|
||||
button.connect("clicked", self._on_save_clicked, entry, context)
|
||||
entry.connect("insert-text", self._on_text_typed, context, button)
|
||||
entry.connect("activate", self._on_field_activated, context, button)
|
||||
entry.get_property("buffer").connect(
|
||||
"deleted-text", self._on_text_deleted, context, button
|
||||
)
|
||||
|
||||
if private:
|
||||
entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL)
|
||||
entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
|
||||
entry.connect("icon-release", self._on_icon_release)
|
||||
entry.set_visibility(False)
|
||||
|
||||
if context == Preferences.STEAM:
|
||||
self.steam_entry = entry
|
||||
else:
|
||||
self.bm_entry = entry
|
||||
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
|
||||
box.add(entry)
|
||||
box.add(button)
|
||||
|
||||
return box
|
||||
|
||||
def _on_field_activated(
|
||||
self, entry: Gtk.Entry, context: Preferences, button: Gtk.Button
|
||||
) -> None:
|
||||
text = entry.get_text()
|
||||
if not self._is_valid_text(text, context):
|
||||
return
|
||||
self._on_save_clicked(button, entry, context)
|
||||
|
||||
def _make_grid(self, rows: list) -> Gtk.Grid:
|
||||
grid = Gtk.Grid(
|
||||
orientation=Gtk.Orientation.VERTICAL,
|
||||
@@ -270,38 +410,6 @@ class Options(Gtk.Box):
|
||||
row += 1
|
||||
return grid
|
||||
|
||||
def _on_save_clicked(
|
||||
self, button: Gtk.Button, entry: Gtk.Entry, enum: Preferences
|
||||
) -> None:
|
||||
old_text = self.controller.query_config(enum)
|
||||
self.old_text = old_text
|
||||
self.old_entry = entry
|
||||
|
||||
button.set_sensitive(False)
|
||||
match enum:
|
||||
case Preferences.NAME:
|
||||
value = entry.get_text().strip()
|
||||
self.controller.update_config(enum, value)
|
||||
case Preferences.BM | Preferences.STEAM:
|
||||
text = "".join(entry.get_text().split())
|
||||
self.controller.update_api_key(enum, text)
|
||||
|
||||
def _on_api_change_failed(self, emitter: "Emitter") -> None:
|
||||
self.old_entry.set_text(self.old_text)
|
||||
# TODO: use popover
|
||||
dialog = ExceptionDialog(self.controller, errors.api_validation_error)
|
||||
dialog.run()
|
||||
|
||||
def restore_api_text(self, text: str, entry: Gtk.Entry) -> None:
|
||||
entry.set_text(text)
|
||||
|
||||
def revert(self, mode: Preferences) -> None:
|
||||
if mode == Preferences.STEAM:
|
||||
self.steam_entry.set_text(self.old_steam)
|
||||
else:
|
||||
self.bm_entry.set_text(self.old_bm)
|
||||
pass
|
||||
|
||||
def _on_start_tab_changed(self, combo: Gtk.ComboBoxText) -> None:
|
||||
_iter = combo.get_active_iter()
|
||||
if _iter is None:
|
||||
@@ -317,81 +425,12 @@ class Options(Gtk.Box):
|
||||
real_cmd = combo.get_model()[_iter][1]
|
||||
self.controller.update_config(Preferences.CLIENT, real_cmd)
|
||||
|
||||
def _on_radio_toggled(self, button: Gtk.RadioButton, context: Preferences) -> None:
|
||||
try:
|
||||
self.controller.toggle_config(context)
|
||||
except Exception:
|
||||
button.handler_block_by_func(self._on_radio_toggled)
|
||||
self.populate_settings()
|
||||
button.handler_unblock_by_func(self._on_radio_toggled)
|
||||
|
||||
def _is_valid_text(self, text: str, context: Preferences) -> bool:
|
||||
if text.isspace():
|
||||
return False
|
||||
if len(text) == 0:
|
||||
return False
|
||||
|
||||
match context:
|
||||
case Preferences.NAME:
|
||||
old = self.old_name
|
||||
case Preferences.STEAM:
|
||||
old = self.old_steam
|
||||
case Preferences.BM:
|
||||
old = self.old_bm
|
||||
if text == old:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _on_text_deleted(
|
||||
self,
|
||||
buffer: Gtk.EntryBuffer,
|
||||
position: int,
|
||||
chars: int,
|
||||
context: Preferences,
|
||||
button: Gtk.Button,
|
||||
) -> None:
|
||||
|
||||
text = buffer.get_text()
|
||||
state = self._is_valid_text(text, context)
|
||||
button.set_sensitive(state)
|
||||
|
||||
def _on_text_typed(
|
||||
self,
|
||||
entry: Gtk.Entry,
|
||||
text: str,
|
||||
length: int,
|
||||
pos: int,
|
||||
context: Preferences,
|
||||
button: Gtk.Button,
|
||||
) -> None:
|
||||
|
||||
buffer = entry.get_property("buffer")
|
||||
text = buffer.get_text() + text
|
||||
state = self._is_valid_text(text, context)
|
||||
button.set_sensitive(state)
|
||||
|
||||
def make_binary_radio(
|
||||
self,
|
||||
first_option: str,
|
||||
second_option: str,
|
||||
context: Preferences,
|
||||
) -> Gtk.Box:
|
||||
|
||||
hbox = Gtk.Box(spacing=5, halign=Gtk.Align.START)
|
||||
radio1 = Gtk.RadioButton.new_with_label(None, first_option)
|
||||
radio2 = Gtk.RadioButton.new_from_widget(radio1)
|
||||
radio2.set_label(second_option)
|
||||
radio1.connect("toggled", self._on_radio_toggled, context)
|
||||
hbox.pack_start(radio1, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||
hbox.pack_start(radio2, NO_EXPAND, NO_FILL, NO_PADDING)
|
||||
|
||||
return hbox
|
||||
|
||||
def populate_settings(self) -> None:
|
||||
prefs = self.controller.get_prefs()
|
||||
# NOTE: re-check in case file was removed by user between runs
|
||||
if prefs.paths.config.is_file() is False:
|
||||
dialog = ExceptionDialog(self.controller, strings.config_not_found)
|
||||
dialog.show_all()
|
||||
dialog.run()
|
||||
raise OSError(f"Config file '{prefs.paths.config}' not found")
|
||||
|
||||
@@ -401,37 +440,24 @@ class Options(Gtk.Box):
|
||||
name = self.controller.query_config(Preferences.NAME)
|
||||
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||
steam = self.controller.query_config(Preferences.STEAM)
|
||||
bm = self.controller.query_config(Preferences.BM)
|
||||
|
||||
steam_path = Path(default_steam_path)
|
||||
|
||||
self.old_steam = steam
|
||||
self.old_bm = bm
|
||||
self.old_name = name
|
||||
self.steam_box.set_text(steam)
|
||||
self.player_box.set_text(name)
|
||||
|
||||
self.steam_entry.set_text(steam)
|
||||
self.bm_entry.set_text(bm)
|
||||
p = self.player_box.get_children()[0]
|
||||
if hasattr(p, "set_text"):
|
||||
p.set_text(name)
|
||||
fs = config["fullscreen"]
|
||||
miles = config["use_miles"]
|
||||
|
||||
# NOTE: suppress toggle signal until radios are built
|
||||
self._suppress_toggles(True)
|
||||
for el, conf_state in [
|
||||
(self.fullscreen_toggle, config["fullscreen"]),
|
||||
(self.distance_toggle, config["use_miles"]),
|
||||
]:
|
||||
el.get_children()[conf_state].set_active(True)
|
||||
self._suppress_toggles(False)
|
||||
if prefs.is_steam_deck:
|
||||
self.fullscreen_toggle.set_sensitive(False)
|
||||
eb = InfoEventBox(options.fullscreen_eventbox, self.controller)
|
||||
self.fullscreen_toggle.add(eb)
|
||||
|
||||
# NOTE: disable buttons if no text is set
|
||||
for field in (
|
||||
[name, self.player_box],
|
||||
[steam, self.steam_box],
|
||||
[bm, self.bm_box],
|
||||
):
|
||||
if field[0] == "":
|
||||
field[1].get_children()[1].set_sensitive(False)
|
||||
fs = True
|
||||
|
||||
self.fullscreen_toggle.set_suboption_active(fs)
|
||||
self.distance_toggle.set_suboption_active(miles)
|
||||
|
||||
dayz_version = PeFile.get_pretty_version(steam_path, APPID_DAYZ)
|
||||
if dayz_version is None:
|
||||
@@ -460,19 +486,5 @@ class Options(Gtk.Box):
|
||||
self, toggle.get_children()[0], "_on_radio_toggled", state
|
||||
)
|
||||
|
||||
def _on_icon_release(
|
||||
self,
|
||||
widget: Gtk.Entry,
|
||||
icon_pos: Gtk.EntryIconPosition,
|
||||
event: Gdk.Event,
|
||||
) -> None:
|
||||
visible = widget.get_visibility()
|
||||
if visible:
|
||||
icon, state = VIEW_REVEAL, False
|
||||
else:
|
||||
icon, state = VIEW_CONCEAL, True
|
||||
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
|
||||
widget.set_visibility(state)
|
||||
|
||||
def grab_content_area(self) -> None:
|
||||
return
|
||||
self.grab_focus()
|
||||
@@ -11,6 +11,7 @@ from dzgui.util.keys import is_ctrl_mask
|
||||
from dzgui.util.localize import number
|
||||
from dzgui.strings.server_mods import checkmark
|
||||
from dzgui.strings import preconnect
|
||||
from dzgui.views.dialogs.generic import DebugDialog
|
||||
from dzgui.views.components.frame import HeadingFrame
|
||||
from dzgui.views.trees.tree_server_mods import ServerModTreeView
|
||||
|
||||
@@ -120,6 +121,12 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
spacing=5,
|
||||
)
|
||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||
|
||||
# NOTE: -d flag
|
||||
if self.controller.get_prefs().is_debug:
|
||||
debug = Gtk.Button(label=preconnect.debug, halign=Gtk.Align.START)
|
||||
debug.connect("clicked", self._on_debug_clicked)
|
||||
box.add(debug)
|
||||
for button in self.back, self.ok, self.connect_last:
|
||||
box.add(button)
|
||||
self.button_box.add(box)
|
||||
@@ -150,8 +157,7 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
self.tree_box.add(self.scrolled)
|
||||
self.tree_box.add(self.progress_box)
|
||||
|
||||
# TODO: strings
|
||||
self.mods_placeholder = Placeholder("This server has no mods.")
|
||||
self.mods_placeholder = Placeholder(preconnect.placeholder_no_mods)
|
||||
self.tree_box.add(self.mods_placeholder)
|
||||
|
||||
self.tree_frame = HeadingFrame.new_with_widget_and_label(
|
||||
@@ -161,8 +167,7 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
# TODO: abstract into components
|
||||
self.warning_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||
self.warning_tree = MaskedTree(WARNING)
|
||||
# TODO: strings
|
||||
self.warning_placeholder = Placeholder("No warnings.")
|
||||
self.warning_placeholder = Placeholder(preconnect.placeholder_no_warnings)
|
||||
self.warning_box.add(self.warning_tree)
|
||||
self.warning_box.add(self.warning_placeholder)
|
||||
self.warning_frame = HeadingFrame.new_with_widget_and_label(
|
||||
@@ -223,6 +228,12 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.update_and_load_to_menu()
|
||||
|
||||
def _on_debug_clicked(self, button: Gtk.Button) -> None:
|
||||
args = self.controller.get_debug_args()
|
||||
d = DebugDialog(self.controller, args)
|
||||
d.show_all()
|
||||
d.run()
|
||||
|
||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||
self.controller.update_and_connect()
|
||||
|
||||
@@ -233,52 +244,46 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
warnings: list[str] = []
|
||||
errors: list[str] = []
|
||||
|
||||
resync_msg = (
|
||||
f"If you recently installed {prereqs.build} or moved it to a different drive,\n"
|
||||
"restart Steam to allow these changes to synchronize, then try again."
|
||||
)
|
||||
resync_msg = preconnect.resync.format(prereqs.build)
|
||||
|
||||
"""Errors"""
|
||||
if len(prereqs.invalid_mods) > 0:
|
||||
pairs = [": ".join(sub) for sub in prereqs.invalid_mods]
|
||||
lines = "\n".join(pairs)
|
||||
msg = preconnect.invalid_mods.format(lines)
|
||||
errors.append(msg)
|
||||
if prereqs.binary_missing:
|
||||
errors.append(
|
||||
f"Remote server is running the build '{prereqs.build}', but it is not installed.\n{resync_msg}"
|
||||
)
|
||||
msg = preconnect.version_missing.format(prereqs.build)
|
||||
msg += f"\n{resync_msg}"
|
||||
errors.append(msg)
|
||||
elif prereqs.local_version != prereqs.remote_version:
|
||||
errors.append(
|
||||
f"Local client version '{prereqs.local_version}' does not match remote version '{prereqs.remote_version}'.\n{resync_msg}"
|
||||
msg = preconnect.version_mismatch.format(
|
||||
prereqs.local_version, prereqs.remote_version
|
||||
)
|
||||
msg += f"\n{resync_msg}"
|
||||
errors.append(msg)
|
||||
if prereqs.required_space > prereqs.available_space:
|
||||
required_pretty = number(prereqs.required_space)
|
||||
available_pretty = number(prereqs.available_space)
|
||||
errors.append(
|
||||
f"Need to update {required_pretty} MiB of mods, but installation path only has {available_pretty} MiB."
|
||||
)
|
||||
msg = preconnect.not_enough_space.format(required_pretty, available_pretty)
|
||||
errors.append(msg)
|
||||
if len(prereqs.mods) > 0 and prereqs.game_mode:
|
||||
errors.append("Use Desktop Mode to download mods on Steam Deck")
|
||||
errors.append(preconnect.use_desktop_mode)
|
||||
|
||||
if prereqs.steam_proc.is_running is False:
|
||||
client = prereqs.steam_proc.name
|
||||
errors.append(
|
||||
f"'{client}' is set as the default Steam client, but it is either not installed or not running."
|
||||
)
|
||||
msg = preconnect.steam_not_running.format(client)
|
||||
errors.append(msg)
|
||||
|
||||
"""Warnings"""
|
||||
if prereqs.passworded:
|
||||
warnings.append(
|
||||
"Protected: you will be prompted for a password when connecting to this server."
|
||||
)
|
||||
warnings.append(preconnect.protected_server)
|
||||
if prereqs.dayz_running is True:
|
||||
warnings.append(
|
||||
"It looks like DayZ is already running in the background. Exit DayZ before connecting."
|
||||
)
|
||||
warnings.append(preconnect.dayz_running)
|
||||
|
||||
allows_dl, running_app = prereqs.allows_downloads
|
||||
if len(prereqs.mods) > 0 and allows_dl is False:
|
||||
msg = (
|
||||
f"The game '{running_app}' is currently running in Steam, but background downloads are not enabled.\n"
|
||||
"Either stop the game first, or update your global Steam settings or the game's local settings.\n"
|
||||
"Otherwise, mods may be queued for download but never update."
|
||||
)
|
||||
msg = preconnect.running_app.format(running_app)
|
||||
warnings.append(msg)
|
||||
|
||||
self.add_warnings(warnings)
|
||||
@@ -317,7 +322,7 @@ class PreConnectionAssistant(Gtk.Box):
|
||||
|
||||
if prereqs.required_space != 0:
|
||||
pretty = number(prereqs.required_space)
|
||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
||||
suffix = preconnect.required_space.format(pretty)
|
||||
prefix = preconnect.total_mods
|
||||
self.mod_count.set_text(f"{prefix}{str(total_mods)}.{suffix}")
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import logging
|
||||
|
||||
from typing import Any, Optional, TYPE_CHECKING
|
||||
from warnings import deprecated
|
||||
|
||||
from dzgui.const.constants import APP_NAME, SEPARATOR
|
||||
from dzgui.util.keys import is_ctrl_mask, is_navkey
|
||||
@@ -173,7 +172,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
(model, pathlist) = sels
|
||||
return (model, pathlist)
|
||||
|
||||
@deprecated("Currently unused")
|
||||
# @deprecated("Currently unused")
|
||||
# def get_mpath(self) -> Optional[Gtk.TreePath]:
|
||||
# (model, pathlist) = self.get_model_and_pathlist()
|
||||
# if len(pathlist) < 1:
|
||||
@@ -195,7 +194,7 @@ class TreeView(CursorMixin, Gtk.TreeView): # type: ignore
|
||||
return True
|
||||
return False
|
||||
|
||||
@deprecated("unused")
|
||||
# @deprecated("unused")
|
||||
# def get_selected_row(self) -> Optional[Gtk.TreeModelRow]:
|
||||
# ind = self.get_selected_row_index()
|
||||
# model = self.get_model()
|
||||
|
||||
@@ -53,7 +53,7 @@ class LogTreeView(ContextMixin, TreeView): # type: ignore
|
||||
model = ModelFactory().new_model_from_logfile(filepath)
|
||||
_filter = model.filter_new()
|
||||
_filter.set_visible_func(self._filter_rows)
|
||||
sortable = Gtk.TreeModelSort(_filter)
|
||||
sortable = Gtk.TreeModelSort.new_with_model(_filter)
|
||||
self.set_model(sortable)
|
||||
_filter.refilter()
|
||||
path = Gtk.TreePath.new_from_indices([0])
|
||||
|
||||
@@ -295,6 +295,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
||||
) -> None:
|
||||
if self.loaded is False:
|
||||
return
|
||||
# NOTE: signal triggers twice on right-click events
|
||||
# due to deselect-then-select behavior in GTK
|
||||
sel = self.get_selection()
|
||||
model, row = sel.get_selected()
|
||||
if row is None:
|
||||
return
|
||||
self.start_distcalc()
|
||||
|
||||
def get_name(self) -> str:
|
||||
|
||||
+4
-2
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
||||
authors = [
|
||||
{name = "aclist"}
|
||||
]
|
||||
version = "7.0.0b17"
|
||||
version = "7.0.0b22"
|
||||
license = "GPL-3.0-or-later"
|
||||
license-files = ["LICENSE"]
|
||||
readme = "README.md"
|
||||
@@ -107,11 +107,13 @@ config-settings-package = { pygobject-stubs = { config = "Gtk3,Gdk3,Soup2" } }
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
markers = [
|
||||
"apitest: checks remote endpoints",
|
||||
"config: config file keys/values",
|
||||
"realconfig: depends on actual generated local config file",
|
||||
"mods: tests mod metadata/link creation",
|
||||
"pefile: validate PE files",
|
||||
"post_install: requires a completed installation",
|
||||
"redact: log redaction mechanisms",
|
||||
"slow: long-running tests",
|
||||
"webtest: checks remote endpoints"
|
||||
"webtest: depends on remote endpoint",
|
||||
]
|
||||
@@ -0,0 +1,27 @@
|
||||
from pathlib import Path
|
||||
|
||||
def iterate(search_str: str) -> None:
|
||||
# TODO: exclude files from .gitignore
|
||||
print()
|
||||
print(f"Searching for '{search_str}'")
|
||||
print()
|
||||
files = Path(".").rglob("*")
|
||||
ignore = [".git", ".mypy", "build/", "test.py"]
|
||||
for file in files:
|
||||
if file.is_dir():
|
||||
continue
|
||||
if any(s in str(file) for s in ignore):
|
||||
continue
|
||||
try:
|
||||
text = file.read_text()
|
||||
if search_str in text:
|
||||
c = text.count(search_str)
|
||||
if 0 < c < 2:
|
||||
print(file, c)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
|
||||
imports = ["Pango", "GObject", "GLib", "Gdk", "Gtk"]
|
||||
for i in imports:
|
||||
iterate(i)
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
"users"
|
||||
{
|
||||
"0"
|
||||
{
|
||||
"AccountName" "STEAMUSER"
|
||||
"PersonaName" "STEAMUSER"
|
||||
"RememberPassword" "0"
|
||||
"WantsOfflineMode" "0"
|
||||
"SkipOfflineModeWarning" "0"
|
||||
"AutoLogin" "1"
|
||||
"Timestamp" "0"
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
"users"
|
||||
{
|
||||
"0"
|
||||
{
|
||||
"AccountName" "STEAMUSER"
|
||||
"PersonaName" "STEAMUSER"
|
||||
"RememberPassword" "0"
|
||||
"WantsOfflineMode" "0"
|
||||
"SkipOfflineModeWarning" "0"
|
||||
"AllowAutoLogin" "1"
|
||||
"MostRecent" "1"
|
||||
"Timestamp" "0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
"users"
|
||||
{
|
||||
"0"
|
||||
{
|
||||
"AccountName" "STEAMUSER"
|
||||
"PersonaName" "STEAMUSER"
|
||||
"RememberPassword" "0"
|
||||
"WantsOfflineMode" "0"
|
||||
"SkipOfflineModeWarning" "0"
|
||||
"AllowAutoLogin" "1"
|
||||
"MostRecent" "0"
|
||||
"Timestamp" "0"
|
||||
}
|
||||
"1"
|
||||
{
|
||||
"AccountName" "STEAMUSER2"
|
||||
"PersonaName" "STEAMUSER2"
|
||||
"RememberPassword" "0"
|
||||
"WantsOfflineMode" "0"
|
||||
"SkipOfflineModeWarning" "0"
|
||||
"AllowAutoLogin" "1"
|
||||
"MostRecent" "1"
|
||||
"Timestamp" "0"
|
||||
}
|
||||
}
|
||||
+26
-13
@@ -1,20 +1,33 @@
|
||||
import re
|
||||
import pytest
|
||||
|
||||
from importlib import resources
|
||||
from dzgui.const.constants import APP_NAME_LOWER, CHANGELOG_PATH
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def changelog():
|
||||
path = resources.files(APP_NAME_LOWER).joinpath(CHANGELOG_PATH)
|
||||
return path
|
||||
def changelog(request) -> None:
|
||||
root = request.config.rootpath
|
||||
changelog = Path(root).joinpath("CHANGELOG.md").read_text()
|
||||
return changelog
|
||||
|
||||
|
||||
def test_headings(changelog):
|
||||
with open(changelog, "r") as f:
|
||||
lines = f.readlines()
|
||||
for line in lines:
|
||||
if line.startswith("#"):
|
||||
pass
|
||||
# TODO: use regex
|
||||
pass
|
||||
def count_hash(line: str) -> int:
|
||||
cnt = 0
|
||||
for c in line:
|
||||
if c == "#":
|
||||
cnt += 1
|
||||
return cnt
|
||||
|
||||
|
||||
def test_changelog_prefix(changelog) -> None:
|
||||
r = r".*(\[.*\]).*"
|
||||
lines = changelog.splitlines()
|
||||
sort = sorted(lines)
|
||||
match = [line for line in sort if line.startswith("#")]
|
||||
for m in match:
|
||||
if "Changelog" in m:
|
||||
assert count_hash(m) == 1
|
||||
elif re.match(r, m) is not None:
|
||||
assert count_hash(m) == 2
|
||||
else:
|
||||
assert count_hash(m) == 3
|
||||
@@ -7,13 +7,12 @@ from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
||||
from dzgui.config import convert
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
pytestmark = pytest.mark.config
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_config():
|
||||
return fixture_path("dztuirc_1")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def unset_values():
|
||||
return fixture_path("dztuirc_3")
|
||||
@@ -22,7 +21,6 @@ def unset_values():
|
||||
@pytest.fixture
|
||||
def keys():
|
||||
return [
|
||||
"bm_api",
|
||||
"fav_server",
|
||||
"fav_label",
|
||||
"name",
|
||||
@@ -38,6 +36,7 @@ def keys():
|
||||
|
||||
# TODO: use a static fixture instead of system config
|
||||
@pytest.fixture
|
||||
@pytest.mark.realconfig
|
||||
def config():
|
||||
paths = get_xdg_paths()
|
||||
xdg = parse_filepaths(paths)
|
||||
@@ -45,18 +44,21 @@ def config():
|
||||
|
||||
|
||||
@pytest.mark.post_install
|
||||
@pytest.mark.realconfig
|
||||
def test_invalid_config_value(config):
|
||||
with pytest.raises(Exception):
|
||||
assert config["foo"] is None
|
||||
|
||||
|
||||
@pytest.mark.post_install
|
||||
@pytest.mark.realconfig
|
||||
def test_default_config_values(keys, config):
|
||||
for key in keys:
|
||||
assert config[key] is not None
|
||||
|
||||
|
||||
@pytest.mark.post_install
|
||||
@pytest.mark.realconfig
|
||||
def test_contains_invalid_values(keys, config):
|
||||
for key in config:
|
||||
assert key in keys
|
||||
@@ -81,7 +83,6 @@ def test_key_conversion(legacy_config):
|
||||
j = convert.rc2json(legacy_config)
|
||||
j = json.loads(j)
|
||||
keys = [
|
||||
"api_key",
|
||||
"staging_dir",
|
||||
"src_path",
|
||||
"steam_path",
|
||||
@@ -91,6 +92,7 @@ def test_key_conversion(legacy_config):
|
||||
for key in keys:
|
||||
assert key not in j
|
||||
|
||||
|
||||
def test_missing_values(unset_values):
|
||||
j = convert.rc2json(unset_values)
|
||||
j = json.loads(j)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
from dzgui.api.servers import Details
|
||||
from dzgui.views.dialogs.servers import ServerDetailsDialog
|
||||
|
||||
import gi
|
||||
|
||||
gi.require_version("Gtk", "3.0")
|
||||
from gi.repository import Gtk # noqa E402
|
||||
|
||||
|
||||
class MockController:
|
||||
def __init__(self) -> None:
|
||||
self.window = Gtk.Window()
|
||||
|
||||
def get_window(self) -> Gtk.Window:
|
||||
return self.window
|
||||
|
||||
def get_server_name(self) -> str:
|
||||
return "My server"
|
||||
|
||||
def get_emitter(self) -> None:
|
||||
return None
|
||||
|
||||
|
||||
def test_html_escaping() -> None:
|
||||
text = "<Server description> Game & mods"
|
||||
details = Details([["0", "1"]], text, True)
|
||||
controller = MockController()
|
||||
dialog = ServerDetailsDialog(controller, details)
|
||||
|
||||
assert dialog.description.get_text() == text
|
||||
@@ -1,6 +1,24 @@
|
||||
import pytest
|
||||
|
||||
from dzgui.api.servers import validate_ip
|
||||
from dzgui.views.components.entry import validate_ip_truthy
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"ip",
|
||||
[
|
||||
("foo", False),
|
||||
("999", False),
|
||||
("192.168.1.101", False),
|
||||
("192.168.1.101:", False),
|
||||
(":1", False),
|
||||
("192.168.1.101:27016", True),
|
||||
],
|
||||
)
|
||||
def test_ip_entry(ip) -> None:
|
||||
string, state = ip
|
||||
assert validate_ip_truthy(string) is state
|
||||
|
||||
|
||||
def test_ip_validation() -> None:
|
||||
ip = "192.168.1.1:100"
|
||||
@@ -15,6 +33,7 @@ def test_invalid_port() -> None:
|
||||
with pytest.raises(Exception):
|
||||
validate_ip(ip)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("port", [-1, 65536])
|
||||
def test_port_out_of_range(port: int) -> None:
|
||||
ip = f"192.168.1.1:{port}"
|
||||
@@ -27,6 +46,7 @@ def test_invalid_socket() -> None:
|
||||
with pytest.raises(Exception):
|
||||
validate_ip(ip)
|
||||
|
||||
|
||||
# TODO: ?
|
||||
def test_ipdb() -> None:
|
||||
pass
|
||||
@@ -37,6 +37,14 @@ class RecordsListHandler(logging.Handler):
|
||||
"Error in directory: '/home/SENSITIVE_USERNAME'",
|
||||
"Error in directory: '/home/REDACTED'",
|
||||
),
|
||||
(
|
||||
"User directory: /home/user/.local/share/Steam/userdata/999999/grid",
|
||||
"User directory: /home/REDACTED/.local/share/Steam/userdata/REDACTED/grid",
|
||||
),
|
||||
(
|
||||
"User directory: /drive/.local/share/Steam/userdata/999999",
|
||||
"User directory: /drive/.local/share/Steam/userdata/REDACTED",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_log_redaction(log_error: str, expect: str) -> None:
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from dzgui.api.steam import find_user_id
|
||||
from tests.fixtures import fixture_path
|
||||
|
||||
pytestmark = pytest.mark.apitest
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"fixture, expect",
|
||||
[
|
||||
("api/loginusers_legacy_client.vdf", 0),
|
||||
("api/loginusers_legacy_client_multiple.vdf", 1),
|
||||
("api/loginusers_beta_client.vdf", 0),
|
||||
],
|
||||
)
|
||||
def test_loginusers(monkeypatch, fixture: str, expect: int) -> None:
|
||||
def mock_loginusers(path: Path) -> Path:
|
||||
return fixture_path(fixture)
|
||||
|
||||
monkeypatch.setattr("dzgui.api.steam.find_loginusers", mock_loginusers)
|
||||
uid = find_user_id(Path(""))
|
||||
assert int(uid) == expect
|
||||
@@ -13,13 +13,11 @@ def config():
|
||||
xdg = parse_filepaths(paths)
|
||||
return get_config(xdg.config)
|
||||
|
||||
|
||||
def test_ipdb():
|
||||
assert probe.test_ipdb()
|
||||
|
||||
|
||||
def test_steam(config):
|
||||
key = config["steam_api"]
|
||||
assert probe.test_steam_api(key)
|
||||
|
||||
def test_bm(config):
|
||||
key = config["bm_api"]
|
||||
assert probe.test_bm_api(key)
|
||||
+32
-8
@@ -30,33 +30,33 @@ def test_no_shortcuts(monkeypatch) -> None:
|
||||
@pytest.fixture
|
||||
def dummy_app() -> None:
|
||||
d = {
|
||||
"appname": "TEST APP",
|
||||
"AppName": "TEST APP",
|
||||
"StartDir": "TEST_DIR",
|
||||
"exe": "TEST_DIR/TEST_EXE.EXE",
|
||||
"Exe": "TEST_DIR/TEST_EXE.EXE",
|
||||
"icon": "IMAGES_DIR/TEST_IMAGE.PNG",
|
||||
}
|
||||
return d
|
||||
|
||||
|
||||
def test_wrap_exe(dummy_app) -> None:
|
||||
def test_wrap_exe(dummy_app: dict[str, str]) -> None:
|
||||
s = Shortcuts(Path(""))
|
||||
s.add_shortcut(*dummy_app.values())
|
||||
assert s.shortcuts["shortcuts"]["0"]["exe"][0] == '"'
|
||||
assert s.shortcuts["shortcuts"]["0"]["exe"][-1] == '"'
|
||||
assert s.shortcuts["shortcuts"]["0"]["Exe"][0] == '"'
|
||||
assert s.shortcuts["shortcuts"]["0"]["Exe"][-1] == '"'
|
||||
|
||||
|
||||
def test_add_shortcut(dummy_app) -> None:
|
||||
def test_add_shortcut(dummy_app: dict[str, str]) -> None:
|
||||
s = Shortcuts(Path(""))
|
||||
s.add_shortcut(*dummy_app.values())
|
||||
new = s.shortcuts["shortcuts"]
|
||||
ind = str(len(new) - 1)
|
||||
for k, v in dummy_app.items():
|
||||
if k == "exe":
|
||||
if k == "Exe":
|
||||
v = f'"{v}"'
|
||||
assert new[ind][k] == v
|
||||
|
||||
|
||||
def test_save_shortcut(dummy_app) -> None:
|
||||
def test_save_shortcut(dummy_app: dict[str, str]) -> None:
|
||||
s = Shortcuts(Path(""))
|
||||
s.add_shortcut(*dummy_app.values())
|
||||
with tempfile.NamedTemporaryFile() as f:
|
||||
@@ -65,3 +65,27 @@ def test_save_shortcut(dummy_app) -> None:
|
||||
s.save_shortcuts()
|
||||
s._load_shortcuts(tmp)
|
||||
assert len(s.shortcuts["shortcuts"]) == 1
|
||||
|
||||
|
||||
def test_shortcut_crc(dummy_app: dict[str, str]) -> None:
|
||||
s = Shortcuts(Path(""))
|
||||
s.add_shortcut(*dummy_app.values())
|
||||
for key in s.shortcuts["shortcuts"].keys():
|
||||
entry = s.shortcuts["shortcuts"][key]
|
||||
name = entry["AppName"]
|
||||
exe = entry["Exe"]
|
||||
uid = name + exe
|
||||
bpid = s.gen_bpid(uid)
|
||||
assert entry["appid"] & 0xFFFFFFFF == bpid
|
||||
|
||||
|
||||
def test_reverse_crc(dummy_app: dict[str, str]) -> None:
|
||||
s = Shortcuts(Path(""))
|
||||
s.add_shortcut(*dummy_app.values())
|
||||
for key in s.shortcuts["shortcuts"].keys():
|
||||
entry = s.shortcuts["shortcuts"][key]
|
||||
name = entry["AppName"]
|
||||
exe = entry["Exe"]
|
||||
uid = name + exe
|
||||
bpid = s.gen_bpid(uid)
|
||||
assert s.find_appname_by_unsigned_id(bpid) == name
|
||||
@@ -0,0 +1,23 @@
|
||||
import re
|
||||
|
||||
from dzgui.model.proxy_model import DAY_REG, NIGHT_REG
|
||||
|
||||
|
||||
def iterate(h: str, r: str) -> None:
|
||||
for m in range(60):
|
||||
time = f"{h:02}:{m:02}"
|
||||
assert re.match(r, time) is not None
|
||||
|
||||
|
||||
def test_day() -> None:
|
||||
for h in range(17):
|
||||
if h < 7:
|
||||
continue
|
||||
iterate(h, DAY_REG)
|
||||
|
||||
|
||||
def test_night() -> None:
|
||||
for h in range(24):
|
||||
if 6 < h < 17:
|
||||
continue
|
||||
iterate(h, NIGHT_REG)
|
||||
Reference in new issue
Block a user