- Add a real QMenuBar (Help -> About...). - AboutDialog: app icon, name, version (core.__version__), and links to the repo/issues/license opened via QDesktopServices.openUrl (system browser, never in-app navigation). macOS unsigned-app note. - "Check for updates" button + UpdateCheckWorker (QThread) runs core.fetch_latest_release() off the UI thread; shows "up to date" or "vX.Y.Z available" with a button to open the releases page. No binary download, ever. - Optional quiet startup auto-check, throttled to once/day via a QSettings timestamp, off-thread, silent on failure, toggle lives in the About dialog.
This commit is contained in:
@@ -11,13 +11,23 @@ Run: python mcp_manager.py
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, Signal
|
from PySide6.QtCore import QRect, QSettings, QSize, Qt, QThread, QTimer, QUrl, Signal
|
||||||
from PySide6.QtGui import QAction, QColor, QGuiApplication, QPainter
|
from PySide6.QtGui import (
|
||||||
|
QAction,
|
||||||
|
QColor,
|
||||||
|
QDesktopServices,
|
||||||
|
QGuiApplication,
|
||||||
|
QIcon,
|
||||||
|
QPainter,
|
||||||
|
QPixmap,
|
||||||
|
)
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QAbstractItemView,
|
QAbstractItemView,
|
||||||
QApplication,
|
QApplication,
|
||||||
|
QCheckBox,
|
||||||
QComboBox,
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
@@ -147,6 +157,23 @@ class SpawnTester(QThread):
|
|||||||
self.done.emit(result)
|
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)
|
# Small reusable: key/value editor (for env and headers)
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -1090,6 +1117,153 @@ class PasteDialog(QDialog):
|
|||||||
self.err.setText(str(e))
|
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
|
# Main window
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -1099,6 +1273,7 @@ class MainWindow(QMainWindow):
|
|||||||
self.setWindowTitle("Better Claude Config")
|
self.setWindowTitle("Better Claude Config")
|
||||||
self.resize(940, 640)
|
self.resize(940, 640)
|
||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
|
self._build_menu_bar()
|
||||||
|
|
||||||
self.profiles: list[core.Profile] = []
|
self.profiles: list[core.Profile] = []
|
||||||
self.full_config: dict = {}
|
self.full_config: dict = {}
|
||||||
@@ -1141,6 +1316,37 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
self._restore_layout()
|
self._restore_layout()
|
||||||
self.reload_profiles()
|
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 ---------------------------------------------- #
|
# --- layout persistence ---------------------------------------------- #
|
||||||
def _restore_layout(self):
|
def _restore_layout(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user