mirror of
https://github.com/aclist/dztui.git
synced 2026-08-27 02:07:18 +02:00
feat: ping column
This commit is contained in:
parent
99e13be3e8
commit
b4ae6a9e8b
17
CHANGELOG.md
17
CHANGELOG.md
@ -1,19 +1,20 @@
|
||||
# Changelog
|
||||
|
||||
## [6.0.0-beta.1] 2025-07-25
|
||||
## [6.0.0-beta.1] 2025-07-28
|
||||
## Added
|
||||
- Additional validation on entry dialogs to prevent submitting empty text
|
||||
- Added "Return to main menu" button to dialog windows when failing to load server table
|
||||
- Speed up load time of server tables
|
||||
- More robust threading and cache system when filtering servers
|
||||
- Ping column to server browsing contexts
|
||||
- Filter by modded servers
|
||||
- Refresh server ping context menu and hotkey
|
||||
- Disable "show mods" context menu if server has no mods
|
||||
- Breadcrumbs showing current menu context to top of window
|
||||
- 0, minus, and backslash keybindings for filter toggles
|
||||
- Speed up load time and navigation of server tables
|
||||
- Dedicated changelog page
|
||||
- Dedicated keybindings page
|
||||
- Vim-style navigation keybindings
|
||||
- Additional validation on entry dialogs to prevent submitting empty text
|
||||
- Added "Return to main menu" button to dialog windows when failing to load server table
|
||||
- More robust threading and cache system when filtering servers
|
||||
- Refresh server ping: context menu and hotkey
|
||||
- Disable "show mods" context menu if server has no mods
|
||||
- 0, minus, and backslash keybindings for filter toggles
|
||||
|
||||
## Fixed
|
||||
- Entry dialog sensitivity when validating API keys
|
||||
|
||||
4
dzgui.sh
4
dzgui.sh
@ -585,11 +585,11 @@ fetch_helpers_by_sum(){
|
||||
[[ -f "$config_file" ]] && source "$config_file"
|
||||
declare -A sums
|
||||
sums=(
|
||||
["ui.py"]="c05cb7eaf40347b49cdd71030ca9cc39"
|
||||
["ui.py"]="168865d5a0e7936e0f72da59dde6b6f4"
|
||||
["query_v2.py"]="55d339ba02512ac69de288eb3be41067"
|
||||
["vdf2json.py"]="2f49f6f5d3af919bebaab2e9c220f397"
|
||||
["funcs"]="5d244114c501754a630988354306e470"
|
||||
["servers.py"]="e5eae96d11cb1e551aaae74a469c4f5f"
|
||||
["servers.py"]="dc23a9f15e5014be171054792cf3fc16"
|
||||
)
|
||||
local author="aclist"
|
||||
local repo="dztui"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
@ -123,6 +124,11 @@ def parse_json(json: list) -> list:
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
try:
|
||||
ping = row["ping"]
|
||||
except KeyError:
|
||||
ping = 9999
|
||||
|
||||
mapname = row["map"].lower()
|
||||
players = row["players"]
|
||||
max_players = row["max_players"]
|
||||
@ -136,6 +142,7 @@ def parse_json(json: list) -> list:
|
||||
int(queue),
|
||||
ip,
|
||||
int(qport),
|
||||
ping,
|
||||
provider,
|
||||
modded,
|
||||
]
|
||||
@ -143,9 +150,9 @@ def parse_json(json: list) -> list:
|
||||
return rows
|
||||
|
||||
|
||||
def query_direct(ip: str, qport: int) -> dict | None:
|
||||
def query_direct(ip: str, qport: int, TIMEOUT=3.0) -> dict | None:
|
||||
try:
|
||||
info = a2s.info((ip, qport))
|
||||
info = a2s.info((ip, qport), TIMEOUT)
|
||||
|
||||
name = info.server_name
|
||||
mapname = info.map_name
|
||||
@ -155,6 +162,12 @@ def query_direct(ip: str, qport: int) -> dict | None:
|
||||
max_players = info.max_players
|
||||
keywords = info.keywords
|
||||
|
||||
try:
|
||||
ping = info.ping
|
||||
ping = math.floor(info.ping * 1000)
|
||||
except AttributeError:
|
||||
ping = 9999
|
||||
|
||||
res = {}
|
||||
res["name"] = name
|
||||
res["map"] = mapname
|
||||
@ -163,6 +176,7 @@ def query_direct(ip: str, qport: int) -> dict | None:
|
||||
res["max_players"] = max_players
|
||||
res["addr"] = address
|
||||
res["gameport"] = gameport
|
||||
res["ping"] = ping
|
||||
return res
|
||||
except TimeoutError:
|
||||
return None
|
||||
@ -177,6 +191,40 @@ class Res:
|
||||
json: Union[str, None]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ping:
|
||||
addr: str
|
||||
iteration: int
|
||||
ping: int
|
||||
|
||||
|
||||
def ping(iteration: int, row: list) -> Ping:
|
||||
addr = row[7]
|
||||
qport = row[8]
|
||||
|
||||
res = None
|
||||
|
||||
if row[9] != 9999:
|
||||
return Ping(addr, iteration, row[9])
|
||||
|
||||
try:
|
||||
ip = addr.split(":")[0]
|
||||
except IndexError:
|
||||
ping = 9999
|
||||
|
||||
try:
|
||||
res = query_direct(ip, qport, 0.5)
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
if res is None:
|
||||
ping = 9999
|
||||
else:
|
||||
ping = res["ping"]
|
||||
|
||||
return Ping(addr, iteration, ping)
|
||||
|
||||
|
||||
def query_api(key: str, param: str) -> Res:
|
||||
LIMIT = 10000
|
||||
url = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
||||
|
||||
108
helpers/ui.py
108
helpers/ui.py
@ -16,11 +16,12 @@ import warnings
|
||||
from enum import Enum
|
||||
from concurrent.futures import wait
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Literal, Self, Any
|
||||
|
||||
sys.path.append("servers")
|
||||
import servers as Servers
|
||||
import servers as Servers # noqa E402
|
||||
|
||||
locale.setlocale(locale.LC_ALL, "")
|
||||
|
||||
@ -1013,6 +1014,7 @@ class OuterWindow(Gtk.Window):
|
||||
self.show_all()
|
||||
|
||||
self.grid.right_panel.filters_vbox.set_visible(False)
|
||||
self.grid.right_panel.enable_ping_button(False)
|
||||
self.grid.sel_panel.set_visible(False)
|
||||
|
||||
# convenience to avoid deep calls
|
||||
@ -1085,16 +1087,30 @@ class RightPanel(Gtk.Box):
|
||||
self.pack_start(self.button_vbox, False, False, 0)
|
||||
self.pack_start(self.filters_vbox, False, False, 0)
|
||||
|
||||
tooltip = (
|
||||
debug_tooltip = (
|
||||
"Used to perform a dry run without\n"
|
||||
"actually connecting to a server"
|
||||
)
|
||||
ping_tooltip = (
|
||||
"Refresh the ping for visible servers.\n"
|
||||
"Available once per unique filter context"
|
||||
)
|
||||
|
||||
self.ping = Gtk.Button(
|
||||
label="Ping servers",
|
||||
margin_top=10,
|
||||
margin_start=80,
|
||||
margin_end=80,
|
||||
tooltip_text=ping_tooltip,
|
||||
)
|
||||
self.ping.connect("clicked", self._on_ping_clicked)
|
||||
|
||||
self.debug_toggle = Gtk.ToggleButton(
|
||||
label="Debug mode",
|
||||
margin_top=10,
|
||||
margin_start=80,
|
||||
margin_end=80,
|
||||
tooltip_text=tooltip,
|
||||
tooltip_text=debug_tooltip,
|
||||
)
|
||||
|
||||
if query_config("debug")[0] == "1":
|
||||
@ -1110,9 +1126,13 @@ class RightPanel(Gtk.Box):
|
||||
)
|
||||
self.question.connect("clicked", self._on_question_clicked)
|
||||
|
||||
self.pack_start(self.ping, False, True, 0)
|
||||
self.pack_start(self.debug_toggle, False, True, 0)
|
||||
self.pack_start(self.question, False, True, 0)
|
||||
|
||||
def enable_ping_button(self, state: bool) -> None:
|
||||
self.ping.set_visible(state)
|
||||
|
||||
def reinit_maps(self, rows: list) -> None:
|
||||
map_store.clear()
|
||||
map_store.append(["All maps"])
|
||||
@ -1129,6 +1149,37 @@ class RightPanel(Gtk.Box):
|
||||
grid.statusbar.refresh()
|
||||
App.grid.notebook.focus_current()
|
||||
|
||||
def _on_ping_clicked(self, button: Gtk.Button) -> None:
|
||||
block_signals()
|
||||
|
||||
def _update_pings():
|
||||
rows = ModelManager.get_filtered()
|
||||
with ThreadPoolExecutor(100) as executor:
|
||||
futures = [
|
||||
executor.submit(Servers.ping, i, row)
|
||||
for i, row in enumerate(rows)
|
||||
]
|
||||
wait(futures)
|
||||
for future in futures:
|
||||
res = future.result()
|
||||
path = Gtk.TreePath.new_from_indices([res.iteration])
|
||||
temp_model[path][9] = res.ping
|
||||
ModelManager.ping_cache[res.addr] = res.ping
|
||||
App.treeview.set_model(temp_model)
|
||||
App.treeview.wait_dialog.destroy()
|
||||
App.treeview.enable_ping_column(True)
|
||||
App.treeview.grab_focus()
|
||||
App.right_panel.ping.set_sensitive(False)
|
||||
|
||||
unblock_signals()
|
||||
|
||||
temp_model = App.treeview.get_model()
|
||||
App.treeview.set_model(None)
|
||||
App.treeview.wait_dialog = GenericDialog("Pinging servers", Popup.WAIT)
|
||||
App.treeview.wait_dialog.show_all()
|
||||
thread = threading.Thread(target=_update_pings, args=())
|
||||
thread.start()
|
||||
|
||||
def _on_question_clicked(self, button: Gtk.Button) -> None:
|
||||
App.grid.notebook.toggle_keybindings()
|
||||
|
||||
@ -1250,6 +1301,7 @@ class ModelManagerSingleton:
|
||||
def __init__(self):
|
||||
# packed ListStores
|
||||
self.filter_cache = {}
|
||||
self.ping_cache = {}
|
||||
# stringwise (list) representation of the model
|
||||
self.control_model = None
|
||||
self.filtered = None
|
||||
@ -1285,9 +1337,11 @@ class ModelManagerSingleton:
|
||||
if prior_map == "All maps":
|
||||
rows = self.filter_map(filters)
|
||||
else:
|
||||
App.right_panel.ping.set_sensitive(True)
|
||||
rows = self.filter_toggle_on(filters, *args)
|
||||
|
||||
case FilterMode.KEYWORD:
|
||||
App.right_panel.ping.set_sensitive(True)
|
||||
rows = self.filter_toggle_on(filters, *args)
|
||||
|
||||
case FilterMode.TOGGLE_OFF:
|
||||
@ -1296,8 +1350,14 @@ class ModelManagerSingleton:
|
||||
rows = self.filtered
|
||||
|
||||
case FilterMode.TOGGLE_ON:
|
||||
App.right_panel.ping.set_sensitive(True)
|
||||
rows = self.filter_toggle_on(filters, *args)
|
||||
|
||||
if mode is not FilterMode.INITIAL:
|
||||
for row in rows:
|
||||
if row[7] in self.ping_cache:
|
||||
row[9] = self.ping_cache[row[7]]
|
||||
|
||||
if len(rows) > 0:
|
||||
clone = ModelManager.new_model()
|
||||
rows = self.sort_rows(rows)
|
||||
@ -1370,9 +1430,9 @@ class ModelManagerSingleton:
|
||||
case "1PP":
|
||||
rows = [row for row in rows if row[2] != "1PP"]
|
||||
case "Official":
|
||||
rows = [row for row in rows if row[9] != "Official"]
|
||||
rows = [row for row in rows if row[10] != "Official"]
|
||||
case "Unoffic.":
|
||||
rows = [row for row in rows if row[9] != "Unoffic."]
|
||||
rows = [row for row in rows if row[10] != "Unoffic."]
|
||||
case "Empty":
|
||||
rows = [row for row in rows if row[4] != 0]
|
||||
case "Full":
|
||||
@ -1397,7 +1457,7 @@ class ModelManagerSingleton:
|
||||
case "Low pop":
|
||||
rows = [row for row in rows if (row[4] / row[5] * 100) > 30]
|
||||
case "Modded":
|
||||
rows = [row for row in rows if not row[10]]
|
||||
rows = [row for row in rows if not row[11]]
|
||||
return rows
|
||||
|
||||
def filter_toggle_on(self, filters: tuple, *args: str) -> list:
|
||||
@ -1417,7 +1477,7 @@ class ModelManagerSingleton:
|
||||
|
||||
def new_model(self) -> Gtk.ListStore:
|
||||
return Gtk.ListStore(
|
||||
str, str, str, str, int, int, int, str, int, str, bool
|
||||
str, str, str, str, int, int, int, str, int, int, str, bool
|
||||
)
|
||||
|
||||
def resync_model(self, addr: str, qport: int) -> None:
|
||||
@ -1433,6 +1493,8 @@ class ModelManagerSingleton:
|
||||
filters = App.right_panel.filters_vbox.get_filters()
|
||||
refiltered = self.filter_toggle_on(filters)
|
||||
self.set_filtered(refiltered)
|
||||
self.set_success(True)
|
||||
GLib.idle_add(App.treeview._filter_cleanup)
|
||||
|
||||
def convert_model_to_list(self, model: Gtk.ListStore) -> list:
|
||||
return [[el for el in row] for row in model]
|
||||
@ -1442,6 +1504,9 @@ class ModelManagerSingleton:
|
||||
rows = []
|
||||
self.filtered = rows
|
||||
|
||||
def get_filtered(self) -> list:
|
||||
return self.filtered
|
||||
|
||||
def set_store(self, model: Gtk.ListStore | None) -> None:
|
||||
self.store = model
|
||||
|
||||
@ -1461,6 +1526,7 @@ class ModelManagerSingleton:
|
||||
self.success = True
|
||||
self.filtered = None
|
||||
self.filter_cache = {}
|
||||
self.ping_cache = {}
|
||||
if full:
|
||||
self.control_model = None
|
||||
|
||||
@ -1610,9 +1676,12 @@ class TreeView(Gtk.TreeView):
|
||||
addr = model.get_value(it, 7)
|
||||
qport = model.get_value(it, 8)
|
||||
model.remove(it)
|
||||
print(addr)
|
||||
|
||||
ModelManager.resync_model(addr, qport)
|
||||
block_signals()
|
||||
thread = threading.Thread(
|
||||
target=ModelManager.resync_model, args=(addr, qport)
|
||||
)
|
||||
thread.start()
|
||||
|
||||
def remove_from_history(self) -> None:
|
||||
record = self.get_record_string()
|
||||
@ -1660,7 +1729,7 @@ class TreeView(Gtk.TreeView):
|
||||
(model, pathlist) = sels
|
||||
path = pathlist[0]
|
||||
tree_iter = model.get_iter(path)
|
||||
mods = model.get_value(tree_iter, 10)
|
||||
mods = model.get_value(tree_iter, 11)
|
||||
return mods
|
||||
|
||||
def is_in_favs(self) -> bool:
|
||||
@ -1973,7 +2042,7 @@ class TreeView(Gtk.TreeView):
|
||||
path = pathlist[0]
|
||||
model = self.get_model()
|
||||
if not model:
|
||||
return
|
||||
return None
|
||||
addr = model[path][7]
|
||||
qport = model[path][8]
|
||||
ip = addr.split(":")[0]
|
||||
@ -1994,6 +2063,12 @@ class TreeView(Gtk.TreeView):
|
||||
path = self.get_mpath()
|
||||
model[path][6] = players
|
||||
|
||||
def enable_ping_column(self, state: bool) -> None:
|
||||
columns = self.get_columns()
|
||||
for column in columns:
|
||||
if column.get_title() == "Ping":
|
||||
column.set_visible(state)
|
||||
|
||||
def select_first_row(self):
|
||||
sel = self.get_selection()
|
||||
self._on_tree_selection_changed(sel)
|
||||
@ -2102,8 +2177,11 @@ class TreeView(Gtk.TreeView):
|
||||
case RowType.SCAN_LAN:
|
||||
parsed = self._dump_lan(port)
|
||||
case RowType.SERVER_BROWSER:
|
||||
App.treeview.enable_ping_column(False)
|
||||
App.right_panel.enable_ping_button(True)
|
||||
parsed = self._dump_api()
|
||||
case RowType.SAVED_SERVERS:
|
||||
App.treeview.enable_ping_column(True)
|
||||
favs = query_favorites()
|
||||
if not favs:
|
||||
ModelManager.set_success(False)
|
||||
@ -2112,6 +2190,7 @@ class TreeView(Gtk.TreeView):
|
||||
return
|
||||
parsed = self._dump_servers(favs)
|
||||
case RowType.RECENT_SERVERS:
|
||||
App.treeview.enable_ping_column(True)
|
||||
history = query_history()
|
||||
if not history:
|
||||
ModelManager.set_success(False)
|
||||
@ -2309,6 +2388,7 @@ class TreeView(Gtk.TreeView):
|
||||
"Queue",
|
||||
"IP",
|
||||
"Qport",
|
||||
"Ping",
|
||||
]
|
||||
for i, column_title in enumerate(browser_cols):
|
||||
renderer = Gtk.CellRendererText()
|
||||
@ -2318,7 +2398,10 @@ class TreeView(Gtk.TreeView):
|
||||
column.set_sort_column_id(i)
|
||||
|
||||
if valid_json:
|
||||
saved_size = data["cols"][column_title]
|
||||
try:
|
||||
saved_size = data["cols"][column_title]
|
||||
except KeyError:
|
||||
saved_size = 100
|
||||
column.set_fixed_width(saved_size)
|
||||
column.set_expand(True)
|
||||
else:
|
||||
@ -2428,6 +2511,7 @@ class TreeView(Gtk.TreeView):
|
||||
model.clear()
|
||||
ModelManager.wipe_cache(full=True)
|
||||
|
||||
App.right_panel.enable_ping_button(False)
|
||||
App.right_panel.filters_vbox.reinit_panel()
|
||||
self.set_selection_mode(Gtk.SelectionMode.SINGLE)
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user