mirror of
https://github.com/aclist/dztui.git
synced 2026-11-06 18:10:58 +01:00
Compare commits
49
Commits
a8b7ea5008
...
7.0.0b13
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f839001097 | ||
|
|
ab2b651b6c | ||
|
|
e5934848c7 | ||
|
|
cab10efc96 | ||
|
|
0a4efba905 | ||
|
|
1e4e09e52c | ||
|
|
910c068109 | ||
|
|
1dd773a9bf | ||
|
|
f4d065d766 | ||
|
|
1fe66076c6 | ||
|
|
668ec1d7a0 | ||
|
|
412f0d3193 | ||
|
|
894a9b106d | ||
|
|
1b0a9c9a69 | ||
|
|
c4818d2097 | ||
|
|
69135150c7 | ||
|
|
4d8949427b | ||
|
|
0139f6bfba | ||
|
|
8d77d46eda | ||
|
|
179f92234a | ||
|
|
650a5ecd7b | ||
|
|
8cebb362e9 | ||
|
|
bfd1d3e0f9 | ||
|
|
e8b18d83a5 | ||
|
|
d305f47bb9 | ||
|
|
43f6c17c4b | ||
|
|
38bfef3836 | ||
|
|
c818945a28 | ||
|
|
5cb4f69500 | ||
|
|
cee9052dea | ||
|
|
b3b98aa407 | ||
|
|
b8230cbf43 | ||
|
|
4b16edbbee | ||
|
|
966d935032 | ||
|
|
8b8c6268ab | ||
|
|
513c445b24 | ||
|
|
55f82c2e91 | ||
|
|
ad589ba850 | ||
|
|
85f6fa053d | ||
|
|
f7a42e2cdb | ||
|
|
e1cb682e74 | ||
|
|
20dab38fff | ||
|
|
1ddc4a5ff1 | ||
|
|
d605447fca | ||
|
|
94fe3f9dff | ||
|
|
a966993999 | ||
|
|
8ee3b36816 | ||
|
|
3af53775cd | ||
|
|
e2d6a462db |
No files matched your search
@@ -57,6 +57,7 @@
|
|||||||
- Disable overlay scrollbars on server tables
|
- Disable overlay scrollbars on server tables
|
||||||
- Reduce size of geolocation DB on disk (~100MB)
|
- Reduce size of geolocation DB on disk (~100MB)
|
||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
## Dropped
|
## Dropped
|
||||||
- Debug mode
|
- Debug mode
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import re
|
||||||
|
from collections.abc import Iterator
|
||||||
|
from typing import Any
|
||||||
|
from warnings import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Use dzgui.api.steam.unsubscribe()")
|
||||||
|
class WorkshopACF:
|
||||||
|
def __init__(self, file: str) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.dict: dict[str, Any]
|
||||||
|
self.load(file)
|
||||||
|
|
||||||
|
def as_dict(self) -> dict[str, Any]:
|
||||||
|
return self.dict
|
||||||
|
|
||||||
|
def load(self, file: str) -> None:
|
||||||
|
delimiter = r"\t\t"
|
||||||
|
lines = []
|
||||||
|
with open(file, "r", encoding="utf-8") as f:
|
||||||
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
els = re.split(delimiter, line, maxsplit=1)
|
||||||
|
lines.append(els)
|
||||||
|
self.dict = self.parse(iter(lines))
|
||||||
|
|
||||||
|
def parse(self, lines: Iterator[list[str]]) -> dict[str, str]:
|
||||||
|
acf: dict[str, Any] = {}
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
line = next(lines)
|
||||||
|
if len(line) == 1:
|
||||||
|
key = line[0]
|
||||||
|
if key == "{":
|
||||||
|
continue
|
||||||
|
return acf
|
||||||
|
elif key == "}":
|
||||||
|
return acf
|
||||||
|
else:
|
||||||
|
key = self.dequote(key)
|
||||||
|
acf[key] = self.parse(lines)
|
||||||
|
elif len(line) == 2:
|
||||||
|
k, v = line
|
||||||
|
k = self.dequote(k)
|
||||||
|
v = self.dequote(v)
|
||||||
|
try:
|
||||||
|
n = list(acf.keys())[-1]
|
||||||
|
acf[n][k] = v
|
||||||
|
except Exception:
|
||||||
|
acf[k] = v
|
||||||
|
except StopIteration:
|
||||||
|
return acf
|
||||||
|
|
||||||
|
def unpack(self, d: dict, lines: list[Any] = []) -> str:
|
||||||
|
t1 = "AppWorkshop"
|
||||||
|
t2 = ("WorkshopItemsInstalled", "WorkshopItemDetails")
|
||||||
|
for k, v in d.items():
|
||||||
|
if type(v) is dict:
|
||||||
|
if k in t1:
|
||||||
|
self.indent = 0
|
||||||
|
elif k in t2:
|
||||||
|
self.indent = 1
|
||||||
|
else:
|
||||||
|
self.indent = 2
|
||||||
|
pref = "\t" * self.indent
|
||||||
|
lines.append(pref + self.enquote(k))
|
||||||
|
lines.append(pref + "{")
|
||||||
|
self.unpack(v, lines)
|
||||||
|
lines.append(pref + "}")
|
||||||
|
else:
|
||||||
|
pref = "\t" * (self.indent + 1)
|
||||||
|
lines.append(pref + self.enquote(k) + "\t\t" + self.enquote(v))
|
||||||
|
s = ""
|
||||||
|
for line in lines:
|
||||||
|
s += line + "\n"
|
||||||
|
return s
|
||||||
|
|
||||||
|
def to_file(self, file: str) -> None:
|
||||||
|
s = self.unpack(self.dict)
|
||||||
|
with open(file, "w") as f:
|
||||||
|
f.write(s)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def enquote(cls, s: str) -> str:
|
||||||
|
return f'"{s}"'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def dequote(cls, s: str) -> str:
|
||||||
|
return s.rstrip('"').lstrip('"')
|
||||||
|
|
||||||
|
def delete(self, modid: int) -> None:
|
||||||
|
del self.dict["AppWorkshop"]["WorkshopItemsInstalled"][modid]
|
||||||
|
del self.dict["AppWorkshop"]["WorkshopItemDetails"][modid]
|
||||||
@@ -12,6 +12,7 @@ from dzgui.api.servers import get_rules, fqip_to_record
|
|||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
|
DAYZ_COMMUNITY_ROOT,
|
||||||
LIBRARYFOLDERS_PATH,
|
LIBRARYFOLDERS_PATH,
|
||||||
WORKSHOP_PATH,
|
WORKSHOP_PATH,
|
||||||
)
|
)
|
||||||
@@ -50,6 +51,9 @@ def get_local_mods(workshop_path: Path) -> list[Path]:
|
|||||||
|
|
||||||
def is_mission(path: Path) -> bool:
|
def is_mission(path: Path) -> bool:
|
||||||
# TODO: parse integrity of other files
|
# TODO: parse integrity of other files
|
||||||
|
parent = path.parent.name
|
||||||
|
if parent != DAYZ_COMMUNITY_ROOT:
|
||||||
|
return False
|
||||||
file = path / "init.c"
|
file = path / "init.c"
|
||||||
return file.exists()
|
return file.exists()
|
||||||
|
|
||||||
@@ -81,6 +85,7 @@ def get_mod_size(path: Path) -> float:
|
|||||||
|
|
||||||
|
|
||||||
def get_custom_mods(path: Path) -> list[Any]:
|
def get_custom_mods(path: Path) -> list[Any]:
|
||||||
|
# TODO: rename this function for clarity ("get local vs. get custom")
|
||||||
mods = get_local_mods(path)
|
mods = get_local_mods(path)
|
||||||
# TODO: error handling
|
# TODO: error handling
|
||||||
return parse_mods(mods, use_custom=True)
|
return parse_mods(mods, use_custom=True)
|
||||||
|
|||||||
+51
-25
@@ -3,20 +3,25 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from typing import Union
|
||||||
|
from warnings import deprecated
|
||||||
|
|
||||||
from shlex import shlex
|
from shlex import shlex
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dzgui.init.prereqs import has_steam_client
|
from dzgui.init.prereqs import has_steam_client
|
||||||
|
from dzgui.api.mods import _hash
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
|
APPID_DAYZ,
|
||||||
APP_NAME,
|
APP_NAME,
|
||||||
DEBIAN_STEAM_PATH,
|
DEBIAN_STEAM_PATH,
|
||||||
DEFAULT_STEAM_PATH,
|
DEFAULT_STEAM_PATH,
|
||||||
FLATPAK_STEAM_PATH,
|
FLATPAK_STEAM_PATH,
|
||||||
UBUNTU_STEAM_PATH,
|
UBUNTU_STEAM_PATH,
|
||||||
|
REQUEST_TIMEOUT,
|
||||||
VDF_PATH,
|
VDF_PATH,
|
||||||
)
|
)
|
||||||
from dzgui.const.endpoints import STEAM_PUBLISHED_FILES
|
from dzgui.const.endpoints import SUB_ENDPOINT, STEAM_PUBLISHED_FILES, UNSUB_ENDPOINT
|
||||||
from dzgui.strings import wizard
|
from dzgui.strings import wizard
|
||||||
from dzgui.util.bash import concat_bash_args
|
from dzgui.util.bash import concat_bash_args
|
||||||
|
|
||||||
@@ -53,8 +58,6 @@ def get_steam_paths() -> list[tuple[Path, str]]:
|
|||||||
|
|
||||||
|
|
||||||
def concat_mods(mods: list[str]) -> str:
|
def concat_mods(mods: list[str]) -> str:
|
||||||
from dzgui.util.symlink import _hash
|
|
||||||
|
|
||||||
hashes = []
|
hashes = []
|
||||||
for mod in mods:
|
for mod in mods:
|
||||||
md5sum = _hash(mod)
|
md5sum = _hash(mod)
|
||||||
@@ -68,31 +71,21 @@ def get_local_signatures(version_file: Path) -> dict[str, int]:
|
|||||||
for line in lines:
|
for line in lines:
|
||||||
data = line.split(",")
|
data = line.split(",")
|
||||||
_id = data[0]
|
_id = data[0]
|
||||||
_hash = int(data[1])
|
mod_hash = int(data[1])
|
||||||
hashes[_id] = _hash
|
hashes[_id] = mod_hash
|
||||||
return hashes
|
return hashes
|
||||||
|
|
||||||
|
|
||||||
def enqueue_mod(mod: str, appid: int) -> None:
|
|
||||||
args = [
|
|
||||||
"steam",
|
|
||||||
f"steam://url/CommunityFilePage/{mod}+workshop_download_item",
|
|
||||||
str(appid),
|
|
||||||
mod,
|
|
||||||
]
|
|
||||||
subprocess.Popen(["/usr/bin/env", "bash", *args])
|
|
||||||
|
|
||||||
|
|
||||||
def get_needs_update(
|
def get_needs_update(
|
||||||
version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
|
version_file: Path, remote_hashes: list[tuple[str, str, int, int]]
|
||||||
) -> list[tuple[str, str, int, int]]:
|
) -> list[tuple[str, str, int, int]]:
|
||||||
local_hashes = get_local_signatures(version_file)
|
local_stamps = get_local_signatures(version_file)
|
||||||
needs_update: list[tuple[str, str, int, int]] = []
|
needs_update: list[tuple[str, str, int, int]] = []
|
||||||
for title, _id, _hash, size in remote_hashes:
|
for title, _id, stamp, size in remote_hashes:
|
||||||
if _id not in local_hashes:
|
if _id not in local_stamps:
|
||||||
needs_update.append((title, _id, _hash, size))
|
needs_update.append((title, _id, stamp, size))
|
||||||
elif _hash != local_hashes[_id]:
|
elif stamp != local_stamps[_id]:
|
||||||
needs_update.append((title, _id, _hash, size))
|
needs_update.append((title, _id, stamp, size))
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
return needs_update
|
return needs_update
|
||||||
@@ -162,9 +155,12 @@ def load_to_menu(client: str, appid: int, name: str, mods: list[str]) -> int:
|
|||||||
proc = subprocess.run([*client_args, *params])
|
proc = subprocess.run([*client_args, *params])
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
def launch_offline(client: str, appid: int, name: str, mods: list[str], mission: str) -> int:
|
|
||||||
|
def launch_offline(
|
||||||
|
client: str, appid: int, name: str, mods: list[str], mission: str
|
||||||
|
) -> int:
|
||||||
"""Launch offline with specific mods/missions"""
|
"""Launch offline with specific mods/missions"""
|
||||||
concat = concat_mods(mods)
|
symlinks = ";".join(mods)
|
||||||
client_args = concat_bash_args(client)
|
client_args = concat_bash_args(client)
|
||||||
params = [
|
params = [
|
||||||
"-applaunch",
|
"-applaunch",
|
||||||
@@ -173,12 +169,13 @@ def launch_offline(client: str, appid: int, name: str, mods: list[str], mission:
|
|||||||
"-nosplash",
|
"-nosplash",
|
||||||
"-skipintro",
|
"-skipintro",
|
||||||
f"-name={name}",
|
f"-name={name}",
|
||||||
f"-mod={concat}",
|
f"-mod={symlinks}",
|
||||||
f"-mission={mission}"
|
f"-mission={mission}",
|
||||||
]
|
]
|
||||||
proc = subprocess.run([*client_args, *params])
|
proc = subprocess.run([*client_args, *params])
|
||||||
return proc.returncode
|
return proc.returncode
|
||||||
|
|
||||||
|
|
||||||
def find_user_id(path: Path) -> str | None:
|
def find_user_id(path: Path) -> str | None:
|
||||||
resolved_path = path / "config" / "loginusers.vdf"
|
resolved_path = path / "config" / "loginusers.vdf"
|
||||||
try:
|
try:
|
||||||
@@ -233,6 +230,29 @@ def vdf2json(path: Path) -> str:
|
|||||||
jbuf += "\n"
|
jbuf += "\n"
|
||||||
|
|
||||||
|
|
||||||
|
def update_workshop(key: str, mod: int, endpoint: str) -> None:
|
||||||
|
payload: dict[str, Union[int, str]] = {
|
||||||
|
"publishedfileid": mod,
|
||||||
|
"appid": APPID_DAYZ,
|
||||||
|
"key": key,
|
||||||
|
"list_type": 1,
|
||||||
|
"notify_client": 1,
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
res = requests.post(endpoint, params=payload, timeout=REQUEST_TIMEOUT)
|
||||||
|
res.raise_for_status()
|
||||||
|
except Exception as e:
|
||||||
|
logger.critical(e)
|
||||||
|
|
||||||
|
|
||||||
|
def subscribe(key: str, mod: int) -> None:
|
||||||
|
update_workshop(key, mod, SUB_ENDPOINT)
|
||||||
|
|
||||||
|
|
||||||
|
def unsubscribe(key: str, mod: int) -> None:
|
||||||
|
update_workshop(key, mod, UNSUB_ENDPOINT)
|
||||||
|
|
||||||
|
|
||||||
def gen_shortcut() -> None:
|
def gen_shortcut() -> None:
|
||||||
# TODO:
|
# TODO:
|
||||||
"""
|
"""
|
||||||
@@ -245,3 +265,9 @@ def gen_shortcut() -> None:
|
|||||||
# or get right-most 32 bits
|
# or get right-most 32 bits
|
||||||
# STEAMID_64 & 0xFFFFFFFF
|
# STEAMID_64 & 0xFFFFFFFF
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated("Use subscribe()")
|
||||||
|
def enqueue_mod(client: str, mod: str, appid: int) -> None:
|
||||||
|
client_args = concat_bash_args(client)
|
||||||
|
subprocess.Popen([*client_args, "+workshop_download_item", str(appid), mod])
|
||||||
@@ -4,6 +4,7 @@ UDP_PORT = 27016
|
|||||||
VM_FILE = "/proc/sys/vm/max_map_count"
|
VM_FILE = "/proc/sys/vm/max_map_count"
|
||||||
MIN_COUNT = 1048576
|
MIN_COUNT = 1048576
|
||||||
|
|
||||||
|
RATE_LIMIT_THRESHOLD = 3
|
||||||
REQUEST_TIMEOUT = 10
|
REQUEST_TIMEOUT = 10
|
||||||
|
|
||||||
APPNAME_DAYZ = "DayZ"
|
APPNAME_DAYZ = "DayZ"
|
||||||
@@ -32,10 +33,12 @@ HEX_GREEN = "#32CD32"
|
|||||||
HEX_RED = "#FF0000"
|
HEX_RED = "#FF0000"
|
||||||
HEX_ORANGE = "#FFAC1C"
|
HEX_ORANGE = "#FFAC1C"
|
||||||
|
|
||||||
|
# FIXME: normalize icon names
|
||||||
CARET_DOWN = "go-down-symbolic"
|
CARET_DOWN = "go-down-symbolic"
|
||||||
CARET_UP = "go-up-symbolic"
|
CARET_UP = "go-up-symbolic"
|
||||||
CLIPBOARD = "edit-copy-symbolic"
|
CLIPBOARD = "edit-copy-symbolic"
|
||||||
FOLDER = "folder-symbolic"
|
FOLDER = "folder-symbolic"
|
||||||
|
EDIT_DELETE = "edit-delete-symbolic"
|
||||||
ERROR = "dialog-error-symbolic"
|
ERROR = "dialog-error-symbolic"
|
||||||
HELP_BUBBLE = "help-about-symbolic"
|
HELP_BUBBLE = "help-about-symbolic"
|
||||||
INPUT_KEYBOARD = "input-keyboard-symbolic"
|
INPUT_KEYBOARD = "input-keyboard-symbolic"
|
||||||
@@ -81,3 +84,5 @@ LOG_FILTERS = ("CRITICAL", "WARNING", "INFO", "DEBUG")
|
|||||||
TMP_PATH = "/tmp"
|
TMP_PATH = "/tmp"
|
||||||
TMP_TARBALL = "/tmp/dzgui.tar.gz"
|
TMP_TARBALL = "/tmp/dzgui.tar.gz"
|
||||||
TMP_EXE = "/tmp/dzgui/dzgui"
|
TMP_EXE = "/tmp/dzgui/dzgui"
|
||||||
|
|
||||||
|
DAYZ_COMMUNITY_ROOT = "Missions"
|
||||||
@@ -1,6 +1,11 @@
|
|||||||
# internal
|
# internal
|
||||||
STEAM_PUBLISHED_FILES = "https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1/?format=json"
|
STEAM_PUBLISHED_FILES = (
|
||||||
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1/?"
|
"https://api.steampowered.com/ISteamRemoteStorage/GetPublishedFileDetails/v1"
|
||||||
|
)
|
||||||
|
STEAM_SERVERS = "https://api.steampowered.com/IGameServersService/GetServerList/v1"
|
||||||
|
SUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Subscribe/v1"
|
||||||
|
UNSUB_ENDPOINT = "https://api.steampowered.com/IPublishedFileService/Unsubscribe/v1"
|
||||||
|
|
||||||
BM_SERVERS = "https://api.battlemetrics.com/servers?"
|
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"
|
||||||
|
|||||||
+6
-5
@@ -168,7 +168,7 @@ class ContextMenu(EnumWithAttrs):
|
|||||||
COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
|
COPY_LOG_CLIPBOARD = {"label": strings.copy_log}
|
||||||
COPY_SERVER_IP = {"label": strings.copy_ip}
|
COPY_SERVER_IP = {"label": strings.copy_ip}
|
||||||
COPY_SERVER_NAME = {"label": strings.copy_name}
|
COPY_SERVER_NAME = {"label": strings.copy_name}
|
||||||
DELETE_MOD = {"label": strings.delete_mod}
|
UNSUB_MOD = {"label": strings.unsub_mod}
|
||||||
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
OPEN_WORKSHOP = {"label": strings.open_workshop}
|
||||||
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
REFRESH_PLAYERS = {"label": strings.refresh_players}
|
||||||
REMOVE_HISTORY = {"label": strings.remove_history}
|
REMOVE_HISTORY = {"label": strings.remove_history}
|
||||||
@@ -184,7 +184,8 @@ class ContextMenuGroup(Enum):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
SERVER_MOD = (ContextMenu.OPEN_WORKSHOP,)
|
||||||
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.DELETE_MOD)
|
MOD = (ContextMenu.OPEN_WORKSHOP, ContextMenu.UNSUB_MOD)
|
||||||
|
MOD_OFFLINE = (None,)
|
||||||
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
LOG = (ContextMenu.COPY_LOG_CLIPBOARD,)
|
||||||
SERVER_BROWSER = (
|
SERVER_BROWSER = (
|
||||||
ContextMenu.CONNECT,
|
ContextMenu.CONNECT,
|
||||||
@@ -249,9 +250,9 @@ class ModButton(EnumWithAttrs):
|
|||||||
"label": strings.mod_panel.unhighlight_stale,
|
"label": strings.mod_panel.unhighlight_stale,
|
||||||
"tooltip": strings.mod_panel.unhighlight_stale_tooltip,
|
"tooltip": strings.mod_panel.unhighlight_stale_tooltip,
|
||||||
}
|
}
|
||||||
DELETE_SELECTED = {
|
UNSUB_SELECTED = {
|
||||||
"label": strings.mod_panel.delete_selected,
|
"label": strings.mod_panel.unsub_selected,
|
||||||
"tooltip": strings.mod_panel.delete_selected_tooltip,
|
"tooltip": strings.mod_panel.unsub_selected_tooltip,
|
||||||
}
|
}
|
||||||
SELECT_STALE = {
|
SELECT_STALE = {
|
||||||
"label": strings.mod_panel.select_stale,
|
"label": strings.mod_panel.select_stale,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from gi.repository import Gdk, GObject # noqa E402
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.const.enum import NotebookPage, ServerTab
|
from dzgui.const.enum import NotebookPage, ServerTab
|
||||||
from dzgui.model.model_factory import FastInsertListStore
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
|
from dzgui.views.pages.offline import FolderHBox
|
||||||
|
|
||||||
|
|
||||||
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal
|
# TODO: if servers fail to load, may leave dangling widgets waiting for a signal
|
||||||
@@ -160,6 +161,14 @@ class Emitter(GObject.GObject):
|
|||||||
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str) -> None:
|
def custom_mods_loaded(self, store: "FastInsertListStore", folder: str) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(object,))
|
||||||
|
def custom_mods_unloaded(self, widget: "FolderHBox") -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str, bool))
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=(str, bool))
|
||||||
def custom_mission_loaded(self, folder: str, is_valid: bool) -> None:
|
def custom_mission_loaded(self, folder: str, is_valid: bool) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@GObject.Signal(flags=GObject.SignalFlags.RUN_LAST, arg_types=())
|
||||||
|
def invalid_custom_mods(self) -> None:
|
||||||
|
pass
|
||||||
@@ -233,7 +233,7 @@ class Controller(GObject.GObject):
|
|||||||
mod_man = self.mediator.modtreeview.get_mod_man()
|
mod_man = self.mediator.modtreeview.get_mod_man()
|
||||||
mod_man.toggle_mod_selection(state)
|
mod_man.toggle_mod_selection(state)
|
||||||
|
|
||||||
def delete_mods(
|
def unsub_mods(
|
||||||
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
|
self, treeview: Union["ModTreeView", "OfflineModTreeView", None] = None
|
||||||
) -> None:
|
) -> None:
|
||||||
if treeview is None:
|
if treeview is None:
|
||||||
@@ -241,7 +241,7 @@ class Controller(GObject.GObject):
|
|||||||
else:
|
else:
|
||||||
view = treeview
|
view = treeview
|
||||||
mod_man = view.get_mod_man()
|
mod_man = view.get_mod_man()
|
||||||
mod_man.delete_mods()
|
mod_man.unsub_mods()
|
||||||
|
|
||||||
def get_mod_store(self) -> Gtk.TreeModel | None:
|
def get_mod_store(self) -> Gtk.TreeModel | None:
|
||||||
return self.mediator.modtreeview.get_model()
|
return self.mediator.modtreeview.get_model()
|
||||||
@@ -523,11 +523,11 @@ class Controller(GObject.GObject):
|
|||||||
ind = self.config_man.get_start_tab()
|
ind = self.config_man.get_start_tab()
|
||||||
self.get_servers().notebook.set_current_page(ind)
|
self.get_servers().notebook.set_current_page(ind)
|
||||||
|
|
||||||
def update_and_load_to_menu(self, raise_window: bool) -> None:
|
def update_and_load_to_menu(self) -> None:
|
||||||
self.connection_man.update_and_connect(raise_window, menu_only=True)
|
self.connection_man.update_and_connect(menu_only=True)
|
||||||
|
|
||||||
def update_and_connect(self, raise_window: bool) -> None:
|
def update_and_connect(self) -> None:
|
||||||
self.connection_man.update_and_connect(raise_window)
|
self.connection_man.update_and_connect()
|
||||||
|
|
||||||
def update_status(self) -> None:
|
def update_status(self) -> None:
|
||||||
self.mediator.preconnect.mark_finished()
|
self.mediator.preconnect.mark_finished()
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
- Preboot progress dialog
|
- Preboot progress dialog
|
||||||
- Choose to jump into splash screen instead of server
|
- Choose to jump into splash screen instead of server
|
||||||
- Collapsible connection panel
|
- Collapsible connection panel
|
||||||
|
- Play offline (load mods directly)
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
- Conform to PEP 440 versioning for beta versions
|
- Conform to PEP 440 versioning for beta versions
|
||||||
@@ -56,6 +57,7 @@
|
|||||||
- Disable overlay scrollbars on server tables
|
- Disable overlay scrollbars on server tables
|
||||||
- Reduce size of geolocation DB on disk (~100MB)
|
- Reduce size of geolocation DB on disk (~100MB)
|
||||||
- Enable LAN page Empty/Full filters on first run of app
|
- Enable LAN page Empty/Full filters on first run of app
|
||||||
|
- Propagate subscribed mods to Steam client
|
||||||
|
|
||||||
## Dropped
|
## Dropped
|
||||||
- Debug mode
|
- Debug mode
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import dzgui.api.servers as Servers
|
|||||||
|
|
||||||
from dzgui.api.steam import (
|
from dzgui.api.steam import (
|
||||||
connect,
|
connect,
|
||||||
enqueue_mod,
|
|
||||||
get_remote_signatures,
|
get_remote_signatures,
|
||||||
get_needs_update,
|
get_needs_update,
|
||||||
load_to_menu,
|
load_to_menu,
|
||||||
|
subscribe,
|
||||||
)
|
)
|
||||||
|
|
||||||
from dzgui.api.mods import (
|
from dzgui.api.mods import (
|
||||||
@@ -30,6 +30,7 @@ from dzgui.const.constants import (
|
|||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
|
RATE_LIMIT_THRESHOLD,
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import NotebookPage, Preferences
|
from dzgui.const.enum import NotebookPage, Preferences
|
||||||
from dzgui.init.proc import is_dayz_running, is_steam_running
|
from dzgui.init.proc import is_dayz_running, is_steam_running
|
||||||
@@ -88,6 +89,7 @@ class ConnectionManager:
|
|||||||
self.record: Record
|
self.record: Record
|
||||||
self.workshop: Path
|
self.workshop: Path
|
||||||
|
|
||||||
|
self.client: str
|
||||||
self.remote_mod_ids: list[str] = []
|
self.remote_mod_ids: list[str] = []
|
||||||
self.missing_mods: list[tuple[str, str, int, int]] = []
|
self.missing_mods: list[tuple[str, str, int, int]] = []
|
||||||
|
|
||||||
@@ -171,6 +173,7 @@ class ConnectionManager:
|
|||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
running = is_steam_running(client)
|
running = is_steam_running(client)
|
||||||
steam_proc = SteamProcess(client_name, running)
|
steam_proc = SteamProcess(client_name, running)
|
||||||
|
self.client = client
|
||||||
|
|
||||||
game_mode = prefs.is_game_mode
|
game_mode = prefs.is_game_mode
|
||||||
|
|
||||||
@@ -263,11 +266,10 @@ class ConnectionManager:
|
|||||||
def _connect_steam(self, menu_only: bool) -> None:
|
def _connect_steam(self, menu_only: bool) -> None:
|
||||||
addr = f"{self.record.ip}:{self.record.gameport}"
|
addr = f"{self.record.ip}:{self.record.gameport}"
|
||||||
playername = self.controller.query_config(Preferences.NAME)
|
playername = self.controller.query_config(Preferences.NAME)
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
|
||||||
if menu_only:
|
if menu_only:
|
||||||
rc = load_to_menu(client, self.appid, playername, self.remote_mod_ids)
|
rc = load_to_menu(self.client, self.appid, playername, self.remote_mod_ids)
|
||||||
else:
|
else:
|
||||||
rc = connect(client, addr, self.appid, playername, self.remote_mod_ids)
|
rc = connect(self.client, addr, self.appid, playername, self.remote_mod_ids)
|
||||||
if rc != 0:
|
if rc != 0:
|
||||||
# TODO: log/pop the error
|
# TODO: log/pop the error
|
||||||
func = StoredFunc(self.controller.update_status)
|
func = StoredFunc(self.controller.update_status)
|
||||||
@@ -277,6 +279,7 @@ class ConnectionManager:
|
|||||||
self.thread_man.show_cancel(False)
|
self.thread_man.show_cancel(False)
|
||||||
self.thread_man.update_dialog(waiting_for_launch)
|
self.thread_man.update_dialog(waiting_for_launch)
|
||||||
while True:
|
while True:
|
||||||
|
# FIXME: cancel should not be visible here per setting above
|
||||||
if self.controller.is_cancel_pending():
|
if self.controller.is_cancel_pending():
|
||||||
# TODO: some facility to also close spawned steam process
|
# TODO: some facility to also close spawned steam process
|
||||||
return
|
return
|
||||||
@@ -291,19 +294,16 @@ class ConnectionManager:
|
|||||||
self.controller.add_to_history(self.history, self.record)
|
self.controller.add_to_history(self.history, self.record)
|
||||||
self.controller.open_page(NotebookPage.SERVERS)
|
self.controller.open_page(NotebookPage.SERVERS)
|
||||||
|
|
||||||
def _update_mods(self, raise_window: bool, menu_only: bool = False) -> None:
|
def _update_mods(self, menu_only: bool = False) -> None:
|
||||||
# NOTE: fast enqueue all mods in auto mode
|
|
||||||
prefs = self.controller.get_prefs()
|
prefs = self.controller.get_prefs()
|
||||||
|
config_man = self.controller.get_config_man()
|
||||||
|
key = config_man.lookup(Preferences.STEAM)
|
||||||
|
|
||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
if self.controller.is_cancel_pending():
|
if self.controller.is_cancel_pending():
|
||||||
return
|
return
|
||||||
enqueue_mod(mod, self.appid)
|
subscribe(key, int(mod))
|
||||||
time.sleep(3)
|
time.sleep(RATE_LIMIT_THRESHOLD)
|
||||||
|
|
||||||
if raise_window is True:
|
|
||||||
logger.info("Bringing window to foreground")
|
|
||||||
GLib.idle_add(self.controller.present_window)
|
|
||||||
|
|
||||||
for title, mod, stamp, size in self.missing_mods:
|
for title, mod, stamp, size in self.missing_mods:
|
||||||
mod_path = self.workshop / mod
|
mod_path = self.workshop / mod
|
||||||
@@ -327,8 +327,8 @@ class ConnectionManager:
|
|||||||
self._connect_steam(menu_only)
|
self._connect_steam(menu_only)
|
||||||
|
|
||||||
@call_on_thread(waiting_for_mods, show_cancel=True)
|
@call_on_thread(waiting_for_mods, show_cancel=True)
|
||||||
def update_and_connect(self, raise_window: bool, menu_only: bool = False) -> None:
|
def update_and_connect(self, menu_only: bool = False) -> None:
|
||||||
if len(self.missing_mods) > 0:
|
if len(self.missing_mods) > 0:
|
||||||
self._update_mods(raise_window, menu_only)
|
self._update_mods(menu_only)
|
||||||
else:
|
else:
|
||||||
self._connect_steam(menu_only)
|
self._connect_steam(menu_only)
|
||||||
@@ -80,8 +80,8 @@ class ContextMenuManager:
|
|||||||
|
|
||||||
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
if isinstance(self.treeview, (ModTreeView, OfflineModTreeView)):
|
||||||
match action:
|
match action:
|
||||||
case ContextMenu.DELETE_MOD:
|
case ContextMenu.UNSUB_MOD:
|
||||||
self.controller.delete_mods(self.treeview)
|
self.controller.unsub_mods(self.treeview)
|
||||||
case ContextMenu.OPEN_WORKSHOP:
|
case ContextMenu.OPEN_WORKSHOP:
|
||||||
self.open_mod_page()
|
self.open_mod_page()
|
||||||
|
|
||||||
|
|||||||
@@ -89,8 +89,9 @@ class FilterManager:
|
|||||||
continue
|
continue
|
||||||
self.append_map([m])
|
self.append_map([m])
|
||||||
|
|
||||||
|
# TODO: currently unused
|
||||||
def get_unique_maps(self) -> list[str]:
|
def get_unique_maps(self) -> list[str]:
|
||||||
return [row[0] for row in self.map_store if row != "All maps"]
|
return [row[0] for row in self.map_store if row[0] != "All maps"]
|
||||||
|
|
||||||
def get_all_filters(self) -> tuple:
|
def get_all_filters(self) -> tuple:
|
||||||
map_name = self.get_active_map_name()
|
map_name = self.get_active_map_name()
|
||||||
|
|||||||
+29
-16
@@ -1,17 +1,22 @@
|
|||||||
import logging
|
import logging
|
||||||
import shutil
|
import time
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from dzgui.api.steam import unsubscribe
|
||||||
from dzgui.api.mods import (
|
from dzgui.api.mods import (
|
||||||
get_delimited_mods,
|
get_delimited_mods,
|
||||||
get_local_mod_path,
|
|
||||||
find_stale_mods,
|
find_stale_mods,
|
||||||
_hash,
|
_hash,
|
||||||
remove_stale_signatures,
|
remove_stale_signatures,
|
||||||
)
|
)
|
||||||
from dzgui.const.constants import APP_NAME, APPID_DAYZ, APPID_DAYZ_EXP
|
from dzgui.const.constants import (
|
||||||
|
APP_NAME,
|
||||||
|
APPID_DAYZ,
|
||||||
|
APPID_DAYZ_EXP,
|
||||||
|
RATE_LIMIT_THRESHOLD,
|
||||||
|
)
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
from dzgui.model.model_factory import FastInsertListStore, ModelFactory
|
||||||
@@ -66,6 +71,9 @@ class ModManager:
|
|||||||
func = StoredFunc(self._on_mods_loaded)
|
func = StoredFunc(self._on_mods_loaded)
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
|
def set_store(self, store: "FastInsertListStore") -> None:
|
||||||
|
self.store = store
|
||||||
|
|
||||||
def _on_mods_loaded(self) -> None:
|
def _on_mods_loaded(self) -> None:
|
||||||
self.treeview.set_model(self.store)
|
self.treeview.set_model(self.store)
|
||||||
if self.store is None:
|
if self.store is None:
|
||||||
@@ -74,7 +82,7 @@ class ModManager:
|
|||||||
total_mods = len(self.store)
|
total_mods = len(self.store)
|
||||||
self.emitter.emit("mods_updated", msg, total_mods)
|
self.emitter.emit("mods_updated", msg, total_mods)
|
||||||
|
|
||||||
def delete_mods(self) -> None:
|
def unsub_mods(self) -> None:
|
||||||
sel = self.treeview.get_selection()
|
sel = self.treeview.get_selection()
|
||||||
model, pathlist = sel.get_selected_rows()
|
model, pathlist = sel.get_selected_rows()
|
||||||
# NOTE: reverse when multiple selection
|
# NOTE: reverse when multiple selection
|
||||||
@@ -85,7 +93,7 @@ class ModManager:
|
|||||||
continue
|
continue
|
||||||
mod, _iter = res
|
mod, _iter = res
|
||||||
mods.append((mod, _iter))
|
mods.append((mod, _iter))
|
||||||
self.delete_mods_on_system(mods)
|
self.unsub_all_mods(mods)
|
||||||
|
|
||||||
def get_mod_from_tree_path(
|
def get_mod_from_tree_path(
|
||||||
self, tree_path: Gtk.TreePath
|
self, tree_path: Gtk.TreePath
|
||||||
@@ -98,24 +106,28 @@ class ModManager:
|
|||||||
return mod, tree_iter
|
return mod, tree_iter
|
||||||
|
|
||||||
@call_on_thread(dialogs.deleting_mods)
|
@call_on_thread(dialogs.deleting_mods)
|
||||||
def delete_mods_on_system(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
def unsub_all_mods(self, mods: list[tuple[str, Gtk.TreeIter]]) -> None:
|
||||||
for mod, _iter in mods:
|
for mod, _iter in mods:
|
||||||
self.delete_single_mod(mod)
|
self.unsub_atomic_mod(mod)
|
||||||
|
|
||||||
iters = [_iter for mod, _iter in mods]
|
iters = [_iter for mod, _iter in mods]
|
||||||
func = StoredFunc(self._on_mods_deleted, iters)
|
func = StoredFunc(self._on_mods_unsubbed, iters)
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def delete_single_mod(self, mod: str) -> None:
|
def unsub_atomic_mod(self, mod: str) -> None:
|
||||||
|
config_man = self.controller.get_config_man()
|
||||||
|
key = config_man.lookup(Preferences.STEAM)
|
||||||
|
unsubscribe(key, int(mod))
|
||||||
|
|
||||||
steam_path = Path(self.path)
|
steam_path = Path(self.path)
|
||||||
mods_path = get_local_mod_path(steam_path)
|
|
||||||
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
app_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
|
|
||||||
md5 = _hash(mod)
|
try:
|
||||||
symlink = app_path / md5
|
md5 = _hash(mod)
|
||||||
symlink.unlink()
|
symlink = app_path / md5
|
||||||
shutil.rmtree(mods_path / mod)
|
symlink.unlink()
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
# NOTE: second pass to unlink DAYZ_EXP mods
|
# NOTE: second pass to unlink DAYZ_EXP mods
|
||||||
# TODO: test this with working APPID_DAYZ_EXP installation
|
# TODO: test this with working APPID_DAYZ_EXP installation
|
||||||
try:
|
try:
|
||||||
@@ -124,8 +136,9 @@ class ModManager:
|
|||||||
symlink.unlink()
|
symlink.unlink()
|
||||||
except PeFile.AppNotInstalledError:
|
except PeFile.AppNotInstalledError:
|
||||||
pass
|
pass
|
||||||
|
time.sleep(RATE_LIMIT_THRESHOLD)
|
||||||
|
|
||||||
def _on_mods_deleted(self, iters: list[Gtk.TreeIter]) -> None:
|
def _on_mods_unsubbed(self, iters: list[Gtk.TreeIter]) -> None:
|
||||||
if self.store is None:
|
if self.store is None:
|
||||||
return
|
return
|
||||||
for _iter in iters:
|
for _iter in iters:
|
||||||
|
|||||||
+73
-38
@@ -1,17 +1,25 @@
|
|||||||
|
import logging
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import Callable, TYPE_CHECKING, Union
|
||||||
|
|
||||||
|
import dzgui.api.pefile as PeFile
|
||||||
from dzgui.api.mods import is_mission, get_custom_mods
|
from dzgui.api.mods import is_mission, get_custom_mods
|
||||||
from dzgui.api.steam import launch_offline
|
from dzgui.api.steam import launch_offline
|
||||||
|
from dzgui.const.constants import APP_NAME, APPID_DAYZ_EXP
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
|
from dzgui.init.proc import is_dayz_running
|
||||||
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
from dzgui.managers.threading import call_on_thread, StoredFunc, ThreadingManager
|
||||||
from dzgui.model.model_factory import ModelFactory
|
from dzgui.model.model_factory import ModelFactory
|
||||||
from dzgui.strings import dialogs
|
from dzgui.strings import dialogs
|
||||||
from dzgui.util.symlink import clone_symlinks
|
from dzgui.util.symlink import create_custom_symlinks, rebuild_symlinks, symlink_mission
|
||||||
from dzgui.views.dialogs.filepicker import FolderPicker
|
from dzgui.views.dialogs.filepicker import FolderPicker
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
|
|
||||||
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
class OfflineManager:
|
class OfflineManager:
|
||||||
@@ -26,74 +34,101 @@ class OfflineManager:
|
|||||||
self.thread_man = ThreadingManager(controller)
|
self.thread_man = ThreadingManager(controller)
|
||||||
|
|
||||||
self.appid: int
|
self.appid: int
|
||||||
self.mission_folder: str
|
|
||||||
self.local_mods: list[str] | None
|
self.local_mods: list[str] | None
|
||||||
self.custom_mods: list[str] | None
|
self.custom_mods: list[str] | None
|
||||||
|
|
||||||
|
# TODO: more robust test
|
||||||
|
def is_custom_folder_valid(self, folder: str) -> bool:
|
||||||
|
return Path(folder).exists()
|
||||||
|
|
||||||
|
# TODO: set properties as members of offline manager on demand and check them here
|
||||||
|
# rather than passing strings again
|
||||||
|
def is_mission_valid(self, folder: str) -> bool:
|
||||||
|
return is_mission(Path(folder))
|
||||||
|
|
||||||
def get_mission(self) -> None:
|
def get_mission(self) -> None:
|
||||||
folder = self.open_folderpicker(dialogs.mission_dialog)
|
folder = self.open_folderpicker(dialogs.mission_dialog)
|
||||||
if folder is None:
|
if folder is None:
|
||||||
return
|
return
|
||||||
is_valid = is_mission(folder)
|
is_valid = is_mission(folder)
|
||||||
# TODO: stop storing values in here
|
|
||||||
if is_valid:
|
|
||||||
# TODO: str
|
|
||||||
self.mission_folder = str(folder)
|
|
||||||
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
self.emitter.emit("custom_mission_loaded", str(folder), is_valid)
|
||||||
|
|
||||||
def get_custom_mods(self, local_mods: list[str]) -> None:
|
def find_custom_mods(self, callback: Callable) -> None:
|
||||||
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
folder = self.open_folderpicker(dialogs.custom_mod_dialog)
|
||||||
|
window = self.controller.get_window()
|
||||||
|
window.set_sensitive(False)
|
||||||
if folder is None:
|
if folder is None:
|
||||||
|
window.set_sensitive(True)
|
||||||
return
|
return
|
||||||
self.parse_custom_mods(local_mods, folder)
|
# NOTE: starts spinner in calling UI
|
||||||
|
callback()
|
||||||
|
self.parse_custom_mods(folder)
|
||||||
|
|
||||||
@call_on_thread(dialogs.parsing_mods)
|
@call_on_thread(dialogs.parsing_mods, show_dialog=False)
|
||||||
def parse_custom_mods(self, local_mods: list[str], folder: str) -> None:
|
def parse_custom_mods(self, folder: str) -> None:
|
||||||
mods = get_custom_mods(Path(folder))
|
mods = get_custom_mods(Path(folder))
|
||||||
store = ModelFactory().make_mod_store()
|
store = ModelFactory().make_mod_store()
|
||||||
store.extend(mods)
|
store.extend(mods)
|
||||||
|
func = StoredFunc(self.post_mod_loading, store, folder)
|
||||||
func = StoredFunc(
|
|
||||||
lambda: self.emitter.emit("custom_mods_loaded", store, folder)
|
|
||||||
)
|
|
||||||
self.thread_man.set_cleanup_func(func)
|
self.thread_man.set_cleanup_func(func)
|
||||||
|
|
||||||
def setup(
|
def post_mod_loading(self, store: "FastInsertListStore", folder: str) -> None:
|
||||||
self,
|
window = self.controller.get_window()
|
||||||
appid: int,
|
window.set_sensitive(True)
|
||||||
mission: str = "",
|
self.emitter.emit("custom_mods_loaded", store, folder)
|
||||||
local_mods: list[str] | None = None,
|
|
||||||
custom_mods: list[str] | None = None,
|
|
||||||
) -> None:
|
|
||||||
|
|
||||||
self.appid = appid
|
|
||||||
self.local_mods = local_mods
|
|
||||||
self.custom_mods = custom_mods
|
|
||||||
|
|
||||||
self.launch()
|
|
||||||
|
|
||||||
@call_on_thread(dialogs.waiting_for_launch)
|
@call_on_thread(dialogs.waiting_for_launch)
|
||||||
def launch(self) -> None:
|
def launch(
|
||||||
|
self,
|
||||||
|
appid: int,
|
||||||
|
mission: str,
|
||||||
|
local_mods: list[str],
|
||||||
|
custom_folder: str,
|
||||||
|
custom_mods: list[str],
|
||||||
|
) -> None:
|
||||||
|
|
||||||
|
# NOTE: local_mods and custom_mods are lists of symlinks
|
||||||
client = self.controller.query_config(Preferences.CLIENT)
|
client = self.controller.query_config(Preferences.CLIENT)
|
||||||
name = self.controller.query_config(Preferences.NAME)
|
name = self.controller.query_config(Preferences.NAME)
|
||||||
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
|
|
||||||
new_symlinks: list[str] = []
|
|
||||||
|
|
||||||
combined_mods: list[str] = []
|
combined_mods: list[str] = []
|
||||||
if self.local_mods is not None:
|
|
||||||
combined_mods.extend(self.local_mods)
|
|
||||||
|
|
||||||
if self.custom_mods is not None:
|
if len(local_mods) > 0:
|
||||||
# TODO: new function that creates symlinks in game path
|
combined_mods.extend(local_mods)
|
||||||
# based on selected mods
|
config = self.controller.get_prefs().paths.config
|
||||||
clone_symlinks(Path(steam_path))
|
rebuild_symlinks(config)
|
||||||
|
|
||||||
|
if len(custom_mods) > 0:
|
||||||
|
new_symlinks = create_custom_symlinks(
|
||||||
|
Path(steam_path), Path(custom_folder), custom_mods
|
||||||
|
)
|
||||||
combined_mods.extend(new_symlinks)
|
combined_mods.extend(new_symlinks)
|
||||||
|
|
||||||
launch_offline(client, self.appid, name, combined_mods, self.mission_folder)
|
relative_link = ""
|
||||||
|
if len(mission) > 0:
|
||||||
|
relative_link = symlink_mission(Path(steam_path), mission)
|
||||||
|
|
||||||
|
launch_offline(client, appid, name, combined_mods, relative_link)
|
||||||
|
while True:
|
||||||
|
if is_dayz_running():
|
||||||
|
break
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
def open_folderpicker(self, title: str) -> Union["Path", None]:
|
def open_folderpicker(self, title: str) -> Union["Path", None]:
|
||||||
picker = FolderPicker(self.controller.get_window(), title)
|
picker = FolderPicker(self.controller.get_window(), title)
|
||||||
folder = picker.pick_folder()
|
folder = picker.pick_folder()
|
||||||
picker.destroy()
|
picker.destroy()
|
||||||
return folder
|
return folder
|
||||||
|
|
||||||
|
def has_dayz_exp(self) -> bool:
|
||||||
|
try:
|
||||||
|
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
|
steam_path = Path(default_steam_path)
|
||||||
|
dayz_exp = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
|
||||||
|
if dayz_exp is None:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning(e)
|
||||||
|
return False
|
||||||
@@ -374,7 +374,7 @@ class ServerModelManager:
|
|||||||
proxy = self._get_proxy_man().get_proxy_model()
|
proxy = self._get_proxy_man().get_proxy_model()
|
||||||
self.tv.set_model(proxy)
|
self.tv.set_model(proxy)
|
||||||
|
|
||||||
if self.controller.get_active_treeview().get_enum == ServerTab.SAVED:
|
if self.controller.get_active_treeview().get_enum() == ServerTab.SAVED:
|
||||||
self.emitter.emit("servers_loaded", self.enum)
|
self.emitter.emit("servers_loaded", self.enum)
|
||||||
|
|
||||||
filter_man = self.tv.get_filter_man()
|
filter_man = self.tv.get_filter_man()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ update_success = "Updated successfully. Please exit and relaunch."
|
|||||||
|
|
||||||
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
|
load_error_lan = "Failed to find any servers on your network.\nCheck the server query port or your firewall settings."
|
||||||
fetching_mods = "Fetching mod metadata"
|
fetching_mods = "Fetching mod metadata"
|
||||||
deleting_mods = "Deleting mods"
|
deleting_mods = "Unsubscribing mods"
|
||||||
scanning_mods = "Scanning mods"
|
scanning_mods = "Scanning mods"
|
||||||
parsing_mods = "Parsing mods"
|
parsing_mods = "Parsing mods"
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,13 @@ custom_button = "Set custom mod folder"
|
|||||||
|
|
||||||
local_frame = "Installed mods"
|
local_frame = "Installed mods"
|
||||||
|
|
||||||
|
no_local_mods = "No local mods found"
|
||||||
no_mods = "No valid mods found"
|
no_mods = "No valid mods found"
|
||||||
no_mission = "Not a valid mission"
|
no_mission = "Not a valid mission"
|
||||||
|
folder_changed = "Folder missing or changed on disk"
|
||||||
|
|
||||||
|
custom_eventbox = "Set the root folder. Mods must be in atomic folders and have a meta.cpp file at a minimum."
|
||||||
|
mission_eventbox = "Select a single mission folder containing an init.c file."
|
||||||
|
|
||||||
|
folder_prefix = "<b>Current folder: </b>"
|
||||||
|
unset_button = "Unset folder"
|
||||||
@@ -52,7 +52,7 @@ show_mods = "Show server-side mods"
|
|||||||
show_details = "Show server details"
|
show_details = "Show server details"
|
||||||
refresh_players = "Refresh player count"
|
refresh_players = "Refresh player count"
|
||||||
open_workshop = "Open in Steam Workshop"
|
open_workshop = "Open in Steam Workshop"
|
||||||
delete_mod = "Delete mod"
|
unsub_mod = "Unsubscribe mod"
|
||||||
copy_name = "Copy name to clipboard"
|
copy_name = "Copy name to clipboard"
|
||||||
copy_ip = "Copy IP to clipboard"
|
copy_ip = "Copy IP to clipboard"
|
||||||
copy_log = "Copy record(s) to clipboard"
|
copy_log = "Copy record(s) to clipboard"
|
||||||
@@ -215,8 +215,8 @@ class ModPanelStrings:
|
|||||||
unhighlight_stale_tooltip: str
|
unhighlight_stale_tooltip: str
|
||||||
highlight_stale: str
|
highlight_stale: str
|
||||||
highlight_stale_tooltip: str
|
highlight_stale_tooltip: str
|
||||||
delete_selected: str
|
unsub_selected: str
|
||||||
delete_selected_tooltip: str
|
unsub_selected_tooltip: str
|
||||||
unselect_all: str
|
unselect_all: str
|
||||||
unselect_all_tooltip: str
|
unselect_all_tooltip: str
|
||||||
select_all: str
|
select_all: str
|
||||||
@@ -337,8 +337,8 @@ mod_panel = ModPanelStrings(
|
|||||||
"Shows locally-installed mods which are not used by any server "
|
"Shows locally-installed mods which are not used by any server "
|
||||||
"in your Saved Servers"
|
"in your Saved Servers"
|
||||||
),
|
),
|
||||||
delete_selected="Delete selected",
|
unsub_selected="Unsubscribe selected",
|
||||||
delete_selected_tooltip="Deletes selected mods from the system",
|
unsub_selected_tooltip="Unsubscribes from selected mods",
|
||||||
unselect_all="Unselect all",
|
unselect_all="Unselect all",
|
||||||
unselect_all_tooltip="Bulk unselects all mods",
|
unselect_all_tooltip="Bulk unselects all mods",
|
||||||
select_all="Select all",
|
select_all="Select all",
|
||||||
@@ -367,6 +367,7 @@ thanks = Thanks(
|
|||||||
"Johnofwrong",
|
"Johnofwrong",
|
||||||
"MatheusLasserr",
|
"MatheusLasserr",
|
||||||
"nolan-perez",
|
"nolan-perez",
|
||||||
|
"OnniSaarni",
|
||||||
"scandalouss",
|
"scandalouss",
|
||||||
"SnackSBR",
|
"SnackSBR",
|
||||||
"StevelDusa",
|
"StevelDusa",
|
||||||
@@ -463,7 +464,7 @@ crumbs = Crumbs(
|
|||||||
thanks="Help > Special thanks",
|
thanks="Help > Special thanks",
|
||||||
developers="Options > Developers",
|
developers="Options > Developers",
|
||||||
default="Servers > ",
|
default="Servers > ",
|
||||||
offline="Mods > Play offline"
|
offline="Mods > Play offline",
|
||||||
)
|
)
|
||||||
|
|
||||||
no_mods = "No local mods found."
|
no_mods = "No local mods found."
|
||||||
|
|||||||
+44
-4
@@ -2,7 +2,7 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
from dzgui.api.mods import get_local_mod_path, get_local_mod_ids, _hash
|
||||||
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME
|
from dzgui.const.constants import APPID_DAYZ, APPID_DAYZ_EXP, APP_NAME, DAYZ_COMMUNITY_ROOT
|
||||||
from dzgui.const.enum import Preferences
|
from dzgui.const.enum import Preferences
|
||||||
from dzgui.config.query import lookup
|
from dzgui.config.query import lookup
|
||||||
|
|
||||||
@@ -11,14 +11,23 @@ import dzgui.api.pefile as PeFile
|
|||||||
logger = logging.getLogger(APP_NAME)
|
logger = logging.getLogger(APP_NAME)
|
||||||
|
|
||||||
|
|
||||||
|
def expunge_link(file: Path) -> None:
|
||||||
|
if not file.is_symlink():
|
||||||
|
return
|
||||||
|
# NOTE: unlink stale symlinks
|
||||||
|
if file.exists() is False:
|
||||||
|
file.unlink()
|
||||||
|
if str(file.stem)[:2] == "@C":
|
||||||
|
file.unlink()
|
||||||
|
|
||||||
def rebuild_symlinks(config: Path) -> None:
|
def rebuild_symlinks(config: Path) -> None:
|
||||||
|
# TODO: pass direct path as argument
|
||||||
path = lookup(config, Preferences.DEFAULT)
|
path = lookup(config, Preferences.DEFAULT)
|
||||||
steam_path = Path(path)
|
steam_path = Path(path)
|
||||||
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
# NOTE: unlink stale symlinks
|
|
||||||
for file in dayz_path.iterdir():
|
for file in dayz_path.iterdir():
|
||||||
if file.is_symlink() and file.exists() is False:
|
expunge_link(file)
|
||||||
file.unlink()
|
|
||||||
workshop = get_local_mod_path(steam_path)
|
workshop = get_local_mod_path(steam_path)
|
||||||
# NOTE: create symlinks for missing mods
|
# NOTE: create symlinks for missing mods
|
||||||
for mod_id in get_local_mod_ids(steam_path):
|
for mod_id in get_local_mod_ids(steam_path):
|
||||||
@@ -30,6 +39,37 @@ def rebuild_symlinks(config: Path) -> None:
|
|||||||
clone_symlinks(steam_path)
|
clone_symlinks(steam_path)
|
||||||
|
|
||||||
|
|
||||||
|
def create_custom_symlinks(
|
||||||
|
steam_path: Path, custom_dir: Path, uids: list[str]
|
||||||
|
) -> list[str]:
|
||||||
|
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
|
links = []
|
||||||
|
for uid in uids:
|
||||||
|
md5sum = _hash(uid, use_custom=True)
|
||||||
|
source = dayz_path.joinpath(md5sum)
|
||||||
|
target = custom_dir.joinpath(uid)
|
||||||
|
links.append(md5sum)
|
||||||
|
if source.exists() is False:
|
||||||
|
source.symlink_to(target)
|
||||||
|
clone_symlinks(steam_path)
|
||||||
|
return links
|
||||||
|
|
||||||
|
|
||||||
|
def symlink_mission(steam_path: Path, target: str) -> str:
|
||||||
|
dayz_path = PeFile.get_nested_app_path(steam_path, APPID_DAYZ)
|
||||||
|
for file in dayz_path.iterdir():
|
||||||
|
expunge_link(file)
|
||||||
|
path = Path(target)
|
||||||
|
stem = path.name
|
||||||
|
parent = path.parent
|
||||||
|
source = dayz_path.joinpath(DAYZ_COMMUNITY_ROOT)
|
||||||
|
try:
|
||||||
|
source.symlink_to(parent)
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
return f"{DAYZ_COMMUNITY_ROOT}/{stem}"
|
||||||
|
|
||||||
|
|
||||||
def clone_symlinks(steam_path: Path) -> None:
|
def clone_symlinks(steam_path: Path) -> None:
|
||||||
"""
|
"""
|
||||||
Shares symlinks between builds. Used after any symlink operation
|
Shares symlinks between builds. Used after any symlink operation
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ class OuterWindow(Gtk.Window):
|
|||||||
MainController.set_start_tab()
|
MainController.set_start_tab()
|
||||||
self.grid.hide_widgets_on_init()
|
self.grid.hide_widgets_on_init()
|
||||||
|
|
||||||
# TODO: POC, trigger page change here
|
|
||||||
MainController.loaded = True
|
MainController.loaded = True
|
||||||
MainController.populate_model(MainController.get_active_treeview())
|
MainController.populate_model(MainController.get_active_treeview())
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from typing import Sequence
|
||||||
|
|
||||||
|
import gi
|
||||||
|
|
||||||
|
gi.require_version("Gtk", "3.0")
|
||||||
|
from gi.repository import Gtk # noqa
|
||||||
|
|
||||||
|
|
||||||
|
class GenericBox(Gtk.Box):
|
||||||
|
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
|
||||||
|
super().__init__(orientation=orientation, spacing=spacing)
|
||||||
|
|
||||||
|
def extend(self, els: Sequence[Gtk.Widget]) -> None:
|
||||||
|
for el in els:
|
||||||
|
self.add(el)
|
||||||
|
|
||||||
|
|
||||||
|
class HBox(GenericBox):
|
||||||
|
def __init__(self, spacing: int = 0) -> None:
|
||||||
|
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
|
||||||
|
|
||||||
|
|
||||||
|
class VBox(GenericBox):
|
||||||
|
def __init__(self, spacing: int = 0) -> None:
|
||||||
|
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
||||||
@@ -208,6 +208,8 @@ class SteamWorkshopButton(SteamTextButton):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__(label=buttons.workshop)
|
super().__init__(label=buttons.workshop)
|
||||||
self.set_tooltip_text(buttons.workshop_tooltip)
|
self.set_tooltip_text(buttons.workshop_tooltip)
|
||||||
|
self.set_margin_top(10)
|
||||||
|
self.set_margin_bottom(10)
|
||||||
|
|
||||||
|
|
||||||
class AddButton(IconTextButton):
|
class AddButton(IconTextButton):
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ gi.require_version("Gtk", "3.0")
|
|||||||
from gi.repository import Gtk, Gdk # noqa E402
|
from gi.repository import Gtk, Gdk # noqa E402
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller, Emitter
|
from dzgui.controllers.mc import Controller
|
||||||
|
from dzgui.controllers.emitter import Emitter
|
||||||
|
|
||||||
COLS = 1
|
COLS = 1
|
||||||
ROWS = 1
|
ROWS = 1
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ gi.require_version("Gtk", "3.0")
|
|||||||
from gi.repository import Gtk # noqa E402
|
from gi.repository import Gtk # noqa E402
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller, Emitter
|
from dzgui.controllers.mc import Controller
|
||||||
|
from dzgui.controllers.emitter import Emitter
|
||||||
|
|
||||||
|
|
||||||
class EnumeratedModButton(Gtk.Button):
|
class EnumeratedModButton(Gtk.Button):
|
||||||
@@ -48,7 +49,7 @@ class ModSelectionPanel(Gtk.Box):
|
|||||||
buttons = (
|
buttons = (
|
||||||
ModButton.SELECT_ALL,
|
ModButton.SELECT_ALL,
|
||||||
ModButton.UNSELECT_ALL,
|
ModButton.UNSELECT_ALL,
|
||||||
ModButton.DELETE_SELECTED,
|
ModButton.UNSUB_SELECTED,
|
||||||
)
|
)
|
||||||
for button in buttons:
|
for button in buttons:
|
||||||
b = EnumeratedModButton(button)
|
b = EnumeratedModButton(button)
|
||||||
@@ -100,8 +101,8 @@ class ModSelectionPanel(Gtk.Box):
|
|||||||
self.controller.toggle_mod_selection(True)
|
self.controller.toggle_mod_selection(True)
|
||||||
case ModButton.UNSELECT_ALL:
|
case ModButton.UNSELECT_ALL:
|
||||||
self.controller.toggle_mod_selection(False)
|
self.controller.toggle_mod_selection(False)
|
||||||
case ModButton.DELETE_SELECTED:
|
case ModButton.UNSUB_SELECTED:
|
||||||
self.controller.delete_mods()
|
self.controller.unsub_mods()
|
||||||
case ModButton.HIGHLIGHT_STALE:
|
case ModButton.HIGHLIGHT_STALE:
|
||||||
self.controller.highlight_stale()
|
self.controller.highlight_stale()
|
||||||
case ModButton.UNHIGHLIGHT_STALE:
|
case ModButton.UNHIGHLIGHT_STALE:
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ class Statusbar(Gtk.Grid):
|
|||||||
self.controller.register_widget("statusbar", self)
|
self.controller.register_widget("statusbar", self)
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
|
self.prior_enum: NotebookPage
|
||||||
|
self.prior_status: str
|
||||||
self.playercount = ""
|
self.playercount = ""
|
||||||
self.statusbar = Gtk.Statusbar()
|
self.statusbar = Gtk.Statusbar()
|
||||||
|
|
||||||
@@ -71,6 +73,7 @@ class Statusbar(Gtk.Grid):
|
|||||||
NotebookPage.CONNECTION,
|
NotebookPage.CONNECTION,
|
||||||
NotebookPage.OFFLINE,
|
NotebookPage.OFFLINE,
|
||||||
):
|
):
|
||||||
|
self.prior_enum = enum
|
||||||
self.set_by_context(enum, esc_to_return)
|
self.set_by_context(enum, esc_to_return)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -80,8 +83,9 @@ class Statusbar(Gtk.Grid):
|
|||||||
case NotebookPage.KEYS:
|
case NotebookPage.KEYS:
|
||||||
bar = question_to_return
|
bar = question_to_return
|
||||||
case _:
|
case _:
|
||||||
return
|
bar = self.prior_status
|
||||||
|
|
||||||
|
self.prior_enum = enum
|
||||||
self.set_by_context(enum, bar)
|
self.set_by_context(enum, bar)
|
||||||
|
|
||||||
def _on_server_row_changed(self, statusbar: Self) -> None:
|
def _on_server_row_changed(self, statusbar: Self) -> None:
|
||||||
@@ -97,8 +101,13 @@ class Statusbar(Gtk.Grid):
|
|||||||
context: Union["ServerTab", NotebookPage],
|
context: Union["ServerTab", NotebookPage],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.spinner.stop()
|
self.spinner.stop()
|
||||||
|
|
||||||
|
if type(context) is NotebookPage:
|
||||||
|
self.prior_enum = context
|
||||||
|
self.prior_status = ""
|
||||||
# FIXME: CalcDist is being called when table is not loaded
|
# FIXME: CalcDist is being called when table is not loaded
|
||||||
if dist is None:
|
if dist is None:
|
||||||
|
self.set_by_context(context, "")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
pretty = self.append_distance(dist)
|
pretty = self.append_distance(dist)
|
||||||
@@ -121,6 +130,8 @@ class Statusbar(Gtk.Grid):
|
|||||||
def set_by_context(
|
def set_by_context(
|
||||||
self, context: Union[NotebookPage, "ServerTab"], string: str
|
self, context: Union[NotebookPage, "ServerTab"], string: str
|
||||||
) -> None:
|
) -> None:
|
||||||
|
if context != NotebookPage.KEYS:
|
||||||
|
self.prior_status = string
|
||||||
meta = self.statusbar.get_context_id(str(context))
|
meta = self.statusbar.get_context_id(str(context))
|
||||||
self.statusbar.push(meta, string)
|
self.statusbar.push(meta, string)
|
||||||
self.set_cache(string)
|
self.set_cache(string)
|
||||||
|
|||||||
@@ -54,7 +54,15 @@ class FolderPicker(Gtk.FileChooserDialog):
|
|||||||
return None
|
return None
|
||||||
if res == Gtk.ResponseType.OK:
|
if res == Gtk.ResponseType.OK:
|
||||||
folder = self.get_current_folder()
|
folder = self.get_current_folder()
|
||||||
if folder is not None:
|
if folder is None:
|
||||||
self.destroy()
|
try:
|
||||||
return Path(folder)
|
uri = self.get_uri()
|
||||||
|
if uri is None:
|
||||||
|
return None
|
||||||
|
return Path.from_uri(uri)
|
||||||
|
except Exception:
|
||||||
|
# FIXME: edge cases
|
||||||
|
return Path("/")
|
||||||
|
self.destroy()
|
||||||
|
return Path(folder)
|
||||||
return None
|
return None
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from typing import Self, TYPE_CHECKING
|
from typing import Self, TYPE_CHECKING
|
||||||
|
from dzgui.api.steam import find_user_id
|
||||||
|
from dzgui.const.enum import NotebookPage, Preferences
|
||||||
|
from dzgui.views.components.box import HBox
|
||||||
|
from dzgui.views.components.buttons import SteamWorkshopButton
|
||||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||||
from dzgui.views.trees.tree_mods import ModTreeView
|
from dzgui.views.trees.tree_mods import ModTreeView
|
||||||
|
|
||||||
@@ -20,10 +26,22 @@ class Mods(Gtk.Box):
|
|||||||
self.tree.set_vexpand(True)
|
self.tree.set_vexpand(True)
|
||||||
self.box.add(self.tree)
|
self.box.add(self.tree)
|
||||||
|
|
||||||
|
self.statusbar_cache = ""
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.controller.register_widget("modtreeview", self.tree)
|
self.controller.register_widget("modtreeview", self.tree)
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
|
# TODO: move
|
||||||
|
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
||||||
|
steam_path = Path(default_steam_path)
|
||||||
|
uid = find_user_id(steam_path)
|
||||||
|
|
||||||
|
pretty_uid = "" if uid is None else uid
|
||||||
|
hbox = HBox(spacing=10)
|
||||||
|
workshop_button = SteamWorkshopButton()
|
||||||
|
workshop_button.connect(
|
||||||
|
"clicked", lambda _: self.controller.open_user_workshop(pretty_uid)
|
||||||
|
)
|
||||||
self.offline_button = Gtk.Button(
|
self.offline_button = Gtk.Button(
|
||||||
label="Play offline",
|
label="Play offline",
|
||||||
halign=Gtk.Align.START,
|
halign=Gtk.Align.START,
|
||||||
@@ -32,13 +50,17 @@ class Mods(Gtk.Box):
|
|||||||
)
|
)
|
||||||
self.offline_button.connect("clicked", self._on_offline_clicked)
|
self.offline_button.connect("clicked", self._on_offline_clicked)
|
||||||
|
|
||||||
self.add(self.offline_button)
|
hbox.add(workshop_button)
|
||||||
|
hbox.add(self.offline_button)
|
||||||
|
|
||||||
|
self.add(hbox)
|
||||||
self.add(self.box)
|
self.add(self.box)
|
||||||
|
|
||||||
self.connect("map", self._on_map)
|
self.connect("map", self._on_map)
|
||||||
self.connect("unmap", self._on_unmap)
|
self.connect("unmap", self._on_unmap)
|
||||||
|
|
||||||
def _on_offline_clicked(self, button: Gtk.Button) -> None:
|
def _on_offline_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.statusbar_cache = self.controller.get_statusbar().get_cache()
|
||||||
self.controller.open_offline()
|
self.controller.open_offline()
|
||||||
|
|
||||||
def _on_unmap(self, widget: Self) -> None:
|
def _on_unmap(self, widget: Self) -> None:
|
||||||
@@ -46,6 +68,13 @@ class Mods(Gtk.Box):
|
|||||||
|
|
||||||
def _on_map(self, widget: Self) -> None:
|
def _on_map(self, widget: Self) -> None:
|
||||||
self.emitter.emit("mod_page_toggled", True)
|
self.emitter.emit("mod_page_toggled", True)
|
||||||
|
# TODO: delegation
|
||||||
|
# NOTE: handles going back from offline mods page
|
||||||
|
# more generic cache restoration method
|
||||||
|
if len(self.statusbar_cache) > 0:
|
||||||
|
self.controller.get_statusbar().set_by_context(
|
||||||
|
NotebookPage.MODS, self.statusbar_cache
|
||||||
|
)
|
||||||
|
|
||||||
def grab_content_area(self) -> None:
|
def grab_content_area(self) -> None:
|
||||||
self.tree.grab_focus()
|
self.tree.grab_focus()
|
||||||
+222
-101
@@ -1,20 +1,24 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from pathlib import Path
|
from enum import Enum
|
||||||
from typing import Self, Sequence, TYPE_CHECKING, Union
|
from typing import Self, TYPE_CHECKING, Union
|
||||||
|
|
||||||
from dzgui.util import css
|
from dzgui.util import css
|
||||||
import dzgui.api.pefile as PeFile
|
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
APPID_DAYZ_EXP,
|
APPID_DAYZ_EXP,
|
||||||
APPNAME_DAYZ,
|
APPNAME_DAYZ,
|
||||||
APPNAME_DAYZ_EXP_HUMAN,
|
APPNAME_DAYZ_EXP_HUMAN,
|
||||||
|
EDIT_DELETE,
|
||||||
|
ERROR,
|
||||||
FOLDER,
|
FOLDER,
|
||||||
|
WARNING,
|
||||||
)
|
)
|
||||||
from dzgui.const.enum import NotebookPage, Preferences
|
from dzgui.const.enum import ContextMenuGroup, NotebookPage
|
||||||
from dzgui.managers.offline import OfflineManager
|
from dzgui.managers.offline import OfflineManager
|
||||||
from dzgui.strings import generic, offline
|
from dzgui.strings import generic, offline
|
||||||
from dzgui.views.components.buttons import IconTextButton
|
from dzgui.views.components.box import HBox, VBox
|
||||||
|
from dzgui.views.components.buttons import Icon, IconTextButton
|
||||||
|
from dzgui.views.components.eventbox import InfoEventBox
|
||||||
from dzgui.views.components.frame import HeadingFrame
|
from dzgui.views.components.frame import HeadingFrame
|
||||||
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
from dzgui.views.components.scrollable import NoOverlayScrolledWindow
|
||||||
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
from dzgui.views.trees.tree_mods import OfflineModTreeView
|
||||||
@@ -23,7 +27,7 @@ from dzgui.views.trees.tree_mods import OfflineModTreeView
|
|||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version("Gtk", "3.0")
|
gi.require_version("Gtk", "3.0")
|
||||||
from gi.repository import Gtk, Gdk # noqa
|
from gi.repository import Gtk, Gdk, GObject # noqa
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
@@ -31,23 +35,10 @@ if TYPE_CHECKING:
|
|||||||
from dzgui.model.model_factory import FastInsertListStore
|
from dzgui.model.model_factory import FastInsertListStore
|
||||||
|
|
||||||
|
|
||||||
class GenericBox(Gtk.Box):
|
class FolderError(Enum):
|
||||||
def __init__(self, orientation: Gtk.Orientation, spacing: int = 0) -> None:
|
NO_VALID_MODS = 1
|
||||||
super().__init__(orientation=orientation, spacing=spacing)
|
NO_VALID_MISSION = 2
|
||||||
|
FOLDER_CHANGED = 3
|
||||||
def extend(self, els: Sequence[Gtk.Widget]) -> None:
|
|
||||||
for el in els:
|
|
||||||
self.add(el)
|
|
||||||
|
|
||||||
|
|
||||||
class HBox(GenericBox):
|
|
||||||
def __init__(self, spacing: int = 0) -> None:
|
|
||||||
super().__init__(orientation=Gtk.Orientation.HORIZONTAL, spacing=spacing)
|
|
||||||
|
|
||||||
|
|
||||||
class VBox(GenericBox):
|
|
||||||
def __init__(self, spacing: int = 0) -> None:
|
|
||||||
super().__init__(orientation=Gtk.Orientation.VERTICAL, spacing=spacing)
|
|
||||||
|
|
||||||
|
|
||||||
class PageHeading(Gtk.Label):
|
class PageHeading(Gtk.Label):
|
||||||
@@ -57,37 +48,128 @@ class PageHeading(Gtk.Label):
|
|||||||
css.add_class(self, "page-heading")
|
css.add_class(self, "page-heading")
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorPopover(Gtk.Popover):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(position=Gtk.PositionType.RIGHT)
|
||||||
|
|
||||||
|
self.hbox = HBox()
|
||||||
|
self.label = Gtk.Label(label="", margin_start=10, margin_end=10)
|
||||||
|
error_icon = Icon(ERROR, margin_start=10)
|
||||||
|
self.hbox.extend([error_icon, self.label])
|
||||||
|
self.add(self.hbox)
|
||||||
|
self.show_all()
|
||||||
|
self.popdown()
|
||||||
|
|
||||||
|
def set_label(self, error: FolderError, msg: str) -> None:
|
||||||
|
match error:
|
||||||
|
case FolderError.NO_VALID_MODS:
|
||||||
|
prefix = offline.no_mods
|
||||||
|
case FolderError.NO_VALID_MISSION:
|
||||||
|
prefix = offline.no_mission
|
||||||
|
case FolderError.FOLDER_CHANGED:
|
||||||
|
prefix = offline.folder_changed
|
||||||
|
self.label.set_label(prefix)
|
||||||
|
return
|
||||||
|
self.label.set_label(f"{prefix}: '{msg}'")
|
||||||
|
|
||||||
|
|
||||||
class FolderHBox(HBox):
|
class FolderHBox(HBox):
|
||||||
def __init__(self, btn_label: str) -> None:
|
def __init__(self, controller: "Controller", btn_label: str, eb_text: str) -> None:
|
||||||
super().__init__(spacing=10)
|
super().__init__(spacing=10)
|
||||||
|
|
||||||
self.set_margin_start(10)
|
self.set_margin_start(5)
|
||||||
self.set_margin_end(10)
|
self.set_margin_end(10)
|
||||||
self.set_margin_bottom(5)
|
self.set_margin_bottom(5)
|
||||||
|
|
||||||
# TODO: alternate class for left-aligned icons
|
self.folder = ""
|
||||||
|
self.controller = controller
|
||||||
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
|
self.eb = InfoEventBox(eb_text, controller)
|
||||||
self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT)
|
self.button = IconTextButton(FOLDER, btn_label, Gtk.PositionType.LEFT)
|
||||||
self.button.set_halign(Gtk.Align.START)
|
self.button.set_halign(Gtk.Align.START)
|
||||||
self.button.set_image_position(Gtk.PositionType.LEFT)
|
self.button.set_image_position(Gtk.PositionType.LEFT)
|
||||||
|
|
||||||
|
self.set_can_focus(True)
|
||||||
|
|
||||||
self.scrolled_label = Gtk.ScrolledWindow(
|
self.scrolled_label = Gtk.ScrolledWindow(
|
||||||
propagate_natural_width=True, halign=Gtk.Align.START
|
propagate_natural_width=True, halign=Gtk.Align.START
|
||||||
)
|
)
|
||||||
self.label = Gtk.Label()
|
self.label = Gtk.Label()
|
||||||
self.scrolled_label.add(self.label)
|
self.scrolled_label.add(self.label)
|
||||||
self.extend([self.button, self.scrolled_label])
|
|
||||||
|
self.unset_button = IconTextButton(EDIT_DELETE, offline.unset_button)
|
||||||
|
self.unset_button.connect("clicked", self._on_unset_clicked)
|
||||||
|
|
||||||
|
self.spinner = Gtk.Spinner()
|
||||||
|
self.extend(
|
||||||
|
[self.eb, self.button, self.spinner, self.scrolled_label, self.unset_button]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.pop = ErrorPopover()
|
||||||
|
self.pop.set_relative_to(self.button)
|
||||||
|
self.pop.connect("unmap", lambda _: self.grab_focus())
|
||||||
|
|
||||||
|
self.sidepop = ErrorPopover()
|
||||||
|
self.sidepop.set_position(Gtk.PositionType.BOTTOM)
|
||||||
|
self.sidepop.set_relative_to(self.scrolled_label)
|
||||||
|
self.sidepop.connect("unmap", lambda _: self.grab_focus())
|
||||||
|
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
self.connect("unmap", self._on_unmap)
|
||||||
|
|
||||||
|
def start_spinner(self) -> None:
|
||||||
|
self.spinner.show()
|
||||||
|
self.spinner.start()
|
||||||
|
|
||||||
|
def stop_spinner(self) -> None:
|
||||||
|
self.spinner.hide()
|
||||||
|
self.spinner.stop()
|
||||||
|
|
||||||
|
def _on_unmap(self, widget: Self) -> None:
|
||||||
|
self.unset_button.hide()
|
||||||
|
self.label.set_label("")
|
||||||
|
self.scrolled_label.hide()
|
||||||
|
|
||||||
|
def _on_map(self, widget: Self) -> None:
|
||||||
|
self.spinner.hide()
|
||||||
|
self.scrolled_label.show()
|
||||||
|
self.unset_button.hide()
|
||||||
|
|
||||||
|
def _on_unset_clicked(self, button: Gtk.Button) -> None:
|
||||||
|
self.folder = ""
|
||||||
|
self.label.set_label("")
|
||||||
|
self.unset_button.hide()
|
||||||
|
# FIXME: rename signal
|
||||||
|
self.emitter.emit("custom_mods_unloaded", self)
|
||||||
|
|
||||||
def get_button(self) -> Gtk.Button:
|
def get_button(self) -> Gtk.Button:
|
||||||
return self.button
|
return self.button
|
||||||
|
|
||||||
def set_label(self, label: str) -> None:
|
|
||||||
# TODO: strings
|
|
||||||
prefix = "Current folder: "
|
|
||||||
self.label.set_label(prefix + label)
|
|
||||||
|
|
||||||
def hide_label(self) -> None:
|
def hide_label(self) -> None:
|
||||||
self.label.hide()
|
self.label.hide()
|
||||||
|
|
||||||
|
def get_folder(self) -> str:
|
||||||
|
return self.folder
|
||||||
|
|
||||||
|
def set_folder(self, folder: str) -> None:
|
||||||
|
prefix = offline.folder_prefix
|
||||||
|
self.folder = folder
|
||||||
|
self.label.set_markup(prefix + folder)
|
||||||
|
self.label.show()
|
||||||
|
self.unset_button.show()
|
||||||
|
|
||||||
|
def present_error(self, error: FolderError, msg: str) -> None:
|
||||||
|
if error == FolderError.FOLDER_CHANGED:
|
||||||
|
self.sidepop.set_label(error, msg)
|
||||||
|
self.sidepop.popup()
|
||||||
|
return
|
||||||
|
self.folder = ""
|
||||||
|
self.label.set_text("")
|
||||||
|
self.unset_button.hide()
|
||||||
|
self.pop.set_label(error, msg)
|
||||||
|
self.pop.popup()
|
||||||
|
|
||||||
|
|
||||||
class ModFrame(HeadingFrame):
|
class ModFrame(HeadingFrame):
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -114,45 +196,54 @@ class ModFrame(HeadingFrame):
|
|||||||
self.tree_vbox = VBox()
|
self.tree_vbox = VBox()
|
||||||
self.tree_vbox.extend([self.scrolled, self.status])
|
self.tree_vbox.extend([self.scrolled, self.status])
|
||||||
|
|
||||||
|
self.no_mods = HBox(spacing=10)
|
||||||
|
no_mods_icon = Icon(WARNING, margin_start=10)
|
||||||
|
no_mods_label = Gtk.Label(label=offline.no_local_mods, halign=Gtk.Align.START)
|
||||||
|
self.no_mods.extend([no_mods_icon, no_mods_label])
|
||||||
|
|
||||||
|
self.vbox.pack_end(self.no_mods, expand=True, fill=True, padding=3)
|
||||||
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
|
self.vbox.pack_end(self.tree_vbox, expand=True, fill=True, padding=3)
|
||||||
self.frame.add(self.vbox)
|
self.frame.add(self.vbox)
|
||||||
|
|
||||||
sel = self.tree.get_selection()
|
sel = self.tree.get_selection()
|
||||||
sel.connect("changed", self._on_selection_changed)
|
sel.connect("changed", self._on_selection_changed)
|
||||||
|
|
||||||
# TODO: inherit icons from warnings area in preconnect dialog
|
self.connect("map", self._on_map)
|
||||||
# TODO: strings
|
self.connect("unmap", self._on_unmap)
|
||||||
self.error_label = Gtk.Label(
|
|
||||||
label="No mods found",
|
|
||||||
halign=Gtk.Align.START,
|
|
||||||
margin_start=10,
|
|
||||||
margin_bottom=5,
|
|
||||||
)
|
|
||||||
self.vbox.pack_end(self.error_label, expand=True, fill=True, padding=3)
|
|
||||||
|
|
||||||
def set_error(self, msg: str) -> None:
|
|
||||||
self.error_label.set_label(msg)
|
|
||||||
self.error_label.show()
|
|
||||||
|
|
||||||
def hide_errors(self) -> None:
|
|
||||||
self.error_label.hide()
|
|
||||||
|
|
||||||
def start_empty(self) -> None:
|
def start_empty(self) -> None:
|
||||||
self.error_label.show()
|
self.tree_vbox.hide()
|
||||||
self.collapse_tree()
|
self.no_mods.show()
|
||||||
|
|
||||||
def hide_all(self) -> None:
|
def _on_map(self, widget: Self) -> None:
|
||||||
self.error_label.hide()
|
self.no_mods.hide()
|
||||||
self.collapse_tree()
|
self.tree.set_model(None)
|
||||||
|
self.tree_vbox.hide()
|
||||||
|
|
||||||
# TODO: warnings area
|
def _on_unmap(self, widget: Self) -> None:
|
||||||
# name collision within custom mods
|
self.tree.set_model(None)
|
||||||
|
self.tree_vbox.hide()
|
||||||
|
self.no_mods.hide()
|
||||||
|
|
||||||
|
def start(self, store: "FastInsertListStore") -> None:
|
||||||
|
# TODO: this manipulates mod man store out of band
|
||||||
|
self.tree.set_model(store)
|
||||||
|
self.tree.mod_man.set_store(store)
|
||||||
|
self.show_tree()
|
||||||
|
|
||||||
def get_mods(self) -> list[str]:
|
def get_mods(self) -> list[str]:
|
||||||
model, treeiters = self.tree.get_selection().get_selected_rows()
|
model, treeiters = self.tree.get_selection().get_selected_rows()
|
||||||
if model is None:
|
if model is None:
|
||||||
return []
|
return []
|
||||||
return [model[_iter][1] for _iter in treeiters]
|
if type(self) is CustomModFrame:
|
||||||
|
# NOTE: custom mod dirs to be hashed
|
||||||
|
return [model[_iter][2] for _iter in treeiters]
|
||||||
|
else:
|
||||||
|
# NOTE: pre-existing, canonical symlinks to published mods
|
||||||
|
return [model[_iter][1] for _iter in treeiters]
|
||||||
|
|
||||||
|
def show_tree(self) -> None:
|
||||||
|
self.tree_vbox.show()
|
||||||
|
|
||||||
def collapse_tree(self) -> None:
|
def collapse_tree(self) -> None:
|
||||||
self.tree_vbox.hide()
|
self.tree_vbox.hide()
|
||||||
@@ -175,6 +266,8 @@ class ModFrame(HeadingFrame):
|
|||||||
else:
|
else:
|
||||||
status = f"Mods selected: {len(rows)}"
|
status = f"Mods selected: {len(rows)}"
|
||||||
self.status.set_label(status)
|
self.status.set_label(status)
|
||||||
|
# TODO: cleaner delegation
|
||||||
|
# simply send int value or use emitter
|
||||||
self.parent.check_button()
|
self.parent.check_button()
|
||||||
|
|
||||||
def set_cursor(self) -> None:
|
def set_cursor(self) -> None:
|
||||||
@@ -189,30 +282,49 @@ class CustomModFrame(ModFrame):
|
|||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(parent, controller, heading)
|
super().__init__(parent, controller, heading)
|
||||||
|
|
||||||
|
self.tree.set_menu(ContextMenuGroup.MOD_OFFLINE)
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
# TODO: descriptive text here explaining how this area works
|
self.custom_hbox = FolderHBox(
|
||||||
self.custom_hbox = FolderHBox(offline.custom_button)
|
controller, offline.custom_button, offline.custom_eventbox
|
||||||
|
)
|
||||||
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
|
self.custom_hbox.get_button().connect("clicked", self._on_custom_button_clicked)
|
||||||
|
|
||||||
self.pack(self.custom_hbox)
|
self.pack(self.custom_hbox)
|
||||||
|
|
||||||
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
|
self.emitter.connect("custom_mods_loaded", self._on_custom_mods_loaded)
|
||||||
|
self.emitter.connect("custom_mods_unloaded", self._on_custom_mods_unloaded)
|
||||||
|
|
||||||
|
self.connect("map", self._on_map)
|
||||||
|
|
||||||
|
def _on_map(self, widget: Self) -> None:
|
||||||
|
self.hide_tree()
|
||||||
|
|
||||||
|
def _on_custom_mods_unloaded(self, emitter: "Emitter", widget: FolderHBox) -> None:
|
||||||
|
# TODO: kludgy workaround for generic button emitting global signal
|
||||||
|
if widget == self.custom_hbox:
|
||||||
|
self.hide_tree()
|
||||||
|
|
||||||
def hide_tree(self) -> None:
|
def hide_tree(self) -> None:
|
||||||
# TODO: no mods message is different from collision message
|
|
||||||
self.set_error(offline.no_mods)
|
|
||||||
self.custom_hbox.hide_label()
|
self.custom_hbox.hide_label()
|
||||||
|
self.tree.set_model(None)
|
||||||
self.tree_vbox.hide()
|
self.tree_vbox.hide()
|
||||||
|
self.no_mods.hide()
|
||||||
|
|
||||||
|
def present_folder_changed(self, folder: str) -> None:
|
||||||
|
self.custom_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
|
||||||
|
|
||||||
|
def present_error(self, folder: str) -> None:
|
||||||
|
self.hide_tree()
|
||||||
|
self.custom_hbox.present_error(FolderError.NO_VALID_MODS, folder)
|
||||||
|
|
||||||
def present_tree(self, store: "FastInsertListStore", folder: str) -> None:
|
def present_tree(self, store: "FastInsertListStore", folder: str) -> None:
|
||||||
self.custom_hbox.set_label(folder)
|
self.custom_hbox.set_folder(folder)
|
||||||
self.tree.set_model(store)
|
self.tree.set_model(store)
|
||||||
self.tree_vbox.show()
|
self.tree_vbox.show()
|
||||||
self.hide_errors()
|
|
||||||
|
|
||||||
def _on_custom_mods_loaded(
|
def _on_custom_mods_loaded(
|
||||||
self,
|
self,
|
||||||
@@ -220,14 +332,19 @@ class CustomModFrame(ModFrame):
|
|||||||
store: "FastInsertListStore",
|
store: "FastInsertListStore",
|
||||||
folder: str,
|
folder: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
self.custom_hbox.stop_spinner()
|
||||||
if len(store) == 0:
|
if len(store) == 0:
|
||||||
self.hide_tree()
|
self.present_error(folder)
|
||||||
else:
|
else:
|
||||||
self.present_tree(store, folder)
|
self.present_tree(store, folder)
|
||||||
|
|
||||||
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
|
def _on_custom_button_clicked(self, button: Gtk.Button) -> None:
|
||||||
local_mods = self.get_mods()
|
callback = self.custom_hbox.start_spinner
|
||||||
self.parent.offline_man.get_custom_mods(local_mods)
|
# TODO: cleaner delegation
|
||||||
|
self.parent.offline_man.find_custom_mods(callback)
|
||||||
|
|
||||||
|
def get_folder(self) -> str:
|
||||||
|
return self.custom_hbox.get_folder()
|
||||||
|
|
||||||
|
|
||||||
class MissionFrame(HeadingFrame):
|
class MissionFrame(HeadingFrame):
|
||||||
@@ -238,35 +355,37 @@ class MissionFrame(HeadingFrame):
|
|||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.emitter = controller.get_emitter()
|
self.emitter = controller.get_emitter()
|
||||||
|
|
||||||
self.mission_hbox = FolderHBox(offline.mission_button)
|
self.mission_hbox = FolderHBox(
|
||||||
|
controller, offline.mission_button, offline.mission_eventbox
|
||||||
|
)
|
||||||
self.mission_button = self.mission_hbox.get_button()
|
self.mission_button = self.mission_hbox.get_button()
|
||||||
self.mission_button.connect("clicked", self._on_mission_button_clicked)
|
self.mission_button.connect("clicked", self._on_mission_button_clicked)
|
||||||
|
|
||||||
self.warning = Gtk.Label(
|
self.frame.add(self.mission_hbox)
|
||||||
halign=Gtk.Align.START, margin_start=10, margin_bottom=5
|
|
||||||
)
|
|
||||||
self.vbox = VBox()
|
|
||||||
self.vbox.extend([self.mission_hbox, self.warning])
|
|
||||||
self.frame.add(self.vbox)
|
|
||||||
|
|
||||||
self.emitter.connect("custom_mission_loaded", self._on_mission_loaded)
|
self.emitter.connect("custom_mission_loaded", self._on_mission_loaded)
|
||||||
|
|
||||||
def _on_mission_loaded(
|
def _on_mission_loaded(
|
||||||
self, emitter: "Emitter", folder: str, is_valid: bool
|
self, emitter: "Emitter", folder: str, is_valid: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
self.mission_hbox.set_label(folder)
|
if is_valid:
|
||||||
if not is_valid:
|
self.mission_hbox.set_folder(folder)
|
||||||
self.warning.show()
|
|
||||||
self.warning.set_label(offline.no_mission)
|
|
||||||
else:
|
else:
|
||||||
self.warning.hide()
|
self.mission_hbox.present_error(FolderError.NO_VALID_MISSION, folder)
|
||||||
|
self.parent.check_button()
|
||||||
|
|
||||||
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
|
def _on_mission_button_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.parent.offline_man.get_mission()
|
self.parent.offline_man.get_mission()
|
||||||
|
|
||||||
|
def get_mission(self) -> str:
|
||||||
|
return self.mission_hbox.get_folder()
|
||||||
|
|
||||||
|
def present_folder_changed(self, folder: str) -> None:
|
||||||
|
self.mission_hbox.present_error(FolderError.FOLDER_CHANGED, folder)
|
||||||
|
|
||||||
|
|
||||||
class RadioFrame(HeadingFrame):
|
class RadioFrame(HeadingFrame):
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(self, parent: OfflineLoader, controller: "Controller") -> None:
|
||||||
super().__init__(heading=offline.version)
|
super().__init__(heading=offline.version)
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
@@ -286,13 +405,10 @@ class RadioFrame(HeadingFrame):
|
|||||||
|
|
||||||
self.frame.add(self.radio_box)
|
self.frame.add(self.radio_box)
|
||||||
|
|
||||||
# TODO: abstract out of here
|
# TODO: cleaner delegation
|
||||||
default_steam_path = self.controller.query_config(Preferences.DEFAULT)
|
has_dayz_exp = parent.offline_man.has_dayz_exp()
|
||||||
steam_path = Path(default_steam_path)
|
|
||||||
dayz_exp = PeFile.get_pretty_version(steam_path, APPID_DAYZ_EXP)
|
|
||||||
|
|
||||||
self.dayz.connect("toggled", self._on_radio_toggled)
|
self.dayz.connect("toggled", self._on_radio_toggled)
|
||||||
if dayz_exp is None:
|
if has_dayz_exp is False:
|
||||||
self.dayz_exp.set_sensitive(False)
|
self.dayz_exp.set_sensitive(False)
|
||||||
|
|
||||||
def _on_radio_toggled(self, radio: Gtk.RadioButton) -> None:
|
def _on_radio_toggled(self, radio: Gtk.RadioButton) -> None:
|
||||||
@@ -315,6 +431,7 @@ class OfflineLoader(Gtk.Box):
|
|||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.controller.register_widget("offline_loader", self)
|
self.controller.register_widget("offline_loader", self)
|
||||||
|
self.emitter = controller.get_emitter()
|
||||||
self.offline_man = OfflineManager(controller)
|
self.offline_man = OfflineManager(controller)
|
||||||
|
|
||||||
self.add(PageHeading(offline.heading))
|
self.add(PageHeading(offline.heading))
|
||||||
@@ -324,7 +441,7 @@ class OfflineLoader(Gtk.Box):
|
|||||||
|
|
||||||
self.custom_tree = OfflineModTreeView(controller)
|
self.custom_tree = OfflineModTreeView(controller)
|
||||||
self.mission_frame = MissionFrame(self, controller)
|
self.mission_frame = MissionFrame(self, controller)
|
||||||
self.radio_frame = RadioFrame(controller)
|
self.radio_frame = RadioFrame(self, controller)
|
||||||
|
|
||||||
self.scrollable = Gtk.ScrolledWindow(
|
self.scrollable = Gtk.ScrolledWindow(
|
||||||
vexpand=True, propagate_natural_height=True
|
vexpand=True, propagate_natural_height=True
|
||||||
@@ -343,12 +460,13 @@ class OfflineLoader(Gtk.Box):
|
|||||||
# TODO: share ConnectBox class with preconnect dialog?
|
# TODO: share ConnectBox class with preconnect dialog?
|
||||||
self.button_box = HBox(spacing=5)
|
self.button_box = HBox(spacing=5)
|
||||||
self.button_box.set_halign(Gtk.Align.END)
|
self.button_box.set_halign(Gtk.Align.END)
|
||||||
self.button_box.set_margin_top(5)
|
self.button_box.set_margin_top(15)
|
||||||
self.back = Gtk.Button(label="Back")
|
self.back = Gtk.Button(label="Back")
|
||||||
self.ok = Gtk.Button(label="Launch", sensitive=False)
|
self.ok = Gtk.Button(label="Launch", sensitive=False)
|
||||||
self.back.connect("clicked", self._on_back_clicked)
|
self.back.connect("clicked", self._on_back_clicked)
|
||||||
self.ok.connect("clicked", self._on_ok_clicked)
|
self.ok.connect("clicked", self._on_ok_clicked)
|
||||||
self.connect("key-press-event", self._on_keypress)
|
self.connect("key-press-event", self._on_keypress)
|
||||||
|
self.emitter.connect("custom_mods_unloaded", lambda _, __: self.check_button())
|
||||||
|
|
||||||
self.button_box.extend([self.back, self.ok])
|
self.button_box.extend([self.back, self.ok])
|
||||||
|
|
||||||
@@ -359,7 +477,8 @@ class OfflineLoader(Gtk.Box):
|
|||||||
def check_button(self) -> None:
|
def check_button(self) -> None:
|
||||||
local_mods = self.local_frame.get_mods()
|
local_mods = self.local_frame.get_mods()
|
||||||
custom_mods = self.custom_frame.get_mods()
|
custom_mods = self.custom_frame.get_mods()
|
||||||
if len(local_mods) == 0 and len(custom_mods) == 0:
|
mission = self.mission_frame.get_mission()
|
||||||
|
if len(local_mods) == 0 and len(custom_mods) == 0 and len(mission) == 0:
|
||||||
self.ok.set_sensitive(False)
|
self.ok.set_sensitive(False)
|
||||||
else:
|
else:
|
||||||
self.ok.set_sensitive(True)
|
self.ok.set_sensitive(True)
|
||||||
@@ -369,24 +488,26 @@ class OfflineLoader(Gtk.Box):
|
|||||||
self.back.emit("clicked")
|
self.back.emit("clicked")
|
||||||
|
|
||||||
def populate(self, store: Union["FastInsertListStore", None]) -> None:
|
def populate(self, store: Union["FastInsertListStore", None]) -> None:
|
||||||
self.local_frame.set_model(store)
|
|
||||||
if store is None:
|
if store is None:
|
||||||
self.local_frame.start_empty()
|
self.local_frame.start_empty()
|
||||||
else:
|
return
|
||||||
self.local_frame.hide_errors()
|
self.local_frame.start(store)
|
||||||
# NOTE: suppress custom tree until explicitly loaded
|
|
||||||
self.custom_frame.hide_all()
|
|
||||||
|
|
||||||
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.open_page(NotebookPage.MODS)
|
self.controller.open_page(NotebookPage.MODS)
|
||||||
|
|
||||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||||
# TODO: grab all values in one pass
|
appid = self.radio_frame.get_appid()
|
||||||
# appid = self.radio_frame.get_appid()
|
mission = self.mission_frame.get_mission()
|
||||||
# mission = self.mission_frame.get_mission()
|
local_mods = self.local_frame.get_mods()
|
||||||
# local_mods = self.local_frame.get_mods()
|
custom_folder = self.custom_frame.get_folder()
|
||||||
# custom_mods = self.custom_frame.get_mods()
|
custom_mods = self.custom_frame.get_mods()
|
||||||
# cf. api.mods._hash(uid, use_custom=True)
|
|
||||||
# TODO: set up all symlinks prior to launch, including custom ones
|
if custom_folder and not self.offline_man.is_custom_folder_valid(custom_folder):
|
||||||
# self.offline_man.setup(appid, mission, local_mods, custom_mods)
|
self.custom_frame.present_folder_changed(mission)
|
||||||
pass
|
return
|
||||||
|
|
||||||
|
if mission and not self.offline_man.is_mission_valid(mission):
|
||||||
|
self.mission_frame.present_folder_changed(mission)
|
||||||
|
return
|
||||||
|
self.offline_man.launch(appid, mission, local_mods, custom_folder, custom_mods)
|
||||||
@@ -2,7 +2,7 @@ from pathlib import Path
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from dzgui.api import pefile as PeFile
|
from dzgui.api import pefile as PeFile
|
||||||
from dzgui.api.steam import find_user_id
|
|
||||||
from dzgui.config import query
|
from dzgui.config import query
|
||||||
from dzgui.const.constants import (
|
from dzgui.const.constants import (
|
||||||
APPID_DAYZ,
|
APPID_DAYZ,
|
||||||
@@ -19,9 +19,8 @@ from dzgui.const.endpoints import STEAM_API_SETUP, BM_API_SETUP
|
|||||||
from dzgui.const.enum import Preferences, ServerTab
|
from dzgui.const.enum import Preferences, ServerTab
|
||||||
from dzgui.strings import errors, options
|
from dzgui.strings import errors, options
|
||||||
from dzgui.util import strings, css, open_links
|
from dzgui.util import strings, css, open_links
|
||||||
from dzgui.views.components.buttons import SteamWorkshopButton
|
|
||||||
from dzgui.views.components.labels import LeftLabel
|
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.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.components.misc import ClientCombo
|
||||||
@@ -134,16 +133,6 @@ class Options(Gtk.Box):
|
|||||||
[LeftLabel(strings.options.name), self.player_box],
|
[LeftLabel(strings.options.name), self.player_box],
|
||||||
]
|
]
|
||||||
|
|
||||||
eb = InfoEventBox(options.workshop_eventbox, controller)
|
|
||||||
|
|
||||||
workshop_button = SteamWorkshopButton()
|
|
||||||
workshop_button.connect(
|
|
||||||
"clicked", lambda _: self.controller.open_user_workshop(self.uid)
|
|
||||||
)
|
|
||||||
mod_rows = [
|
|
||||||
[LeftLabel(options.workshop_label), workshop_button, eb],
|
|
||||||
]
|
|
||||||
|
|
||||||
self.dayz_version_label = Gtk.Label(label=strings.null)
|
self.dayz_version_label = Gtk.Label(label=strings.null)
|
||||||
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
|
self.dayz_exp_version_label = Gtk.Label(label=strings.null)
|
||||||
|
|
||||||
@@ -169,7 +158,6 @@ class Options(Gtk.Box):
|
|||||||
api_box.add(api_links_box)
|
api_box.add(api_links_box)
|
||||||
|
|
||||||
prefs_grid = self._make_grid(pref_rows)
|
prefs_grid = self._make_grid(pref_rows)
|
||||||
mods_grid = self._make_grid(mod_rows)
|
|
||||||
version_grid = self._make_grid(version_rows)
|
version_grid = self._make_grid(version_rows)
|
||||||
|
|
||||||
col = 1
|
col = 1
|
||||||
@@ -190,7 +178,6 @@ class Options(Gtk.Box):
|
|||||||
for pair in [
|
for pair in [
|
||||||
(api_box, strings.options.api_keys),
|
(api_box, strings.options.api_keys),
|
||||||
(prefs_grid, strings.options.prefs),
|
(prefs_grid, strings.options.prefs),
|
||||||
(mods_grid, strings.options.mods),
|
|
||||||
(version_grid, strings.options.version),
|
(version_grid, strings.options.version),
|
||||||
]:
|
]:
|
||||||
|
|
||||||
@@ -416,9 +403,6 @@ class Options(Gtk.Box):
|
|||||||
bm = self.controller.query_config(Preferences.BM)
|
bm = self.controller.query_config(Preferences.BM)
|
||||||
|
|
||||||
steam_path = Path(default_steam_path)
|
steam_path = Path(default_steam_path)
|
||||||
# NOTE: this is a best effort guess at the most recent user
|
|
||||||
uid = find_user_id(steam_path)
|
|
||||||
self.uid = "" if uid is None else uid
|
|
||||||
|
|
||||||
self.old_steam = steam
|
self.old_steam = steam
|
||||||
self.old_bm = bm
|
self.old_bm = bm
|
||||||
|
|||||||
@@ -112,18 +112,6 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
tooltip_text=preconnect.connect_last_tooltip,
|
tooltip_text=preconnect.connect_last_tooltip,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: abstract
|
|
||||||
self.raise_window = Gtk.CheckButton(
|
|
||||||
label="Foreground DZGUI while downloading",
|
|
||||||
halign=Gtk.Align.END,
|
|
||||||
hexpand=True,
|
|
||||||
valign=Gtk.Align.END,
|
|
||||||
visible=False,
|
|
||||||
has_tooltip=True,
|
|
||||||
sensitive=False,
|
|
||||||
tooltip_text="Foreground the DZGUI window after mod downloads are queued",
|
|
||||||
active=True,
|
|
||||||
)
|
|
||||||
self.button_box = Gtk.Box(
|
self.button_box = Gtk.Box(
|
||||||
orientation=Gtk.Orientation.VERTICAL,
|
orientation=Gtk.Orientation.VERTICAL,
|
||||||
valign=Gtk.Align.END,
|
valign=Gtk.Align.END,
|
||||||
@@ -133,8 +121,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
box = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL, spacing=5)
|
||||||
for button in self.back, self.ok, self.connect_last:
|
for button in self.back, self.ok, self.connect_last:
|
||||||
box.add(button)
|
box.add(button)
|
||||||
for el in self.raise_window, box:
|
self.button_box.add(box)
|
||||||
self.button_box.add(el)
|
|
||||||
|
|
||||||
self.back.connect("clicked", self._on_back_clicked)
|
self.back.connect("clicked", self._on_back_clicked)
|
||||||
self.ok.connect("clicked", self._on_ok_clicked)
|
self.ok.connect("clicked", self._on_ok_clicked)
|
||||||
@@ -217,8 +204,6 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
for child in widgets:
|
for child in widgets:
|
||||||
child.set_visible(True)
|
child.set_visible(True)
|
||||||
|
|
||||||
self.raise_window.set_visible(False)
|
|
||||||
self.raise_window.set_sensitive(False)
|
|
||||||
self.ok.set_sensitive(True)
|
self.ok.set_sensitive(True)
|
||||||
self.ok.set_label(preconnect.update_mods)
|
self.ok.set_label(preconnect.update_mods)
|
||||||
|
|
||||||
@@ -230,10 +215,10 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
self.ok.emit("clicked")
|
self.ok.emit("clicked")
|
||||||
|
|
||||||
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
def _on_connect_last_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_load_to_menu(self.raise_window.get_active())
|
self.controller.update_and_load_to_menu()
|
||||||
|
|
||||||
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
def _on_ok_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.update_and_connect(self.raise_window.get_active())
|
self.controller.update_and_connect()
|
||||||
|
|
||||||
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
def _on_back_clicked(self, button: Gtk.Button) -> None:
|
||||||
self.controller.open_page(NotebookPage.SERVERS)
|
self.controller.open_page(NotebookPage.SERVERS)
|
||||||
@@ -317,10 +302,6 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
|
|
||||||
if prereqs.required_space == 0:
|
if prereqs.required_space == 0:
|
||||||
self.ok.set_label(preconnect.connect)
|
self.ok.set_label(preconnect.connect)
|
||||||
self.raise_window.set_visible(False)
|
|
||||||
else:
|
|
||||||
self.raise_window.set_visible(True)
|
|
||||||
self.raise_window.set_sensitive(True)
|
|
||||||
|
|
||||||
pretty = number(prereqs.required_space)
|
pretty = number(prereqs.required_space)
|
||||||
suffix = f" Need to download {pretty} MiB of mod updates."
|
suffix = f" Need to download {pretty} MiB of mod updates."
|
||||||
@@ -333,10 +314,7 @@ class PreConnectionAssistant(Gtk.Box):
|
|||||||
self.connect_last.hide()
|
self.connect_last.hide()
|
||||||
|
|
||||||
self._process_warnings(prereqs)
|
self._process_warnings(prereqs)
|
||||||
if self.tree.is_visible():
|
self.tree.grab_focus()
|
||||||
self.tree.grab_focus()
|
|
||||||
else:
|
|
||||||
self.grab_focus()
|
|
||||||
|
|
||||||
def mark_finished(self) -> None:
|
def mark_finished(self) -> None:
|
||||||
self.mod_count.set_label(preconnect.all_updated)
|
self.mod_count.set_label(preconnect.all_updated)
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from dzgui.controllers.mc import Controller
|
from dzgui.controllers.mc import Controller
|
||||||
from dzgui.controllers.mc import Emitter
|
from dzgui.controllers.emitter import Emitter
|
||||||
|
|
||||||
|
|
||||||
class ScrollableTree(Gtk.ScrolledWindow):
|
class ScrollableTree(Gtk.ScrolledWindow):
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ logger = logging.getLogger(APP_NAME)
|
|||||||
|
|
||||||
|
|
||||||
class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
|
class ModTreeView(ModsMixin, ContextMixin, TreeView): # type: ignore
|
||||||
def __init__(self, controller: "Controller") -> None:
|
def __init__(
|
||||||
super().__init__(controller, menu=ContextMenuGroup.MOD)
|
self, controller: "Controller", menu: ContextMenuGroup = ContextMenuGroup.MOD
|
||||||
|
) -> None:
|
||||||
|
super().__init__(controller, menu=menu)
|
||||||
|
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
self.mod_man = ModManager(self, controller)
|
self.mod_man = ModManager(self, controller)
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ description = "DayZ server browser and mod manager for Linux"
|
|||||||
authors = [
|
authors = [
|
||||||
{name = "aclist"}
|
{name = "aclist"}
|
||||||
]
|
]
|
||||||
version = "7.0.0b12"
|
version = "7.0.0b13"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
license-files = ["LICENSE"]
|
license-files = ["LICENSE"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
Reference in new issue
Block a user