Merge pull request #229 from aclist/prerelease/6.0.0-beta.7

Prerelease/6.0.0-beta.7
This commit is contained in:
aclist 2025-09-20 18:11:57 +09:00 committed by GitHub
commit 9db40fe664
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 171 additions and 80 deletions

View File

@ -1,6 +1,13 @@
# Changelog
## [6.0.0-beta.5] 2025-09-12
## [6.0.0-beta.7] 2025-09-18
## Added
- Users can save text notes to describe servers
## Changed
- State file serialization methods
## [6.0.0-beta.6] 2025-09-12
## Added
- Support DayZ Experimental
- Show additional client information in Options menu

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -o pipefail
version=6.0.0.beta-6
version=6.0.0.beta-7
#CONSTANTS
aid=221100
@ -585,10 +585,10 @@ fetch_helpers_by_sum(){
[[ -f "$config_file" ]] && source "$config_file"
declare -A sums
sums=(
["funcs"]="a286cc402bfccd39493fe32c53148a95"
["funcs"]="c27a81577c818b4f4d786ada52ad5042"
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
["servers.py"]="7f83d5c1ca54acb12f1bd6657feb2ecf"
["ui.py"]="cd9f4b3bc9b1922bb10cbc0c579cf2c0"
["ui.py"]="8d6e52b3e9032acbb84ba6cf137f455a"
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
["pefile.py"]="21531f2c0d9dfa5f110cf6779f9d22c0"
)

View File

@ -1,6 +1,6 @@
#!/usr/bin/env bash
set -o pipefail
version="6.0.0-beta.6"
version="6.0.0-beta.7"
#CONSTANTS
aid=221100

View File

@ -29,7 +29,7 @@ from pefile import (
AppMovedError,
PeFileError,
)
from pefile import VersionMatch, DayZVersion
from pefile import VersionMatch
locale.setlocale(locale.LC_ALL, "")
@ -51,6 +51,7 @@ APPID_DAYZ_EXP = 1024020
cache: dict[str, int] = {}
config_vals: list[str] = []
notes_cache: dict[str, str] = {}
_VERSION: str
IS_GAME_MODE: bool
@ -77,6 +78,7 @@ servers_path = f"{cache_path}/{app_name_abbr}.servers"
config_path = f"{user_path}/.config/dztui"
config_file = f"{config_path}/dztuirc"
history_file = f"{state_path}/{app_name_abbr}.history"
notes_file = f"{state_path}/{app_name_abbr}.notes.json"
logger = logging.getLogger(__name__)
log_file = f"{log_path}/{app_name}_DEBUG.log"
@ -424,6 +426,7 @@ class ContextMenu(EnumWithAttrs):
"label": "Copy IP to clipboard",
"action": "copy_clipboard",
}
ADD_NOTE = {"label": "Add note", "action": "add_note"}
SHOW_MODS = {"label": "Show server-side mods", "action": "show_mods"}
SHOW_DETAILS = {"label": "Server details", "action": "show_details"}
REFRESH_PLAYERS = {
@ -658,32 +661,42 @@ def block_signals(state: bool = True) -> None:
)
def read_json(path: str) -> str:
try:
with open(path, "r") as infile:
try:
data = json.load(infile)
except json.decoder.JSONDecodeError as e:
raise e
except OSError as e:
raise e
return data
def write_json(data: str, path: str) -> None:
try:
j = json.dumps(data, indent=2)
except Exception as e:
raise e
try:
with open(path, "w") as outfile:
outfile.write(j)
except OSError as e:
raise e
def save_res_and_quit(*args) -> None:
if App.window.props.is_maximized:
Gtk.main_quit()
return
rect = App.window.get_size()
def write_json(rect):
data = {"res": {"width": rect.width, "height": rect.height}}
j = json.dumps(data, indent=2)
with open(res_path, "w") as outfile:
outfile.write(j)
logger.info(f"Wrote window size to '{res_path}'")
if os.path.isfile(res_path):
with open(res_path, "r") as infile:
try:
data = json.load(infile)
data["res"]["width"] = rect.width
data["res"]["height"] = rect.height
with open(res_path, "w") as outfile:
outfile.write(json.dumps(data, indent=2))
except json.decoder.JSONDecodeError:
logger.critical(f"JSON decode error in '{res_path}'")
write_json(rect)
else:
write_json(rect)
data = {"res": {"width": rect.width, "height": rect.height}}
try:
write_json(data, res_path)
except Exception as e:
logger.critical(e)
Gtk.main_quit()
@ -1219,6 +1232,12 @@ class OuterWindow(Gtk.Window):
self.grid.right_panel.enable_ping_button(False)
self.grid.sel_panel.set_visible(False)
global notes_cache
try:
notes_cache = read_json(notes_file)
except Exception as e:
logger.warning(e)
# convenience to avoid deep calls
App.window = self
App.grid = self.grid
@ -1249,15 +1268,11 @@ class OuterWindow(Gtk.Window):
self.fullscreen()
try:
with open(res_path, "r") as infile:
try:
data = json.load(infile)
valid_json = True
except json.decoder.JSONDecodeError:
logger.critical(f"JSON decode error in '{res_path}'")
valid_json = False
except OSError:
data = read_json(res_path)
valid_json = True
except Exception as e:
valid_json = False
logger.critical(e)
if valid_json:
res = data["res"]
@ -1265,7 +1280,10 @@ class OuterWindow(Gtk.Window):
logger.info(f"Restoring window size to {w},{h}")
self.set_default_size(w, h)
else:
self.set_default_size(1400, 800)
w = 1400
h = 800
logger.info(f"Using default window size {w},{h}")
self.set_default_size(w, h)
def _on_delete_event(
self, window: "OuterWindow", event: Gdk.EventKey
@ -1812,6 +1830,8 @@ class TreeView(Gtk.TreeView):
self.sel_blocked = False
self.set_fixed_height_mode(True)
self.set_has_tooltip(True)
self.connect("query-tooltip", self._on_tooltip)
self.queue = multiprocessing.Queue()
self.current_proc = None
@ -1842,6 +1862,32 @@ class TreeView(Gtk.TreeView):
self.connect("key-press-event", self._on_keypress)
self.connect("key-release-event", self._on_key_release)
def _on_tooltip(
self,
widget: Gtk.Widget,
x: int,
y: int,
keyboard_mode: bool,
tooltip: Gtk.Tooltip,
) -> bool:
if self.is_server_context(self.view) is False:
return
coords = widget.convert_widget_to_bin_window_coords(x, y)
path = self.get_path_at_pos(coords.bx, coords.by)
if path is None:
return False
model = self.get_model()
tree_iter = model.get_iter(path[0])
ip = model.get_value(tree_iter, 7)
qport = model.get_value(tree_iter, 8)
addr = ip + ":" + str(qport)
if addr not in notes_cache:
return False
tooltip.set_text(notes_cache[addr])
self.set_tooltip_row(tooltip, path[0])
return True
def _update_mod_store(self) -> None:
(model, pathlist) = self.get_selection().get_selected_rows()
for p in reversed(pathlist):
@ -1871,6 +1917,43 @@ class TreeView(Gtk.TreeView):
if self.current_proc and self.current_proc.is_alive():
self.current_proc.terminate()
def _delete_note(
self, button: Gtk.Button, user_entry: Gtk.Box, addr: str
) -> None:
box = button.get_parent()
dialog = box.get_parent()
try:
write_json(notes_cache, notes_file)
del notes_cache[addr]
except Exception as e:
logger.critical(e)
dialog.destroy()
def add_note(self) -> None:
user_entry = EntryDialog(
"Add a short note/reminder. Limit: 30 chars", Popup.ENTRY, ""
)
entry = user_entry.get_entry()
entry.set_max_length(30)
addr = self.get_record_string()
if addr in notes_cache:
entry.set_text(notes_cache[addr])
button = Gtk.Button(label="Delete note")
button.connect("clicked", self._delete_note, user_entry, addr)
button.set_margin_start(50)
button.set_margin_end(50)
user_entry.dialogBox.pack_end(button, False, False, 0)
response = user_entry.get_input()
if response is None:
return
notes_cache[addr] = response
try:
write_json(notes_cache, notes_file)
except Exception as e:
logger.critical(e)
def copy_name(self) -> None:
self.clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
record = self.get_name()
@ -1942,7 +2025,7 @@ class TreeView(Gtk.TreeView):
model = self.get_model()
it = self.get_current_iter()
name = model.get_value(it, 0)
record = self.get_record_dict()
record = self.get_record()
DetailsDialog(name, record.ip, record.qport)
def show_mods(self) -> None:
@ -2028,6 +2111,7 @@ class TreeView(Gtk.TreeView):
ContextMenu.ADD_SERVER,
ContextMenu.COPY_NAME,
ContextMenu.COPY_CLIPBOARD,
ContextMenu.ADD_NOTE,
ContextMenu.SHOW_MODS,
ContextMenu.SHOW_DETAILS,
ContextMenu.REFRESH_PLAYERS,
@ -2035,6 +2119,7 @@ class TreeView(Gtk.TreeView):
RowType.SCAN_LAN: [
ContextMenu.COPY_NAME,
ContextMenu.COPY_CLIPBOARD,
ContextMenu.ADD_NOTE,
ContextMenu.SHOW_MODS,
ContextMenu.SHOW_DETAILS,
ContextMenu.REFRESH_PLAYERS,
@ -2043,6 +2128,7 @@ class TreeView(Gtk.TreeView):
ContextMenu.REMOVE_SERVER,
ContextMenu.COPY_NAME,
ContextMenu.COPY_CLIPBOARD,
ContextMenu.ADD_NOTE,
ContextMenu.SHOW_MODS,
ContextMenu.SHOW_DETAILS,
ContextMenu.REFRESH_PLAYERS,
@ -2051,6 +2137,7 @@ class TreeView(Gtk.TreeView):
ContextMenu.ADD_SERVER,
ContextMenu.REMOVE_HISTORY,
ContextMenu.COPY_NAME,
ContextMenu.ADD_NOTE,
ContextMenu.COPY_CLIPBOARD,
ContextMenu.SHOW_MODS,
ContextMenu.SHOW_DETAILS,
@ -2077,6 +2164,10 @@ class TreeView(Gtk.TreeView):
if row == ContextMenu.SHOW_MODS:
if not self.has_mods():
item.set_sensitive(False)
if row == ContextMenu.ADD_NOTE:
if self.get_record_string() in notes_cache:
item.set_label("Edit note")
self.menu.show_all()
if event.type is Gdk.EventType.KEY_PRESS and event.keyval is Gdk.KEY_l:
@ -2126,7 +2217,7 @@ class TreeView(Gtk.TreeView):
self.view == WindowContext.TABLE_API
or self.view == WindowContext.TABLE_SERVER
):
record = self.get_record_dict()
record = self.get_record()
if not record:
grid.statusbar.update_server_meta()
return
@ -2276,7 +2367,7 @@ class TreeView(Gtk.TreeView):
qport = self.get_value_at_index(8)
return f"{addr}:{qport}"
def get_record_dict(self) -> dict | None:
def get_record(self) -> dict | None:
select = self.get_selection()
sels = select.get_selected_rows()
(model, pathlist) = sels
@ -2334,7 +2425,7 @@ class TreeView(Gtk.TreeView):
wait_dialog = GenericDialog("Refreshing player count", Popup.WAIT)
wait_dialog.show_all()
record = self.get_record_dict()
record = self.get_record()
if not record:
return
data = call_out("get_player_count", record.ip, str(record.qport))
@ -2593,39 +2684,27 @@ class TreeView(Gtk.TreeView):
def _on_col_width_changed(
self, col: Gtk.TreeViewColumn, width: GObject.ParamSpecInt
) -> None:
def write_json(title, size):
data = {"cols": {title: size}}
j = json.dumps(data, indent=2)
with open(geometry_path, "w") as outfile:
outfile.write(j)
logger.info(f"Wrote initial column widths to '{geometry_path}'")
title = col.get_title()
size = col.get_width()
if os.path.isfile(geometry_path):
with open(geometry_path, "r") as infile:
try:
data = json.load(infile)
data["cols"][title] = size
with open(geometry_path, "w") as outfile:
outfile.write(json.dumps(data, indent=2))
except json.decoder.JSONDecodeError:
logger.critical(f"JSON decode error in '{geometry_path}'")
write_json(title, size)
else:
write_json(title, size)
try:
data = read_json(geometry_path)
data["cols"][title] = size
except Exception as e:
logger.critical(e)
data = {"cols": {title: size}}
try:
write_json(data, geometry_path)
except Exception as e:
logger.critical(e)
def initialize_columns(self) -> None:
if os.path.isfile(geometry_path):
with open(geometry_path, "r") as infile:
try:
data = json.load(infile)
valid_json = True
except json.decoder.JSONDecodeError:
logger.critical(f"JSON decode error in '{geometry_path}'")
valid_json = False
else:
try:
data = read_json(geometry_path)
valid_json = True
except Exception as e:
logger.critical(e)
valid_json = False
browser_cols = [
@ -2878,7 +2957,7 @@ class TreeView(Gtk.TreeView):
logger.critical(
"Config file has no value set for 'default_steam_path'"
)
msg = f"Local Steam installation is not set, possibly malformed config file."
msg = "Local Steam installation is not set, possibly malformed config file."
spawn_dialog(msg, Popup.NOTIFY)
return None
@ -3034,7 +3113,7 @@ class TreeView(Gtk.TreeView):
case WindowContext.TABLE_MODS | WindowContext.TABLE_LOG:
self.update_quad_column(cr)
case WindowContext.TABLE_SERVER | WindowContext.TABLE_API:
record = self.get_record_dict()
record = self.get_record()
if record is None:
return
thread_new_with_dialog(
@ -3461,7 +3540,7 @@ class ModDialog(GenericDialog):
self.run()
self.destroy()
record = App.treeview.get_record_dict()
record = App.treeview.get_record()
if not record:
return
data = call_out("show_server_modlist", record.ip, str(record.qport))
@ -3541,7 +3620,9 @@ class EntryDialog(GenericDialog):
super().__init__(text, mode)
"""
Returns user input as a string or None
Wraps Gtk.Entry in a dialog and provides basic response handling.
Returns user input as a string or None.
The Entry widget itself can be manipulated via the get_entry() method.
"""
self.dialog = GenericDialog(text, mode)
@ -3549,12 +3630,12 @@ class EntryDialog(GenericDialog):
self.dialog.set_default_response(Gtk.ResponseType.OK)
self.dialog.set_size_request(500, 0)
self.userEntry = Gtk.Entry()
set_surrounding_margins(self.userEntry, 20)
self.userEntry.set_margin_top(0)
self.userEntry.set_size_request(250, 0)
self.userEntry.set_activates_default(True)
self.dialogBox.pack_start(self.userEntry, False, False, 0)
self.user_entry = Gtk.Entry()
set_surrounding_margins(self.user_entry, 20)
self.user_entry.set_margin_top(0)
self.user_entry.set_size_request(250, 0)
self.user_entry.set_activates_default(True)
self.dialogBox.pack_start(self.user_entry, False, False, 0)
if link:
button = Gtk.Button(label=link)
@ -3565,8 +3646,8 @@ class EntryDialog(GenericDialog):
self.ok = self.dialog.action_area.get_children()[1]
self.ok.set_sensitive(False)
self.userEntry.connect("insert-text", self._on_text_typed)
self.userEntry.get_property("buffer").connect(
self.user_entry.connect("insert-text", self._on_text_typed)
self.user_entry.get_property("buffer").connect(
"deleted-text", self._on_text_deleted
)
@ -3594,11 +3675,14 @@ class EntryDialog(GenericDialog):
label = button.get_label()
call_bash_func("Open link", label)
def get_entry(self) -> Gtk.Entry:
return self.user_entry
def get_input(self) -> str | None:
self.dialog.show_all()
response = self.dialog.run()
text = self.userEntry.get_text()
text = self.user_entry.get_text()
self.dialog.destroy()
if (response == Gtk.ResponseType.OK) and (text != ""):
return text