Check updates in the background

This commit is contained in:
DazedAnon 2026-06-15 17:12:22 -05:00
parent 9349374ab9
commit 367e974b77
7 changed files with 311 additions and 63 deletions

View file

@ -178,21 +178,6 @@ if errorlevel 1 (
)
echo.
:: RPG Maker Ace tools (RV2JSON + decrypter) — downloaded from GitHub, not stored in git
echo Checking RPG Maker Ace tools...
python -m util.ace.update_tools
if errorlevel 1 (
echo WARNING: Ace tool download failed. Ace translation features may not work until fixed.
)
echo.
echo Checking Forge plugins...
python -m util.forge.update_tools
if errorlevel 1 (
echo WARNING: Forge update failed. Playtest Forge features use the bundled upstream copy.
)
echo.
:: Launch the GUI
echo ==========================================
echo Launching DazedMTLTool GUI...

View file

@ -220,18 +220,6 @@ else
fi
echo
echo "Checking RPG Maker Ace tools..."
if ! python -m util.ace.update_tools; then
echo "WARNING: Ace tool download failed. Ace translation features may not work until fixed."
fi
echo
echo "Checking Forge plugins..."
if ! python -m util.forge.update_tools; then
echo "WARNING: Forge update failed. Playtest Forge features use the bundled upstream copy."
fi
echo
echo "=========================================="
echo " Launching DazedMTLTool GUI..."
echo "=========================================="

View file

