Merge pull request #33 from aclist/feat/setup-wizard

feat: setup wizard
This commit is contained in:
aclist 2026-05-17 22:59:38 +09:00 committed by GitHub
commit 8a505fbf88
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 1007 additions and 255 deletions

2
.gitignore vendored
View File

@ -9,3 +9,5 @@ build/
notes.otl
uv.lock
.coverage
pyapp-latest/
dist/

View File

@ -1,4 +1,5 @@
## Added
- Setup wizard
- Changelog text wrapping and formatting
- Changelog ships with source
- Documentation ships with source
@ -36,6 +37,7 @@
## Changed
- Conform to PEP 440 versioning for beta versions
- Reduce padding on keys button
- Boldface breadcrumbs
- Bold labels inside frames

View File

@ -1,19 +1,57 @@
import json
import logging
import os
import requests
import subprocess
from shlex import shlex
from pathlib import Path
from dzgui.const.constants import APP_NAME
from dzgui.init.prereqs import has_steam_client
from dzgui.const.constants import (
APP_NAME,
DEBIAN_STEAM_PATH,
DEFAULT_STEAM_PATH,
FLATPAK_STEAM_PATH,
UBUNTU_STEAM_PATH,
VDF_PATH,
)
from dzgui.const.endpoints import STEAM_PUBLISHED_FILES
from dzgui.strings import wizard
from dzgui.util.bash import concat_bash_args
logger = logging.getLogger(APP_NAME)
def get_steam_paths() -> list[tuple[Path, str]]:
paths = []
if has_steam_client():
HOME = Path.home()
env = os.environ.get("XDG_DATA_HOME")
if env is not None:
XDG_DATA_HOME = env
else:
XDG_DATA_HOME = DEFAULT_STEAM_PATH
DEFAULT_PATH = HOME.joinpath(XDG_DATA_HOME)
FLATPAK_PATH = HOME.joinpath(FLATPAK_STEAM_PATH)
UBUNTU_PATH = HOME.joinpath(UBUNTU_STEAM_PATH)
DEBIAN_PATH = HOME.joinpath(DEBIAN_STEAM_PATH)
human = {
DEFAULT_PATH: wizard.desc_default_path,
FLATPAK_PATH: wizard.desc_flatpak_path,
UBUNTU_PATH: wizard.desc_ubuntu_path,
DEBIAN_PATH: wizard.desc_debian_path,
}
for path in DEFAULT_PATH, FLATPAK_PATH, UBUNTU_PATH, DEBIAN_PATH:
if path.joinpath(VDF_PATH).is_file():
paths.append((path, human[path]))
return paths
def concat_mods(mods: list[str]) -> str:
from dzgui.util.symlink import _hash

View File

@ -4,6 +4,9 @@ from dzgui.const.enum import Preferences
from dzgui.config.query import get_config, enum_to_key
from dzgui.util._json import write_json
# TODO: drop deprecated
# TEST: parametrize writing config values and checking config output in test
def toggle_config(path: Path, key: Preferences) -> None:
real_key = enum_to_key(key)
@ -15,6 +18,7 @@ def toggle_config(path: Path, key: Preferences) -> None:
except Exception as e:
raise e
def write_config(path: Path, key: Preferences, value: str) -> None:
real_key = enum_to_key(key)
try:

View File

@ -14,6 +14,6 @@ class UserPrefs:
is_debug: bool
coords: Union["Coords", None]
version: str
allow_updates: bool
paths: "Xdg"
update_available: bool
use_miles: bool

View File

@ -5,6 +5,7 @@ from dataclasses import dataclass
from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG
@dataclass
class Xdg:
config: Path
@ -73,7 +74,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
system = state / "logs" / SYSTEM_LOG
debug = state / "logs" / DEBUG_LOG
columns = state / "dzg.cols.json"
columns = state / "dzg.columns.json"
notes = state / "dzg.notes.json"
resolution = state / "dzg.res.json"
history = state / "dzg.history"

View File

@ -0,0 +1,16 @@
"""
Generic defaults used when initializing a config file from scratch
"""
config_boilerplate = {
"bm_api": "",
"fav_server": "",
"fav_label": "",
"name": "",
"fullscreen": False,
"steam_api": "",
"default_steam_path": "",
"client": "",
"ip_list": [],
"use_miles": False,
"start_tab": 0,
}

View File

@ -64,8 +64,12 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv"
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
CHANGELOG_PATH = "data/CHANGELOG.md"
HERO_PATH = "data/images/hero.png"
CSS_PATH = "data/app.css"
VDF_PATH = "steamapps/libraryfolders.vdf"
DEFAULT_STEAM_PATH = ".local/share/Steam"
FLATPAK_STEAM_PATH = ".var/app/com.valvesoftware.Steam/data/Steam"
UBUNTU_STEAM_PATH = ".steam/steam"
DEBIAN_STEAM_PATH = ".steam/debian-installation"
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
# TODO: drop
FOREGROUND_CMDS = ("wmctrl", "xdotool")

View File

@ -5,6 +5,7 @@ BM_SERVERS = "https://api.battlemetrics.com/servers?"
GITHUB = "https://github.com/aclist"
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/releases/latest"
GITHUB_USER_RELEASES = "https://github.com/aclist/dztui/releases"
DB_IP = "https://db-ip.com/db/download/ip-to-city-lite"
IP_ECHO = "https://ipecho.net/plain"
COORDS_API = "http://ip-api.com/json"

View File

@ -1,15 +0,0 @@
"""
This file is intended to be patched by package maintainers
repackaging DZGUI for use in different distributions.
If DZGUI is going to be installed globally via a package manager
or other means and will reside in an immutable location, or needs to be
explicitly bound to a certain version, set the flag below to False.
This will disable the following:
- Version update checks at startup
- Ability to toggle between Stable/Testing branches in the Options menu
"""
ALLOW_UPDATES = True

View File

