feat: add various system and theme icons, configuration files, and support assets

This commit is contained in:
jschantli committed 2026-10-03 08:14:42 -07:00
1 parent 848dac4264
commit 54baea79f4
15893 files changed
+781426 -2

No files matched your search

+207
View File
@@ -0,0 +1,207 @@
"""Content pane: screenshot, metadata, and Files / Conflicts tabs for the selected mod."""
from __future__ import annotations
from typing import Callable
import gi
gi.require_version("Gtk", "4.0")
gi.require_version("Adw", "1")
from gi.repository import Adw, Gtk
from ..core.library import Mod
from ..core.manager import Manager
MAX_FILE_LINES = 2000
class DetailsPane(Gtk.Stack):
def __init__(self, manager: Manager, on_edit: Callable[[str], None], on_delete: Callable[[str], None],
on_choose_dll: Callable[[str], None], on_import: Callable[[], None]):
super().__init__()
self.m = manager
self.on_edit, self.on_delete, self.on_choose_dll = on_edit, on_delete, on_choose_dll
self.mod_id: str | None = None
self._loading = False
self.empty = Adw.StatusPage(icon_name="package-x-generic-symbolic", title="Select a mod",
description="Pick a mod from the list to see its details.")
self.first_run = Adw.StatusPage(icon_name="folder-download-symbolic", title="No mods yet",
description="Import a mod archive (.zip, .7z, .rar) or drop one onto this window.")
btn = Gtk.Button(label="Import Mod…", halign=Gtk.Align.CENTER)
btn.add_css_class("suggested-action")
btn.add_css_class("pill")
btn.connect("clicked", lambda *_: on_import())
self.first_run.set_child(btn)
self.add_named(self.empty, "empty")
self.add_named(self.first_run, "first-run")
self.add_named(self._build_detail(), "detail")
def _build_detail(self) -> Gtk.Widget:
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12, margin_top=12, margin_bottom=24,
margin_start=12, margin_end=12)
self.picture = Gtk.Picture(can_shrink=True, content_fit=Gtk.ContentFit.COVER, height_request=260)
self.picture.add_css_class("card")
box.append(self.picture)
self.title = Gtk.Label(xalign=0, wrap=True, selectable=True)
self.title.add_css_class("title-1")
box.append(self.title)
self.meta = Gtk.Label(xalign=0, wrap=True)
self.meta.add_css_class("dim-label")
box.append(self.meta)
self.banner = Adw.Banner.new("")
self.banner.set_button_label("Choose…")
self.banner.connect("button-clicked", lambda *_: self.mod_id and self.on_choose_dll(self.mod_id))
box.append(self.banner)
self.desc = Gtk.Label(xalign=0, wrap=True, selectable=True)
box.append(self.desc)
edit = Adw.PreferencesGroup(title="Details")
self.name_row = Adw.EntryRow(title="Name", show_apply_button=True)
self.cat_row = Adw.EntryRow(title="Category", show_apply_button=True)
self.ver_row = Adw.EntryRow(title="Version", show_apply_button=True)
for key, row in (("name", self.name_row), ("category", self.cat_row), ("version", self.ver_row)):
row.connect("apply", self._apply, key)
edit.add(row)
self.path_row = Adw.EntryRow(title="Installed Path", editable=False)
edit.add(self.path_row)
self.game_path_row = Adw.EntryRow(title="Game Directory", editable=False)
edit.add(self.game_path_row)
self.nexus_row = Adw.ActionRow(title="Nexus Mods", subtitle="Not linked")
self.nexus_link_btn = Gtk.LinkButton(label="Open Page")
self.nexus_row.add_suffix(self.nexus_link_btn)
self.nexus_update_btn = Gtk.Button(label="Check Update", valign=Gtk.Align.CENTER)
self.nexus_update_btn.connect("clicked", self._check_update_clicked)
self.nexus_row.add_suffix(self.nexus_update_btn)
edit.add(self.nexus_row)
box.append(edit)
stack = Adw.ViewStack()
self.files_view = Gtk.TextView(editable=False, cursor_visible=False, monospace=True, top_margin=8,
bottom_margin=8, left_margin=8, right_margin=8)
files_scroll = Gtk.ScrolledWindow(min_content_height=220, child=self.files_view)
self.conflicts_view = Gtk.TextView(editable=False, cursor_visible=False, wrap_mode=Gtk.WrapMode.WORD_CHAR,
top_margin=8, bottom_margin=8, left_margin=8, right_margin=8)
conf_scroll = Gtk.ScrolledWindow(min_content_height=220, child=self.conflicts_view)
self.files_page = stack.add_titled(files_scroll, "files", "Files")
self.conf_page = stack.add_titled(conf_scroll, "conflicts", "Conflicts")
switcher = Adw.ViewSwitcher(stack=stack, policy=Adw.ViewSwitcherPolicy.WIDE, halign=Gtk.Align.START)
box.append(switcher)
box.append(stack)
delete = Gtk.Button(label="Delete Mod…", halign=Gtk.Align.START)
delete.add_css_class("destructive-action")
delete.connect("clicked", lambda *_: self.mod_id and self.on_delete(self.mod_id))
box.append(delete)
clamp = Adw.Clamp(maximum_size=760, child=box)
return Gtk.ScrolledWindow(child=clamp, hscrollbar_policy=Gtk.PolicyType.NEVER)
def _check_update_clicked(self, btn) -> None:
if not self.mod_id: return
mod = self.m.library.get(self.mod_id)
if not mod or not mod.nexus_mod_id: return
def work():
from ..core.nexus import KeyStore, NexusClient, NxmLink
client = NexusClient(KeyStore(self.m.paths.config_dir / "nexus.json").get())
return client.mod_info(NxmLink(game="monsterhunterwilds", mod_id=int(mod.nexus_mod_id), file_id=0))
def done(info, err):
if err:
self.nexus_row.set_subtitle(f"Check failed: {err}")
elif info:
latest = info.get("version", "Unknown")
if latest != mod.version:
self.nexus_row.set_subtitle(f"Update available! Latest: v{latest}")
self.nexus_update_btn.add_css_class("suggested-action")
else:
self.nexus_row.set_subtitle("Up to date")
self.nexus_update_btn.remove_css_class("suggested-action")
self.nexus_row.set_subtitle("Checking...")
from .tasks import run_async
run_async(work, done)
def _apply(self, row: Adw.EntryRow, key: str) -> None:
if self._loading or not self.mod_id:
return
text = row.get_text().strip()
if key == "name" and not text:
return
self.m.update_mod(self.mod_id, **{key: text})
self.on_edit(self.mod_id)
def clear(self) -> None:
self.mod_id = None
self.set_visible_child_name("empty" if self.m.library.mods else "first-run")
def show_mod(self, mod: Mod) -> None:
self._loading = True
self.mod_id = mod.id
shot = mod.screenshot_path
self.picture.set_filename(str(shot) if shot else None)
self.picture.set_visible(bool(shot))
self.title.set_label(mod.name)
bits = [b for b in (f"v{mod.version}" if mod.version else "", f"by {mod.author}" if mod.author else "",
mod.category) if b]
self.meta.set_label(" · ".join(bits) or "No metadata")
self.desc.set_label(mod.description)
self.desc.set_visible(bool(mod.description))
for row, val in ((self.name_row, mod.name), (self.cat_row, mod.category), (self.ver_row, mod.version)):
row.set_text(val)
self.path_row.set_text(str(mod.files_root))
if mod.nexus_url:
self.nexus_row.set_visible(True)
self.nexus_link_btn.set_uri(mod.nexus_url)
self.nexus_row.set_subtitle(f"Linked (ID: {mod.nexus_mod_id})")
self.nexus_update_btn.remove_css_class("suggested-action")
else:
self.nexus_row.set_visible(False)
cls = self.m.classification(mod)
if self.m.game_dir and cls.entries:
import os
paths = [os.path.dirname(e.dest) for e in cls.entries if e.dest]
if not paths:
self.game_path_row.set_text(str(self.m.game_dir))
else:
common = os.path.commonpath(paths) if len(paths) > 1 else paths[0]
self.game_path_row.set_text(str(self.m.game_dir / common))
elif self.m.game_dir:
self.game_path_row.set_text(str(self.m.game_dir))
else:
self.game_path_row.set_text("Not deployed or no files")
self.banner.set_title(f"{len(cls.ambiguous)} .dll file(s) at the top level: plugin or root-level DLL?")
self.banner.set_revealed(bool(cls.ambiguous))
lines = [f"[{e.kind}] {e.dest or e.src.name}" + (f" ({e.chain} chain)" if e.kind == "pak" else "")
for e in cls.entries]
lines += [f"[?] {p.name} (undecided .dll)" for p in cls.ambiguous]
lines += [f"note: {n}" for n in cls.notes]
if len(lines) > MAX_FILE_LINES:
lines = lines[:MAX_FILE_LINES] + [f"… {len(lines) - MAX_FILE_LINES} more"]
self.files_view.get_buffer().set_text("\n".join(lines) or "No deployable files")
confs = self.m.conflicts_for(mod.id)
names = lambda ids: ", ".join(self.m.library.get(i).name if self.m.library.get(i) else i for i in ids)
if not confs:
text = "No conflicts with other enabled mods." if self.m.profiles.is_enabled(mod.id) \
else "Enable this mod to check for conflicts."
else:
text = "\n\n".join(
f"{c.key}\n {'PAK / loose' if c.kind == 'pak-loose' else 'PAK' if c.kind == 'pak-pak' else 'Loose file'}"
f" · winner: {names([c.winner])}\n overridden: {names(c.losers)}" for c in confs)
self.conflicts_view.get_buffer().set_text(text)
self.conf_page.set_badge_number(len(confs)) if hasattr(self.conf_page, "set_badge_number") else None
self.set_visible_child_name("detail")
self._loading = False