@ -40,6 +40,86 @@ def load_application_icon() -> QIcon:
return QIcon()
def check_tool_update() -> str | None:
"""Return latest commit SHA when a tool update is available, else None."""
try:
api = (
f"https://gitgud.io/api/v4/projects/"
f"{UpdateThread.REPO_USER}%2F{UpdateThread.REPO_NAME}"
f"/repository/branches/{UpdateThread.REPO_BRANCH}"
)
req = urllib.request.Request(api, headers={"User-Agent": "DazedMTLTool"})
with urllib.request.urlopen(req, timeout=15) as resp:
latest_sha = json.loads(resp.read())["commit"]["id"]
sha_path = Path(UpdateThread.SHA_FILE)
current_sha = sha_path.read_text().strip() if sha_path.is_file() else ""
if latest_sha != current_sha:
return latest_sha
except Exception:
pass
return None
def check_all_updates() -> dict:
"""Check tool, Ace, and Forge for available updates."""
results = {"tool": None, "ace": False, "forge": False}
results["tool"] = check_tool_update()
try:
from util.ace.update_tools import check_ace_tools_update
results["ace"] = check_ace_tools_update()
except Exception:
pass
try:
from util.forge.update_tools import check_forge_plugins_update
results["forge"] = check_forge_plugins_update()
except Exception:
pass
return results
def any_updates_available(results: dict) -> bool:
return bool(results.get("tool")) or results.get("ace") or results.get("forge")
class BackgroundUpdateCheckThread(QThread):
"""Checks for tool, Ace, and Forge updates without blocking the UI."""
finished = pyqtSignal(dict)
def run(self):
self.finished.emit(check_all_updates())
class AuxToolsUpdateThread(QThread):
"""Download Ace and/or Forge updates without updating the main tool."""
progress = pyqtSignal(str)
finished = pyqtSignal(bool, str)
def __init__(self, update_ace: bool = False, update_forge: bool = False, parent=None):
super().__init__(parent)
self.update_ace = update_ace
self.update_forge = update_forge
def run(self):
try:
if self.update_ace:
from util.ace.update_tools import ensure_ace_tools
self.progress.emit("Updating RPG Maker Ace tools…")
if not ensure_ace_tools(log_fn=lambda m: self.progress.emit(m)):
self.finished.emit(False, "Ace tool update failed.")
return
if self.update_forge:
from util.forge.update_tools import ensure_forge_plugins
self.progress.emit("Updating Forge plugins…")
if not ensure_forge_plugins(log_fn=lambda m: self.progress.emit(m)):
self.finished.emit(False, "Forge plugin update failed.")
return
self.finished.emit(True, "components_updated")
except Exception as exc:
self.finished.emit(False, str(exc))
class UpdateThread(QThread):
"""Downloads and applies a tool update from the GitGud repository."""
@ -79,18 +159,22 @@ class UpdateThread(QThread):
except Exception as exc:
self.finished.emit(False, str(exc))
# ------------------------------------------------------------------ #
def _fetch_latest_sha(self):
@classmethod
def fetch_latest_sha(cls) -> str:
api = (
f"https://gitgud.io/api/v4/projects/"
f"{self.REPO_USER}%2F{self.REPO_NAME}"
f"/repository/branches/{self.REPO_BRANCH}"
f"{cls.REPO_USER}%2F{cls.REPO_NAME}"
f"/repository/branches/{cls.REPO_BRANCH}"
)
req = urllib.request.Request(api, headers={"User-Agent": "DazedMTLTool"})
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())["commit"]["id"]
# ------------------------------------------------------------------ #
def _fetch_latest_sha(self):
return self.fetch_latest_sha()
def _read_stored_sha(self):
p = Path(self.SHA_FILE)
return p.read_text().strip() if p.exists() else ""
@ -150,7 +234,7 @@ class UpdateThread(QThread):
class UpdateDialog(QDialog):
"""Modal dialog that shows update progress and result."""
def __init__(self, parent=None):
def __init__(self, parent=None, pending_updates: dict | None = None):
super().__init__(parent)
self.setWindowTitle("Tool Update")
self.setMinimumWidth(420)
@ -172,13 +256,55 @@ class UpdateDialog(QDialog):
layout.addWidget(self.close_btn)
self._thread = None
self._pending = pending_updates or {}
def start(self, check_only=False):
self._thread = UpdateThread(check_only=check_only, parent=self)
self._thread.progress.connect(self.status_label.setText)
self._thread.finished.connect(self._on_finished)
if self._pending:
self._show_pending_updates()
return
self._thread = BackgroundUpdateCheckThread(parent=self)
self._thread.finished.connect(self._on_check_finished)
self._thread.start()
def _update_labels(self) -> list[str]:
labels = []
if self._pending.get("tool"):
labels.append("DazedMTLTool")
if self._pending.get("ace"):
labels.append("RPG Maker Ace tools")
if self._pending.get("forge"):
labels.append("Forge plugins")
return labels
def _show_pending_updates(self):
self.progress_bar.setRange(0, 1)
self.progress_bar.setValue(1)
self.close_btn.setText("Close")
labels = self._update_labels()
if not labels:
self.status_label.setText("✅ Already up to date.")
return
detail = "\n".join(f"{label}" for label in labels)
tool_sha = self._pending.get("tool")
if tool_sha:
detail = f" • DazedMTLTool ({tool_sha[:8]})\n" + "\n".join(
f"{label}" for label in labels if label != "DazedMTLTool"
)
self.status_label.setText(
f"🆕 Updates available:\n{detail}\n\nClick 'Update Now' to install."
)
self.close_btn.setText("Cancel")
update_btn = QPushButton("Update Now")
update_btn.clicked.connect(self._do_update)
self.layout().insertWidget(self.layout().count() - 1, update_btn)
def _on_check_finished(self, results: dict):
self._pending = results
self._show_pending_updates()
def _on_finished(self, success, message):
self.progress_bar.setRange(0, 1)
self.progress_bar.setValue(1)
@ -190,6 +316,11 @@ class UpdateDialog(QDialog):
if message == "already_up_to_date":
self.status_label.setText("✅ Already up to date.")
elif message == "components_updated":
self._pending = {"tool": None, "ace": False, "forge": False}
self.status_label.setText("✅ Component updates installed.")
if isinstance(self.parent(), DazedMTLGUI):
self.parent().clear_update_indicator()
elif message.startswith("update_available:"):
sha = message.split(":", 1)[1]
self.status_label.setText(
@ -201,12 +332,14 @@ class UpdateDialog(QDialog):
self.layout().insertWidget(self.layout().count() - 1, update_btn)
elif message.startswith("updated:"):
sha = message.split(":", 1)[1]
self._pending = {"tool": None, "ace": False, "forge": False}
self.status_label.setText(
f"✅ Updated to {sha}.\n\nPlease restart the tool for changes to take effect."
)
if isinstance(self.parent(), DazedMTLGUI):
self.parent().clear_update_indicator()
def _do_update(self):
# Remove any extra buttons so only the close button remains
def _clear_extra_buttons(self):
for i in reversed(range(self.layout().count())):
item = self.layout().itemAt(i)
if item and item.widget() and item.widget() is not self.close_btn:
@ -215,9 +348,37 @@ class UpdateDialog(QDialog):
if w.text() != "Close":
self.layout().removeWidget(w)
w.deleteLater()
def _do_update(self):
self._clear_extra_buttons()
self.progress_bar.setRange(0, 0)
self.status_label.setText("Downloading update…")
self.start(check_only=False)
self.close_btn.setText("Cancel")
if self._pending.get("tool"):
self.status_label.setText("Downloading update…")
self._thread = UpdateThread(check_only=False, parent=self)
self._thread.progress.connect(self.status_label.setText)
self._thread.finished.connect(self._on_finished)
self._thread.start()
return
update_ace = bool(self._pending.get("ace"))
update_forge = bool(self._pending.get("forge"))
if update_ace or update_forge:
self.status_label.setText("Downloading component updates…")
self._thread = AuxToolsUpdateThread(
update_ace=update_ace,
update_forge=update_forge,
parent=self,
)
self._thread.progress.connect(self.status_label.setText)
self._thread.finished.connect(self._on_finished)
self._thread.start()
return
self.progress_bar.setRange(0, 1)
self.progress_bar.setValue(1)
self.status_label.setText("✅ Already up to date.")
# Import configuration widgets
from gui.config_tab import ConfigTab
@ -230,10 +391,15 @@ class DazedMTLGUI(QMainWindow):
def __init__(self):
super().__init__()
self.settings = QSettings("DazedTranslations", "DazedMTLTool")
self._pending_updates: dict = {}
self._update_check_thread = None
self._update_icon = "🔄"
self.btn_update = None
self.init_ui()
self.setup_status_bar()
self.setup_font_scaling()
self.restore_window_state()
QTimer.singleShot(500, self.start_background_update_check)
def restore_window_state(self):
"""Restore window geometry and state from settings."""
@ -427,10 +593,10 @@ class DazedMTLGUI(QMainWindow):
sidebar_layout.addStretch()
# Update button at the bottom of the sidebar
btn_update = self.create_nav_button("🔄", "Check for Updates")
btn_update.setCheckable(False)
btn_update.clicked.connect(self.show_update_dialog)
sidebar_layout.addWidget(btn_update)
self.btn_update = self.create_nav_button(self._update_icon, "Check for Updates")
self.btn_update.setCheckable(False)
self.btn_update.clicked.connect(self.show_update_dialog)
sidebar_layout.addWidget(self.btn_update)
sidebar.setLayout(sidebar_layout)
@ -485,11 +651,52 @@ class DazedMTLGUI(QMainWindow):
self.config_tab.config_changed.connect(self.on_config_changed)
self.content_stack.addWidget(self.config_tab)
def start_background_update_check(self):
"""Check for tool, Ace, and Forge updates after the GUI is visible."""
if self._update_check_thread and self._update_check_thread.isRunning():
return
self._update_check_thread = BackgroundUpdateCheckThread(self)
self._update_check_thread.finished.connect(self._on_background_update_check)
self._update_check_thread.start()
def _on_background_update_check(self, results: dict):
self._pending_updates = results
if any_updates_available(results):
self.set_update_indicator()
else:
self.clear_update_indicator()
def set_update_indicator(self):
"""Highlight the update button when updates are available."""
if not self.btn_update:
return
self.btn_update.setToolTip("Updates available — click to install")
from gui.platform_glyph import configure_nav_toolbutton
configure_nav_toolbutton(
self.btn_update, self._update_icon, horizontal=False, update_available=True,
)
def clear_update_indicator(self):
"""Restore the default update button appearance."""
self._pending_updates = {}
if not self.btn_update:
return
self.btn_update.setToolTip("Check for Updates")
from gui.platform_glyph import configure_nav_toolbutton
configure_nav_toolbutton(
self.btn_update, self._update_icon, horizontal=False, update_available=False,
)
def show_update_dialog(self):
"""Open the update dialog and check for a newer version."""
dlg = UpdateDialog(self)
dlg = UpdateDialog(self, pending_updates=self._pending_updates.copy())
dlg.start(check_only=True)
dlg.exec_()
self._pending_updates = dlg._pending
if any_updates_available(dlg._pending):
self.set_update_indicator()
else:
self.clear_update_indicator()
def on_config_changed(self):
"""Handle configuration changes."""

View file

@ -84,12 +84,14 @@ def _linux_nav_pixmap(glyph: str, *, size: int, font_px: int, color: QColor) ->
return pixmap
def _linux_nav_icon(icon_text: str, *, size: int, font_px: int) -> QIcon:
def _linux_nav_icon(icon_text: str, *, size: int, font_px: int, normal_color: QColor | None = None) -> QIcon:
"""Render a nav glyph into a fixed-size pixmap so every tab aligns the same."""
glyph = platform_nav_glyph(icon_text)
gray_color = normal_color or QColor("#cccccc")
white_color = QColor("#ffffff") if normal_color is None else normal_color.lighter(125)
icon = QIcon()
gray = _linux_nav_pixmap(glyph, size=size, font_px=font_px, color=QColor("#cccccc"))
white = _linux_nav_pixmap(glyph, size=size, font_px=font_px, color=QColor("#ffffff"))
gray = _linux_nav_pixmap(glyph, size=size, font_px=font_px, color=gray_color)
white = _linux_nav_pixmap(glyph, size=size, font_px=font_px, color=white_color)
icon.addPixmap(gray, QIcon.Normal, QIcon.Off)
icon.addPixmap(gray, QIcon.Disabled, QIcon.Off)
icon.addPixmap(white, QIcon.Active, QIcon.Off)
@ -97,15 +99,18 @@ def _linux_nav_icon(icon_text: str, *, size: int, font_px: int) -> QIcon:
return icon
def _nav_toolbutton_stylesheet(*, horizontal: bool, icon_only: bool) -> str:
def _nav_toolbutton_stylesheet(*, horizontal: bool, icon_only: bool, update_available: bool = False) -> str:
border_prop = "border-bottom" if horizontal else "border-left"
accent = "#ff5252" if update_available else "#007acc"
text_color = "#ff5252" if update_available else "#cccccc"
checked_color = "#ff8a80" if update_available else "#ffffff"
if icon_only:
return f"""
QToolButton {{
background-color: transparent;
border: none;
{border_prop}: 3px solid transparent;
color: #cccccc;
color: {text_color};
padding: 0px;
margin: 0px;
}}
@ -114,8 +119,8 @@ def _nav_toolbutton_stylesheet(*, horizontal: bool, icon_only: bool) -> str:
}}
QToolButton:checked {{
background-color: #37373d;
{border_prop}: 3px solid #007acc;
color: #ffffff;
{border_prop}: 3px solid {accent};
color: {checked_color};
}}
"""
@ -125,7 +130,7 @@ def _nav_toolbutton_stylesheet(*, horizontal: bool, icon_only: bool) -> str:
background-color: transparent;
border: none;
{border_prop}: 3px solid transparent;
color: #cccccc;
color: {text_color};
font-size: {font_size};
font-family: 'DejaVu Sans Mono', monospace;
padding: 0px;
@ -136,26 +141,37 @@ def _nav_toolbutton_stylesheet(*, horizontal: bool, icon_only: bool) -> str:
}}
QToolButton:checked {{
background-color: #37373d;
{border_prop}: 3px solid #007acc;
color: #ffffff;
{border_prop}: 3px solid {accent};
color: {checked_color};
}}
"""
def configure_nav_toolbutton(btn, icon_text: str, *, horizontal: bool = False) -> None:
def configure_nav_toolbutton(
btn,
icon_text: str,
*,
horizontal: bool = False,
update_available: bool = False,
) -> None:
"""Apply nav icon — on Linux render BMP symbols into fixed pixmaps for alignment."""
if sys.platform.startswith("linux"):
# Scale to the button cell (leave room for the 3px active border).
icon_dim = max(28, min(btn.width(), btn.height()) - 8)
font_px = max(22, int(icon_dim * 0.78))
btn.setIcon(_linux_nav_icon(icon_text, size=icon_dim, font_px=font_px))
normal_color = QColor("#ff5252") if update_available else None
btn.setIcon(_linux_nav_icon(icon_text, size=icon_dim, font_px=font_px, normal_color=normal_color))
btn.setIconSize(QSize(icon_dim, icon_dim))
btn.setText("")
btn.setToolButtonStyle(Qt.ToolButtonIconOnly)
btn.setStyleSheet(_nav_toolbutton_stylesheet(horizontal=horizontal, icon_only=True))
btn.setStyleSheet(_nav_toolbutton_stylesheet(
horizontal=horizontal, icon_only=True, update_available=update_available,
))
return
btn.setIcon(QIcon())
btn.setText(icon_text)
btn.setToolButtonStyle(Qt.ToolButtonTextOnly)
btn.setStyleSheet(_nav_toolbutton_stylesheet(horizontal=horizontal, icon_only=False))
btn.setStyleSheet(_nav_toolbutton_stylesheet(
horizontal=horizontal, icon_only=False, update_available=update_available,
))

View file

@ -45,14 +45,14 @@ def main():
sys.exit(1)
try:
from util.ace.update_tools import ensure_ace_tools
ensure_ace_tools()
from util.ace.update_tools import seed_ace_tools
seed_ace_tools()
except Exception as exc:
print(f"Warning: Ace tool setup failed ({exc}). Ace features may be unavailable.")
try:
from util.forge.update_tools import ensure_forge_plugins
ensure_forge_plugins()
from util.forge.update_tools import seed_forge_plugins
seed_forge_plugins()
except Exception as exc:
print(f"Warning: Forge plugin setup failed ({exc}). Playtest Forge may be unavailable.")

View file

@ -183,6 +183,38 @@ def ensure_decrypter(force: bool = False, log_fn=print) -> bool:
return True
def seed_ace_tools(log_fn=None) -> None:
"""Copy offline-bundled Ace tools if missing (no network)."""
_seed_from_offline(RV2JSON_LOCAL, "RV2JSON.exe", log_fn)
_seed_from_offline(DECRYPTER_LOCAL, DECRYPTER_ASSET, log_fn)
def check_ace_tools_update() -> bool:
"""Return True if upstream Ace tools differ from the local copies."""
seed_ace_tools()
versions = _load_versions()
try:
remote_sha, _ = _rv2json_upstream()
if not RV2JSON_LOCAL.is_file() or versions.get("rv2json_sha", "") != remote_sha:
return True
except Exception:
if not RV2JSON_LOCAL.is_file():
return True
try:
tag, _ = _decrypter_upstream()
if not (DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file()):
return True
if DECRYPTER_LOCAL.is_file() and versions.get("decrypter_tag", "") != tag:
return True
except Exception:
if not (DECRYPTER_LOCAL.is_file() or DECRYPTER_LEGACY.is_file()):
return True
return False
def ensure_ace_tools(force: bool = False, log_fn=print) -> bool:
"""Ensure both Ace tools are present (download / update if needed)."""
ok_rv = ensure_rv2json(force=force, log_fn=log_fn)

View file

@ -151,6 +151,26 @@ def refresh_forge_plugins(log_fn=print) -> bool:
return True
def seed_forge_plugins(log_fn=None) -> None:
"""Copy offline-bundled Forge plugins if missing (no network)."""
for engine in PLUGIN_BY_ENGINE:
_seed_from_offline(engine, log_fn)
def check_forge_plugins_update() -> bool:
"""Return True if upstream Forge plugins differ from the local copies."""
seed_forge_plugins()
missing = [e for e in PLUGIN_BY_ENGINE if not bundled_plugin_path(e).is_file()]
if missing:
return True
versions = _load_versions()
local_commit = versions.get("commit", "")
try:
return _upstream_commit() != local_commit
except Exception:
return False
def ensure_forge_plugins(force: bool = False, log_fn=print) -> bool:
"""Ensure Forge plugins are present; fetch upstream when missing or stale."""
for engine in PLUGIN_BY_ENGINE: