mirror of
https://github.com/aclist/dztui.git
synced 2026-10-31 07:02:50 +01:00
Compare commits
31
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1752018331 | ||
|
|
01be1247b4 | ||
|
|
27d5c4b4b2 | ||
|
|
045e47037e | ||
|
|
3bc2a54ee4 | ||
|
|
4e22b82703 | ||
|
|
492ed221c4 | ||
|
|
71a61a9201 | ||
|
|
516735defa | ||
|
|
9a3df70914 | ||
|
|
c41e47859f | ||
|
|
11bbe15ecb | ||
|
|
d40512dfa0 | ||
|
|
f07e503b74 | ||
|
|
3caeb823a2 | ||
|
|
6fcd887d56 | ||
|
|
c15b77d470 | ||
|
|
cee1e3b8af | ||
|
|
e8ccdd7cbb | ||
|
|
2629fe254e | ||
|
|
3b15bf58e7 | ||
|
|
e773a4909b | ||
|
|
95380681f9 | ||
|
|
18192a4667 | ||
|
|
f07a2dea5f | ||
|
|
0e58cf514e | ||
|
|
84c6a3868b | ||
|
|
7c0981981f | ||
|
|
e653c61955 | ||
|
|
d4bf9f71f6 | ||
|
|
cb5c233f45 |
No files matched your search
@@ -52,6 +52,7 @@
|
|||||||
- Filter button tooltips
|
- Filter button tooltips
|
||||||
- Expand/collapse changelog versions
|
- Expand/collapse changelog versions
|
||||||
- Support local light/dark mode changes on system theme
|
- Support local light/dark mode changes on system theme
|
||||||
|
- Server time calculator
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import subprocess
|
|||||||
import vdf # type: ignore
|
import vdf # type: ignore
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from requests.exceptions import HTTPError
|
||||||
from typing import Any, Union
|
from typing import Any, Union
|
||||||
from warnings import deprecated
|
from warnings import deprecated
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ from dzgui.const.endpoints import (
|
|||||||
UNSUB_ENDPOINT,
|
UNSUB_ENDPOINT,
|
||||||
)
|
)
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
|
from dzgui.strings.errors import api_validation_error, workshop_endpoint_error
|
||||||
from dzgui.util.bash import concat_bash_args
|
from dzgui.util.bash import concat_bash_args
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
@@ -262,6 +264,16 @@ def find_user_id_32(path: Path) -> int:
|
|||||||
return int(uid) & 0xFFFFFFFF
|
return int(uid) & 0xFFFFFFFF
|
||||||
|
|
||||||
|
|
||||||
|
class SteamAPIKeyError(Exception):
|
||||||
|
def __init__(self, msg=None):
|
||||||
|
super().__init__(msg or api_validation_error)
|
||||||
|
|
||||||
|
|
||||||
|
class SteamWorkshopEndpointError(Exception):
|
||||||
|
def __init__(self, msg=None):
|
||||||
|
super().__init__(msg or workshop_endpoint_error)
|
||||||
|
|
||||||
|
|
||||||
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
||||||
payload: dict[str, Union[int, str]] = {
|
payload: dict[str, Union[int, str]] = {
|
||||||
"publishedfileid": mod,
|
"publishedfileid": mod,
|
||||||
@@ -273,8 +285,19 @@ def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
|||||||
try:
|
try:
|
||||||
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
|
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
|
||||||
res.raise_for_status()
|
res.raise_for_status()
|
||||||
|
except HTTPError as e:
|
||||||
|
logger.critical(e)
|
||||||
|
match res.status_code:
|
||||||
|
case 403:
|
||||||
|
raise SteamAPIKeyError()
|
||||||
|
case 404:
|
||||||
|
raise SteamWorkshopEndpointError()
|
||||||
|
case _:
|
||||||
|
# TODO: apply redaction filter
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def subscribe(key: str, mod: int) -> None:
|
def subscribe(key: str, mod: int) -> None:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ HEX_GREEN = "#32CD32"
|
|||||||
HEX_RED = "#FF0000"
|
HEX_RED = "#FF0000"
|
||||||
HEX_ORANGE = "#FFAC1C"
|
HEX_ORANGE = "#FFAC1C"
|
||||||
|
|
||||||
# FIXME: normalize icon names
|
# TODO: normalize icon names
|
||||||
CALCULATOR = "accessories-calculator-symbolic"
|
CALCULATOR = "accessories-calculator-symbolic"
|
||||||
CARET_DOWN = "go-down-symbolic"
|
CARET_DOWN = "go-down-symbolic"
|
||||||
CARET_UP = "go-up-symbolic"
|
CARET_UP = "go-up-symbolic"
|
||||||
@@ -55,6 +55,8 @@ VIEW_CONCEAL = "view-conceal-symbolic"
|
|||||||
VIEW_REVEAL = "view-reveal-symbolic"
|
VIEW_REVEAL = "view-reveal-symbolic"
|
||||||
WARNING = "dialog-warning-symbolic"
|
WARNING = "dialog-warning-symbolic"
|
||||||
WEB_BROWSER = "web-browser-symbolic"
|
WEB_BROWSER = "web-browser-symbolic"
|
||||||
|
DAY_ICON = "weather-clear-symbolic"
|
||||||
|
NIGHT_ICON = "weather-clear-night-symbolic"
|
||||||
|
|
||||||
SEPARATOR = "SEPARATOR"
|
SEPARATOR = "SEPARATOR"
|
||||||
|
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
- Filter button tooltips
|
- Filter button tooltips
|
||||||
- Expand/collapse changelog versions
|
- Expand/collapse changelog versions
|
||||||
- Support local light/dark mode changes on system theme
|
- Support local light/dark mode changes on system theme
|
||||||
|
- Server time calculator
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
|
|||||||
+9
-2
@@ -4,17 +4,24 @@ from pathlib import Path
|
|||||||
|
|
||||||
from dzgui.api.steam import get_app_path
|
from dzgui.api.steam import get_app_path
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
from dzgui.const.constants import APPID_DAYZ, APP_NAME
|
from dzgui.const.constants import APPID_DAYZ, APP_NAME, APPNAME_DAYZ
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
|
|
||||||
|
from dzgui.strings.preconnect import resync
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
class VDFSyncError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def is_dayz_installed(config: Path) -> None:
|
def is_dayz_installed(config: Path) -> None:
|
||||||
try:
|
try:
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
get_app_path(Path(path), APPID_DAYZ)
|
get_app_path(Path(path), APPID_DAYZ)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(e)
|
logger.critical(e)
|
||||||
raise e
|
msg = resync.format(APPNAME_DAYZ)
|
||||||
|
raise VDFSyncError(msg)
|
||||||
@@ -48,7 +48,7 @@ from dzgui.strings.dialogs import (
|
|||||||
)
|
)
|
||||||
from dzgui.strings import kb
|
from dzgui.strings import kb
|
||||||
from dzgui.strings.server_mods import checkmark, resync
|
from dzgui.strings.server_mods import checkmark, resync
|
||||||
from dzgui.util.format import format_mib
|
from dzgui.util.format import format_exception, format_mib
|
||||||
from dzgui.util.strings import dialog, server_timeout
|
from dzgui.util.strings import dialog, server_timeout
|
||||||
from dzgui.util.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
@@ -300,6 +300,11 @@ class ConnectionManager:
|
|||||||
dialog = ServerDetailsDialog(self.controller, details)
|
dialog = ServerDetailsDialog(self.controller, details)
|
||||||
dialog.run()
|
dialog.run()
|
||||||
|
|
||||||
|
def _workshop_timeout(self, msg: str) -> None:
|
||||||
|
dialog = ExceptionDialog(self.controller, msg)
|
||||||
|
dialog.show_all()
|
||||||
|
dialog.run()
|
||||||
|
|
||||||
def _server_timeout(self) -> None:
|
def _server_timeout(self) -> None:
|
||||||
dialog = ExceptionDialog(self.controller, server_timeout)
|
dialog = ExceptionDialog(self.controller, server_timeout)
|
||||||
dialog.set_details_buffer(kb.DZG_006)
|
dialog.set_details_buffer(kb.DZG_006)
|
||||||
@@ -351,8 +356,14 @@ class ConnectionManager:
|
|||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
if self.controller.is_cancel_pending():
|
if self.controller.is_cancel_pending():
|
||||||
return
|
return
|
||||||
|
try:
|
||||||
subscribe(key, int(mod))
|
subscribe(key, int(mod))
|
||||||
time.sleep(API_RATE_LIMIT)
|
time.sleep(API_RATE_LIMIT)
|
||||||
|
except Exception as e:
|
||||||
|
msg = format_exception(e)
|
||||||
|
func = StoredFunc(self._workshop_timeout, msg)
|
||||||
|
self.thread_man.set_cleanup_func(func, destroy_first=True)
|
||||||
|
return
|
||||||
|
|
||||||
self.thread_man.update_dialog(waiting_for_directories)
|
self.thread_man.update_dialog(waiting_for_directories)
|
||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
|
|||||||
@@ -122,7 +122,11 @@ class ModManager:
|
|||||||
def unsub_atomic_mod(self, mod: str) -> None:
|
def unsub_atomic_mod(self, mod: str) -> None:
|
||||||
config_man = self.controller.get_config_man()
|
config_man = self.controller.get_config_man()
|
||||||
key = config_man.lookup(Preferences.STEAM)
|
key = config_man.lookup(Preferences.STEAM)
|
||||||
|
try:
|
||||||
unsubscribe(key, int(mod))
|
unsubscribe(key, int(mod))
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
return
|
||||||
|
|
||||||
steam_path = Path(self.path)
|
steam_path = Path(self.path)
|
||||||
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
add_button="Add"
|
from dzgui.const.constants import UDP_PORT
|
||||||
add_con_heading="Add/connect"
|
|
||||||
|
add_button = "Add"
|
||||||
|
add_con_heading = "Add/connect"
|
||||||
add_popover = "This address is already in your Saved Servers"
|
add_popover = "This address is already in your Saved Servers"
|
||||||
add_tooltip="Add to Saved Servers"
|
add_tooltip = "Add to Saved Servers"
|
||||||
connect_button="Connect"
|
connect_button = "Connect"
|
||||||
connect_tooltip="Connect to this server"
|
connect_tooltip = "Connect to this server"
|
||||||
connect_entry_placeholder="Enter IP:Query port (e.g., 192.168.1.1:27016)"
|
connect_entry_placeholder = f"Enter IP:Query port (e.g., 192.168.1.1:{UDP_PORT})"
|
||||||
|
|
||||||
fav_heading="Favorite server"
|
fav_heading = "Favorite server"
|
||||||
favs_empty="None set. Right click a server and select 'Set favorite' to set."
|
favs_empty = "None set. Right click a server and select 'Set favorite' to set."
|
||||||
|
|
||||||
lan_heading="LAN query port"
|
lan_heading = "LAN query port"
|
||||||
lan_default_button="Default port (27016)"
|
lan_default_button = f"Default port ({UDP_PORT})"
|
||||||
lan_custom_button="Custom port"
|
lan_custom_button = "Custom port"
|
||||||
lan_scan_button="Scan"
|
lan_scan_button = "Scan"
|
||||||
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
lan_abort_tooltip = "Unless you have multiple DayZ servers on your LAN,\nleave this checked to get results faster"
|
||||||
lan_checkbox = "Stop scanning on first hit"
|
lan_checkbox = "Stop scanning on first hit"
|
||||||
lan_entry_placeholder="Enter the query port (1-65535)"
|
lan_entry_placeholder = "Enter the query port (1-65535)"
|
||||||
lan_entry_tooltip="Specify the port to search for DayZ servers on the local network"
|
lan_entry_tooltip = "Specify the port to search for DayZ servers on the local network"
|
||||||
lan_scan_tooltip="Scan for servers"
|
lan_scan_tooltip = "Scan for servers"
|
||||||
@@ -4,3 +4,4 @@ api_validation_error = (
|
|||||||
|
|
||||||
api_popover = "API key validation failed."
|
api_popover = "API key validation failed."
|
||||||
dayz_not_found = "Could not find a valid DayZ installation."
|
dayz_not_found = "Could not find a valid DayZ installation."
|
||||||
|
workshop_endpoint_error = "Steam Workshop service unavailable, try again later."
|
||||||
@@ -11,8 +11,6 @@ mods = "Mods"
|
|||||||
total_mods = "Total mods: "
|
total_mods = "Total mods: "
|
||||||
up_to_date = "All mods are up to date."
|
up_to_date = "All mods are up to date."
|
||||||
all_updated = "All mods updated."
|
all_updated = "All mods updated."
|
||||||
"If you recently installed {build} or moved it to a different drive, "
|
|
||||||
"restart Steam to allow these changes to synchronize, then try again."
|
|
||||||
required_space = " Need to download {0} MiB of mod updates."
|
required_space = " Need to download {0} MiB of mod updates."
|
||||||
|
|
||||||
placeholder_no_mods = "This server has no mods."
|
placeholder_no_mods = "This server has no mods."
|
||||||
@@ -36,6 +34,7 @@ steam_not_running = "'{0}' is set as the default Steam client, but it is either
|
|||||||
use_desktop_mode = "Use Desktop Mode to download mods on Steam Deck."
|
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_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."
|
version_missing = "Remote server is running the build '{0}', but it is not installed."
|
||||||
|
no_errors = "No errors."
|
||||||
|
|
||||||
|
|
||||||
# Warnings
|
# Warnings
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ path_remove_prefix = "The following path will be removed:"
|
|||||||
not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall."
|
not_installed = "It looks like DZGUI was not previously set up; nothing to uninstall."
|
||||||
standalone_uninstall = (
|
standalone_uninstall = (
|
||||||
"It looks like you are running the standalone version of DZGUI.\n"
|
"It looks like you are running the standalone version of DZGUI.\n"
|
||||||
"To finalize the uninstall process, you can remove the 'dzgui' file."
|
"To finalize the uninstall process, invoke './dzgui self remove',\n"
|
||||||
|
"then remove the 'dzgui' file."
|
||||||
)
|
)
|
||||||
system_uninstall = (
|
system_uninstall = (
|
||||||
"It looks like DZGUI was installed via your system package manager or directly from source code.\n"
|
"It looks like DZGUI was installed via your system package manager or directly from source code.\n"
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
||||||
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME, DAYZ_COMMUNITY_ROOT
|
from dzgui.const.constants import (
|
||||||
|
APPID_DAYZ,
|
||||||
|
APPID_DAYZ_EXP,
|
||||||
|
APP_NAME,
|
||||||
|
DAYZ_COMMUNITY_ROOT,
|
||||||
|
)
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
|
|
||||||
@@ -20,6 +25,7 @@ def expunge_link(file: Path) -> None:
|
|||||||
if str(file.stem)[:2] == "@C":
|
if str(file.stem)[:2] == "@C":
|
||||||
file.unlink()
|
file.unlink()
|
||||||
|
|
||||||
|
|
||||||
def rebuild_symlinks(config: Path) -> None:
|
def rebuild_symlinks(config: Path) -> None:
|
||||||
# TODO: pass direct path as argument
|
# TODO: pass direct path as argument
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
@@ -63,10 +69,9 @@ def symlink_mission(steam_path: Path, target: str) -> str:
|
|||||||
stem = path.name
|
stem = path.name
|
||||||
parent = path.parent
|
parent = path.parent
|
||||||
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
|
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
|
||||||
try:
|
if source.exists():
|
||||||
|
source.unlink()
|
||||||
source.symlink_to(parent)
|
source.symlink_to(parent)
|
||||||
except Exception:
|
|
||||||
return ""
|
|
||||||
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
|
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -9,3 +9,6 @@ class Icon(Gtk.Image):
|
|||||||
super().__init__(
|
super().__init__(
|
||||||
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
|
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def set_icon_name(self, name: str) -> None:
|
||||||
|
super().set_from_icon_name(name, Gtk.IconSize.BUTTON)
|
||||||
@@ -102,7 +102,7 @@ class BootDialog(ColorAwareApp, Gtk.Dialog): # type: ignore
|
|||||||
margin_top=30,
|
margin_top=30,
|
||||||
margin_bottom=30,
|
margin_bottom=30,
|
||||||
)
|
)
|
||||||
self.error_label = Gtk.Label()
|
self.error_label = Gtk.Label(margin_start=15, margin_end=15)
|
||||||
|
|
||||||
# TODO: abstract class
|
# TODO: abstract class
|
||||||
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
self.copy_button = ClipboardButton(None, lambda: self.error_label.get_text())
|
||||||
|
|||||||
+95
-115
@@ -3,17 +3,19 @@ from dataclasses import dataclass
|
|||||||
from datetime import datetime, time, timedelta
|
from datetime import datetime, time, timedelta
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from dzgui.views.components.labels import BoldLabel
|
from dzgui.const.constants import DAY_ICON, NIGHT_ICON
|
||||||
from dzgui.strings import calc
|
from dzgui.strings import calc
|
||||||
from dzgui.views.components.box import VBox
|
from dzgui.views.components.icon import Icon
|
||||||
|
from dzgui.views.components.labels import BoldLabel
|
||||||
|
from dzgui.views.components.box import VBox, HBox
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, GLib, GObject # type: ignore # noqa E402
|
from gi.repository import Gtk, GLib, GObject # type: ignore # noqa E402
|
||||||
|
|
||||||
DAY_START_TIME = time(hour=6, minute=0, second=0)
|
DAY_START_TIME = time(hour=6, minute=0, second=0)
|
||||||
DAY_END_TIME = time(hour=17, minute=59, second=59)
|
DAY_END_TIME = time(hour=18, minute=0, second=0)
|
||||||
NIGHT_START_TIME = time(hour=18, minute=0, second=0)
|
NIGHT_START_TIME = time(hour=18, minute=0, second=0)
|
||||||
NIGHT_END_TIME = time(hour=5, minute=59, second=59)
|
NIGHT_END_TIME = time(hour=6, minute=0, second=0)
|
||||||
EXTENDED_TIME_FORMAT = "%H:%M:%S"
|
EXTENDED_TIME_FORMAT = "%H:%M:%S"
|
||||||
|
|
||||||
|
|
||||||
@@ -44,6 +46,10 @@ class Emitter(GObject.GObject):
|
|||||||
def target_time_changed(self, time: timedelta) -> None:
|
def target_time_changed(self, time: timedelta) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def target_time_reset(self, time: timedelta) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
def remaining_time_changed(self, time: datetime) -> None:
|
def remaining_time_changed(self, time: datetime) -> None:
|
||||||
pass
|
pass
|
||||||
@@ -57,6 +63,7 @@ class LocalClock:
|
|||||||
def __init__(self, emitter: Emitter) -> None:
|
def __init__(self, emitter: Emitter) -> None:
|
||||||
self.emitter = emitter
|
self.emitter = emitter
|
||||||
self.start_time = datetime.now()
|
self.start_time = datetime.now()
|
||||||
|
# NOTE: highest theoretical accel interval is 24 * 64
|
||||||
GLib.timeout_add(500, self.check_time)
|
GLib.timeout_add(500, self.check_time)
|
||||||
|
|
||||||
def get_time(self) -> str:
|
def get_time(self) -> str:
|
||||||
@@ -87,131 +94,80 @@ class ServerClock:
|
|||||||
self.start_time = datetime.combine(datetime.today(), time(hour, minute))
|
self.start_time = datetime.combine(datetime.today(), time(hour, minute))
|
||||||
self.cur_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
|
self._last_elapsed = self.local_clock.get_elapsed_time()
|
||||||
if self.is_night():
|
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
|
||||||
GLib.timeout_add(500, self.increment_night)
|
|
||||||
else:
|
|
||||||
GLib.timeout_add(500, self.increment_day)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def split_time(server_time: Time) -> tuple[int, int]:
|
def split_time(server_time: Time) -> tuple[int, int]:
|
||||||
split = server_time.time.split(":")
|
split = server_time.time.split(":")
|
||||||
return int(split[0]), int(split[1])
|
return int(split[0]), int(split[1])
|
||||||
|
|
||||||
def _increment_with_offset(self, offset: float) -> None:
|
def is_day(self) -> bool:
|
||||||
elapsed = self.local_clock.get_elapsed_time()
|
if DAY_START_TIME <= self.cur_time.time() <= DAY_END_TIME:
|
||||||
virtual_elapsed = elapsed * offset
|
return True
|
||||||
self.cur_time = self.start_time + virtual_elapsed
|
return False
|
||||||
|
|
||||||
|
def _on_local_time_incremented(
|
||||||
|
self,
|
||||||
|
emitter: Emitter,
|
||||||
|
time: datetime,
|
||||||
|
elapsed: timedelta,
|
||||||
|
) -> Literal[True]:
|
||||||
|
delta = elapsed - self._last_elapsed
|
||||||
|
self._last_elapsed = elapsed
|
||||||
|
factor = self.get_accel_factor(self.cur_time)
|
||||||
|
self.cur_time += delta * factor
|
||||||
self.emitter.emit("server_time_incremented", self.cur_time)
|
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
|
return True
|
||||||
|
|
||||||
def increment_night(self) -> bool:
|
def get_accel_factor(self, dt: datetime) -> float:
|
||||||
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 (
|
return (
|
||||||
self.cur_time.time() >= NIGHT_START_TIME
|
self.day_accel
|
||||||
or self.cur_time.time() <= NIGHT_END_TIME
|
if DAY_START_TIME <= dt.time() < DAY_END_TIME
|
||||||
|
else (self.night_accel * self.day_accel)
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_values(self) -> tuple[int, int]:
|
def get_values(self) -> tuple[int, int]:
|
||||||
return self.cur_time.hour, self.cur_time.minute
|
return self.cur_time.hour, self.cur_time.minute
|
||||||
|
|
||||||
|
def get_datetime(self) -> datetime:
|
||||||
|
return self.cur_time
|
||||||
|
|
||||||
def get_time(self) -> time:
|
def get_time(self) -> time:
|
||||||
return self.cur_time.time()
|
return self.cur_time.time()
|
||||||
|
|
||||||
def get_cycle_remainder(self, accel: bool = True) -> float:
|
def calc_delta(self, end_time: datetime) -> timedelta:
|
||||||
# NOTE: calc from cycle start time to current point
|
total = 0.0
|
||||||
if self.is_night():
|
current_time = self.cur_time
|
||||||
cutoff = DAY_START_TIME
|
|
||||||
offset = self.day_accel * self.night_accel
|
if current_time > end_time:
|
||||||
|
end_time += timedelta(days=1)
|
||||||
|
|
||||||
|
while current_time < end_time:
|
||||||
|
factor = self.get_accel_factor(current_time)
|
||||||
|
day_start = datetime.combine(current_time.date(), DAY_START_TIME)
|
||||||
|
day_end = datetime.combine(current_time.date(), DAY_END_TIME)
|
||||||
|
|
||||||
|
if current_time.time() < DAY_START_TIME:
|
||||||
|
edge = day_start
|
||||||
|
elif current_time.time() < DAY_END_TIME:
|
||||||
|
edge = day_end
|
||||||
else:
|
else:
|
||||||
cutoff = NIGHT_START_TIME
|
edge = datetime.combine(
|
||||||
offset = self.day_accel
|
current_time.date() + timedelta(days=1), DAY_START_TIME
|
||||||
start = datetime.combine(self.cur_time, cutoff)
|
)
|
||||||
|
|
||||||
if accel is False:
|
chunk_end = min(end_time, edge)
|
||||||
offset = 1
|
total += (chunk_end - current_time).total_seconds() / factor
|
||||||
return (start - self.cur_time).total_seconds() / offset
|
current_time = chunk_end
|
||||||
|
return timedelta(seconds=total)
|
||||||
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:
|
class RemainderClock:
|
||||||
def __init__(self, emitter: Emitter) -> None:
|
def __init__(self, emitter: Emitter) -> None:
|
||||||
self.emitter = emitter
|
self.emitter = emitter
|
||||||
self.emitter.connect("target_time_changed", self._on_target_time_changed)
|
self.emitter.connect("target_time_changed", self._on_target_time_changed)
|
||||||
|
self.emitter.connect("target_time_reset", self._on_target_time_reset)
|
||||||
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
|
self.emitter.connect("local_time_incremented", self._on_local_time_incremented)
|
||||||
self.reset_time(True)
|
self.reset_time(True)
|
||||||
|
|
||||||
@@ -238,8 +194,14 @@ class RemainderClock:
|
|||||||
self.previous = elapsed
|
self.previous = elapsed
|
||||||
self.emitter.emit("remaining_time_changed", self.time)
|
self.emitter.emit("remaining_time_changed", self.time)
|
||||||
|
|
||||||
|
def _on_target_time_reset(self, emitter: Emitter, time: timedelta) -> None:
|
||||||
|
self.reset_time(True)
|
||||||
|
self.emitter.emit("remaining_time_changed", self.time)
|
||||||
|
# NOTE: restore adjusted time to current time
|
||||||
|
self.emitter.emit("adjusted_local_time_changed", datetime.now())
|
||||||
|
|
||||||
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
|
def _on_target_time_changed(self, emitter: Emitter, time: timedelta) -> None:
|
||||||
self.reset_time()
|
self.reset_time(True)
|
||||||
self.time += time
|
self.time += time
|
||||||
self.emitter.emit("remaining_time_changed", self.time)
|
self.emitter.emit("remaining_time_changed", self.time)
|
||||||
|
|
||||||
@@ -321,23 +283,29 @@ class TimePicker(Gtk.Box):
|
|||||||
def get_minute(self) -> int:
|
def get_minute(self) -> int:
|
||||||
return int(self.minute_spin.get_adjustment().get_value())
|
return int(self.minute_spin.get_adjustment().get_value())
|
||||||
|
|
||||||
def get_time(self) -> time:
|
def get_time(self) -> datetime:
|
||||||
|
"""
|
||||||
|
NOTE: interpolating seconds/microseconds from the current time
|
||||||
|
may prevent abrupt visual changes when setting a new TimePicker time
|
||||||
|
while a clock tick is updating the local remainder label.
|
||||||
|
However, this can introduce false negatives when running tests due to microsecond drift.
|
||||||
|
Ideally, tests should be decoupled from this logic.
|
||||||
|
"""
|
||||||
d = datetime.now()
|
d = datetime.now()
|
||||||
return time(
|
return datetime.combine(
|
||||||
|
d,
|
||||||
|
time(
|
||||||
hour=self.get_hour(),
|
hour=self.get_hour(),
|
||||||
minute=self.get_minute(),
|
minute=self.get_minute(),
|
||||||
second=d.second,
|
second=0,
|
||||||
microsecond=d.microsecond,
|
microsecond=0,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_time(self, hour: int, minute: int) -> None:
|
def set_time(self, hour: int, minute: int) -> None:
|
||||||
self.hour_spin.get_adjustment().set_value(hour)
|
self.hour_spin.get_adjustment().set_value(hour)
|
||||||
self.minute_spin.get_adjustment().set_value(minute)
|
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):
|
class AdjustedTimeFrame(Gtk.Frame):
|
||||||
def __init__(self, emitter: Emitter) -> None:
|
def __init__(self, emitter: Emitter) -> None:
|
||||||
@@ -417,6 +385,13 @@ class TimePickerFrame(Gtk.Frame):
|
|||||||
h1 = BoldLabel(calc.heading_server_time)
|
h1 = BoldLabel(calc.heading_server_time)
|
||||||
h2 = BoldLabel(calc.heading_target_time)
|
h2 = BoldLabel(calc.heading_target_time)
|
||||||
|
|
||||||
|
self.icon_indicator = Icon(DAY_ICON)
|
||||||
|
self.update_server_icon()
|
||||||
|
|
||||||
|
server_time_box = HBox(10)
|
||||||
|
server_time_box.set_halign(Gtk.Align.CENTER)
|
||||||
|
server_time_box.extend([self.server_timelabel, self.icon_indicator])
|
||||||
|
|
||||||
flowbox = Gtk.FlowBox(
|
flowbox = Gtk.FlowBox(
|
||||||
row_spacing=10,
|
row_spacing=10,
|
||||||
column_spacing=50,
|
column_spacing=50,
|
||||||
@@ -430,14 +405,19 @@ class TimePickerFrame(Gtk.Frame):
|
|||||||
flowbox.add(h1)
|
flowbox.add(h1)
|
||||||
flowbox.add(h2)
|
flowbox.add(h2)
|
||||||
flowbox.add(self.local_timelabel)
|
flowbox.add(self.local_timelabel)
|
||||||
flowbox.add(self.server_timelabel)
|
flowbox.add(server_time_box)
|
||||||
flowbox.add(self.picker)
|
flowbox.add(self.picker)
|
||||||
|
|
||||||
self.add(flowbox)
|
self.add(flowbox)
|
||||||
|
|
||||||
|
def update_server_icon(self) -> None:
|
||||||
|
icon = DAY_ICON if self.server_time.is_day() else NIGHT_ICON
|
||||||
|
self.icon_indicator.set_icon_name(icon)
|
||||||
|
|
||||||
def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None:
|
def _on_server_clock_increment(self, emitter: Emitter, time_now: datetime) -> None:
|
||||||
t = time_now.time().strftime("%H:%M")
|
t = time_now.time().strftime("%H:%M")
|
||||||
self.server_timelabel.set_text(t)
|
self.server_timelabel.set_text(t)
|
||||||
|
self.update_server_icon()
|
||||||
|
|
||||||
def _on_local_clock_increment(
|
def _on_local_clock_increment(
|
||||||
self, emitter: Emitter, time_now: datetime, elapsed: timedelta
|
self, emitter: Emitter, time_now: datetime, elapsed: timedelta
|
||||||
@@ -448,10 +428,10 @@ class TimePickerFrame(Gtk.Frame):
|
|||||||
def _on_calculate_button_clicked(self, button: Gtk.Button) -> None:
|
def _on_calculate_button_clicked(self, button: Gtk.Button) -> None:
|
||||||
if (self.picker.get_values()) == (self.server_time.get_values()):
|
if (self.picker.get_values()) == (self.server_time.get_values()):
|
||||||
total_time = timedelta(seconds=0)
|
total_time = timedelta(seconds=0)
|
||||||
|
self.emitter.emit("target_time_reset", total_time)
|
||||||
else:
|
else:
|
||||||
picker_time = self.picker.get_time()
|
picker_time = self.picker.get_time()
|
||||||
is_night = self.picker.is_night()
|
total_time = self.server_time.calc_delta(picker_time)
|
||||||
total_time = self.server_time.calc_delta(picker_time, is_night)
|
|
||||||
self.emitter.emit("target_time_changed", total_time)
|
self.emitter.emit("target_time_changed", total_time)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -176,8 +176,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
|
|
||||||
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
self.error_box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL)
|
||||||
self.error_tree = MaskedTree(ERROR)
|
self.error_tree = MaskedTree(ERROR)
|
||||||
# TODO: strings
|
self.error_placeholder = Placeholder(preconnect.no_errors)
|
||||||
self.error_placeholder = Placeholder("No errors.")
|
|
||||||
self.error_box.add(self.error_tree)
|
self.error_box.add(self.error_tree)
|
||||||
self.error_box.add(self.error_placeholder)
|
self.error_box.add(self.error_placeholder)
|
||||||
self.error_frame = HeadingFrame.new_with_widget_and_label(
|
self.error_frame = HeadingFrame.new_with_widget_and_label(
|
||||||
|
|||||||
@@ -252,7 +252,6 @@ class ServerTreeView(ContextMixin, TreeView): # type: ignore
|
|||||||
case Gdk.KEY_c:
|
case Gdk.KEY_c:
|
||||||
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
self.controller.menu_action(ContextMenu.COPY_SERVER_IP, self)
|
||||||
case Gdk.KEY_r:
|
case Gdk.KEY_r:
|
||||||
# TODO: unimplemented, needs threading
|
|
||||||
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
self.controller.menu_action(ContextMenu.REFRESH_PLAYERS, self)
|
||||||
else:
|
else:
|
||||||
match event.keyval:
|
match event.keyval:
|
||||||
|
|||||||
+2
-1
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b23"
|
version = "7.0.0b25"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -47,6 +47,7 @@ docs = [
|
|||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
"black==25.1.0",
|
"black==25.1.0",
|
||||||
|
"hypothesis==6.167.1",
|
||||||
"pre-commit==4.4.0",
|
"pre-commit==4.4.0",
|
||||||
"pytest==8.4.2",
|
"pytest==8.4.2",
|
||||||
"pytest-cov==7.0.0",
|
"pytest-cov==7.0.0",
|
||||||
|
|||||||
@@ -22,9 +22,14 @@ class MockController:
|
|||||||
|
|
||||||
|
|
||||||
def test_html_escaping() -> None:
|
def test_html_escaping() -> None:
|
||||||
text = "<Server description> Game & mods"
|
name = "Test server"
|
||||||
details = Details([["0", "1"]], text, True)
|
desc = "<Server description> Game & mods"
|
||||||
|
gametime = "05:30"
|
||||||
|
day_accel = 5.0
|
||||||
|
night_accel = 1.0
|
||||||
|
details = Details([["0", "1"]], name, desc, gametime, day_accel, night_accel)
|
||||||
controller = MockController()
|
controller = MockController()
|
||||||
dialog = ServerDetailsDialog(controller, details)
|
dialog = ServerDetailsDialog(controller, details)
|
||||||
|
dialog.destroy()
|
||||||
|
|
||||||
assert dialog.description.get_text() == text
|
assert dialog.description.get_text() == desc
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from hypothesis import given, strategies as st
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from dzgui.views.dialogs.calc import (
|
||||||
|
LocalClock,
|
||||||
|
ServerClock,
|
||||||
|
Time,
|
||||||
|
TimePicker,
|
||||||
|
DAY_START_TIME,
|
||||||
|
DAY_END_TIME,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DummyEmitter:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=[("05:00", 10.0), ("15:30", 5.0)])
|
||||||
|
def accel_server_clock(request) -> None:
|
||||||
|
clock_time, expect = request.param
|
||||||
|
cur_time = Time(clock_time, 5.0, 2.0)
|
||||||
|
emitter = DummyEmitter()
|
||||||
|
local_clock = LocalClock(emitter)
|
||||||
|
server_clock = ServerClock(cur_time, emitter, local_clock)
|
||||||
|
return expect, server_clock
|
||||||
|
|
||||||
|
|
||||||
|
def test_accel_factor(accel_server_clock) -> None:
|
||||||
|
"""Static test checking times at known boundaries"""
|
||||||
|
expect, clock = accel_server_clock
|
||||||
|
assert clock.get_accel_factor(clock.cur_time) == expect
|
||||||
|
|
||||||
|
|
||||||
|
def reference_calc(start, end, day_factor, night_factor):
|
||||||
|
"""
|
||||||
|
Reference implementation to test validity of UI-side calculator
|
||||||
|
Aggregates total seconds and applies accel factors after
|
||||||
|
"""
|
||||||
|
day_start = datetime.combine(start.date(), DAY_START_TIME)
|
||||||
|
day_end = datetime.combine(start.date(), DAY_END_TIME)
|
||||||
|
night_factor = day_factor * night_factor
|
||||||
|
|
||||||
|
if end < start:
|
||||||
|
end += timedelta(days=1)
|
||||||
|
|
||||||
|
day_max = max(start, day_start)
|
||||||
|
day_min = min(end, day_end)
|
||||||
|
|
||||||
|
next_day_start = datetime.combine(end.date(), DAY_START_TIME)
|
||||||
|
next_day_end = datetime.combine(end.date(), DAY_END_TIME)
|
||||||
|
|
||||||
|
range1 = (day_end - day_max).total_seconds()
|
||||||
|
range2 = (min(end, next_day_end) - next_day_start).total_seconds()
|
||||||
|
|
||||||
|
if end.date() > start.date():
|
||||||
|
day_seconds = max(0, range1) + max(0, range2)
|
||||||
|
else:
|
||||||
|
day_seconds = max(0, (day_min - day_max).total_seconds())
|
||||||
|
|
||||||
|
total_seconds = (end - start).total_seconds()
|
||||||
|
night_seconds = (total_seconds - day_seconds) / night_factor
|
||||||
|
day_seconds = day_seconds / day_factor
|
||||||
|
total = night_seconds + day_seconds
|
||||||
|
|
||||||
|
return timedelta(seconds=total)
|
||||||
|
|
||||||
|
|
||||||
|
day_accel = st.floats(
|
||||||
|
min_value=1.0, max_value=24.0, allow_nan=False, allow_infinity=False
|
||||||
|
)
|
||||||
|
night_accel = st.floats(
|
||||||
|
min_value=1.0, max_value=64.0, allow_nan=False, allow_infinity=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@st.composite
|
||||||
|
def time_ranges(draw):
|
||||||
|
start_time = draw(st.times())
|
||||||
|
deltas = st.timedeltas(min_value=timedelta(0), max_value=timedelta(days=1))
|
||||||
|
duration = draw(deltas)
|
||||||
|
|
||||||
|
start = datetime.combine(datetime.now(), start_time)
|
||||||
|
end = start + duration
|
||||||
|
return start, end
|
||||||
|
|
||||||
|
|
||||||
|
@given(span=time_ranges(), day_factor=day_accel, night_factor=night_accel)
|
||||||
|
def test_time_aggregation(span, day_factor, night_factor) -> None:
|
||||||
|
start, end = span
|
||||||
|
|
||||||
|
strtime = start.strftime("%H:%M")
|
||||||
|
t = Time(strtime, day_factor, night_factor)
|
||||||
|
emitter = DummyEmitter()
|
||||||
|
local_clock = LocalClock(emitter)
|
||||||
|
server_clock = ServerClock(t, emitter, local_clock)
|
||||||
|
h = end.time().hour
|
||||||
|
m = end.time().minute
|
||||||
|
picker = TimePicker(h, m)
|
||||||
|
|
||||||
|
# NOTE: truncates microseconds to mimic widget HH:MM selection behavior
|
||||||
|
start = server_clock.get_datetime()
|
||||||
|
end = picker.get_time()
|
||||||
|
actual = server_clock.calc_delta(end)
|
||||||
|
expect = reference_calc(start, end, day_factor, night_factor)
|
||||||
|
|
||||||
|
assert actual == expect
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
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):
|
||||||
|
|||||||
Reference in new issue
Block a user