Add GUI desktop application
- Add gui/ package: CustomTkinter app with dashboard, mods, tools, logs, and settings views; first-run SetupWizard for config.json generation - Add gui.py root entry point (calls gui.run_app()) - Add selection.json for GUI selection state persistence - Add customtkinter to requirements.txt - Fix link_mods.py minor issues surfaced during GUI integration - Add modlist_html/Test_Preset_A.html and Test_Preset_B.html as example inputs - Update README.md: add GUI prerequisites, gui.py script section, gui/ folder structure, customtkinter to prerequisites table - Update CLAUDE.md: add python gui.py to common commands, document GUI package architecture and views Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
187
gui/wizard.py
Normal file
187
gui/wizard.py
Normal file
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
import customtkinter as ctk
|
||||
from tkinter import filedialog
|
||||
|
||||
from gui._constants import COLOR_OK, COLOR_ERROR, PROJECT_ROOT
|
||||
|
||||
|
||||
class SetupWizard(ctk.CTkToplevel):
|
||||
"""Modal first-run wizard that writes config.json."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent: ctk.CTk,
|
||||
on_complete: Callable[[], None],
|
||||
) -> None:
|
||||
super().__init__(parent)
|
||||
self.title("Setup — Arma Mod Manager")
|
||||
self.geometry("500x420")
|
||||
self.resizable(False, False)
|
||||
self.grab_set()
|
||||
self.lift()
|
||||
self.focus_force()
|
||||
|
||||
self._on_complete = on_complete
|
||||
self._url = ctk.StringVar(value="https://")
|
||||
self._user = ctk.StringVar()
|
||||
self._pw = ctk.StringVar()
|
||||
self._arma = ctk.StringVar()
|
||||
|
||||
self._body = ctk.CTkFrame(self, fg_color="transparent")
|
||||
self._body.pack(fill="both", expand=True, padx=28, pady=24)
|
||||
|
||||
self._show(0)
|
||||
|
||||
def _clear(self) -> None:
|
||||
for w in self._body.winfo_children():
|
||||
w.destroy()
|
||||
|
||||
def _show(self, step: int) -> None:
|
||||
self._clear()
|
||||
[self._page_server, self._page_paths, self._page_review][step]()
|
||||
|
||||
# ── Page 1: server ───────────────────────────────────────────────────────
|
||||
|
||||
def _page_server(self) -> None:
|
||||
ctk.CTkLabel(
|
||||
self._body, text="Step 1 of 3 — Server Connection",
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
).pack(anchor="w")
|
||||
ctk.CTkLabel(
|
||||
self._body, text="Enter the details for your Caddy mod server.",
|
||||
text_color="gray",
|
||||
).pack(anchor="w", pady=(4, 18))
|
||||
|
||||
for lbl, var, show in [
|
||||
("Server URL", self._url, ""),
|
||||
("Username", self._user, ""),
|
||||
("Password", self._pw, "•"),
|
||||
]:
|
||||
ctk.CTkLabel(self._body, text=lbl).pack(anchor="w")
|
||||
ctk.CTkEntry(self._body, textvariable=var, width=440, show=show).pack(
|
||||
anchor="w", pady=(2, 10))
|
||||
|
||||
foot = ctk.CTkFrame(self._body, fg_color="transparent")
|
||||
foot.pack(fill="x", pady=(8, 0))
|
||||
self._conn_lbl = ctk.CTkLabel(foot, text="", text_color="gray")
|
||||
self._conn_lbl.pack(side="left")
|
||||
ctk.CTkButton(foot, text="Next →", width=90,
|
||||
command=lambda: self._show(1)).pack(side="right")
|
||||
ctk.CTkButton(foot, text="Test Connection", width=140,
|
||||
fg_color="transparent", border_width=1,
|
||||
text_color=("gray10", "gray90"),
|
||||
command=self._test).pack(side="right", padx=(0, 8))
|
||||
|
||||
def _test(self) -> None:
|
||||
self._conn_lbl.configure(text="Testing…", text_color="gray")
|
||||
self.update()
|
||||
try:
|
||||
import requests
|
||||
r = requests.get(self._url.get(),
|
||||
auth=(self._user.get(), self._pw.get()),
|
||||
timeout=8)
|
||||
if r.ok:
|
||||
self._conn_lbl.configure(text="✓ Connected", text_color=COLOR_OK)
|
||||
else:
|
||||
self._conn_lbl.configure(text=f"✗ HTTP {r.status_code}",
|
||||
text_color=COLOR_ERROR)
|
||||
except Exception as e:
|
||||
self._conn_lbl.configure(text=f"✗ {e}", text_color=COLOR_ERROR)
|
||||
|
||||
# ── Page 2: paths ────────────────────────────────────────────────────────
|
||||
|
||||
def _page_paths(self) -> None:
|
||||
ctk.CTkLabel(
|
||||
self._body, text="Step 2 of 3 — Arma 3 Server Folder",
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
).pack(anchor="w")
|
||||
ctk.CTkLabel(
|
||||
self._body,
|
||||
text="Point to your Arma 3 Server installation. "
|
||||
"Links (junctions) will be created here.",
|
||||
text_color="gray", wraplength=440, justify="left",
|
||||
).pack(anchor="w", pady=(4, 18))
|
||||
|
||||
ctk.CTkLabel(self._body, text="Arma 3 Server folder").pack(anchor="w")
|
||||
row = ctk.CTkFrame(self._body, fg_color="transparent")
|
||||
row.pack(fill="x", pady=(2, 8))
|
||||
ctk.CTkEntry(row, textvariable=self._arma, width=350).pack(side="left")
|
||||
ctk.CTkButton(row, text="Browse", width=80,
|
||||
command=self._browse_arma).pack(side="left", padx=8)
|
||||
|
||||
ctk.CTkLabel(
|
||||
self._body,
|
||||
text="All other folders (downloads, presets) will be created "
|
||||
"automatically next to this tool.",
|
||||
text_color="gray", font=ctk.CTkFont(size=11),
|
||||
wraplength=440, justify="left",
|
||||
).pack(anchor="w", pady=(8, 0))
|
||||
|
||||
foot = ctk.CTkFrame(self._body, fg_color="transparent")
|
||||
foot.pack(fill="x", pady=(20, 0))
|
||||
ctk.CTkButton(foot, text="← Back", width=80,
|
||||
fg_color="transparent", border_width=1,
|
||||
text_color=("gray10", "gray90"),
|
||||
command=lambda: self._show(0)).pack(side="left")
|
||||
ctk.CTkButton(foot, text="Next →", width=80,
|
||||
command=lambda: self._show(2)).pack(side="right")
|
||||
|
||||
def _browse_arma(self) -> None:
|
||||
d = filedialog.askdirectory(title="Select Arma 3 Server folder")
|
||||
if d:
|
||||
self._arma.set(d)
|
||||
|
||||
# ── Page 3: review + save ────────────────────────────────────────────────
|
||||
|
||||
def _page_review(self) -> None:
|
||||
ctk.CTkLabel(
|
||||
self._body, text="Step 3 of 3 — Review & Save",
|
||||
font=ctk.CTkFont(size=16, weight="bold"),
|
||||
).pack(anchor="w")
|
||||
ctk.CTkLabel(
|
||||
self._body, text="Check your settings, then click Save.",
|
||||
text_color="gray",
|
||||
).pack(anchor="w", pady=(4, 14))
|
||||
|
||||
summary = (
|
||||
f"Server URL: {self._url.get()}\n"
|
||||
f"Username: {self._user.get()}\n"
|
||||
f"Arma folder: {self._arma.get() or '(not set)'}\n"
|
||||
)
|
||||
box = ctk.CTkTextbox(self._body, height=90,
|
||||
font=ctk.CTkFont(family="Consolas", size=12))
|
||||
box.insert("1.0", summary)
|
||||
box.configure(state="disabled")
|
||||
box.pack(fill="x", pady=(0, 16))
|
||||
|
||||
foot = ctk.CTkFrame(self._body, fg_color="transparent")
|
||||
foot.pack(fill="x")
|
||||
ctk.CTkButton(foot, text="← Back", width=80,
|
||||
fg_color="transparent", border_width=1,
|
||||
text_color=("gray10", "gray90"),
|
||||
command=lambda: self._show(1)).pack(side="left")
|
||||
ctk.CTkButton(foot, text="Save & Open", width=120,
|
||||
command=self._save).pack(side="right")
|
||||
|
||||
def _save(self) -> None:
|
||||
cfg = {
|
||||
"server": {
|
||||
"base_url": self._url.get(),
|
||||
"username": self._user.get(),
|
||||
"password": self._pw.get(),
|
||||
},
|
||||
"paths": {
|
||||
"arma_dir": self._arma.get(),
|
||||
"downloads": "downloads",
|
||||
"modlist_html": "modlist_html",
|
||||
"modlist_json": "modlist_json",
|
||||
},
|
||||
}
|
||||
(PROJECT_ROOT / "config.json").write_text(
|
||||
json.dumps(cfg, indent=2), encoding="utf-8")
|
||||
self.destroy()
|
||||
self._on_complete()
|
||||
Reference in New Issue
Block a user