summaryrefslogtreecommitdiff
path: root/ui/windows/settings_window.py
diff options
context:
space:
mode:
Diffstat (limited to 'ui/windows/settings_window.py')
-rw-r--r--ui/windows/settings_window.py159
1 files changed, 131 insertions, 28 deletions
diff --git a/ui/windows/settings_window.py b/ui/windows/settings_window.py
index a1d9de1..a45413d 100644
--- a/ui/windows/settings_window.py
+++ b/ui/windows/settings_window.py
@@ -52,6 +52,7 @@ class SettingsWindow(QWidget):
self.engine = engine
self._plugin_mgr = plugin_manager
self._plugin_buttons: dict = {} # plugin_id -> QPushButton
+ self._plugins_scroll = None
self.cfg = dict(self._defaults)
if current:
self.cfg.update(current)
@@ -119,6 +120,23 @@ class SettingsWindow(QWidget):
)
lay.addRow("Developer mode:", self._dev_chk)
+ from core.version import __version__ as _app_version
+ version_lbl = QLabel(f"v{_app_version}")
+ version_lbl.setObjectName("traceSource")
+ lay.addRow("Version:", version_lbl)
+
+ update_row = QHBoxLayout()
+ self._update_btn = QPushButton("Check for Updates")
+ self._update_btn.setObjectName("configButton")
+ self._update_btn.clicked.connect(self._check_for_updates)
+ update_row.addWidget(self._update_btn)
+ update_row.addStretch()
+ lay.addRow("Updates:", update_row)
+ self._update_status_lbl = QLabel("")
+ self._update_status_lbl.setObjectName("traceSource")
+ self._update_status_lbl.setWordWrap(True)
+ lay.addRow("", self._update_status_lbl)
+
rst = QPushButton("Reset to Defaults"); rst.setObjectName("configButton")
rst.clicked.connect(self._reset_to_defaults)
lay.addRow("", rst)
@@ -191,48 +209,57 @@ class SettingsWindow(QWidget):
def _plugins_tab(self):
w = QWidget()
+ vl = QVBoxLayout(w); vl.setContentsMargins(0,0,0,0); vl.setSpacing(0)
+
+ # Install bar
+ bar = QWidget(); bar.setObjectName("cfgBottomBar")
+ bl = QHBoxLayout(bar); bl.setContentsMargins(12,6,12,6)
+ bl.addStretch()
+ inst_btn = QPushButton("Install Plugin…"); inst_btn.setObjectName("configButton")
+ inst_btn.clicked.connect(self._install_plugin_from_file)
+ bl.addWidget(inst_btn)
+ vl.addWidget(bar)
+
scroll = QScrollArea(); scroll.setWidgetResizable(True)
scroll.setObjectName("deviceScroll")
- cont = QWidget(); lay = QVBoxLayout(cont)
+ self._plugins_scroll = scroll
+ vl.addWidget(scroll, 1)
+ self._refresh_plugins_list()
+ return w
+
+ def _refresh_plugins_list(self):
+ self._plugin_buttons.clear()
+ cont = QWidget()
+ lay = QVBoxLayout(cont)
lay.setContentsMargins(14, 12, 14, 12); lay.setSpacing(10)
if self._plugin_mgr is None:
lay.addWidget(QLabel("Plugin manager not available."))
lay.addStretch()
- scroll.setWidget(cont)
- root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0)
- root.addWidget(scroll); return w
+ self._plugins_scroll.setWidget(cont)
+ return
manifests = self._plugin_mgr.get_manifests()
-
if not manifests:
info = QLabel(
- "No plugins found.\n\n"
- "Drop a plugin folder into the plugins/ directory next to main.py.\n"
- "Each plugin needs a manifest.json and a plugin.py."
+ "No plugins installed.\n\n"
+ "Click 'Install Plugin…' above to install a plugin from a .zip file."
)
info.setObjectName("traceSource"); info.setWordWrap(True)
lay.addWidget(info)
lay.addStretch()
- scroll.setWidget(cont)
- root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0)
- root.addWidget(scroll); return w
+ self._plugins_scroll.setWidget(cont)
+ return
for manifest in manifests:
lay.addWidget(self._plugin_card(manifest))
-
lay.addStretch()
- scroll.setWidget(cont)
- root = QVBoxLayout(w); root.setContentsMargins(0,0,0,0)
- root.addWidget(scroll); return w
+ self._plugins_scroll.setWidget(cont)
def _plugin_card(self, manifest):
- """One card per discovered plugin."""
- card = QGroupBox()
- card.setObjectName("pluginCard")
+ card = QGroupBox(); card.setObjectName("pluginCard")
cl = QVBoxLayout(card); cl.setContentsMargins(10, 8, 10, 8); cl.setSpacing(4)
- # Header row: name + version + enable toggle
hdr = QHBoxLayout()
name_lbl = QLabel(f"<b>{manifest.name}</b> <small>v{manifest.version}</small>")
name_lbl.setObjectName("traceLabel")
@@ -240,33 +267,36 @@ class SettingsWindow(QWidget):
enabled = self._plugin_mgr.is_enabled(manifest.plugin_id)
toggle = QPushButton("Disable" if enabled else "Enable")
- toggle.setObjectName("configButton")
- toggle.setFixedWidth(72)
+ toggle.setObjectName("configButton"); toggle.setFixedWidth(72)
toggle.clicked.connect(
lambda _, pid=manifest.plugin_id, btn=toggle: self._toggle_plugin(pid, btn)
)
self._plugin_buttons[manifest.plugin_id] = toggle
hdr.addWidget(toggle)
+
+ if self._plugin_mgr.is_user_installed(manifest.plugin_id):
+ rm_btn = QPushButton("Remove")
+ rm_btn.setObjectName("configButton"); rm_btn.setFixedWidth(72)
+ rm_btn.clicked.connect(
+ lambda _, pid=manifest.plugin_id, pname=manifest.name: self._remove_plugin(pid, pname)
+ )
+ hdr.addWidget(rm_btn)
+
cl.addLayout(hdr)
- # Description / author
if manifest.description:
desc = QLabel(manifest.description)
desc.setObjectName("traceSource"); desc.setWordWrap(True)
cl.addWidget(desc)
-
if manifest.author:
- author = QLabel(f"Author: {manifest.author}")
- author.setObjectName("traceSource")
- cl.addWidget(author)
+ cl.addWidget(QLabel(f"Author: {manifest.author}").also(
+ lambda w: w.setObjectName("traceSource")))
- # Plugin-specific settings widget (only when loaded)
plugin = self._plugin_mgr.get_plugin(manifest.plugin_id)
if plugin:
sw = plugin.get_settings_widget()
if sw is not None:
cl.addWidget(sw)
-
return card
def _toggle_plugin(self, plugin_id: str, btn: QPushButton):
@@ -285,8 +315,81 @@ class SettingsWindow(QWidget):
if btn is not None and self._plugin_mgr is not None:
btn.setText("Disable" if self._plugin_mgr.is_enabled(plugin_id) else "Enable")
+ def _install_plugin_from_file(self):
+ path, _ = QFileDialog.getOpenFileName(
+ self, "Install Plugin", "", "Plugin Archives (*.zip)"
+ )
+ if not path:
+ return
+ try:
+ manifest = self._plugin_mgr.install_from_zip(path)
+ self._refresh_plugins_list()
+ QMessageBox.information(
+ self, "Plugin Installed",
+ f"'{manifest.name}' v{manifest.version} installed successfully."
+ )
+ except Exception as exc:
+ QMessageBox.critical(self, "Install Failed", str(exc))
+
+ def _remove_plugin(self, plugin_id: str, plugin_name: str):
+ if self._plugin_mgr.is_enabled(plugin_id):
+ QMessageBox.warning(self, "Cannot Remove",
+ "Disable the plugin before removing it.")
+ return
+ reply = QMessageBox.question(
+ self, "Remove Plugin",
+ f"Remove '{plugin_name}'? This deletes the plugin files.",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ QMessageBox.StandardButton.No,
+ )
+ if reply == QMessageBox.StandardButton.Yes:
+ try:
+ self._plugin_mgr.uninstall(plugin_id)
+ self._refresh_plugins_list()
+ except Exception as exc:
+ QMessageBox.critical(self, "Remove Failed", str(exc))
+
# ── Actions ───────────────────────────────────────────────────────────
+ def _check_for_updates(self):
+ from core.updater import is_frozen
+
+ if not is_frozen():
+ self._update_status_lbl.setText("Not available outside the packaged app (dev mode).")
+ return
+
+ from PyQt6.QtWidgets import QApplication
+ from core.updater import apply_update, check_for_update
+
+ self._update_btn.setEnabled(False)
+ self._update_status_lbl.setText("Checking…")
+ QApplication.processEvents()
+ try:
+ new_version = check_for_update()
+ except Exception as e:
+ self._update_status_lbl.setText(f"Check failed: {e}")
+ self._update_btn.setEnabled(True)
+ return
+ self._update_btn.setEnabled(True)
+
+ if not new_version:
+ self._update_status_lbl.setText("Up to date.")
+ return
+
+ self._update_status_lbl.setText(f"Version {new_version} available.")
+ reply = QMessageBox.question(
+ self, "Update Available",
+ f"Version {new_version} is available. Download and install now?\n\n"
+ f"The app will close and relaunch to complete the update.",
+ QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
+ QMessageBox.StandardButton.No,
+ )
+ if reply == QMessageBox.StandardButton.Yes:
+ try:
+ apply_update() # may close/relaunch the process during this call
+ except Exception as e:
+ QMessageBox.critical(self, "Update Failed", str(e))
+
def _reset_to_defaults(self):
reply = QMessageBox.question(
self, "Reset Settings",