@ -1,4 +1,5 @@
## Added
- Setup wizard
- Changelog text wrapping and formatting
- Changelog ships with source
- Documentation ships with source
@ -32,20 +33,27 @@
- Refresh servers button
- Early load alerts button
- Preconnect dialog
- Preconnect warnings/failsafes like filesize
## Changed
- Conform to PEP 440 versioning for beta versions
- Reduce padding on keys button
- Boldface breadcrumbs
- Bold labels inside frames
- Sidebar buttos do not steal focus
- Sidebar buttons do not steal focus
- Copy IP copies IP:queryport only instead of IP:gameport:queryport, mimics syntax needed by add by ip method
- Load new model into view without flushing
- Cull servers with abnormal queue values (integer overflow: 2147483647)
- Packaging structure
- Suppress log messages from imported modules
- Embed Workshop link in Options menu
## Dropped
- Debug mode
- Branch switching
- Manual mod install mode (describe rationale)
- Force update mods
## Fixed
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
@ -57,12 +65,10 @@
- Load offline mods
- Choose to jump into splash screen instead of server
- Setup wizard
- Move debug mode to developers only
- Local documentation
- Raw debug command in context menu
## Developer-facing
- Add pyproject.toml file
- Show deprecation warnings
- Options -> Dev page
- Raw debug command
- Moved debug log to this mode

View File

@ -35,3 +35,6 @@
.masked-tree {
background-color: transparent;
}
.error-frame border {
border: 2px solid red;
}

View File

@ -1,5 +1,4 @@
import shutil
import sys
from pathlib import Path
from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_PATH
@ -7,16 +6,11 @@ from dzgui.config.convert import rc2json
from dzgui.util._json import read_json, write_json
# TODO: move to setup wizard
def migrate_legacy_conf(config: Path) -> None:
old_conf = Path.home() / LEGACY_CONFIG_PATH
if old_conf.is_file():
j = rc2json(old_conf)
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text(j)
else:
print("Unimplemented. You must have a working dztuirc.")
sys.exit(1)
j = rc2json(old_conf)
config.parent.mkdir(parents=True, exist_ok=True)
config.write_text(j)
def has_new_config(config: Path) -> bool:
@ -24,6 +18,7 @@ def has_new_config(config: Path) -> bool:
def migrate_cols_file(res: Path) -> None:
# NOTE: dzg.columns.json is API 7 spec
old_res = Path.home() / LEGACY_COLS_PATH
if old_res.is_file():
j = read_json(old_res)
@ -38,11 +33,20 @@ def migrate_cols_file(res: Path) -> None:
def copy_state_files(state_path: Path) -> None:
home = Path.home()
legacy = home / ".local/state/dzgui"
to_copy = [
"dzg.res.json",
"dzg.notes.json",
"dzg.history",
"dzg.versions",
"ips.csv",
".month",
]
if state_path == legacy:
# TODO: log this
return
for file in legacy.iterdir():
shutil.copy(file, state_path / file.name)
if file.name in to_copy:
shutil.copy(file, state_path / file.name)
def copy_ipdb(ips_path: Path) -> None:

View File

@ -1,51 +0,0 @@
import shutil
import subprocess
from pathlib import Path
"""
Find possible locations of default steam path
"""
def get_steam_dirs() -> list[Path]:
dirs = []
# pass 1
if shutil.which("locate") is not None:
# TODO: config or steamapps path? cf. LIBRARYFOLDERS_PATH
proc = subprocess.run(
["/usr/bin/locate", "Steam/config/libraryfolders.vdf"],
capture_output=True,
text=True
)
if proc.returncode == 0:
for d in proc.stdout.splitlines():
dirs.append(Path(d))
return dirs
# pass 2
for d in Path.home().rglob("Steam/config/libraryfolders.vdf"):
dirs.append(d)
# pass 3
# if still nothing, let them select it
# prompt what the dir should look like
if len(dirs) == 0:
print("none found")
return dirs
dirs = get_steam_dirs()
print(dirs)
# TODO: present these in a group of radio buttons
"""
when user toggles radio button, updates text saying
"this is the standard steam location on debian" etc.
if None or if not satisfied, pop a filepicker and choose
if selection was custom, it has to have a libraryfolders.vdf
keeps next button grayed out until requirements are satisfied
Cancel Next
"""

View File

@ -1,14 +1,10 @@
import logging
import requests
import subprocess
import sys
from importlib import resources
from packaging.version import Version
from dzgui.const.constants import APP_NAME, APP_NAME_LOWER, REQUEST_TIMEOUT
from dzgui.const.constants import APP_NAME, REQUEST_TIMEOUT
from dzgui.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES
from dzgui.init.prefix import is_prefix_writeable
logger = logging.getLogger(APP_NAME)
@ -28,31 +24,13 @@ def get_latest_release() -> str | None:
return tag
def allow_updates(allow: bool) -> bool:
if allow is False:
return False
if allow is True:
return is_prefix_writeable()
def check_updates(version: str) -> None:
def check_updates(version: str) -> bool:
try:
latest = get_latest_release()
prefix = sys.prefix
if latest is None:
return
return False
if Version(version) >= Version(latest):
return
# TODO: test update logic
print("UNIMPLEMENTED: fetches in-app updates")
return
with resources.path(APP_NAME_LOWER, "scripts/update.sh") as path:
proc = subprocess.Popen(["/usr/bin/env", "bash", path, latest, prefix])
if proc != 0:
# TODO: pop a dialog
pass
sys.exit(proc)
return False
return True
except Exception:
return
return False

@ -1 +1 @@
Subproject commit a22a9f428cbe075d7dda62f78000296955eea92a
Subproject commit 07483b88ed096327ebca752f5e177011b604d7fe

View File

