Skip to content

Commit 5d302a1

Browse files
committed
Program is ready for 1.0.0
Also added the functionality to download game profiles from the github.
1 parent d14f2d0 commit 5d302a1

File tree

3 files changed

+155
-21
lines changed

3 files changed

+155
-21
lines changed

build_release.sh

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,22 +32,10 @@ mkdir -p "${RELEASE_DIR}"
3232
cp dist/ModManager "${RELEASE_DIR}/ModManager"
3333
chmod +x "${RELEASE_DIR}/ModManager"
3434

35-
# game_profiles — copy only the profile JSON + create empty data dirs
36-
# Skip any game directory that has no matching <id>.json (e.g. palworld stub)
37-
for profile_dir in game_profiles/*/; do
38-
game_id="$(basename "${profile_dir}")"
39-
json="${profile_dir}${game_id}.json"
40-
41-
if [ ! -f "${json}" ]; then
42-
echo "[skip] game_profiles/${game_id}/ has no profile JSON, skipping"
43-
continue
44-
fi
45-
46-
dest="${RELEASE_DIR}/game_profiles/${game_id}"
47-
mkdir -p "${dest}/compressed" "${dest}/compressed-disabled" "${dest}/mods"
48-
cp "${json}" "${dest}/"
49-
echo "[copy] game_profiles/${game_id}/${game_id}.json"
50-
done
35+
# game_profiles — create an empty directory only.
36+
# Profiles are downloaded from GitHub on first launch by mm/profiles.py.
37+
mkdir -p "${RELEASE_DIR}/game_profiles"
38+
echo "[skip] game_profiles/ — profiles are fetched from GitHub at runtime"
5139

5240
# Template config.json — game_root left blank for the user to fill in
5341
cat > "${RELEASE_DIR}/config.json" << 'JSON'

mm/gui/app.py

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,17 @@ def __init__(self):
8585
threading.Thread(target=self._preload_nexus_cache, daemon=True).start()
8686

8787
def _preload_nexus_cache(self):
88-
"""Read all cached Nexus JSON files from disk (background thread)."""
88+
"""Sync game profiles from GitHub, then read cached Nexus JSON files (background thread)."""
89+
# Silently download any missing game profiles
90+
try:
91+
from mm.profiles import sync_profiles
92+
downloaded, _ = sync_profiles(_gc._PROFILES_DIR)
93+
if downloaded:
94+
self.after(0, lambda d=downloaded: self._log_write(
95+
f"[profiles] Downloaded: {', '.join(d)}\n"))
96+
except Exception:
97+
pass
98+
8999
cache = {}
90100
cache_dir = _gc._NEXUS_CACHE_DIR # capture at thread-start time
91101
if cache_dir.exists():
@@ -278,8 +288,8 @@ def _open_settings(self):
278288

279289
win = ctk.CTkToplevel(self)
280290
win.title(f"Settings — {game_name_label}")
281-
win.geometry("580x530")
282-
win.minsize(480, 440)
291+
win.geometry("580x620")
292+
win.minsize(480, 500)
283293
win.resizable(True, True)
284294
win.transient(self)
285295
win.withdraw() # hide until fully built (prevents blank flash on Linux)
@@ -290,8 +300,8 @@ def _open_settings(self):
290300
# Center over main window
291301
self.update_idletasks()
292302
wx = self.winfo_x() + (self.winfo_width() - 580) // 2
293-
wy = self.winfo_y() + (self.winfo_height() - 530) // 2
294-
win.geometry(f"580x530+{wx}+{wy}")
303+
wy = self.winfo_y() + (self.winfo_height() - 620) // 2
304+
win.geometry(f"580x620+{wx}+{wy}")
295305

296306
scroll = ctk.CTkScrollableFrame(win, fg_color="transparent")
297307
scroll.grid(row=0, column=0, sticky="nsew")
@@ -387,6 +397,45 @@ def _clear_cache():
387397
).grid(row=0, column=0, sticky="w")
388398
cache_info.grid(row=0, column=1, sticky="w", padx=(12, 0))
389399

400+
# ── Game Profiles ─────────────────────────────────────────────
401+
row = _section_header("GAME PROFILES", row)
402+
403+
prof_frame = ctk.CTkFrame(scroll, fg_color="transparent")
404+
prof_frame.grid(row=row, column=0, sticky="ew", padx=16, pady=(4, 0))
405+
row += 1
406+
407+
prof_status = ctk.CTkLabel(prof_frame, text="",
408+
font=ctk.CTkFont(size=11),
409+
text_color=("gray50", "gray55"))
410+
411+
def _update_profiles():
412+
prof_status.configure(text="Checking GitHub…")
413+
prof_frame.update_idletasks()
414+
415+
def _do():
416+
try:
417+
from mm.profiles import sync_profiles
418+
downloaded, failed = sync_profiles(_gc._PROFILES_DIR, force=True)
419+
if downloaded:
420+
msg = f"Updated: {', '.join(downloaded)}"
421+
elif failed:
422+
msg = f"Failed: {', '.join(failed)}"
423+
else:
424+
msg = "All profiles are up to date."
425+
except Exception as exc:
426+
msg = f"Error: {exc}"
427+
self.after(0, lambda: prof_status.configure(text=msg))
428+
429+
threading.Thread(target=_do, daemon=True).start()
430+
431+
ctk.CTkButton(
432+
prof_frame, text="Update Game Profiles", width=160, height=28,
433+
fg_color=("gray72", "gray30"), hover_color=("gray62", "gray38"),
434+
font=ctk.CTkFont(size=11),
435+
command=_update_profiles,
436+
).grid(row=0, column=0, sticky="w")
437+
prof_status.grid(row=0, column=1, sticky="w", padx=(12, 0))
438+
390439
# ── Paths ─────────────────────────────────────────────────────
391440
row = _section_header("PATHS", row)
392441

mm/profiles.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
"""
2+
mm/profiles.py — download game profiles from the GitHub repository.
3+
4+
Profiles live at:
5+
game_profiles/<game_id>/<game_id>.json
6+
in the main branch of the repo. The GitHub Contents API is used to discover
7+
which profiles exist, then each JSON is fetched via the raw CDN.
8+
"""
9+
10+
import json
11+
import urllib.request
12+
import urllib.error
13+
from pathlib import Path
14+
15+
GITHUB_REPO = "Bugiboop/LinuxNMModManager"
16+
GITHUB_BRANCH = "main"
17+
18+
_CONTENTS_API = (
19+
f"https://api.github.com/repos/{GITHUB_REPO}"
20+
f"/contents/game_profiles?ref={GITHUB_BRANCH}"
21+
)
22+
_RAW_BASE = (
23+
f"https://raw.githubusercontent.com/{GITHUB_REPO}"
24+
f"/{GITHUB_BRANCH}/game_profiles"
25+
)
26+
_UA = "ModManager/1.0"
27+
_TIMEOUT = 10
28+
29+
30+
def list_remote_profiles() -> list:
31+
"""
32+
Return the list of game IDs available on GitHub.
33+
Queries the GitHub Contents API for subdirectories of game_profiles/.
34+
Returns [] on any network or API error.
35+
"""
36+
try:
37+
req = urllib.request.Request(
38+
_CONTENTS_API,
39+
headers={
40+
"User-Agent": _UA,
41+
"Accept": "application/vnd.github.v3+json",
42+
},
43+
)
44+
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
45+
entries = json.loads(resp.read())
46+
return [e["name"] for e in entries if e.get("type") == "dir"]
47+
except Exception:
48+
return []
49+
50+
51+
def download_profile(game_id: str, profiles_dir: Path) -> bool:
52+
"""
53+
Download game_profiles/<game_id>/<game_id>.json from GitHub into
54+
profiles_dir/<game_id>/<game_id>.json.
55+
Creates the game directory and standard subdirectories if they don't exist.
56+
Returns True on success, False on any error.
57+
"""
58+
url = f"{_RAW_BASE}/{game_id}/{game_id}.json"
59+
dest_dir = profiles_dir / game_id
60+
dest = dest_dir / f"{game_id}.json"
61+
62+
dest_dir.mkdir(parents=True, exist_ok=True)
63+
for subdir in ("mods", "compressed", "compressed-disabled"):
64+
(dest_dir / subdir).mkdir(exist_ok=True)
65+
66+
try:
67+
req = urllib.request.Request(url, headers={"User-Agent": _UA})
68+
with urllib.request.urlopen(req, timeout=_TIMEOUT) as resp:
69+
dest.write_bytes(resp.read())
70+
return True
71+
except Exception:
72+
return False
73+
74+
75+
def sync_profiles(profiles_dir: Path, force: bool = False) -> tuple:
76+
"""
77+
Download any profiles not yet present locally (or all profiles if
78+
force=True).
79+
80+
Returns (downloaded, failed) — each a list of game ID strings.
81+
Returns ([], []) on network failure (list_remote_profiles returns []).
82+
"""
83+
remote_ids = list_remote_profiles()
84+
if not remote_ids:
85+
return [], []
86+
87+
downloaded, failed = [], []
88+
for game_id in remote_ids:
89+
local_json = profiles_dir / game_id / f"{game_id}.json"
90+
if local_json.exists() and not force:
91+
continue
92+
if download_profile(game_id, profiles_dir):
93+
downloaded.append(game_id)
94+
else:
95+
failed.append(game_id)
96+
97+
return downloaded, failed

0 commit comments

Comments
 (0)