release: v1.2.0 — About+update checker, log viewer, restart button, dup-name fix, stale-file hardening #26
@@ -11,13 +11,23 @@ Run: python mcp_manager.py
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, Signal
|
||||
from PySide6.QtGui import QAction, QColor, QGuiApplication, QPainter
|
||||
from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, QUrl, Signal
|
||||
from PySide6.QtGui import (
|
||||
QAction,
|
||||
QColor,
|
||||
QDesktopServices,
|
||||
QGuiApplication,
|
||||
QIcon,
|
||||
QPainter,
|
||||
QPixmap,
|
||||
)
|
||||
from PySide6.QtWidgets import (
|
||||
QAbstractItemView,
|
||||
QApplication,
|
||||
QCheckBox,
|
||||
QComboBox,
|
||||
QDialog,
|
||||
QDialogButtonBox,
|
||||
@@ -147,6 +157,23 @@ class SpawnTester(QThread):
|
||||
self.done.emit(result)
|
||||
|
||||
|
||||
class UpdateCheckWorker(QThread):
|
||||
"""
|
||||
Fetches the latest release from the Gitea releases API off the UI thread.
|
||||
|
||||
Notify-only: `core.fetch_latest_release()` only ever reads release
|
||||
metadata (a version tag + a URL) and never downloads or replaces the
|
||||
running binary. Fails quiet — emits None on any network problem — so
|
||||
it's safe to fire unattended from a silent startup check as well as from
|
||||
the About dialog's "Check for updates" button.
|
||||
"""
|
||||
|
||||
done = Signal(object) # dict | None
|
||||
|
||||
def run(self):
|
||||
self.done.emit(core.fetch_latest_release())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Small reusable: key/value editor (for env and headers)
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -1090,6 +1117,153 @@ class PasteDialog(QDialog):
|
||||
self.err.setText(str(e))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# About dialog
|
||||
# --------------------------------------------------------------------------- #
|
||||
class AboutDialog(QDialog):
|
||||
"""
|
||||
App info + a manual "Check for updates" action.
|
||||
|
||||
Shows nothing sensitive: app name, icon, version (from
|
||||
core.__version__), and links opened in the system browser via
|
||||
QDesktopServices.openUrl — never navigated to in-app. The update check
|
||||
itself only ever reads release metadata (see UpdateCheckWorker); it
|
||||
never downloads or replaces the running binary.
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setWindowTitle("About Better Claude Config")
|
||||
self.setFixedWidth(440)
|
||||
self._worker: UpdateCheckWorker | None = None
|
||||
self._release_url: str | None = None
|
||||
|
||||
icon_path = (
|
||||
Path(__file__).resolve().parent / "icons" / "twin-gears" / "rounded" / "icon-128.png"
|
||||
)
|
||||
if icon_path.is_file():
|
||||
self.setWindowIcon(QIcon(str(icon_path)))
|
||||
|
||||
v = QVBoxLayout(self)
|
||||
v.setSpacing(10)
|
||||
|
||||
head = QHBoxLayout()
|
||||
head.setSpacing(12)
|
||||
icon_lbl = QLabel()
|
||||
if icon_path.is_file():
|
||||
icon_lbl.setPixmap(
|
||||
QPixmap(str(icon_path)).scaled(
|
||||
56,
|
||||
56,
|
||||
Qt.AspectRatioMode.KeepAspectRatio,
|
||||
Qt.TransformationMode.SmoothTransformation,
|
||||
)
|
||||
)
|
||||
head.addWidget(icon_lbl)
|
||||
|
||||
title_box = QVBoxLayout()
|
||||
title_box.setSpacing(2)
|
||||
name_lbl = QLabel("Better Claude Config")
|
||||
name_lbl.setObjectName("h1")
|
||||
title_box.addWidget(name_lbl)
|
||||
ver_lbl = QLabel(f"Version {core.__version__}")
|
||||
ver_lbl.setObjectName("muted")
|
||||
title_box.addWidget(ver_lbl)
|
||||
head.addLayout(title_box, 1)
|
||||
v.addLayout(head)
|
||||
|
||||
desc = QLabel(
|
||||
"A cross-platform GUI for editing the mcpServers block of Claude "
|
||||
"Desktop and Claude Code configs."
|
||||
)
|
||||
desc.setObjectName("muted")
|
||||
desc.setWordWrap(True)
|
||||
v.addWidget(desc)
|
||||
|
||||
if sys.platform == "darwin":
|
||||
note = QLabel(
|
||||
"This build isn't notarized by Apple. If macOS blocks it on first "
|
||||
"launch, right-click the app ▸ Open, or allow it under System "
|
||||
"Settings ▸ Privacy & Security."
|
||||
)
|
||||
note.setObjectName("muted")
|
||||
note.setWordWrap(True)
|
||||
v.addWidget(note)
|
||||
|
||||
links = QHBoxLayout()
|
||||
for text, url in (
|
||||
("Repository", core.REPO_URL),
|
||||
("Issues", core.ISSUES_URL),
|
||||
("MIT License", core.LICENSE_URL),
|
||||
):
|
||||
btn = QPushButton(text)
|
||||
btn.setFlat(True)
|
||||
btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
btn.clicked.connect(lambda _=False, u=url: QDesktopServices.openUrl(QUrl(u)))
|
||||
links.addWidget(btn)
|
||||
links.addStretch()
|
||||
v.addLayout(links)
|
||||
|
||||
# --- update check ----------------------------------------------- #
|
||||
upd_row = QHBoxLayout()
|
||||
self.check_btn = QPushButton("Check for updates")
|
||||
self.check_btn.clicked.connect(self._check_for_updates)
|
||||
upd_row.addWidget(self.check_btn)
|
||||
self.update_status = QLabel("")
|
||||
self.update_status.setObjectName("muted")
|
||||
self.update_status.setWordWrap(True)
|
||||
upd_row.addWidget(self.update_status, 1)
|
||||
v.addLayout(upd_row)
|
||||
|
||||
self.release_btn = QPushButton("Open releases page")
|
||||
self.release_btn.setCursor(Qt.CursorShape.PointingHandCursor)
|
||||
self.release_btn.clicked.connect(self._open_release_page)
|
||||
self.release_btn.hide()
|
||||
v.addWidget(self.release_btn)
|
||||
|
||||
self.auto_check_box = QCheckBox("Automatically check for updates on startup")
|
||||
st = QSettings("BCC", "BetterClaudeConfig")
|
||||
self.auto_check_box.setChecked(bool(st.value("update/autoCheck", True, type=bool)))
|
||||
self.auto_check_box.toggled.connect(self._toggle_auto_check)
|
||||
v.addWidget(self.auto_check_box)
|
||||
|
||||
btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Close)
|
||||
btns.rejected.connect(self.reject)
|
||||
btns.button(QDialogButtonBox.StandardButton.Close).clicked.connect(self.accept)
|
||||
v.addWidget(btns)
|
||||
|
||||
def _toggle_auto_check(self, on: bool):
|
||||
QSettings("BCC", "BetterClaudeConfig").setValue("update/autoCheck", on)
|
||||
|
||||
def _check_for_updates(self):
|
||||
self.check_btn.setEnabled(False)
|
||||
self.release_btn.hide()
|
||||
self.update_status.setStyleSheet(f"color: {MUTED};")
|
||||
self.update_status.setText("Checking…")
|
||||
self._worker = UpdateCheckWorker()
|
||||
self._worker.done.connect(self._on_check_done)
|
||||
self._worker.start()
|
||||
|
||||
def _on_check_done(self, release: dict | None):
|
||||
self.check_btn.setEnabled(True)
|
||||
self._worker = None
|
||||
if not release:
|
||||
self.update_status.setStyleSheet(f"color: {MUTED};")
|
||||
self.update_status.setText("Couldn't check for updates (offline?).")
|
||||
return
|
||||
if core.is_newer_version(core.__version__, release["version"]):
|
||||
self.update_status.setStyleSheet(f"color: {ACCENT};")
|
||||
self.update_status.setText(f"{release['version']} available.")
|
||||
self._release_url = release.get("url") or core.RELEASES_URL
|
||||
self.release_btn.show()
|
||||
else:
|
||||
self.update_status.setStyleSheet(f"color: {GOOD};")
|
||||
self.update_status.setText("You're up to date.")
|
||||
|
||||
def _open_release_page(self):
|
||||
QDesktopServices.openUrl(QUrl(self._release_url or core.RELEASES_URL))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Main window
|
||||
# --------------------------------------------------------------------------- #
|
||||
@@ -1099,6 +1273,7 @@ class MainWindow(QMainWindow):
|
||||
self.setWindowTitle("Better Claude Config")
|
||||
self.resize(940, 640)
|
||||
self.setAcceptDrops(True)
|
||||
self._build_menu_bar()
|
||||
|
||||
self.profiles: list[core.Profile] = []
|
||||
self.full_config: dict = {}
|
||||
@@ -1141,6 +1316,37 @@ class MainWindow(QMainWindow):
|
||||
|
||||
self._restore_layout()
|
||||
self.reload_profiles()
|
||||
self._maybe_auto_check_updates()
|
||||
|
||||
# --- menu bar ---------------------------------------------------------- #
|
||||
def _build_menu_bar(self):
|
||||
help_menu = self.menuBar().addMenu("&Help")
|
||||
about_action = QAction("About Better Claude Config…", self)
|
||||
about_action.triggered.connect(self._show_about)
|
||||
help_menu.addAction(about_action)
|
||||
|
||||
def _show_about(self):
|
||||
AboutDialog(self).exec()
|
||||
|
||||
# --- update check (silent, throttled, off-thread) --------------------- #
|
||||
def _maybe_auto_check_updates(self):
|
||||
st = QSettings("BCC", "BetterClaudeConfig")
|
||||
if not bool(st.value("update/autoCheck", True, type=bool)):
|
||||
return
|
||||
last = float(st.value("update/lastCheck", 0.0, type=float) or 0.0)
|
||||
if (time.time() - last) < 86400: # at most once/day
|
||||
return
|
||||
st.setValue("update/lastCheck", time.time())
|
||||
self._startup_update_worker = UpdateCheckWorker()
|
||||
self._startup_update_worker.done.connect(self._on_startup_update_checked)
|
||||
self._startup_update_worker.start()
|
||||
|
||||
def _on_startup_update_checked(self, release: dict | None):
|
||||
self._startup_update_worker = None
|
||||
if release and core.is_newer_version(core.__version__, release["version"]):
|
||||
self.status.setText(
|
||||
f"Update available: {release['version']} · Help ▸ About to view it."
|
||||
)
|
||||
|
||||
# --- layout persistence ---------------------------------------------- #
|
||||
def _restore_layout(self):
|
||||
|
||||
Reference in New Issue
Block a user