@ -13,29 +13,27 @@ from dzgui.config.ipdb import get_ipdb
from dzgui.config.query import lookup
from dzgui.config.userprefs import UserPrefs
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
from dzgui.const.update import ALLOW_UPDATES
from dzgui.init.coords import get_local_coords
from dzgui.init.dayz import is_dayz_installed
from dzgui.init.flock import lock_acquire
from dzgui.init.migrate import (
has_new_config,
migrate_cols_file,
migrate_legacy_conf,
copy_state_files,
migrate_cols_file,
)
from dzgui.init.prefix import get_version
from dzgui.init.prereqs import has_steam_client
from dzgui.init.update import allow_updates, check_updates
from dzgui.strings import boot
from dzgui.init.update import check_updates
from dzgui.util.map_count import get_map_count
# from dzgui.util.map_count import get_map_count
from dzgui.util.deck import is_steam_deck, is_game_mode
from dzgui.util.localize import set_locale
from dzgui.util.symlink import rebuild_symlinks
from dzgui.util.strings import init, flags
from dzgui.views.base import App
from dzgui.views.dialogs.wizard import SetupWizard
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
if TYPE_CHECKING:
@ -97,33 +95,28 @@ def main() -> None:
if XDG.resolution.parent.is_dir() is False:
make_parents(XDG.resolution)
# TODO: test
if XDG.debug.is_file() is False:
make_parents(XDG.debug)
if has_new_config(XDG.config) is False:
# TODO: handle this in assistant
migrate_legacy_conf(XDG.config)
migrate_cols_file(XDG.columns)
# TODO: copy notes file
copy_state_files(xdg_paths["XDG_STATE_HOME"])
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
f.truncate(0)
_is_steam_deck = is_steam_deck()
_is_game_mode = is_game_mode() if _is_steam_deck else False
if _is_game_mode:
# NOTE: this may no longer be necessary on newer versions of SteamOS
del os.environ["GTK_IM_MODULE"]
# TODO: test spamming timeout
allow = allow_updates(ALLOW_UPDATES)
if allow is True:
check_updates(version)
if has_new_config(XDG.config) is False:
migrate_cols_file(XDG.columns)
copy_state_files(xdg_paths["XDG_STATE_HOME"])
# TODO: add logging inside wizard
SetupWizard(_is_steam_deck, XDG.config)
return
# TODO: config wizard: check has_steam_client() prior to VDF exploration
setup_logger(XDG.debug)
with open(XDG.debug, "w") as f:
f.truncate(0)
update_available = check_updates(version)
if _is_steam_deck is False:
# TODO: sudo escalation dialog
@ -148,10 +141,9 @@ def main() -> None:
is_debug=args.debug,
coords=local_coords,
version=version,
allow_updates=allow,
paths=XDG,
update_available=update_available,
use_miles=use_miles,
)
# TODO: drop allow updates
print(boot.all_ok)
App(prefs)

View File

@ -1,7 +1,7 @@
import inspect
import logging
import threading
from typing import Any, Literal, TYPE_CHECKING
from typing import Any, Literal, TYPE_CHECKING, Union
from functools import wraps
from typing import Callable
@ -52,7 +52,7 @@ class StoredFunc:
class ThreadingManager:
def __init__(self, controller: "Controller") -> None:
def __init__(self, controller: Union["Controller", None]) -> None:
self.controller = controller
self.jobs = 1
self.cleanup_func: StoredFunc | None = None
@ -69,7 +69,7 @@ class ThreadingManager:
func.call()
GLib.idle_add(self._destroy_on_idle)
if show_dialog:
if show_dialog is True and self.controller is not None:
self.wait_dialog = WaitDialog(
self.controller, dialog_str, jobs=self.jobs, show_cancel=show_cancel
)

View File

@ -1,28 +0,0 @@
#!/usr/bin/env bash
# TODO: move this into package data dir
prefix="dzgui"
tag="$1"
prefix="$2"
dir="/tmp/dzgui-${tag}"
mkdir -p "$dir"
git clone -C "$dir" git@github.com:aclist/dzgui.git --branch "${tag}"
git -C "$dir" submodule update --recursive --init
if [[ ! $(which python3.13) ]]; then
echo "Missing python3.13"
exit 1
fi
python3.13 -m venv "${prefix}"
source "$prefix/bin/activate"
rm -rf "$dir"
if [[ $(which uv) ]]; then
uv --directory "$dir" pip install .
else
cd "$dir"
pip install -r requirements.txt
echo "Setup complete. Re-launch DZGUI via the command 'dzgui'"
fi

View File

@ -1,3 +1,5 @@
api_validation_error = (
"API key validation error. Key was typed incorrectly or is defunct."
)
api_popover = "API key validation failed."

70
dzgui/strings/wizard.py Normal file
View File

@ -0,0 +1,70 @@
### IntroductionPage
title_intro = "Welcome"
blurb_intro = """
This wizard will help you set up some common config options before launching the application.
"""
### SteamPathPage
heading_steam_path = "Steam path"
blurb_steam_path = """
DZGUI needs to find the location to your default Steam installation.
This will be used to determine whether (and where) DayZ is installed.
"""
desc_default_path = "This is the default Steam path on <b>most distributions</b>."
desc_flatpak_path = (
"This is the default Steam path if you are using <b>Flatpak Steam</b>."
)
desc_ubuntu_path = "This is the default Steam path on <b>Ubuntu-based systems</b>."
desc_debian_path = "This is the default Steam path on <b>Debian-based systems</b>."
no_valid_paths = (
"No valid Steam paths found on system. Please install Steam to continue."
)
button_scan = "Scan for Steam"
### ConfigMigrationPage
heading_config = "Import files"
blurb_config = """
It looks like you have a DZGUI 6 configuration file on the system.\n
Would you like to import this into DZGUI 7, keeping your existing preferences?\n
In both cases, your DZGUI 6 file will persist separately from DZGUI 7.
"""
config_import_button = "Import DZGUI 6 config to DZGUI 7"
config_import_box = (
"Configuration data imported successfully. Proceed to the next step."
)
config_new_button = "Create new DZGUI 7 config from scratch"
config_new_box = "A new config file will be created. Proceed to the next step."
config_error_box = "Something went wrong while writing a new config file to the system."
### APIValidationPage
entry_placeholder = "Enter API key here"
api_success = "API key set successfully. Please proceed to the next step."
heading_steam_api = "Steam Web API key"
button_web_api = "Web API setup link"
blurb_steam_api = """
You must set up a Steam Web API key in order to browse the global server list.
\nIf you don't have one already, it can be set up via the page below.
\nPlease refer to the DZGUI documentation for more instructions.
"""
heading_bm_api = "Battlemetrics Web API key"
blurb_bm_api = """A Battlemetrics key is <b>optional</b>, but allows you to add/search for servers\n
by numeric ID on the web. For example, in the URL https://www.battlemetrics.net/servers/dayz/24819107,\n
the ID would be <b>24819107</b>.
"""
### PreferencesPage
heading_prefs = "User preferences"
blurb_prefs = """Here you can set up some basic settings. Additional preferences can be\n
configured via the Options menu once DZGUI launches.
"""
label_player = "Player name"
placeholder_player = "Set an in-game player name"
radio_km = "km (kilometers)"
radio_mi = "mi (miles)"
label_dist = "Distance display"
label_client = "Steam client"
### Completion page
heading_completion = "Setup complete"
blurb_completion = "Configuration completed successfully. Please exit and restart DZGUI to apply changes."

