diff options
| author | Christian Kolset <christian.kolset@gmail.com> | 2026-08-02 11:05:48 -0600 |
|---|---|---|
| committer | Christian Kolset <christian.kolset@gmail.com> | 2026-08-02 11:05:48 -0600 |
| commit | 1c349a982026736f4c2a26951d9d0aefe7b963bc (patch) | |
| tree | b66175df392e3d707ca45650facfd18bcba6c337 /ui/windows/settings_window.py | |
| parent | fa0304793a4525db68cb53b527d695bfa2c65966 (diff) | |
| parent | 0aa59c13af65beeae104662593b21ba4d8a37789 (diff) | |
Merge branch 'feat/auto-updater'
Diffstat (limited to 'ui/windows/settings_window.py')
| -rw-r--r-- | ui/windows/settings_window.py | 159 |
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", |
