Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b5a40903ff | |||
| f2f4c372c3 | |||
| ce3154c563 | |||
| 0af5235c98 | |||
| 8770b5a269 |
@@ -17,3 +17,4 @@ dist/
|
||||
.idea/
|
||||
Thumbs.db
|
||||
.DS_Store
|
||||
.venv/
|
||||
|
||||
+5
-3
@@ -1,7 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Octo Updater entry point."""
|
||||
from config import _relocate_legacy_data
|
||||
from platform_ops import enable_dpi_awareness
|
||||
from ui import OctoUpdaterApp
|
||||
import sys
|
||||
from src.config import _relocate_legacy_data
|
||||
from src.platform_ops import enable_dpi_awareness
|
||||
from src.ui import OctoUpdaterApp
|
||||
|
||||
def main() -> None:
|
||||
enable_dpi_awareness()
|
||||
|
||||
@@ -18,9 +18,9 @@ import queue
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
from app_log import log
|
||||
from config import load_config, update_config
|
||||
from platform_ops import display_info
|
||||
from .app_log import log
|
||||
from .config import load_config, update_config
|
||||
from .platform_ops import display_info
|
||||
|
||||
def remove_wdb(client_dir: str):
|
||||
"""Delete the client's WDB folder (server-data cache, safe to drop)."""
|
||||
+16
-18
@@ -15,8 +15,13 @@ import time
|
||||
import math
|
||||
import threading
|
||||
import queue
|
||||
import json
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
from .platform_ops import user_data_dir, user_config_dir
|
||||
|
||||
|
||||
|
||||
|
||||
UPDATER_VERSION = "1.3.1"
|
||||
SERVER = "https://octowow.st"
|
||||
@@ -28,29 +33,21 @@ DOWNLOAD_TIMEOUT = 10 # seconds without any data before a transfer aborts
|
||||
# next to this script — never the current working directory, which varies with
|
||||
# how the app was launched. This anchors the default game folder.
|
||||
if getattr(sys, "frozen", False):
|
||||
APP_DIR = os.path.dirname(os.path.abspath(sys.executable))
|
||||
APP_DIR = os.path.dirname(os.path.abspath(sys.executable)) # Directory where frozen .exe is executed from
|
||||
#TODO : Compile into binary ? Make into AppImage ?
|
||||
else:
|
||||
APP_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
APP_DIR = os.path.dirname(os.path.abspath(__file__)) # Directory where python3 script is executed from
|
||||
|
||||
|
||||
def _default_app_data_dir() -> str:
|
||||
base = os.environ.get("LOCALAPPDATA")
|
||||
if base:
|
||||
path = os.path.join(base, "OctoUpdater")
|
||||
try:
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
except OSError:
|
||||
pass
|
||||
return APP_DIR
|
||||
# Set env vars
|
||||
APP_DATA_DIR = user_data_dir()
|
||||
|
||||
APP_CONFIG_DIR = user_config_dir()
|
||||
|
||||
APP_DATA_DIR = _default_app_data_dir()
|
||||
|
||||
CONFIG_FILE = os.path.join(APP_DATA_DIR, "config.json")
|
||||
CONFIG_FILE = os.path.join(APP_CONFIG_DIR, "config.json") # TODO: Change this to point to ~/.config/OctoUpdater
|
||||
|
||||
# First-run default game folder, anchored to the app dir (not the CWD).
|
||||
DEFAULT_GAME_DIR = os.path.join(APP_DIR, "OctoWoW")
|
||||
DEFAULT_GAME_DIR = os.path.join(APP_DIR, "OctoWoW") # NOTE: May need to change later to point to other dir
|
||||
|
||||
|
||||
def _relocate_legacy_data():
|
||||
@@ -81,17 +78,18 @@ def load_config() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
# NOTE: writes the config , using tempfile to prevent data loss
|
||||
def _atomic_write(path: str, text: str):
|
||||
"""Write via a temp file + atomic rename so a crash mid-write can never
|
||||
leave a truncated/corrupt file at `path`."""
|
||||
tmp = path + ".tmp"
|
||||
tmp = path + ".tmp"
|
||||
with open(tmp, "w") as f:
|
||||
f.write(text)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
os.replace(tmp, path)
|
||||
|
||||
|
||||
# NOTE: executes write to config using thread safe lock ?
|
||||
def save_config(data: dict):
|
||||
with _CONFIG_LOCK:
|
||||
try:
|
||||
@@ -21,9 +21,9 @@ from pathlib import Path
|
||||
import urllib.error
|
||||
import zipfile
|
||||
import io
|
||||
from app_log import log
|
||||
from config import SERVER, UA, UPDATER_VERSION, load_config, update_config, ensure_dir
|
||||
from net import ALLOWED_DOWNLOAD_HOSTS, secure_urlopen
|
||||
from .app_log import log
|
||||
from .config import SERVER, UA, UPDATER_VERSION, load_config, update_config, ensure_dir
|
||||
from .net import ALLOWED_DOWNLOAD_HOSTS, secure_urlopen
|
||||
|
||||
MODS_REGISTRY = [
|
||||
{
|
||||
+1
-1
@@ -18,7 +18,7 @@ import queue
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
from config import UA
|
||||
from .config import UA
|
||||
|
||||
|
||||
|
||||
@@ -5,15 +5,57 @@ import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
|
||||
# Windows: %LOCALAPPDATA%\octo-updater\
|
||||
# Linux: $XDG_DATA_HOME/octo-updater or $HOME/.local/share/octo-updater
|
||||
def user_data_dir() -> Path:
|
||||
"""Alters which directory is used for application data. Returns Path object to directory"""
|
||||
if sys.platform == "win32":
|
||||
local_app_data = os.environ.get("LOCALAPPDATA")
|
||||
if not local_app_data:
|
||||
raise RuntimeError("LOCALAPPDATA is not set")
|
||||
|
||||
return Path(local_app_data)
|
||||
|
||||
# For linux , will first check for XDG_DATA_HOME"
|
||||
local_app_data = os.environ.get("XDG_DATA_HOME")
|
||||
if local_app_data:
|
||||
return Path(local_app_data) / "OctoUpdater"
|
||||
|
||||
return Path.home() / ".local" / "share" / "OctoUpdater"
|
||||
|
||||
|
||||
# Windows: %APPDATA%\octo-updater
|
||||
# Linux: $XDG_CONFIG_HOME/octo-updater or $HOME/.config/octo-updater
|
||||
def user_config_dir() -> Path:
|
||||
"""Alters which directory is used for application data. Returns Path object to directory"""
|
||||
|
||||
if sys.platform == "win32":
|
||||
local_config_data = os.environ.get("APPDATA")
|
||||
if not local_config_data:
|
||||
raise RuntimeError("APPDATA is not set")
|
||||
|
||||
return Path(local_config_data)
|
||||
|
||||
# For linux , will first check for XDG_CONFIG_HOME" , else set to ~/.config/octo-updater
|
||||
local_config_data = os.environ.get("XDG_CONFIG_HOME")
|
||||
if local_config_data:
|
||||
return Path(local_config_data) / "OctoUpdater"
|
||||
|
||||
return Path.home() / ".config" / "OctoUpdater"
|
||||
|
||||
# NOTE: I think this is for opening links in the launcher ?
|
||||
def open_directory(path: str) -> None:
|
||||
if sys.platform == "win32":
|
||||
subprocess.Popen(["explorer.exe", path])
|
||||
else:
|
||||
opener = shutil.which("xdg-open")
|
||||
opener = shutil.which("xdg-open") # Point this to default linux browser ?
|
||||
if not opener:
|
||||
raise RuntimeError("xdg-open is required to open directories")
|
||||
subprocess.Popen([opener, path])
|
||||
|
||||
# NOTE: Creates link to windows junction for torrent ?
|
||||
def create_directory_link(link: str, target: str) -> None:
|
||||
if sys.platform == "win32":
|
||||
flags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
@@ -23,6 +65,7 @@ def create_directory_link(link: str, target: str) -> None:
|
||||
return
|
||||
os.symlink(target, link, target_is_directory=True)
|
||||
|
||||
# NOTE: allows dynamic scaling of UI window ?
|
||||
def enable_dpi_awareness() -> None:
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
@@ -58,9 +101,11 @@ def request_defender_exclusion(path: str) -> bool:
|
||||
return result > 32
|
||||
|
||||
|
||||
# TODO: Find solution to launch client natively on linux , either using wine or bottles ?
|
||||
def launch_executable(executable: str, cwd: str) -> None:
|
||||
"""Launch the managed executable using native process semantics."""
|
||||
if sys.platform != "win32":
|
||||
# TODO: implement solution to run game on linux ....
|
||||
raise RuntimeError("native Linux game launching is not configured")
|
||||
flags = (getattr(subprocess, "DETACHED_PROCESS", 0)
|
||||
| getattr(subprocess, "CREATE_BREAKAWAY_FROM_JOB", 0))
|
||||
@@ -95,3 +140,7 @@ def display_info() -> dict:
|
||||
ctypes.windll.user32.EnumDisplaySettingsW(None, -1, ctypes.byref(dm))
|
||||
return {"width": dm.dmPelsWidth, "height": dm.dmPelsHeight,
|
||||
"refresh_rate": dm.dmDisplayFrequency}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from pathlib import Path
|
||||
from src import config
|
||||
|
||||
|
||||
|
||||
|
||||
def test_load_config(monkeypatch, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text(
|
||||
json.dumps({"game_dir": "/games/OctoWoW"})
|
||||
)
|
||||
|
||||
monkeypatch.setattr(config, "CONFIG_FILE", str(config_file))
|
||||
|
||||
result = config.load_config()
|
||||
|
||||
assert result == {"game_dir": "/games/OctoWoW"}
|
||||
|
||||
|
||||
def test_load_config_missing_file(monkeypatch, tmp_path):
|
||||
config_file = tmp_path / "does_not_exist.json"
|
||||
|
||||
monkeypatch.setattr(config, "CONFIG_FILE", str(config_file))
|
||||
|
||||
result = config.load_config()
|
||||
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_load_config_invalid_json(monkeypatch, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text("this is not json")
|
||||
|
||||
monkeypatch.setattr(config, "CONFIG_FILE", str(config_file))
|
||||
|
||||
result = config.load_config()
|
||||
|
||||
assert result == {}
|
||||
|
||||
|
||||
def test_atomic_write(tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
|
||||
config._atomic_write(
|
||||
str(config_file),
|
||||
'{"test": true}'
|
||||
)
|
||||
|
||||
assert config_file.exists()
|
||||
assert config_file.read_text() == '{"test": true}'
|
||||
assert not Path(str(config_file) + ".tmp").exists()
|
||||
|
||||
|
||||
def test_atomic_write_replaces_existing_file(tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
config_file.write_text("old data")
|
||||
|
||||
config._atomic_write(
|
||||
str(config_file),
|
||||
"new data"
|
||||
)
|
||||
|
||||
assert config_file.read_text() == "new data"
|
||||
|
||||
|
||||
def test_save_config(monkeypatch, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
|
||||
monkeypatch.setattr(config, "CONFIG_FILE", str(config_file))
|
||||
|
||||
config.save_config({
|
||||
"game_dir": "/games/OctoWoW",
|
||||
"enabled": True,
|
||||
})
|
||||
|
||||
result = json.loads(config_file.read_text())
|
||||
|
||||
assert result == {
|
||||
"game_dir": "/games/OctoWoW",
|
||||
"enabled": True,
|
||||
}
|
||||
|
||||
|
||||
def test_update_config(monkeypatch, tmp_path):
|
||||
config_file = tmp_path / "config.json"
|
||||
|
||||
config_file.write_text(
|
||||
json.dumps({
|
||||
"game_dir": "/old/path",
|
||||
"existing_setting": True,
|
||||
})
|
||||
)
|
||||
|
||||
monkeypatch.setattr(config, "CONFIG_FILE", str(config_file))
|
||||
|
||||
def change_game_dir(cfg):
|
||||
cfg["game_dir"] = "/new/path"
|
||||
|
||||
result = config.update_config(change_game_dir)
|
||||
|
||||
assert result["game_dir"] == "/new/path"
|
||||
assert result["existing_setting"] is True
|
||||
|
||||
saved = json.loads(config_file.read_text())
|
||||
|
||||
assert saved["game_dir"] == "/new/path"
|
||||
assert saved["existing_setting"] is True
|
||||
|
||||
|
||||
def test_ensure_dir(tmp_path):
|
||||
new_directory = tmp_path / "one" / "two" / "three"
|
||||
|
||||
config.ensure_dir(new_directory)
|
||||
|
||||
assert new_directory.exists()
|
||||
assert new_directory.is_dir()
|
||||
|
||||
|
||||
def test_relocate_legacy_data(monkeypatch, tmp_path):
|
||||
app_dir = tmp_path / "app"
|
||||
data_dir = tmp_path / "data"
|
||||
config_dir = tmp_path / "config"
|
||||
|
||||
app_dir.mkdir()
|
||||
data_dir.mkdir()
|
||||
config_dir.mkdir()
|
||||
|
||||
old_config = app_dir / "octo_updater_config.json"
|
||||
new_config = config_dir / "config.json"
|
||||
|
||||
old_config.write_text('{"legacy": true}')
|
||||
|
||||
monkeypatch.setattr(config, "APP_DIR", str(app_dir))
|
||||
monkeypatch.setattr(config, "APP_DATA_DIR", str(data_dir))
|
||||
monkeypatch.setattr(config, "CONFIG_FILE", str(new_config))
|
||||
|
||||
config._relocate_legacy_data()
|
||||
|
||||
assert not old_config.exists()
|
||||
assert new_config.exists()
|
||||
assert json.loads(new_config.read_text()) == {
|
||||
"legacy": True
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
from src import platform_ops
|
||||
|
||||
|
||||
def test_user_config_dir_windows(monkeypatch):
|
||||
monkeypatch.setattr(platform_ops.sys, "platform", "win32")
|
||||
monkeypatch.setenv("APPDATA", r"C:\Users\test\AppData\Roaming")
|
||||
|
||||
result = platform_ops.user_config_dir()
|
||||
|
||||
assert result == Path(r"C:\Users\test\AppData\Roaming")
|
||||
|
||||
|
||||
def test_user_config_dir_windows_without_appdata(monkeypatch):
|
||||
monkeypatch.setattr(platform_ops.sys, "platform", "win32")
|
||||
monkeypatch.delenv("APPDATA", raising=False)
|
||||
|
||||
with pytest.raises(RuntimeError, match="APPDATA is not set"):
|
||||
platform_ops.user_config_dir()
|
||||
|
||||
|
||||
def test_user_config_dir_linux_with_xdg_config_home(monkeypatch):
|
||||
monkeypatch.setattr(platform_ops.sys, "platform", "linux")
|
||||
monkeypatch.setenv("XDG_CONFIG_HOME", "/tmp/test-config")
|
||||
|
||||
result = platform_ops.user_config_dir()
|
||||
|
||||
assert result == Path("/tmp/test-config") / "octo-updater"
|
||||
|
||||
|
||||
def test_user_config_dir_linux_without_xdg_config_home(monkeypatch):
|
||||
monkeypatch.setattr(platform_ops.sys, "platform", "linux")
|
||||
monkeypatch.delenv("XDG_CONFIG_HOME", raising=False)
|
||||
monkeypatch.setattr(
|
||||
platform_ops.Path,
|
||||
"home",
|
||||
lambda: Path("/home/testuser"),
|
||||
)
|
||||
|
||||
result = platform_ops.user_config_dir()
|
||||
|
||||
assert result == Path("/home/testuser/.config/octo-updater")
|
||||
|
||||
@@ -18,11 +18,11 @@ import queue
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
from app_log import log
|
||||
from config import APP_DATA_DIR, UA, DOWNLOAD_RETRY, DOWNLOAD_TIMEOUT, ensure_dir, load_config, update_config
|
||||
from net import ALLOWED_DOWNLOAD_HOSTS, secure_urlopen
|
||||
from client import locale_patches, write_config_wtf, update_config_wtf
|
||||
from platform_ops import create_directory_link
|
||||
from .app_log import log
|
||||
from .config import APP_DATA_DIR, UA, DOWNLOAD_RETRY, DOWNLOAD_TIMEOUT, ensure_dir, load_config, update_config
|
||||
from .net import ALLOWED_DOWNLOAD_HOSTS, secure_urlopen
|
||||
from .client import locale_patches, write_config_wtf, update_config_wtf
|
||||
from .platform_ops import create_directory_link
|
||||
|
||||
# Torrent-based client sync (aria2c)
|
||||
# ──────────────────────────────────────────────────────────────────────────────
|
||||
+28
-21
@@ -21,18 +21,21 @@ from pathlib import Path
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
import urllib.error
|
||||
from app_log import LOG_QUEUE as _LOG_Q, log
|
||||
from config import *
|
||||
from net import *
|
||||
from client import *
|
||||
from torrent import *
|
||||
from mods import *
|
||||
from platform_ops import open_directory, launch_executable, request_defender_exclusion
|
||||
from .app_log import LOG_QUEUE as _LOG_Q, log
|
||||
|
||||
#from .config import _relocate_legacy_data
|
||||
from .net import *
|
||||
from .client import *
|
||||
from .torrent import *
|
||||
from .mods import *
|
||||
from .platform_ops import open_directory, launch_executable, request_defender_exclusion
|
||||
from . import config
|
||||
|
||||
|
||||
# News tab: the latest announcement (forum 2, full post) fills the left panel,
|
||||
# the patch-notes list (forum 4) fills the right.
|
||||
NEWS_FEATURED_URL = f"{SERVER}/forum/octonews.php?forum=2&mode=full"
|
||||
PATCHNOTES_URL = f"{SERVER}/forum/octonews.php?mode=list&forum=4&limit=8"
|
||||
NEWS_FEATURED_URL = f"{config.SERVER}/forum/octonews.php?forum=2&mode=full"
|
||||
PATCHNOTES_URL = f"{config.SERVER}/forum/octonews.php?mode=list&forum=4&limit=8"
|
||||
NEWS_TIMEOUT = 8
|
||||
NEWS_CACHE_TTL = 300
|
||||
|
||||
@@ -71,9 +74,11 @@ C_PARCH_LINK = "#a3561c"
|
||||
C_PARCH_EDGE = "#b7a678"
|
||||
|
||||
FONT_BODY = ("Segoe UI", 9)
|
||||
FONT_MONO = ("TkDefaultFont", 10)
|
||||
FONT_VER = ("TkDefaultFont", 8)
|
||||
|
||||
NEWS_FEATURED_URL = f"{SERVER}/forum/octonews.php?forum=2&mode=full"
|
||||
PATCHNOTES_URL = f"{SERVER}/forum/octonews.php?mode=list&forum=4&limit=8"
|
||||
NEWS_FEATURED_URL = f"{config.SERVER}/forum/octonews.php?forum=2&mode=full"
|
||||
PATCHNOTES_URL = f"{config.SERVER}/forum/octonews.php?mode=list&forum=4&limit=8"
|
||||
NEWS_TIMEOUT = 8
|
||||
NEWS_CACHE_TTL = 300
|
||||
|
||||
@@ -103,7 +108,8 @@ def _format_news_date(iso: str) -> str:
|
||||
|
||||
def fetch_patch_notes() -> list:
|
||||
"""Patch-notes list → [{id, title, date, body, url?, author?}, …]"""
|
||||
req = urllib.request.Request(PATCHNOTES_URL, headers={"User-Agent": UA})
|
||||
#req = urllib.request.Request(PATCHNOTES_URL, headers={"User-Agent": config.UA})
|
||||
req = urllib.request.Request(PATCHNOTES_URL)
|
||||
with secure_urlopen(req, timeout=NEWS_TIMEOUT) as r:
|
||||
data = json.load(r)
|
||||
items = data.get("items", [])
|
||||
@@ -115,7 +121,8 @@ def fetch_patch_notes() -> list:
|
||||
|
||||
def fetch_featured_post() -> dict | None:
|
||||
"""Latest announcements-forum post → {id, title, author?, date, url, html}"""
|
||||
req = urllib.request.Request(NEWS_FEATURED_URL, headers={"User-Agent": UA})
|
||||
#req = urllib.request.Request(NEWS_FEATURED_URL, headers={"User-Agent": config.UA})
|
||||
req = urllib.request.Request(NEWS_FEATURED_URL)
|
||||
with secure_urlopen(req, timeout=NEWS_TIMEOUT) as r:
|
||||
data = json.load(r)
|
||||
return data if isinstance(data, dict) and data.get("id") else None
|
||||
@@ -210,10 +217,10 @@ class OctoUpdaterApp(tk.Tk):
|
||||
# Move a pre-1.3 config/cache from beside the .exe into the per-user
|
||||
# data dir before anything reads them, so first-run detection and
|
||||
# load_config() below see the relocated files (see _relocate_legacy_data).
|
||||
_relocate_legacy_data()
|
||||
#config._relocate_legacy_data()
|
||||
|
||||
# Detect first run before anything writes the config.
|
||||
self._first_run = not os.path.exists(CONFIG_FILE)
|
||||
self._first_run = not os.path.exists(config.CONFIG_FILE)
|
||||
# Set when the user adds a Defender exclusion via Settings; checked at
|
||||
# the next reconcile to skip the auto-prompt (so a manual add isn't
|
||||
# double-prompted), then reset — so each folder change offers one unless
|
||||
@@ -247,7 +254,7 @@ class OctoUpdaterApp(tk.Tk):
|
||||
# effect only once Settings is closed (see _close_settings), so no
|
||||
# live trace fires mid-edit.
|
||||
self._game_path = tk.StringVar(
|
||||
value=os.path.normpath(self._cfg.get("out_dir", DEFAULT_GAME_DIR)))
|
||||
value=os.path.normpath(self._cfg.get("out_dir", config.DEFAULT_GAME_DIR)))
|
||||
|
||||
# Count of mods with an update available — shown as a badge on the
|
||||
# MODS nav tab.
|
||||
@@ -293,7 +300,7 @@ class OctoUpdaterApp(tk.Tk):
|
||||
|
||||
self._build()
|
||||
|
||||
out_dir = self._cfg.get("out_dir", DEFAULT_GAME_DIR)
|
||||
out_dir = self._cfg.get("out_dir", config.DEFAULT_GAME_DIR)
|
||||
if not os.path.exists(out_dir):
|
||||
def _wipe(c):
|
||||
c.pop("mods", None)
|
||||
@@ -596,13 +603,13 @@ class OctoUpdaterApp(tk.Tk):
|
||||
hdr.bind("<Leave>", lambda e: self._on_hdr_motion(None))
|
||||
|
||||
self._clear_wdb_var = tk.BooleanVar(
|
||||
value=bool(self._cfg.get("clear_wdb_on_launch", False)))
|
||||
value=bool(self._cfg.get("clear_wdb_on_launch", True)))
|
||||
self._close_on_launch_var = tk.BooleanVar(
|
||||
value=bool(self._cfg.get("close_on_launch", False)))
|
||||
value=bool(self._cfg.get("close_on_launch", True)))
|
||||
self._auto_mods_var = tk.BooleanVar(
|
||||
value=bool(self._cfg.get("auto_install_mods", True)))
|
||||
value=bool(self._cfg.get("auto_install_mods", False)))
|
||||
self._auto_addons_var = tk.BooleanVar(
|
||||
value=bool(self._cfg.get("auto_install_addons", True)))
|
||||
value=bool(self._cfg.get("auto_install_addons", False)))
|
||||
self._ignore_speech_var = tk.BooleanVar(
|
||||
value=bool(self._cfg.get("ignore_speech", False)))
|
||||
# Deferred "install missing" pending from turning an auto-install
|
||||
Reference in New Issue
Block a user