View File

@ -1,10 +1,10 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository.Gdk import SELECTION_CLIPBOARD # noqa E402
from gi.repository.Gtk import Clipboard # noqa E402
gi.require_version("Gdk", "3.0")
from gi.repository import Gtk, Gdk # noqa E402
def copy_clipboard(text: str) -> None:
clipboard = Clipboard.get(SELECTION_CLIPBOARD)
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
clipboard.set_text(text, -1)

View File

@ -44,30 +44,6 @@ logger = logging.getLogger(APP_NAME)
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
## TODO: move to configs/servers
# def query_history() -> list | None:
# history_file = MainController.get_prefs().paths.history
# try:
# with open(history_file, "r") as f:
# rows = [row.rstrip("\n") for row in f]
# except OSError:
# rows = None
# return rows
#
## TODO: belongs in model
# def str_to_record(record: str) -> Record | None:
# r = record.split(":")
# if len(r) != 3:
# return None
# return Record(r[0], int(r[1]), int(r[2]))
#
## TODO: ibid
# def record_to_str(record: Record) -> str:
# return f"{record.ip}:{record.gameport}:{record.qport}"
#
#
class OuterWindow(Gtk.Window):
def __init__(self) -> None:
super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER)

View File

@ -21,7 +21,7 @@ from dzgui.const.constants import (
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib # noqa E402
from gi.repository import Gtk, Gdk, GLib # noqa E402
if TYPE_CHECKING:
from gi.repository import GLib

View File

@ -1,8 +1,10 @@
from typing import Callable, TYPE_CHECKING
from typing import Any, Callable, TYPE_CHECKING
from dzgui.api.servers import validate_ip
from dzgui.const.constants import VIEW_CONCEAL, VIEW_REVEAL
from dzgui.util.css import add_class, remove_class
from dzgui.util.strings import connect_panel, lan_panel
from dzgui.strings.errors import api_popover
import gi
@ -140,3 +142,98 @@ class PortEntry(ValidatedEntry):
)
self.set_placeholder_text(lan_panel.placeholder)
self.set_tooltip_text(lan_panel.entry_tooltip)
# TODO: backport to Options page
class APIEntry(Gtk.Box):
def __init__(self) -> None:
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
self.func: Callable | None = None
# TODO: strings
self.entry = Gtk.Entry(
width_chars=60, hexpand=True, placeholder_text="Enter API key"
)
self.entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, VIEW_REVEAL)
self.entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, True)
self.entry.set_visibility(False)
self.entry.connect("changed", self._on_text_changed)
self.entry.connect("icon-release", self._on_icon_release)
self.entry.connect("activate", self._on_field_activated)
self.submit = Gtk.Button(label="Submit")
self.submit.set_sensitive(False)
self.submit.connect("clicked", self._on_submit)
self.pop = Gtk.Popover()
self.pop_label = Gtk.Label(
label=api_popover,
margin_start=10,
margin_end=10,
)
self.pop.add(self.pop_label)
# NOTE: render once to draw text in bubble
self.pop.show_all()
self.pop.set_margin_start(10)
self.pop.set_relative_to(self.entry)
self.pop.popdown()
for el in self.entry, self.submit:
self.add(el)
def get_entry(self) -> Gtk.Entry:
return self.entry
def popup(self) -> None:
self.pop.popup()
def get_submit(self) -> Gtk.Button:
return self.submit
def _is_valid_text(self, text: str) -> bool:
if text.isspace():
return False
if len(text) == 0:
return False
return True
def _on_text_changed(self, entry: Gtk.Entry) -> None:
text = entry.get_text()
if self._is_valid_text(text):
self.submit.set_sensitive(True)
else:
self.submit.set_sensitive(False)
def _on_icon_release(
self,
widget: Gtk.Entry,
icon_pos: Gtk.EntryIconPosition,
event: Gdk.Event,
) -> None:
visible = widget.get_visibility()
if visible:
icon, state = VIEW_REVEAL, False
else:
icon, state = VIEW_CONCEAL, True
widget.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, icon)
widget.set_visibility(state)
def _on_field_activated(self, entry: Gtk.Entry) -> None:
self.submit.emit("clicked")
def _on_submit(self, button: Gtk.Button) -> Any:
if self.func is None:
return
text = self.entry.get_text()
res = self.func(text)
return res
def set_validation_func(self, func: Callable | None) -> None:
self.func = func
def disable_button(self) -> None:
self.submit.set_sensitive(False)
def enable_button(self) -> None:
self.submit.set_sensitive(True)

View File

