This commit is contained in:
aclist authored and GitHub committed 2026-09-03 19:00:44 +00:00
commit 8a94cd090f
19 files changed
+650 -80

No files matched your search

+20 -25
View File
@@ -54,19 +54,16 @@ class Res:
@dataclass(slots=True, frozen=True) @dataclass(slots=True, frozen=True)
class Details: class Details:
data: Union[list, None] data: list[list[str]]
name: str
description: str description: str
success: bool gametime: str
day_accel: float
night_accel: float
@dataclass(slots=True) @dataclass(slots=True, frozen=True)
class Record: class Record:
# TODO: investigate whether this enum is still being used;
# can this be frozen?
"""
The gameport field is manipulated by the RowType.CONN_BY_IP method
"""
ip: str ip: str
gameport: int gameport: int
qport: int qport: int
@@ -283,43 +280,41 @@ def query_direct(ip: str, qport: int, timeout: float = 3.0) -> dict[str, Any] |
info = a2s.info((ip, qport), timeout) info = a2s.info((ip, qport), timeout)
return source_info_to_dict(ip, qport, info) return source_info_to_dict(ip, qport, info)
except Exception as e: except Exception as e:
logger.warning(e) logger.debug(e)
return None return None
def get_details(record: Record) -> Details: def get_details(record: Record) -> Details | None:
# TODO: consolidate try/except statements
ip = record.ip ip = record.ip
qport = record.qport qport = record.qport
default_str = strings.none_provided default_str = strings.none_provided
try: try:
info = a2s.info((ip, qport)) info = a2s.info((ip, qport))
except (TimeoutError, a2s.exceptions.BrokenMessageError):
return Details(None, default_str, False)
try:
rules = dayzquery.dayz_rules((ip, qport)) rules = dayzquery.dayz_rules((ip, qport))
except Exception:
return Details(None, default_str, False)
try:
keywords = info.keywords.split(",") keywords = info.keywords.split(",")
except AttributeError: except Exception as e:
return Details(None, default_str, False) logger.critical(e)
return None
battleye = strings.disabled battleye = strings.disabled
if "battleye" in keywords: if "battleye" in keywords:
battleye = strings.enabled battleye = strings.enabled
day_accel = 0.0 name = info.server_name
night_accel = 0.0 day_accel = 1.0
night_accel = 1.0
gametime = "00:00"
for keyword in keywords: for keyword in keywords:
if "etm" in keyword: if "etm" in keyword:
day_accel = float(keyword.lstrip("etm")) day_accel = float(keyword.lstrip("etm"))
day_accel = float(f"{day_accel:g}") day_accel = float(f"{day_accel:g}")
if "entm" in keywords: if "entm" in keyword:
night_accel = float(keyword.lstrip("entm")) night_accel = float(keyword.lstrip("entm"))
night_accel = float(f"{night_accel:g}") night_accel = float(f"{night_accel:g}")
time_reg = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]$)"
if re.match(time_reg, keyword):
gametime = keyword
try: try:
password = info.password_protected password = info.password_protected
@@ -385,7 +380,7 @@ def get_details(record: Record) -> Details:
["Version", version], ["Version", version],
] ]
return Details(rows, description, True) return Details(rows, name, description, gametime, day_accel, night_accel)
def ping(ip: str, qport: int) -> int: def ping(ip: str, qport: int) -> int:
+1
View File
@@ -37,6 +37,7 @@ HEX_RED = "#FF0000"
HEX_ORANGE = "#FFAC1C" HEX_ORANGE = "#FFAC1C"
# FIXME: normalize icon names # FIXME: normalize icon names
CALCULATOR = "accessories-calculator-symbolic"
CARET_DOWN = "go-down-symbolic" CARET_DOWN = "go-down-symbolic"
CARET_UP = "go-up-symbolic" CARET_UP = "go-up-symbolic"
CLIPBOARD = "edit-copy-symbolic" CLIPBOARD = "edit-copy-symbolic"
+7 -4
View File
@@ -7,11 +7,12 @@ import dzgui.util._json as JSON # noqa
from dzgui.const.enum import ( from dzgui.const.enum import (
FilterMode,
Preferences,
NotebookPage,
ButtonType, ButtonType,
ContextMenu, ContextMenu,
FilterMode,
NotebookPage,
Preferences,
ServerTab,
) )
from dzgui.config.userprefs import UserPrefs from dzgui.config.userprefs import UserPrefs
@@ -37,7 +38,6 @@ logger = logging.getLogger(APP_NAME)
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import Record from dzgui.api.servers import Record
from dzgui.const.enum import ServerTab
from dzgui.managers.connection import Prerequisites from dzgui.managers.connection import Prerequisites
from dzgui.managers.filter import FilterManager from dzgui.managers.filter import FilterManager
from dzgui.util.dist import Haversine from dzgui.util.dist import Haversine
@@ -498,6 +498,9 @@ class Controller(GObject.GObject):
def set_active_keyword(self, keyword: str) -> None: def set_active_keyword(self, keyword: str) -> None:
self.get_filter_man().set_active_keyword(keyword) self.get_filter_man().set_active_keyword(keyword)
ServerModelManager(self, self.get_active_treeview()).refilter(
FilterMode.TOGGLE_ON
)
def get_modtreeview(self) -> "ModTreeView": def get_modtreeview(self) -> "ModTreeView":
return self.mediator.modtreeview return self.mediator.modtreeview
+4
View File
@@ -24,6 +24,10 @@
.left-label { .left-label {
font-size: 1.3rem; font-size: 1.3rem;
} }
.server-subheading {
font-size: 1.1rem;
font-weight: 700;
}
.details-heading { .details-heading {
font-size: 1.2rem font-size: 1.2rem
} }
+1 -1
View File
@@ -235,7 +235,7 @@ class ConnectionManager:
@call_on_thread(dialog.querying) @call_on_thread(dialog.querying)
def query_details(self, record: Servers.Record) -> None: def query_details(self, record: Servers.Record) -> None:
details = Servers.get_details(record) details = Servers.get_details(record)
if details.success is False: if details is None:
self.thread_man.set_cleanup_func( self.thread_man.set_cleanup_func(
StoredFunc(self._server_timeout), destroy_first=True StoredFunc(self._server_timeout), destroy_first=True
) )
+2 -2
View File
@@ -32,10 +32,10 @@ class FilterManager:
# TODO: strings # TODO: strings
self.tooltips = { self.tooltips = {
strings.filter_1pp: "First-person perspective", strings.filter_1pp: "First-person perspective",
strings.filter_day: "In-game time between 0700 and 1659", strings.filter_day: "In-game time between 0600 and 1759",
strings.filter_empty: "Servers with no players", strings.filter_empty: "Servers with no players",
strings.filter_3pp: "Third-person perspective", strings.filter_3pp: "Third-person perspective",
strings.filter_night: "In-game time between 1700 and 0659", strings.filter_night: "In-game time between 1800 and 0559",
strings.filter_full: "Servers with no open slots", strings.filter_full: "Servers with no open slots",
strings.filter_official: "Bohemia official servers", strings.filter_official: "Bohemia official servers",
strings.filter_nonascii: "Server names using non-standard, complex glyphs", strings.filter_nonascii: "Server names using non-standard, complex glyphs",
+2 -2
View File
@@ -12,8 +12,8 @@ if TYPE_CHECKING:
from dzgui.api.servers import Record from dzgui.api.servers import Record
from dzgui.model.servers import NewPlayerCount from dzgui.model.servers import NewPlayerCount
DAY_REG = r"([0][7-9]|[1][0-6])" DAY_REG = r"([0][6-9]|[1][0-7])"
NIGHT_REG = r"([0][0-6]|[1][7-9]|[2][0-3])" NIGHT_REG = r"([0][0-5]|[1][8-9]|[2][0-3])"
class ProxyModelManager: class ProxyModelManager:
+3 -1
View File
@@ -21,7 +21,7 @@ from dzgui.views.dialogs.generic import ExceptionDialog
import gi import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402 from gi.repository import Gtk, GLib # noqa E402
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import A2SInfo, Record from dzgui.api.servers import A2SInfo, Record
@@ -390,6 +390,8 @@ class ServerModelManager:
def _cleanup_on_success(self) -> None: def _cleanup_on_success(self) -> None:
proxy = self.proxy_man.get_proxy_model() proxy = self.proxy_man.get_proxy_model()
self.tv.set_model(proxy) self.tv.set_model(proxy)
if self.tv.enum == ServerTab.BROWSER:
GLib.idle_add(self.tv.wipe_cache)
if self.preserve_on_fail is True: if self.preserve_on_fail is True:
self.proxy_man.wipe_cache() self.proxy_man.wipe_cache()
+7
View File
@@ -0,0 +1,7 @@
heading_local_time = "Local time"
heading_server_time = "In-game time"
heading_target_time = "Target time"
heading_remainder_time = "Remainder until target"
heading_adjusted_time = "Adjusted local time"
calculate = "Calculate"
disclaimer = "Times shown are approximations.\nServer time may differ based on time of year in game."
+5
View File
@@ -23,3 +23,8 @@ custom_mod_dialog = "Set location to a custom mods folder"
debug_heading = "Debug" debug_heading = "Debug"
debug_secondary = "Launch arguments" debug_secondary = "Launch arguments"
toggle_calc = "Toggle calculator"
server_details = "Server details"
server_message = "Server message"
-2
View File
@@ -11,7 +11,6 @@ delimiter = "␞"
# GenericDialog # GenericDialog
main_menu = "Return to main menu" main_menu = "Return to main menu"
exit_app = "Exit" exit_app = "Exit"
server_details = "Server details"
modlist = "Modlist" modlist = "Modlist"
input_required = "User input required" input_required = "User input required"
confirm = "Confirmation" confirm = "Confirmation"
@@ -59,7 +58,6 @@ browser_cols = [
] ]
# DetailsDialog # DetailsDialog
server_message = "Server message"
workshop = "Enter/double click a row to open in Steam Workshop." workshop = "Enter/double click a row to open in Steam Workshop."
# LanDialog # LanDialog
+1
View File
@@ -24,6 +24,7 @@ class VBox(GenericBox):
def __init__(self, spacing: int = 0) -> None: def __init__(self, spacing: int = 0) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing) super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
class ShortHBox(HBox): class ShortHBox(HBox):
def __init__(self, widget: Gtk.Widget) -> None: def __init__(self, widget: Gtk.Widget) -> None:
super().__init__(spacing=5) super().__init__(spacing=5)
-6
View File
@@ -9,8 +9,6 @@ from dzgui.const.constants import (
NO_PADDING, NO_PADDING,
SEARCH_ICON, SEARCH_ICON,
) )
from dzgui.const.enum import FilterMode
from dzgui.model.servers import ServerModelManager
from dzgui.util.format import format_exception from dzgui.util.format import format_exception
from dzgui.util.strings import all_maps from dzgui.util.strings import all_maps
from dzgui.views.components.maps_combo import MapsCombo from dzgui.views.components.maps_combo import MapsCombo
@@ -148,10 +146,6 @@ class KeywordEntry(Gtk.Entry):
self.controller.set_active_keyword(keyword) self.controller.set_active_keyword(keyword)
logger.info(f"User filtered by keyword '{keyword}'") logger.info(f"User filtered by keyword '{keyword}'")
ServerModelManager(
self.controller, self.controller.get_active_treeview()
).refilter(FilterMode.TOGGLE_ON)
class FilterPanel(Gtk.Box): class FilterPanel(Gtk.Box):
def __init__(self, controller: "Controller") -> None: def __init__(self, controller: "Controller") -> None:
+477
View File
@@ -0,0 +1,477 @@
import gi
from dataclasses import dataclass
from datetime import datetime, time, timedelta
from typing import Literal
from dzgui.views.components.labels import BoldLabel
from dzgui.strings import calc
from dzgui.views.components.box import VBox
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, GObject # type: ignore # noqa E402
DAY_START_TIME = time(hour=6, minute=0, second=0)
DAY_END_TIME = time(hour=17, minute=59, second=59)
NIGHT_START_TIME = time(hour=18, minute=0, second=0)
NIGHT_END_TIME = time(hour=5, minute=59, second=59)
EXTENDED_TIME_FORMAT = "%H:%M:%S"
@dataclass(slots=True, frozen=True)
class Time:
time: str
day_accel: float
night_accel: float
class Emitter(GObject.GObject):
def __init__(self) -> None:
super().__init__()
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object, object))
def local_time_incremented(self, time_now: datetime, elapsed: timedelta) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
def server_time_incremented(self, server_time: datetime) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def calculate_button_clicked(self) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
def target_time_changed(self, time: timedelta) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
def remaining_time_changed(self, time: datetime) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
def adjusted_local_time_changed(self, time: datetime) -> None:
pass
class LocalClock:
def __init__(self, emitter: Emitter) -> None:
self.emitter = emitter
self.start_time = datetime.now()
GLib.timeout_add(500, self.check_time)
def get_time(self) -> str:
return datetime.now().time().strftime(EXTENDED_TIME_FORMAT)
def get_elapsed_time(self) -> timedelta:
return datetime.now() - self.start_time
def check_time(self) -> Literal[True]:
time_now = datetime.now()
elapsed = datetime.now() - self.start_time
self.emitter.emit("local_time_incremented", time_now, elapsed)
return True
class ServerClock:
def __init__(
self, server_time: Time, emitter: Emitter, local_clock: LocalClock
) -> None:
self.local_clock = local_clock
self.emitter = emitter
hour, minute = self.split_time(server_time)
self.day_accel = server_time.day_accel
self.night_accel = server_time.night_accel
self.start_time = datetime.combine(datetime.today(), time(hour, minute))
self.cur_time = datetime.combine(datetime.today(), time(hour, minute))
# NOTE: at highest possible accel interval (64 * 64), real time is 0.876 seconds
if self.is_night():
GLib.timeout_add(500, self.increment_night)
else:
GLib.timeout_add(500, self.increment_day)
@staticmethod
def split_time(server_time: Time) -> tuple[int, int]:
split = server_time.time.split(":")
return int(split[0]), int(split[1])
def _increment_with_offset(self, offset: float) -> None:
elapsed = self.local_clock.get_elapsed_time()
virtual_elapsed = elapsed * offset
self.cur_time = self.start_time + virtual_elapsed
self.emitter.emit("server_time_incremented", self.cur_time)
def increment_day(self) -> bool:
if self.is_night():
GLib.timeout_add(500, self.increment_night)
return False
offset = self.day_accel
self._increment_with_offset(offset)
return True
def increment_night(self) -> bool:
if not self.is_night():
GLib.timeout_add(500, self.increment_day)
return False
offset = self.night_accel * self.day_accel
self._increment_with_offset(offset)
return True
def is_night(self) -> bool:
return (
self.cur_time.time() >= NIGHT_START_TIME
or self.cur_time.time() <= NIGHT_END_TIME
)
def get_values(self) -> tuple[int, int]:
return self.cur_time.hour, self.cur_time.minute
def get_time(self) -> time:
return self.cur_time.time()
def get_cycle_remainder(self, accel: bool = True) -> float:
# NOTE: calc from cycle start time to current point
if self.is_night():
cutoff = DAY_START_TIME
offset = self.day_accel * self.night_accel
else:
cutoff = NIGHT_START_TIME
offset = self.day_accel
start = datetime.combine(self.cur_time, cutoff)
if accel is False:
offset = 1
return (start - self.cur_time).total_seconds() / offset
def get_full_cycle(self, end_time: time, start_time: time) -> float:
end = datetime.combine(self.cur_time, end_time)
start = datetime.combine(self.cur_time, start_time)
if start > end:
end += timedelta(days=1)
return (end - start).total_seconds()
def get_day_cycle(self) -> float:
return self.get_full_cycle(DAY_END_TIME, DAY_START_TIME)
def get_night_cycle(self) -> float:
return self.get_full_cycle(NIGHT_END_TIME, NIGHT_START_TIME)
def get_delta(self, picker_time: time, start_time: time) -> float:
start = datetime.combine(self.cur_time, start_time)
picker_date = datetime.combine(self.cur_time, picker_time)
if start > picker_date:
picker_date += timedelta(days=1)
delta = (picker_date - start).total_seconds()
return delta
def get_next_cycle(self) -> float:
if self.is_night():
return self.get_day_cycle() / self.day_accel
else:
return self.get_night_cycle() / (self.day_accel * self.night_accel)
def get_remaining_delta(self, picker_time: time, is_night: bool) -> float:
if is_night:
start_time = NIGHT_START_TIME
offset = self.day_accel * self.night_accel
else:
start_time = DAY_START_TIME
offset = self.day_accel
return self.get_delta(picker_time, start_time) / offset
def calc_delta(self, picker_time: time, is_night: bool) -> timedelta:
# NOTE: time spans two full cycles
if self.get_time() > picker_time:
to_end = self.get_cycle_remainder()
next_cycle = self.get_next_cycle()
delta = self.get_remaining_delta(picker_time, is_night)
total = to_end + next_cycle + delta
else:
picker_date = datetime.combine(self.cur_time, picker_time)
# NOTE: strip sub-minute values
new = self.cur_time.replace(second=0, microsecond=0)
delta = (picker_date - new).total_seconds()
remainder_till_cycle_end = self.get_cycle_remainder(accel=False)
if delta <= remainder_till_cycle_end:
if self.is_night():
total = delta / (self.day_accel * self.night_accel)
else:
total = delta / self.day_accel
else:
total = self.get_remaining_delta(picker_time, is_night)
total = round(total)
td = timedelta(seconds=total)
return td
class RemainderClock:
def __init__(self, emitter: Emitter) -> None:
self.emitter = emitter
self.emitter.connect("target_time_changed", self._on_target_time_changed)
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
self.reset_time(True)
self.previous = timedelta(0)
def reset_time(self, zero_out: bool = False) -> None:
now = datetime.now()
second, ms = (0, 0) if zero_out else (now.second, now.microsecond)
self.time = datetime.combine(
now, time(hour=0, minute=0, second=second, microsecond=ms)
)
def _on_local_time_incremented(
self, emitter: Emitter, time: datetime, elapsed: timedelta
) -> None:
if self.time == datetime.min.time():
return
delta = elapsed - self.previous
# NOTE: clamp to midnight
self.time = max(
self.time - delta, self.time.replace(hour=0, minute=0, second=0)
)
self.previous = elapsed
self.emitter.emit("remaining_time_changed", self.time)
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
self.reset_time()
self.time += time
self.emitter.emit("remaining_time_changed", self.time)
def get_time(self) -> str:
return self.time.strftime(EXTENDED_TIME_FORMAT)
class AdjustedClock:
def __init__(self, emitter: Emitter) -> None:
self.emitter = emitter
self.emitter.connect("target_time_changed", self._on_target_time_changed)
self.start_time = datetime.now()
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
self.start_time = datetime.now()
adjusted = self.start_time + time
self.emitter.emit("adjusted_local_time_changed", adjusted)
def get_time(self) -> str:
return self.start_time.strftime(EXTENDED_TIME_FORMAT)
class VerticalSpinBox(Gtk.SpinButton):
def __init__(self, value: int, upper: int) -> None:
super().__init__(
adjustment=Gtk.Adjustment(
value=value,
lower=0,
upper=upper,
step_increment=1,
page_increment=1,
page_size=0,
),
orientation=Gtk.Orientation.VERTICAL,
)
class HourSpinBox(VerticalSpinBox):
def __init__(self, hour: int) -> None:
super().__init__(value=hour, upper=23)
class MinuteSpinBox(VerticalSpinBox):
def __init__(self, minute: int) -> None:
super().__init__(value=minute, upper=59)
self.get_adjustment().set_page_increment(10)
class TimePicker(Gtk.Box):
def __init__(self, hour: int, minute: int) -> None:
super().__init__(
orientation=Gtk.Orientation.HORIZONTAL,
halign=Gtk.Align.CENTER,
valign=Gtk.Align.CENTER,
)
self.hour_spin = HourSpinBox(hour)
self.minute_spin = MinuteSpinBox(minute)
self.add(self.hour_spin)
self.add(self.minute_spin)
self.hour_spin.connect("output", self._on_output)
self.minute_spin.connect("output", self._on_output)
def _on_output(self, spin_button: VerticalSpinBox) -> Literal[True]:
val = spin_button.get_value()
spin_button.set_text(f"{int(val):02d}")
# NOTE: indicates that rendering completed
return True
def get_values(self) -> tuple[int, int]:
hour = self.get_hour()
minute = self.get_minute()
return hour, minute
def get_hour(self) -> int:
return int(self.hour_spin.get_adjustment().get_value())
def get_minute(self) -> int:
return int(self.minute_spin.get_adjustment().get_value())
def get_time(self) -> time:
d = datetime.now()
return time(
hour=self.get_hour(),
minute=self.get_minute(),
second=d.second,
microsecond=d.microsecond,
)
def set_time(self, hour: int, minute: int) -> None:
self.hour_spin.get_adjustment().set_value(hour)
self.minute_spin.get_adjustment().set_value(minute)
def is_night(self) -> bool:
t = self.get_time()
return t >= NIGHT_START_TIME or t <= NIGHT_END_TIME
class AdjustedTimeFrame(Gtk.Frame):
def __init__(self, emitter: Emitter) -> None:
super().__init__(margin_start=50, margin_end=50)
self.emitter = emitter
self.emitter.connect(
"adjusted_local_time_changed", self._on_adjusted_time_changed
)
self.emitter.connect("remaining_time_changed", self._on_remainder_changed)
self.remainder_clock = RemainderClock(emitter)
self.remainder_heading = BoldLabel(calc.heading_remainder_time)
self.remaining_time = Gtk.Label(label=self.remainder_clock.get_time())
self.adjusted_clock = AdjustedClock(emitter)
self.adjusted_heading = BoldLabel(calc.heading_adjusted_time)
self.adjusted_local_time = Gtk.Label(label=self.adjusted_clock.get_time())
flowbox = Gtk.FlowBox(
row_spacing=20,
column_spacing=50,
min_children_per_line=2,
max_children_per_line=2,
margin=15,
halign=Gtk.Align.CENTER,
selection_mode=Gtk.SelectionMode.NONE,
)
flowbox.add(self.remainder_heading)
flowbox.add(self.adjusted_heading)
flowbox.add(self.remaining_time)
flowbox.add(self.adjusted_local_time)
recalc = Gtk.Button(
label=calc.calculate, halign=Gtk.Align.CENTER, margin_bottom=20
)
recalc.connect("clicked", self._on_recalc_clicked)
vbox = VBox()
vbox.extend([flowbox, recalc])
self.add(vbox)
def _on_remainder_changed(self, emitter: Emitter, time: datetime) -> None:
strtime = str(time.strftime(EXTENDED_TIME_FORMAT))
self.remaining_time.set_text(strtime)
def _on_adjusted_time_changed(self, emitter: Emitter, time: datetime) -> None:
strtime = str(time.strftime(EXTENDED_TIME_FORMAT))
self.adjusted_local_time.set_text(strtime)
def _on_recalc_clicked(self, button: Gtk.Button) -> None:
self.emitter.emit("calculate_button_clicked")
class TimePickerFrame(Gtk.Frame):
def __init__(self, emitter: Emitter, stime: Time) -> None:
super().__init__(margin_start=50, margin_end=50)
self.emitter = emitter
self.emitter.connect("local_time_incremented", self._on_local_clock_increment)
self.emitter.connect("server_time_incremented", self._on_server_clock_increment)
self.emitter.connect(
"calculate_button_clicked", self._on_calculate_button_clicked
)
self.local_clock = LocalClock(self.emitter)
local_clock_time = self.local_clock.get_time()
self.server_time = ServerClock(stime, self.emitter, self.local_clock)
hour, minute = self.server_time.get_values()
self.picker = TimePicker(hour, minute)
self.local_timelabel = Gtk.Label(label=local_clock_time)
self.server_timelabel = Gtk.Label(label=stime.time, halign=Gtk.Align.CENTER)
h0 = BoldLabel(calc.heading_local_time)
h1 = BoldLabel(calc.heading_server_time)
h2 = BoldLabel(calc.heading_target_time)
flowbox = Gtk.FlowBox(
row_spacing=10,
column_spacing=50,
min_children_per_line=3,
max_children_per_line=3,
margin=15,
halign=Gtk.Align.CENTER,
selection_mode=Gtk.SelectionMode.NONE,
)
flowbox.add(h0)
flowbox.add(h1)
flowbox.add(h2)
flowbox.add(self.local_timelabel)
flowbox.add(self.server_timelabel)
flowbox.add(self.picker)
self.add(flowbox)
def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None:
t = time_now.time().strftime("%H:%M")
self.server_timelabel.set_text(t)
def _on_local_clock_increment(
self, emitter: Emitter, time_now: datetime, elapsed: timedelta
) -> None:
t = time_now.time().strftime(EXTENDED_TIME_FORMAT)
self.local_timelabel.set_text(t)
def _on_calculate_button_clicked(self, button: Gtk.Button) -> None:
if (self.picker.get_values()) == (self.server_time.get_values()):
total_time = timedelta(seconds=0)
else:
picker_time = self.picker.get_time()
is_night = self.picker.is_night()
total_time = self.server_time.calc_delta(picker_time, is_night)
self.emitter.emit("target_time_changed", total_time)
class ServerTimeCalculator(Gtk.ScrolledWindow):
def __init__(self, stime: Time) -> None:
super().__init__(propagate_natural_width=True)
self.emitter = Emitter()
outerbox = Gtk.Box(
halign=Gtk.Align.CENTER,
valign=Gtk.Align.START,
spacing=20,
orientation=Gtk.Orientation.VERTICAL,
)
time_picker_frame = TimePickerFrame(self.emitter, stime)
adjusted_time_frame = AdjustedTimeFrame(self.emitter)
disclaimer = Gtk.Label(
label=calc.disclaimer,
justify=Gtk.Justification.CENTER,
)
for el in time_picker_frame, adjusted_time_frame, disclaimer:
outerbox.add(el)
self.add(outerbox)
+8
View File
@@ -309,6 +309,14 @@ class ExceptionDialog(TextBufferDialog):
content.set_spacing(0) content.set_spacing(0)
content.add(scrollable) content.add(scrollable)
self.connect("map", self._on_map)
def _on_map(self, dialog: Self) -> None:
button = self.get_widget_for_response(Gtk.ResponseType.OK)
if button is None:
return
button.grab_focus()
def _on_page_changed( def _on_page_changed(
self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int self, notebook: Gtk.Notebook, child: Gtk.Box | Gtk.ScrolledWindow, index: int
) -> None: ) -> None:
+46 -25
View File
@@ -1,20 +1,21 @@
import gi
import html import html
from typing import Self, TYPE_CHECKING from typing import Self, TYPE_CHECKING
from dzgui.const.constants import EXPAND, FILL from dzgui.const.constants import CALCULATOR
from dzgui.const.enum import ContextMenuGroup from dzgui.const.enum import ContextMenuGroup
from dzgui.model.model_factory import ModelFactory from dzgui.model.model_factory import ModelFactory
from dzgui.util import css from dzgui.util import css
from dzgui.util import strings
from dzgui.util.format import format_hyperlinks, format_server_mods from dzgui.util.format import format_hyperlinks, format_server_mods
from dzgui.strings import server_mods from dzgui.strings import server_mods, dialogs
from dzgui.views.components.buttons import IconTextButton
from dzgui.views.components.box import VBox
from dzgui.views.dialogs.calc import Time, ServerTimeCalculator
from dzgui.views.dialogs.generic import GenericDialog from dzgui.views.dialogs.generic import GenericDialog
from dzgui.views.trees.tree_base import TreeView from dzgui.views.trees.tree_base import TreeView
import gi
gi.require_version("Gtk", "3.0") gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib, Gdk, GObject, Pango # noqa from gi.repository import Gtk, Gdk, Pango # noqa
if TYPE_CHECKING: if TYPE_CHECKING:
from dzgui.api.servers import Details from dzgui.api.servers import Details
@@ -46,12 +47,19 @@ class ServerDialog(GenericDialog):
self.connect("response", self._on_response) self.connect("response", self._on_response)
self.view.connect("key-press-event", self._on_keypress) self.view.connect("key-press-event", self._on_keypress)
self.scrollable_tree = Gtk.ScrolledWindow() self.scrollable_tree = Gtk.ScrolledWindow(vexpand=True)
self.scrollable_tree.add(self.view) self.scrollable_tree.add(self.view)
self.scrollable_tree.set_size_request(700, 400) self.scrollable_tree.set_size_request(700, 500)
self.stack = Gtk.Stack()
self.content_box = VBox(10)
self.content_box.set_valign(Gtk.Align.FILL)
self.content_box.add(self.scrollable_tree)
self.stack.add_named(self.content_box, "METADATA")
self.content = self.get_content_area() self.content = self.get_content_area()
self.content.pack_start(self.scrollable_tree, EXPAND, FILL, 0) self.content.pack_start(self.stack, expand=True, fill=True, padding=0)
def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> bool: def _on_keypress(self, view: Gtk.TreeView, event: Gdk.EventKey) -> bool:
# NOTE: ESC normally unfocuses treeview instead of destroying dialog # NOTE: ESC normally unfocuses treeview instead of destroying dialog
@@ -66,9 +74,7 @@ class ServerDialog(GenericDialog):
class ServerDetailsDialog(ServerDialog): class ServerDetailsDialog(ServerDialog):
def __init__(self, controller: "Controller", details: "Details"): def __init__(self, controller: "Controller", details: "Details"):
# TODO: server name should also be packed in details struct super().__init__(controller, dialogs.server_details, details.name, menu=None)
name = controller.get_server_name()
super().__init__(controller, strings.server_details, name, menu=None)
self.store = Gtk.ListStore(str, str, Pango.Weight) self.store = Gtk.ListStore(str, str, Pango.Weight)
self.view.connect("row-activated", self._on_row_activated) self.view.connect("row-activated", self._on_row_activated)
@@ -85,14 +91,9 @@ class ServerDetailsDialog(ServerDialog):
column.set_sort_column_id(i) column.set_sort_column_id(i)
column.set_expand(True) column.set_expand(True)
# NOTE: constrain tree area to logical height of metadata scrollable_message = Gtk.ScrolledWindow(vexpand=True)
# leaving room for server message desc = Gtk.Label(label=dialogs.server_message, valign=Gtk.Align.START)
self.scrollable_tree.set_size_request(700, 150) css.add_class(desc, "server-subheading")
# TODO: make "Server message" text boldface
scrollable_message = Gtk.ScrolledWindow()
desc = Gtk.Label(label=strings.server_message, valign=Gtk.Align.START)
css.add_class(desc, "details-heading")
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER) box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER)
self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True) self.description = Gtk.Label(justify=Gtk.Justification.CENTER, wrap=True)
sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL) sep = Gtk.Separator(orientation=Gtk.Orientation.HORIZONTAL)
@@ -100,11 +101,7 @@ class ServerDetailsDialog(ServerDialog):
for el in desc, sep, self.description: for el in desc, sep, self.description:
box.add(el) box.add(el)
scrollable_message.add(box) scrollable_message.add(box)
self.content_box.add(scrollable_message)
self.pack(scrollable_message)
if details.data is None:
return
for row in details.data: for row in details.data:
self.store.append(row + [Pango.Weight.BOLD]) self.store.append(row + [Pango.Weight.BOLD])
@@ -113,8 +110,32 @@ class ServerDetailsDialog(ServerDialog):
text = format_hyperlinks(text) text = format_hyperlinks(text)
self.description.set_markup(html.escape(text)) self.description.set_markup(html.escape(text))
calc_button = IconTextButton(CALCULATOR, dialogs.toggle_calc)
calc_button.connect("clicked", self._on_calc_pressed)
calc_button.set_halign(Gtk.Align.END)
calc_button.set_valign(Gtk.Align.START)
self.content.pack_start(calc_button, False, False, 0)
self.content.reorder_child(calc_button, 2)
self.scrollable_tree.set_size_request(700, 250)
self.calc = ServerTimeCalculator(
Time(details.gametime, details.day_accel, details.night_accel)
)
self.stack.add_named(self.calc, "CALCULATOR")
self.connect("map", self._on_map)
self.show_all() self.show_all()
def _on_map(self, dialog: Self) -> None:
if widget := self.get_widget_for_response(Gtk.ResponseType.OK):
widget.grab_focus()
def _on_calc_pressed(self, button: Gtk.Button) -> None:
if self.stack.get_visible_child_name() == "METADATA":
self.stack.set_visible_child_name("CALCULATOR")
else:
self.stack.set_visible_child_name("METADATA")
def _on_row_activated( def _on_row_activated(
self, self,
treeview: Gtk.TreeView, treeview: Gtk.TreeView,
+18 -11
View File
@@ -66,7 +66,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
self.handler_id: int self.handler_id: int
self.queue: Queue = Queue() self.queue: Queue = Queue()
self.seen_cache: list[str] = [] self.seen_cache: list[Record] = []
prefs = self.controller.get_prefs() prefs = self.controller.get_prefs()
columns = prefs.paths.columns columns = prefs.paths.columns
@@ -341,16 +341,24 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
control = self.proxy_man.get_control() control = self.proxy_man.get_control()
return model, control return model, control
@staticmethod
def ping_server( def ping_server(
self,
model: "FastInsertListStore", model: "FastInsertListStore",
_iter: Gtk.TreeIter, _iter: Gtk.TreeIter,
ip: str, record: Record,
qport: int,
ping_column: int,
) -> None: ) -> None:
_ping = ping(ip, qport) _ping = ping(record.ip, record.qport)
GLib.idle_add(lambda: model.set(_iter, ping_column, _ping)) GLib.idle_add(self._redraw, model, _iter, _ping)
def _redraw(
self, model: "FastInsertListStore", _iter: Gtk.TreeIter, _ping: int
) -> None:
ping_column = 9
model.set(_iter, ping_column, _ping)
self.queue_draw()
def wipe_cache(self) -> None:
self.seen_cache = []
def _get_ping( def _get_ping(
self, self,
@@ -363,13 +371,12 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
addr_column = 7 addr_column = 7
qport_column = 8 qport_column = 8
ping_column = 9
addr = model.get_value(_iter, addr_column).split(":") addr = model.get_value(_iter, addr_column).split(":")
ip = addr[0] ip = addr[0]
gameport = addr[1] gameport = int(addr[1])
qport = model.get_value(_iter, qport_column) qport = model.get_value(_iter, qport_column)
record = f"{addr}:{gameport}:{qport}" record = Record(ip, gameport, qport)
if record in self.seen_cache: if record in self.seen_cache:
return return
@@ -378,7 +385,7 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
thread = threading.Thread( thread = threading.Thread(
daemon=True, daemon=True,
target=self.ping_server, target=self.ping_server,
args=(model, _iter, ip, qport, ping_column), args=(model, _iter, record),
) )
thread.start() thread.start()
+45
View File
@@ -0,0 +1,45 @@
import pytest
import re
from dzgui.api.servers import Details, Record, get_details
@pytest.fixture
def details() -> Details:
"""
Static official server, metadata is expected to be invariate
"""
r = Record("85.190.157.111", 10400, 10401)
return get_details(r)
def test_invalid_server() -> None:
r = Record("127.0.0.1", 10400, 10401)
assert get_details(r) is None
@pytest.mark.parametrize("key, expect", [("day_accel", 2.3), ("night_accel", 6.8)])
def test_time_accel(details, key, expect) -> None:
assert getattr(details, key) == expect
def test_gametime(details) -> None:
time_reg = r"^(0[0-9]|1[0-9]|2[0-3]):([0-5][0-9]$)"
match = re.match(time_reg, details.gametime)
assert match is not None
def test_metadata(details) -> None:
for row in details.data:
label, value = row
match label:
case "Password":
assert value == "Disabled"
case "Platform":
assert value == "Windows"
case "Battleye":
assert value == "Enabled"
case "Valve Anti-Cheat":
assert value == "Enabled"
case _:
pass
+3 -1
View File
@@ -1,7 +1,9 @@
import re import re
from dzgui.model.proxy_model import DAY_REG, NIGHT_REG from dzgui.model.proxy_model import DAY_REG, NIGHT_REG
import pytest
pytestmark = pytest.mark.FOO
def iterate(h: str, r: str) -> None: def iterate(h: str, r: str) -> None:
for m in range(60): for m in range(60):
@@ -18,6 +20,6 @@ def test_day() -> None:
def test_night() -> None: def test_night() -> None:
for h in range(24): for h in range(24):
if 6 < h < 17: if 5 < h < 18:
continue continue
iterate(h, NIGHT_REG) iterate(h, NIGHT_REG)