mirror of
https://github.com/aclist/dztui.git
synced 2026-10-30 14:42:54 +01:00
Merge pull request #33 from aclist/feat/setup-wizard
feat: setup wizard
This commit is contained in:
32 files changed
+1007
-255
No files matched your search
@@ -9,3 +9,5 @@ build/
|
|||||||
notes.otl
|
notes.otl
|
||||||
uv.lock
|
uv.lock
|
||||||
.coverage
|
.coverage
|
||||||
|
pyapp-latest/
|
||||||
|
dist/
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
## Added
|
## Added
|
||||||
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
- Documentation ships with source
|
- Documentation ships with source
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
|
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
- Bold labels inside frames
|
- Bold labels inside frames
|
||||||
|
|||||||
+39
-1
@@ -1,19 +1,57 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from shlex import shlex
|
from shlex import shlex
|
||||||
from pathlib import Path
|
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.const.endpoints import STEAM_PUBLISHED_FILES
|
||||||
|
from dzgui.strings import wizard
|
||||||
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)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
def concat_mods(mods: list[str]) -> str:
|
||||||
from dzgui.util.symlink import _hash
|
from dzgui.util.symlink import _hash
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ from dzgui.const.enum import Preferences
|
|||||||
from dzgui.config.query import get_config, enum_to_key
|
from dzgui.config.query import get_config, enum_to_key
|
||||||
from dzgui.util._json import write_json
|
from dzgui.util._json import write_json
|
||||||
|
|
||||||
|
# TODO: drop deprecated
|
||||||
|
|
||||||
|
|
||||||
# TEST: parametrize writing config values and checking config output in test
|
# TEST: parametrize writing config values and checking config output in test
|
||||||
def toggle_config(path: Path, key: Preferences) -> None:
|
def toggle_config(path: Path, key: Preferences) -> None:
|
||||||
real_key = enum_to_key(key)
|
real_key = enum_to_key(key)
|
||||||
@@ -15,6 +18,7 @@ def toggle_config(path: Path, key: Preferences) -> None:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
||||||
def write_config(path: Path, key: Preferences, value: str) -> None:
|
def write_config(path: Path, key: Preferences, value: str) -> None:
|
||||||
real_key = enum_to_key(key)
|
real_key = enum_to_key(key)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ class UserPrefs:
|
|||||||
is_debug: bool
|
is_debug: bool
|
||||||
coords: Union["Coords", None]
|
coords: Union["Coords", None]
|
||||||
version: str
|
version: str
|
||||||
allow_updates: bool
|
|
||||||
paths: "Xdg"
|
paths: "Xdg"
|
||||||
|
update_available: bool
|
||||||
use_miles: bool
|
use_miles: bool
|
||||||
+2
-1
@@ -5,6 +5,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG
|
from dzgui.const.constants import APP_NAME_LOWER, DEBUG_LOG, SYSTEM_LOG
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Xdg:
|
class Xdg:
|
||||||
config: Path
|
config: Path
|
||||||
@@ -73,7 +74,7 @@ def parse_filepaths(xdg: dict) -> Xdg:
|
|||||||
system = state / "logs" / SYSTEM_LOG
|
system = state / "logs" / SYSTEM_LOG
|
||||||
debug = state / "logs" / DEBUG_LOG
|
debug = state / "logs" / DEBUG_LOG
|
||||||
|
|
||||||
columns = state / "dzg.cols.json"
|
columns = state / "dzg.columns.json"
|
||||||
notes = state / "dzg.notes.json"
|
notes = state / "dzg.notes.json"
|
||||||
resolution = state / "dzg.res.json"
|
resolution = state / "dzg.res.json"
|
||||||
history = state / "dzg.history"
|
history = state / "dzg.history"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
}
|
||||||
@@ -64,8 +64,12 @@ LEGACY_IPS_PATH = ".local/share/dzgui/helpers/ips.csv"
|
|||||||
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
DEBUG_LOG = f"{APP_NAME}_DEBUG.LOG"
|
||||||
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
SYSTEM_LOG = f"{APP_NAME}_SYSTEM.LOG"
|
||||||
CHANGELOG_PATH = "data/CHANGELOG.md"
|
CHANGELOG_PATH = "data/CHANGELOG.md"
|
||||||
|
HERO_PATH = "data/images/hero.png"
|
||||||
CSS_PATH = "data/app.css"
|
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")
|
LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
|
||||||
# TODO: drop
|
|
||||||
FOREGROUND_CMDS = ("wmctrl", "xdotool")
|
|
||||||
@@ -5,6 +5,7 @@ BM_SERVERS = "https://api.battlemetrics.com/servers?"
|
|||||||
GITHUB = "https://github.com/aclist"
|
GITHUB = "https://github.com/aclist"
|
||||||
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
GITHUB_RELEASES = "https://api.github.com/repos/aclist/dztui/releases/latest"
|
||||||
CODEBERG_RELEASES = "https://codeberg.org/api/v1/repos/aclist/dzgui/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"
|
DB_IP = "https://db-ip.com/db/download/ip-to-city-lite"
|
||||||
IP_ECHO = "https://ipecho.net/plain"
|
IP_ECHO = "https://ipecho.net/plain"
|
||||||
COORDS_API = "http://ip-api.com/json"
|
COORDS_API = "http://ip-api.com/json"
|
||||||
|
|||||||
@@ -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
|
|
||||||
+10
-4
@@ -1,4 +1,5 @@
|
|||||||
## Added
|
## Added
|
||||||
|
- Setup wizard
|
||||||
- Changelog text wrapping and formatting
|
- Changelog text wrapping and formatting
|
||||||
- Changelog ships with source
|
- Changelog ships with source
|
||||||
- Documentation ships with source
|
- Documentation ships with source
|
||||||
@@ -32,20 +33,27 @@
|
|||||||
- Refresh servers button
|
- Refresh servers button
|
||||||
- Early load alerts button
|
- Early load alerts button
|
||||||
- Preconnect dialog
|
- Preconnect dialog
|
||||||
|
- Preconnect warnings/failsafes like filesize
|
||||||
|
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
- Conform to PEP 440 versioning for beta versions
|
||||||
- Reduce padding on keys button
|
- Reduce padding on keys button
|
||||||
- Boldface breadcrumbs
|
- Boldface breadcrumbs
|
||||||
- Bold labels inside frames
|
- 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
|
- 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
|
- Load new model into view without flushing
|
||||||
- Cull servers with abnormal queue values (integer overflow: 2147483647)
|
- Cull servers with abnormal queue values (integer overflow: 2147483647)
|
||||||
- Packaging structure
|
- Packaging structure
|
||||||
|
- Suppress log messages from imported modules
|
||||||
|
- Embed Workshop link in Options menu
|
||||||
|
|
||||||
## Dropped
|
## Dropped
|
||||||
- Debug mode
|
- Debug mode
|
||||||
- Branch switching
|
- Branch switching
|
||||||
|
- Manual mod install mode (describe rationale)
|
||||||
|
- Force update mods
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
- Longstanding issue with left clicks not registering as tree selection changes after spamming keyboard input
|
||||||
@@ -57,12 +65,10 @@
|
|||||||
- Load offline mods
|
- Load offline mods
|
||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Setup wizard
|
- Setup wizard
|
||||||
- Move debug mode to developers only
|
|
||||||
- Local documentation
|
- Local documentation
|
||||||
|
- Raw debug command in context menu
|
||||||
|
|
||||||
## Developer-facing
|
## Developer-facing
|
||||||
- Add pyproject.toml file
|
- Add pyproject.toml file
|
||||||
- Show deprecation warnings
|
- Show deprecation warnings
|
||||||
- Options -> Dev page
|
- Options -> Dev page
|
||||||
- Raw debug command
|
|
||||||
- Moved debug log to this mode
|
|
||||||
@@ -35,3 +35,6 @@
|
|||||||
.masked-tree {
|
.masked-tree {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
.error-frame border {
|
||||||
|
border: 2px solid red;
|
||||||
|
}
|
||||||
+14
-10
@@ -1,5 +1,4 @@
|
|||||||
import shutil
|
import shutil
|
||||||
import sys
|
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from dzgui.const.constants import LEGACY_CONFIG_PATH, LEGACY_COLS_PATH, LEGACY_IPS_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
|
from dzgui.util._json import read_json, write_json
|
||||||
|
|
||||||
|
|
||||||
# TODO: move to setup wizard
|
|
||||||
def migrate_legacy_conf(config: Path) -> None:
|
def migrate_legacy_conf(config: Path) -> None:
|
||||||
old_conf = Path.home() / LEGACY_CONFIG_PATH
|
old_conf = Path.home() / LEGACY_CONFIG_PATH
|
||||||
if old_conf.is_file():
|
j = rc2json(old_conf)
|
||||||
j = rc2json(old_conf)
|
config.parent.mkdir(parents=True, exist_ok=True)
|
||||||
config.parent.mkdir(parents=True, exist_ok=True)
|
config.write_text(j)
|
||||||
config.write_text(j)
|
|
||||||
else:
|
|
||||||
print("Unimplemented. You must have a working dztuirc.")
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def has_new_config(config: Path) -> bool:
|
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:
|
def migrate_cols_file(res: Path) -> None:
|
||||||
|
# NOTE: dzg.columns.json is API 7 spec
|
||||||
old_res = Path.home() / LEGACY_COLS_PATH
|
old_res = Path.home() / LEGACY_COLS_PATH
|
||||||
if old_res.is_file():
|
if old_res.is_file():
|
||||||
j = read_json(old_res)
|
j = read_json(old_res)
|
||||||
@@ -38,11 +33,20 @@ def migrate_cols_file(res: Path) -> None:
|
|||||||
def copy_state_files(state_path: Path) -> None:
|
def copy_state_files(state_path: Path) -> None:
|
||||||
home = Path.home()
|
home = Path.home()
|
||||||
legacy = home / ".local/state/dzgui"
|
legacy = home / ".local/state/dzgui"
|
||||||
|
to_copy = [
|
||||||
|
"dzg.res.json",
|
||||||
|
"dzg.notes.json",
|
||||||
|
"dzg.history",
|
||||||
|
"dzg.versions",
|
||||||
|
"ips.csv",
|
||||||
|
".month",
|
||||||
|
]
|
||||||
if state_path == legacy:
|
if state_path == legacy:
|
||||||
# TODO: log this
|
# TODO: log this
|
||||||
return
|
return
|
||||||
for file in legacy.iterdir():
|
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:
|
def copy_ipdb(ips_path: Path) -> None:
|
||||||
|
|||||||
@@ -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
|
|
||||||
"""
|
|
||||||
+6
-28
@@ -1,14 +1,10 @@
|
|||||||
import logging
|
import logging
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from importlib import resources
|
|
||||||
from packaging.version import Version
|
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.const.endpoints import GITHUB_RELEASES, CODEBERG_RELEASES
|
||||||
from dzgui.init.prefix import is_prefix_writeable
|
|
||||||
|
|
||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
@@ -28,31 +24,13 @@ def get_latest_release() -> str | None:
|
|||||||
return tag
|
return tag
|
||||||
|
|
||||||
|
|
||||||
def allow_updates(allow: bool) -> bool:
|
def check_updates(version: str) -> bool:
|
||||||
if allow is False:
|
|
||||||
return False
|
|
||||||
if allow is True:
|
|
||||||
return is_prefix_writeable()
|
|
||||||
|
|
||||||
|
|
||||||
def check_updates(version: str) -> None:
|
|
||||||
try:
|
try:
|
||||||
latest = get_latest_release()
|
latest = get_latest_release()
|
||||||
prefix = sys.prefix
|
|
||||||
if latest is None:
|
if latest is None:
|
||||||
return
|
return False
|
||||||
if Version(version) >= Version(latest):
|
if Version(version) >= Version(latest):
|
||||||
return
|
return False
|
||||||
|
return True
|
||||||
# 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)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return
|
return False
|
||||||
+1
-1
Submodule dzgui/lib/dayzquery updated: a22a9f428c...07483b88ed.
+17
-25
@@ -13,29 +13,27 @@ from dzgui.config.ipdb import get_ipdb
|
|||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
from dzgui.config.userprefs import UserPrefs
|
from dzgui.config.userprefs import UserPrefs
|
||||||
from dzgui.config.xdg import get_xdg_paths, parse_filepaths
|
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.coords import get_local_coords
|
||||||
from dzgui.init.dayz import is_dayz_installed
|
from dzgui.init.dayz import is_dayz_installed
|
||||||
from dzgui.init.flock import lock_acquire
|
from dzgui.init.flock import lock_acquire
|
||||||
from dzgui.init.migrate import (
|
from dzgui.init.migrate import (
|
||||||
has_new_config,
|
has_new_config,
|
||||||
migrate_cols_file,
|
|
||||||
migrate_legacy_conf,
|
|
||||||
copy_state_files,
|
copy_state_files,
|
||||||
|
migrate_cols_file,
|
||||||
)
|
)
|
||||||
from dzgui.init.prefix import get_version
|
from dzgui.init.prefix import get_version
|
||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.init.prereqs import has_steam_client
|
||||||
from dzgui.init.update import allow_updates, check_updates
|
|
||||||
|
|
||||||
from dzgui.strings import boot
|
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.deck import is_steam_deck, is_game_mode
|
||||||
from dzgui.util.localize import set_locale
|
from dzgui.util.localize import set_locale
|
||||||
from dzgui.util.symlink import rebuild_symlinks
|
from dzgui.util.symlink import rebuild_symlinks
|
||||||
from dzgui.util.strings import init, flags
|
from dzgui.util.strings import init, flags
|
||||||
|
|
||||||
from dzgui.views.base import App
|
from dzgui.views.base import App
|
||||||
|
from dzgui.views.dialogs.wizard import SetupWizard
|
||||||
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
from dzgui.views.dialogs.early_alert import EarlyAlertDialog
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -97,33 +95,28 @@ def main() -> None:
|
|||||||
|
|
||||||
if XDG.resolution.parent.is_dir() is False:
|
if XDG.resolution.parent.is_dir() is False:
|
||||||
make_parents(XDG.resolution)
|
make_parents(XDG.resolution)
|
||||||
# TODO: test
|
|
||||||
if XDG.debug.is_file() is False:
|
if XDG.debug.is_file() is False:
|
||||||
make_parents(XDG.debug)
|
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_steam_deck = is_steam_deck()
|
||||||
_is_game_mode = is_game_mode() if _is_steam_deck else False
|
_is_game_mode = is_game_mode() if _is_steam_deck else False
|
||||||
if _is_game_mode:
|
if _is_game_mode:
|
||||||
# NOTE: this may no longer be necessary on newer versions of SteamOS
|
# NOTE: this may no longer be necessary on newer versions of SteamOS
|
||||||
del os.environ["GTK_IM_MODULE"]
|
del os.environ["GTK_IM_MODULE"]
|
||||||
|
|
||||||
# TODO: test spamming timeout
|
if has_new_config(XDG.config) is False:
|
||||||
allow = allow_updates(ALLOW_UPDATES)
|
migrate_cols_file(XDG.columns)
|
||||||
if allow is True:
|
copy_state_files(xdg_paths["XDG_STATE_HOME"])
|
||||||
check_updates(version)
|
# 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:
|
if _is_steam_deck is False:
|
||||||
# TODO: sudo escalation dialog
|
# TODO: sudo escalation dialog
|
||||||
@@ -148,10 +141,9 @@ def main() -> None:
|
|||||||
is_debug=args.debug,
|
is_debug=args.debug,
|
||||||
coords=local_coords,
|
coords=local_coords,
|
||||||
version=version,
|
version=version,
|
||||||
allow_updates=allow,
|
|
||||||
paths=XDG,
|
paths=XDG,
|
||||||
|
update_available=update_available,
|
||||||
use_miles=use_miles,
|
use_miles=use_miles,
|
||||||
)
|
)
|
||||||
# TODO: drop allow updates
|
|
||||||
print(boot.all_ok)
|
print(boot.all_ok)
|
||||||
App(prefs)
|
App(prefs)
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Literal, TYPE_CHECKING
|
from typing import Any, Literal, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
@@ -52,7 +52,7 @@ class StoredFunc:
|
|||||||
|
|
||||||
|
|
||||||
class ThreadingManager:
|
class ThreadingManager:
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, controller: Union["Controller", None]) -> None:
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.jobs = 1
|
self.jobs = 1
|
||||||
self.cleanup_func: StoredFunc | None = None
|
self.cleanup_func: StoredFunc | None = None
|
||||||
@@ -69,7 +69,7 @@ class ThreadingManager:
|
|||||||
func.call()
|
func.call()
|
||||||
GLib.idle_add(self._destroy_on_idle)
|
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.wait_dialog = WaitDialog(
|
||||||
self.controller, dialog_str, jobs=self.jobs, show_cancel=show_cancel
|
self.controller, dialog_str, jobs=self.jobs, show_cancel=show_cancel
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
api_validation_error = (
|
api_validation_error = (
|
||||||
"API key validation error. Key was typed incorrectly or is defunct."
|
"API key validation error. Key was typed incorrectly or is defunct."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
api_popover = "API key validation failed."
|
||||||
@@ -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."
|
||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository.Gdk import SELECTION_CLIPBOARD # noqa E402
|
gi.require_version("Gdk", "3.0")
|
||||||
from gi.repository.Gtk import Clipboard # noqa E402
|
from gi.repository import Gtk, Gdk # noqa E402
|
||||||
|
|
||||||
|
|
||||||
def copy_clipboard(text: str) -> None:
|
def copy_clipboard(text: str) -> None:
|
||||||
clipboard = Clipboard.get(SELECTION_CLIPBOARD)
|
clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD)
|
||||||
clipboard.set_text(text, -1)
|
clipboard.set_text(text, -1)
|
||||||
@@ -44,30 +44,6 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
warnings.filterwarnings("ignore", ".*g_value_get_int", Warning)
|
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):
|
class OuterWindow(Gtk.Window):
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER)
|
super().__init__(title=APP_NAME, border_width=10, icon_name=APP_NAME_LOWER)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from dzgui.const.constants import (
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
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:
|
if TYPE_CHECKING:
|
||||||
from gi.repository import GLib
|
from gi.repository import GLib
|
||||||
|
|||||||
@@ -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.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.css import add_class, remove_class
|
||||||
from dzgui.util.strings import connect_panel, lan_panel
|
from dzgui.util.strings import connect_panel, lan_panel
|
||||||
|
from dzgui.strings.errors import api_popover
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -140,3 +142,98 @@ class PortEntry(ValidatedEntry):
|
|||||||
)
|
)
|
||||||
self.set_placeholder_text(lan_panel.placeholder)
|
self.set_placeholder_text(lan_panel.placeholder)
|
||||||
self.set_tooltip_text(lan_panel.entry_tooltip)
|
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)
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
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 # noqa E402
|
||||||
|
|
||||||
|
|
||||||
class Icon(Gtk.Image):
|
class Icon(Gtk.Image):
|
||||||
def __init__(self, name: str, l_margin=0) -> None:
|
def __init__(self, name: str, l_margin: int = 0) -> None:
|
||||||
super().__init__(icon_name=name,
|
super().__init__(
|
||||||
icon_size=Gtk.IconSize.BUTTON,
|
icon_name=name, icon_size=Gtk.IconSize.BUTTON, margin_start=l_margin
|
||||||
margin_start=l_margin
|
|
||||||
)
|
)
|
||||||
@@ -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)
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
from typing import Literal, TYPE_CHECKING
|
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.const.enum import ServerTab
|
||||||
from dzgui.util.clip import copy_clipboard
|
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.buttonbox import ButtonBox
|
||||||
from dzgui.views.components.filter_panel import FilterPanel
|
from dzgui.views.components.filter_panel import FilterPanel
|
||||||
from dzgui.views.components.mod_panel import ModSelectionPanel
|
from dzgui.views.components.mod_panel import ModSelectionPanel
|
||||||
from dzgui.views.components.buttons import RefreshButton, KeysButton
|
from dzgui.views.components.buttons import IconTextButton, RefreshButton, KeysButton
|
||||||
from dzgui.const.constants import NO_EXPAND, NO_FILL, FILL, NO_PADDING
|
|
||||||
|
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
@@ -40,14 +42,17 @@ class RightPanel(Gtk.Box):
|
|||||||
|
|
||||||
self.copying = False
|
self.copying = False
|
||||||
|
|
||||||
# TODO: strings
|
prefs = self.controller.get_prefs()
|
||||||
version = self.controller.get_prefs().version
|
version = prefs.version
|
||||||
|
update = prefs.update_available
|
||||||
|
|
||||||
self.version_label = Gtk.Label(
|
self.version_label = Gtk.Label(
|
||||||
label=version,
|
label=version,
|
||||||
hexpand=True,
|
hexpand=True,
|
||||||
vexpand=True,
|
vexpand=True,
|
||||||
halign=Gtk.Align.END,
|
halign=Gtk.Align.END,
|
||||||
valign=Gtk.Align.END,
|
valign=Gtk.Align.END,
|
||||||
|
# TODO: strings
|
||||||
tooltip_text="Click to copy to clipboard",
|
tooltip_text="Click to copy to clipboard",
|
||||||
)
|
)
|
||||||
eb = Gtk.EventBox(halign=Gtk.Align.END, valign=Gtk.Align.END)
|
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(el, NO_EXPAND, FILL, NO_PADDING)
|
||||||
|
|
||||||
self.pack_start(self.sel_panel, NO_EXPAND, NO_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(
|
def _on_server_page_changed(
|
||||||
self, emitter: "Emitter", page: "ServerTreeView"
|
self, emitter: "Emitter", page: "ServerTreeView"
|
||||||
|
|||||||
@@ -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)
|
|
||||||
@@ -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()
|
||||||
@@ -9,12 +9,9 @@ from dzgui.const.constants import (
|
|||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
FLATPAK_RUN_CMD,
|
|
||||||
FLATPAK_SANDBOX,
|
|
||||||
NO_EXPAND,
|
NO_EXPAND,
|
||||||
NO_FILL,
|
NO_FILL,
|
||||||
NO_PADDING,
|
NO_PADDING,
|
||||||
STEAM_CMD,
|
|
||||||
VIEW_CONCEAL,
|
VIEW_CONCEAL,
|
||||||
VIEW_REVEAL,
|
VIEW_REVEAL,
|
||||||
)
|
)
|
||||||
@@ -27,6 +24,7 @@ from dzgui.views.components.labels import LeftLabel
|
|||||||
from dzgui.views.components.eventbox import InfoEventBox
|
from dzgui.views.components.eventbox import InfoEventBox
|
||||||
from dzgui.views.components.buttons import WebButton
|
from dzgui.views.components.buttons import WebButton
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
|
from dzgui.views.components.misc import ClientCombo
|
||||||
from dzgui.views.dialogs.generic import ExceptionDialog
|
from dzgui.views.dialogs.generic import ExceptionDialog
|
||||||
|
|
||||||
|
|
||||||
@@ -101,29 +99,7 @@ class Options(Gtk.Box):
|
|||||||
Preferences.WINDOW,
|
Preferences.WINDOW,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: make this an abstract class
|
self.client_combo = ClientCombo()
|
||||||
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.connect("changed", self._on_client_changed)
|
self.client_combo.connect("changed", self._on_client_changed)
|
||||||
|
|
||||||
client_hbox = ShortHBox(self.client_combo)
|
client_hbox = ShortHBox(self.client_combo)
|
||||||
@@ -481,7 +457,7 @@ class Options(Gtk.Box):
|
|||||||
self.dayz_version_label.set_text(dayz_version)
|
self.dayz_version_label.set_text(dayz_version)
|
||||||
self.dayz_exp_version_label.set_text(dayz_exp_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"])
|
active_combo = query.get_client_index(config["client"])
|
||||||
self.client_combo.set_active(active_combo)
|
self.client_combo.set_active(active_combo)
|
||||||
|
|
||||||
|
|||||||
+7
-2
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0"
|
version = "7.0.0b1"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -54,8 +54,12 @@ lint = [
|
|||||||
"types-requests>=2.32.4",
|
"types-requests>=2.32.4",
|
||||||
"types-psutil==7.2.2.20260508",
|
"types-psutil==7.2.2.20260508",
|
||||||
]
|
]
|
||||||
|
build = [
|
||||||
|
"build==1.5.0",
|
||||||
|
]
|
||||||
|
|
||||||
dev = [
|
dev = [
|
||||||
"dzgui[docs,test,lint]"
|
"dzgui[docs,test,lint,build]"
|
||||||
]
|
]
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
@@ -79,6 +83,7 @@ line-length = 88
|
|||||||
python_version = "3.13"
|
python_version = "3.13"
|
||||||
disallow_untyped_defs = true
|
disallow_untyped_defs = true
|
||||||
warn_return_any = true
|
warn_return_any = true
|
||||||
|
exclude = ["lib/*"]
|
||||||
|
|
||||||
[[tool.mypy.overrides]]
|
[[tool.mypy.overrides]]
|
||||||
module = [
|
module = [
|
||||||
|
|||||||
Reference in new issue
Block a user