@ -1,10 +1,11 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa E402
class Icon(Gtk.Image):
def __init__(self, name: str, l_margin=0) -> None:
super().__init__(icon_name=name,
icon_size=Gtk.IconSize.BUTTON,
margin_start=l_margin
def __init__(self, name: str, l_margin: int = 0) -> None:
super().__init__(
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
)

View File

@ -0,0 +1,35 @@
from dzgui.strings import options
from dzgui.const.constants import FLATPAK_RUN_CMD, FLATPAK_SANDBOX, STEAM_CMD
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class ClientCombo(Gtk.ComboBox):
def __init__(self) -> None:
super().__init__()
self.client_store = Gtk.ListStore(str, str)
clients = (
(
options.steam_combo,
STEAM_CMD,
),
(
options.flatpak_combo,
FLATPAK_RUN_CMD,
),
(
options.flatpak_container_combo,
FLATPAK_SANDBOX,
),
)
for client in clients:
self.client_store.append(client)
self.set_model(self.client_store)
renderer_text = Gtk.CellRendererText()
self.pack_start(renderer_text, True)
self.add_attribute(renderer_text, "text", 0)
self.set_active(0)

View File

@ -1,12 +1,14 @@
from typing import Literal, TYPE_CHECKING
from dzgui.const.constants import NO_EXPAND, NO_FILL, FILL, NO_PADDING
from dzgui.const.endpoints import GITHUB_USER_RELEASES
from dzgui.const.enum import ServerTab
from dzgui.util.clip import copy_clipboard
from dzgui.util.open_links import open_link_by_url
from dzgui.views.components.buttonbox import ButtonBox
from dzgui.views.components.filter_panel import FilterPanel
from dzgui.views.components.mod_panel import ModSelectionPanel
from dzgui.views.components.buttons import RefreshButton, KeysButton
from dzgui.const.constants import NO_EXPAND, NO_FILL, FILL, NO_PADDING
from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton
import gi
@ -40,14 +42,17 @@ class RightPanel(Gtk.Box):
self.copying = False
# TODO: strings
version = self.controller.get_prefs().version
prefs = self.controller.get_prefs()
version = prefs.version
update = prefs.update_available
self.version_label = Gtk.Label(
label=version,
hexpand=True,
vexpand=True,
halign=Gtk.Align.END,
valign=Gtk.Align.END,
# TODO: strings
tooltip_text="Click to copy to clipboard",
)
eb = Gtk.EventBox(halign=Gtk.Align.END, valign=Gtk.Align.END)
@ -58,7 +63,26 @@ class RightPanel(Gtk.Box):
self.pack_start(el, NO_EXPAND, FILL, NO_PADDING)
self.pack_start(self.sel_panel, NO_EXPAND, NO_FILL, NO_PADDING)
self.pack_start(eb, NO_EXPAND, FILL, NO_PADDING)
self.version_button = IconTextButton(
"dialog-information-symbolic", label="Updates available"
)
self.gutter_box = Gtk.Box(
orientation=Gtk.Orientation.HORIZONTAL,
halign=Gtk.Align.END,
valign=Gtk.Align.END,
spacing=10,
)
if update:
self.version_button.set_halign(Gtk.Align.END)
self.gutter_box.add(self.version_button)
self.version_button.connect("clicked", self._on_version_button_clicked)
self.gutter_box.add(eb)
self.pack_start(self.gutter_box, NO_EXPAND, FILL, NO_PADDING)
def _on_version_button_clicked(self, button: Gtk.Button) -> None:
open_link_by_url(GITHUB_USER_RELEASES)
def _on_server_page_changed(
self, emitter: "Emitter", page: "ServerTreeView"

View File

@ -1,18 +0,0 @@
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gtk # noqa
class Watcher(Gtk.Dialog):
def __init__():
super().__init__()
pass
def bump_progress(self):
self.set_secondary_text("FOO")
#w = Watcher()
#thread = threading()
## compute here
#GLib.idle_add(self._bump_progress)
## raise window:
#Gdk.present_with_time(Gdk.CURRENT_TIME)

View File

@ -0,0 +1,627 @@
import textwrap
from enum import Enum
from importlib import resources
from pathlib import Path
from typing import Any, Callable, Self
from dzgui.api.probe import test_steam_api, test_bm_api
from dzgui.api.steam import get_steam_paths
from dzgui.const.constants import (
APP_NAME,
APP_NAME_LOWER,
HERO_PATH,
LEGACY_CONFIG_PATH,
)
from dzgui.const.boilerplate import config_boilerplate
from dzgui.const.endpoints import BM_API_SETUP, STEAM_API_SETUP
from dzgui.init.migrate import migrate_legacy_conf
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
from dzgui.strings import wizard
from dzgui.util._json import write_json
from dzgui.util.open_links import open_link_by_url
from dzgui.util.css import add_class, load_css
from dzgui.views.components.buttons import WebButton
from dzgui.views.components.entry import APIEntry
from dzgui.views.components.misc import ClientCombo
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, Gtk, GLib, GObject, GdkPixbuf # noqa E402
# TODO: currently unused, corresponds to linear index
# TODO: use of pagenum enum and page_type attribute is redundant
class PageNum(Enum):
INTRO = 1
HAS_CONFIG = 2
STEAM_PATH = 3
STEAM_API = 4
BM_API = 5
USER_PREFS = 6
FINAL = 7
class DescriptionArea(Gtk.Box):
def __init__(self, text: str):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
wrapped = textwrap.fill(text, width=80)
self.description = Gtk.Label(justify=Gtk.Justification.CENTER)
self.description.set_markup(wrapped)
self.add(self.description)
class Progress(Gtk.ProgressBar):
def __init__(self) -> None:
super().__init__(show_text=True)
class ScrolledWizardPage(Gtk.ScrolledWindow):
def __init__(self, enum: PageNum, heading: str, description: str):
super().__init__()
self.enum = enum
self.page_type: Gtk.AssistantPageType
self.title = heading
self.heading = Heading(heading)
self.description = DescriptionArea(description)
hero = resources.files(APP_NAME_LOWER).joinpath(HERO_PATH)
pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(
filename=str(hero),
width=600,
height=600,
preserve_aspect_ratio=True,
)
image = Gtk.Image.new_from_pixbuf(pixbuf)
self.box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
margin_start=100,
margin_end=100,
margin_top=50,
spacing=20,
)
self.add(self.box)
self.prog = Progress()
self.box.pack_end(self.prog, expand=False, fill=False, padding=0)
self.box.pack_start(image, expand=False, fill=True, padding=0)
self.box.pack_start(self.heading, expand=False, fill=True, padding=0)
self.box.pack_start(self.description, expand=False, fill=True, padding=0)
self.connect("map", self._on_map)
def get_progress_bar(self) -> Progress:
return self.prog
def get_page_type(self) -> Gtk.AssistantPageType:
return self.page_type
def set_title(self, title: str) -> None:
self.title = title
def get_title(self) -> str:
return self.title
def add_start(self, content: Gtk.Widget) -> None:
self.box.add(content)
def add_end(self, content: Gtk.Widget) -> None:
self.box.pack_end(content, expand=False, fill=False, padding=50)
def get_box(self) -> Gtk.Box:
return self.box
def _on_map(self, page: "ScrolledWizardPage") -> None:
pass
class NotificationFrame(Gtk.Frame):
def __init__(self, label: str, error: bool = False) -> None:
super().__init__(halign=Gtk.Align.CENTER)
self.box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
margin_start=50,
margin_end=50,
)
wrapped = textwrap.fill(label, width=80)
self.label = Gtk.Label(
label=wrapped,
justify=Gtk.Justification.CENTER,
margin_top=50,
margin_bottom=50,
)
self.box.add(self.label)
self.add(self.box)
if error:
add_class(self, "error-frame")
class APIValidationPage(ScrolledWizardPage):
def __init__(
self, enum: PageNum, heading: str, description: str, link: str, func: Callable
) -> None:
super().__init__(
enum=enum,
heading=heading,
description=description,
)
self.key = ""
self.link = link
self.thread_man = ThreadingManager(None)
self.page_type = Gtk.AssistantPageType.INTRO
self.validation_func = func
self.validation_box = APIEntry()
self.validation_box.set_halign(Gtk.Align.CENTER)
self.validation_box.set_validation_func(self._pre_validate)
self.link_button = WebButton(label=wizard.button_web_api)
self.link_button.set_halign(Gtk.Align.CENTER)
self.link_button.connect("clicked", self._on_link_clicked)
self.spinner = Gtk.Spinner()
self.success_box = NotificationFrame(wizard.api_success)
self.add_start(self.link_button)
self.add_start(self.validation_box)
self.add_start(self.spinner)
self.add_start(self.success_box)
self.connect("map", lambda _: self.success_box.set_visible(False))
def get_api_key(self) -> str:
return self.key
def _on_link_clicked(self, button: Gtk.Button) -> None:
if self.link == "":
return
open_link_by_url(self.link)
def _pre_validate(self, key: str) -> None:
self.spinner.start()
self.validation_box.disable_button()
self.validation_func(key)
def _cleanup(self, state: bool, key: str) -> None:
if state:
self.key = key
self.validation_box.disable_button()
self.success_box.set_visible(True)
EMITTER.emit("step_complete")
else:
self.validation_box.popup()
self.validation_box.enable_button()
self.spinner.stop()
class BMValidationPage(APIValidationPage):
def __init__(self) -> None:
super().__init__(
enum=PageNum.BM_API,
heading=wizard.heading_bm_api,
description=wizard.blurb_bm_api,
link=BM_API_SETUP,
func=self._validate,
)
@call_on_thread("", show_dialog=False)
def _validate(self, key: str) -> None:
is_valid = test_bm_api(key.strip())
cleanup = StoredFunc(self._cleanup, is_valid, key)
self.thread_man.set_cleanup_func(cleanup)
class SteamValidationPage(APIValidationPage):
def __init__(self) -> None:
super().__init__(
enum=PageNum.STEAM_API,
heading=wizard.heading_steam_api,
description=wizard.blurb_steam_api,
link=STEAM_API_SETUP,
func=self._validate,
)
@call_on_thread("", show_dialog=False)
def _validate(self, key: str) -> None:
is_valid = test_steam_api(key)
cleanup = StoredFunc(self._cleanup, is_valid, key)
self.thread_man.set_cleanup_func(cleanup)
class IntroductionPage(ScrolledWizardPage):
def __init__(self):
super().__init__(
enum=PageNum.INTRO,
heading=f"Welcome to {APP_NAME}!",
description=wizard.blurb_intro,
)
self.page_type = Gtk.AssistantPageType.INTRO
self.set_title(wizard.title_intro)
class Heading(Gtk.Label):
def __init__(self, label: str):
super().__init__(label=label)
add_class(self, "settings-subheading")
class ChunkyButton(Gtk.Button):
def __init__(self, text: str) -> None:
super().__init__()
self.set_size_request(80, 80)
wrapped = textwrap.fill(text, width=40)
label = Gtk.Label(label=wrapped, justify=Gtk.Justification.CENTER)
self.add(label)
class RadioFrame(Gtk.Frame):
def __init__(
self, parent: Gtk.RadioButton | None, button_path: tuple[Path, str]
) -> None:
super().__init__()
self.vbox = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
margin_top=15,
margin_start=10,
margin_end=10,
margin_bottom=15,
)
path, pretty = button_path
if parent is None:
self.button = Gtk.RadioButton.new_with_label(None, str(path))
else:
self.button = Gtk.RadioButton.new_with_label_from_widget(parent, str(path))
self.vbox.add(self.button)
label = Gtk.Label(halign=Gtk.Align.START)
label.set_markup(pretty)
self.vbox.add(label)
self.add(self.vbox)
def get_button(self) -> Gtk.RadioButton:
return self.button
class ConfigMigrationPage(ScrolledWizardPage):
def __init__(self, config: Path) -> None:
super().__init__(
enum=PageNum.HAS_CONFIG,
heading=wizard.heading_config,
description=wizard.blurb_config,
)
self.migrated = False
self.config = config
self.page_type = Gtk.AssistantPageType.INTRO
self.import_button = ChunkyButton(wizard.config_import_button)
self.new_button = ChunkyButton(wizard.config_new_button)
self.grid = Gtk.Grid(column_spacing=30, halign=Gtk.Align.CENTER)
self.grid.set_column_homogeneous(True)
self.grid.attach(self.import_button, 0, 0, 1, 1)
self.grid.attach(self.new_button, 1, 0, 1, 1)
self.add_start(self.grid)
self.err_box = NotificationFrame(wizard.config_error_box, error=True)
self.success_box = NotificationFrame(wizard.config_import_box)
self.from_scratch_box = NotificationFrame(wizard.config_new_box)
for box in self.err_box, self.success_box, self.from_scratch_box:
self.add_start(box)
self.connect("map", self._hide_boxes)
self.import_button.connect("clicked", self._on_import_clicked)
self.new_button.connect("clicked", self._on_new_clicked)
def _hide_boxes(self, page: Self) -> None:
for box in self.success_box, self.from_scratch_box, self.err_box:
box.set_visible(False)
def _on_new_clicked(self, button: Gtk.Button) -> None:
self.grid.set_sensitive(False)
self.from_scratch_box.set_visible(True)
EMITTER.emit("step_complete")
EMITTER.emit("config", False)
def is_migrated(self) -> bool:
return self.migrated
def _on_import_clicked(self, button: Gtk.Button) -> None:
self.grid.set_sensitive(False)
try:
migrate_legacy_conf(self.config)
self.migrated = True
self.success_box.set_visible(True)
except Exception:
self.err_box.set_visible(True)
EMITTER.emit("step_pending")
return
EMITTER.emit("step_complete")
EMITTER.emit("config", True)
class PreferencesPage(ScrolledWizardPage):
def __init__(self) -> None:
super().__init__(
enum=PageNum.USER_PREFS,
heading=wizard.heading_prefs,
description=wizard.blurb_prefs,
)
self.page_type = Gtk.AssistantPageType.INTRO
# TODO: widgets and strings are largely a reimplementation of options page, consolidate
name_label = Gtk.Label(label=wizard.label_player, halign=Gtk.Align.START)
self.name_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
self.name_entry = Gtk.Entry(
placeholder_text=wizard.placeholder_player,
halign=Gtk.Align.END,
width_chars=40,
)
self.name_entry.connect("changed", self._on_entry_changed)
self.name_box.add(name_label)
self.name_box.add(self.name_entry)
self.dist_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
self.radio_km = Gtk.RadioButton.new_with_label(None, wizard.radio_km)
self.radio_miles = Gtk.RadioButton.new_with_label_from_widget(
self.radio_km, wizard.radio_mi
)
dist_label = Gtk.Label(label=wizard.label_dist, halign=Gtk.Align.START)
self.dist_box.add(dist_label)
self.dist_box.add(self.radio_km)
self.dist_box.add(self.radio_miles)
client_label = Gtk.Label(label=wizard.label_client, halign=Gtk.Align.START)
self.client_box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=10)
self.client_combo = ClientCombo()
self.client_box.add(client_label)
self.client_box.add(self.client_combo)
outer_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL, halign=Gtk.Align.CENTER, spacing=10
)
for el in self.name_box, self.client_box, self.dist_box:
outer_box.add(el)
self.add_start(outer_box)
def get_prefs(self) -> tuple[str, bool, str]:
name = self.name_entry.get_text().strip()
use_miles = self.radio_miles.get_active()
model = self.client_combo.get_model()
ind = self.client_combo.get_active()
client = model[ind][1]
return name, use_miles, client
def _on_entry_changed(self, entry: Gtk.Entry) -> None:
text = entry.get_text()
if text.isspace():
EMITTER.emit("step_pending")
return
EMITTER.emit("step_complete")
class CompletionPage(ScrolledWizardPage):
def __init__(self) -> None:
super().__init__(
enum=PageNum.FINAL,
heading=wizard.heading_completion,
description=wizard.blurb_completion,
)
self.page_type = Gtk.AssistantPageType.SUMMARY
self.connect("map", lambda _: EMITTER.emit("step_complete"))
class Assistant(Gtk.Assistant):
def __init__(self, is_deck: bool, config: Path):
super().__init__()
if is_deck:
self.fullscreen()
else:
self.set_default_size(1500, 900)
self.config_path = config
self.config_values: dict[str, Any] = config_boilerplate
self.page1 = IntroductionPage()
self.page2 = ConfigMigrationPage(config)
self.page3 = SteamPathPage()
self.page4 = SteamValidationPage()
self.page5 = BMValidationPage()
self.page6 = PreferencesPage()
self.page7 = CompletionPage()
self.set_forward_page_func(self._advance_page)
EMITTER.connect("step_complete", self._mark_page_complete)
EMITTER.connect("step_pending", self._mark_page_incomplete)
EMITTER.connect("config", self._set_config_state)
legacy_path = Path.home().joinpath(LEGACY_CONFIG_PATH)
self.has_legacy_config = legacy_path.is_file()
for page in (
self.page1,
self.page2,
self.page3,
self.page4,
self.page5,
self.page6,
self.page7,
):
# NOTE: skip config migration page if no legacy config file
if page == self.page2 and self.has_legacy_config is False:
continue
self._add_page(page, page.get_page_type())
self.connect("prepare", self._on_page_prepare)
self.connect("cancel", self.destroy_and_quit)
self.connect("close", self.destroy_and_quit)
self.show_all()
load_css()
def write_config(self) -> None:
write_json(self.config_values, self.config_path)
def _advance_page(self, index: int) -> int:
page = self.get_nth_page(index)
match page:
case self.page1:
pass
case self.page2:
if self.page2.is_migrated():
return self.get_n_pages() - 1
case self.page3:
self.config_values["default_steam_path"] = page.get_path_from_radio()
case self.page4:
self.config_values["steam_api"] = page.get_api_key()
case self.page5:
self.config_values["bm_api"] = page.get_api_key()
# NOTE: collects config values before advancing to last page
case self.page6:
name, use_miles, client = self.page6.get_prefs()
self.config_values["name"] = name
self.config_values["use_miles"] = use_miles
self.config_values["client"] = client
self.write_config()
case _:
raise AttributeError("Trying to advance a non-canonical page")
return index + 1
def destroy_and_quit(self, widget: Self) -> None:
self.destroy()
Gtk.main_quit()
def _mark_page_incomplete(self, emitter: "Emitter") -> None:
page_id = self.get_current_page()
page = self.get_nth_page(page_id)
if page is None:
return
self.set_page_complete(page, False)
def _mark_page_complete(self, emitter: "Emitter") -> None:
page_id = self.get_current_page()
page = self.get_nth_page(page_id)
if page is None:
return
self.set_page_complete(page, True)
def _add_page(self, page: ScrolledWizardPage, ptype: Gtk.AssistantPageType) -> None:
self.append_page(page)
self.set_page_type(page, ptype)
self.set_page_title(page, page.get_title())
self.set_page_complete(page, True)
def _set_config_state(self, emitter: "Emitter", state: bool) -> None:
self.config = state
def _on_page_prepare(self: Self, wizard: Self, page: ScrolledWizardPage) -> None:
page_num = self.get_current_page() + 1
total = self.get_n_pages()
fraction = page_num / total
bar = page.get_progress_bar()
bar.set_fraction(fraction)
bar.set_text(f"{page_num}/{total}")
# NOTE: disable forward action
if page != self.page1:
EMITTER.emit("step_pending")
class SteamPathPage(ScrolledWizardPage):
def __init__(self) -> None:
super().__init__(
enum=PageNum.USER_PREFS,
heading=wizard.heading_steam_path,
description=wizard.blurb_steam_path,
)
self.page_type = Gtk.AssistantPageType.INTRO
self.err_box = NotificationFrame(wizard.no_valid_paths, error=True)
self.scan_button = Gtk.Button(label=wizard.button_scan, halign=Gtk.Align.CENTER)
self.scan_button.connect("clicked", self._on_scan_clicked)
self.add_start(self.scan_button)
self.add_start(self.err_box)
self.connect("map", self._start_incomplete)
def _on_scan_clicked(self, button: Gtk.Button) -> None:
self.scan_button.set_sensitive(False)
paths = get_steam_paths()
button_box = Gtk.Box(
orientation=Gtk.Orientation.VERTICAL,
halign=Gtk.Align.CENTER,
spacing=10,
)
total = len(paths)
if total == 0:
show_errors = True
else:
button_box.add(Gtk.Label(label=f"Steam paths found: {total} total."))
for i, button_path in enumerate(paths):
if i == 0:
frame = RadioFrame(None, button_path)
self.first_button = frame.get_button()
button_box.add(frame)
else:
frame = RadioFrame(self.first_button, button_path)
button_box.add(frame)
self.add_start(button_box)
show_errors = False
EMITTER.emit("step_complete")
self.show_all()
# TODO: more robust approach
self.err_box.set_visible(show_errors)
def get_path_from_radio(self) -> str:
active = next(r for r in self.first_button.get_group() if r.get_active())
return active.get_label()
def _start_incomplete(self, page: Self) -> None:
self.err_box.set_visible(False)
class SetupWizard(Gtk.Application):
def __init__(self, is_deck: bool, config: Path) -> None:
super().__init__()
GLib.set_prgname(APP_NAME)
Window(is_deck, config)
Gtk.main()
class Window(Gtk.Window):
def __init__(self, is_deck: bool, config: Path) -> None:
super().__init__(title=APP_NAME, icon_name=APP_NAME)
Assistant(is_deck, config)
class Emitter(GObject.GObject):
def __init__(self) -> None:
super().__init__()
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def step_complete(self) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
def step_pending(self) -> None:
pass
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(bool,))
def config(self, state: bool) -> None:
pass
EMITTER = Emitter()

View File

@ -9,12 +9,9 @@ from dzgui.const.constants import (
APPID_DAYZ_EXP,
APPNAME_DAYZ,
APPNAME_DAYZ_EXP_HUMAN,
FLATPAK_RUN_CMD,
FLATPAK_SANDBOX,
NO_EXPAND,
NO_FILL,
NO_PADDING,
STEAM_CMD,
VIEW_CONCEAL,
VIEW_REVEAL,
)
@ -27,6 +24,7 @@ from dzgui.views.components.labels import LeftLabel
from dzgui.views.components.eventbox import InfoEventBox
from dzgui.views.components.buttons import WebButton
from dzgui.views.components.frame import HeadingFrame
from dzgui.views.components.misc import ClientCombo
from dzgui.views.dialogs.generic import ExceptionDialog
@ -101,29 +99,7 @@ class Options(Gtk.Box):
Preferences.WINDOW,
)
# TODO: make this an abstract class
client_store = Gtk.ListStore(str, str)
clients = (
(
options.steam_combo,
STEAM_CMD,
),
(
options.flatpak_combo,
FLATPAK_RUN_CMD,
),
(
options.flatpak_container_combo,
FLATPAK_SANDBOX,
),
)
for client in clients:
client_store.append(client)
self.client_combo = Gtk.ComboBox.new_with_model(client_store) # Text()
renderer_text = Gtk.CellRendererText()
self.client_combo.pack_start(renderer_text, True)
self.client_combo.add_attribute(renderer_text, "text", 0)
self.client_combo.set_active(0)
self.client_combo = ClientCombo()
self.client_combo.connect("changed", self._on_client_changed)
client_hbox = ShortHBox(self.client_combo)
@ -481,7 +457,7 @@ class Options(Gtk.Box):
self.dayz_version_label.set_text(dayz_version)
self.dayz_exp_version_label.set_text(dayz_exp_version)
# TODO: not happy with this
# TODO: bicolumn list store with no cell renderer on index 1, use raw command names
active_combo = query.get_client_index(config["client"])
self.client_combo.set_active(active_combo)

View File

@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
authors = [
{name = "aclist"}
]
version = "7.0.0"
version = "7.0.0b1"
license = "GPL-3.0-or-later"
license-files = ["LICENSE"]
readme = "README.md"
@ -54,8 +54,12 @@ lint = [
"types-requests>=2.32.4",
"types-psutil==7.2.2.20260508",
]
build = [
"build==1.5.0",
]
dev = [
"dzgui[docs,test,lint]"
"dzgui[docs,test,lint,build]"
]
[build-system]
@ -79,6 +83,7 @@ line-length = 88
python_version = "3.13"
disallow_untyped_defs = true
warn_return_any = true
exclude = ["lib/*"]
[[tool.mypy.overrides]]
module = [