diff --git a/main.py b/main.py index 86845a0..b8e087f 100644 --- a/main.py +++ b/main.py @@ -1184,6 +1184,7 @@ class MainApplication(QMainWindow): # for stat in stats[:10]: # print(stat) # print("Top 10 growing object types in RAM:") + # objgraph has been removed so it will need to be reinstalled for this to work # objgraph.show_most_common_types(limit=10) def check_if_app_is_dirty(self): diff --git a/package_updates b/package_updates new file mode 100644 index 0000000..c490253 --- /dev/null +++ b/package_updates @@ -0,0 +1,49 @@ +Package Version Latest Type +------------------------- -------- ------------ ----- +certifi 2026.1.4 2026.7.22 wheel +charset-normalizer 3.4.4 3.5.1 wheel +decorator 5.2.1 5.3.1 wheel +fonttools 4.61.1 4.64.0 wheel +h5py 3.15.1 3.16.0 wheel +idna 3.11 3.19 wheel +joblib 1.5.3 1.6.0 wheel +kiwisolver 1.4.9 1.5.1 wheel +lazy_loader 0.4 0.5 wheel +lxml 6.0.2 6.1.2 wheel +matplotlib 3.10.8 3.11.1 wheel +mne 1.11.0 1.12.1 wheel +mne-connectivity 0.7.0 0.9.0 wheel +neurokit2 0.2.12 0.2.13 wheel +nibabel 5.3.3 5.4.2 wheel +nilearn 0.13.0 0.14.0 wheel +numpy 2.4.1 2.5.2 wheel +packaging 25.0 26.3 wheel +pandas 2.3.3 3.0.5 wheel +patsy 1.0.2 1.0.3 wheel +pillow 12.1.0 12.3.0 wheel +pip 25.3 26.2.1 wheel +platformdirs 4.5.1 4.11.7 wheel +pooch 1.8.2 1.9.0 wheel +psutil 7.2.1 7.2.2 wheel +pyinstaller 6.19.0 6.22.2 wheel +pyinstaller-hooks-contrib 2026.3 2026.7 wheel +pyparsing 3.3.1 3.3.2 wheel +PySide6 6.10.1 6.11.2 wheel +PySide6_Addons 6.10.1 6.11.2 wheel +PySide6_Essentials 6.10.1 6.11.2 wheel +pytz 2025.2 2026.3.post1 wheel +pyvista 0.46.5 0.48.4 wheel +pyvistaqt 0.11.3 0.12.0 wheel +requests 2.32.5 2.34.2 wheel +scikit-learn 1.8.0 1.9.0 wheel +scipy 1.17.0 1.18.1 wheel +scooby 0.11.0 0.11.2 wheel +setuptools 82.0.1 84.0.0 wheel +shiboken6 6.10.1 6.11.2 wheel +statsmodels 0.14.6 0.15.0 wheel +tqdm 4.67.1 4.70.0 wheel +typing_extensions 4.15.0 4.16.0 wheel +tzdata 2025.3 2026.3 wheel +urllib3 2.6.3 2.7.0 wheel +vtk 9.5.2 9.7.0 wheel +xarray 2026.1.0 2026.7.0 wheel \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 0c9c6c5..b60a9dc 100644 Binary files a/requirements.txt and b/requirements.txt differ diff --git a/src/window/plugins.py b/src/window/plugins.py index fa496cd..213ed28 100644 --- a/src/window/plugins.py +++ b/src/window/plugins.py @@ -1,6 +1,6 @@ """ Filename: plugins.py -Description: Plugins window +Description: Plugins window with deferred remote fetching and installation state checks. Author: Tyler de Zeeuw License: GPL-3.0 @@ -10,7 +10,7 @@ License: GPL-3.0 from typing import Any # External library imports -from PySide6.QtCore import Qt +from PySide6.QtCore import Qt, QThread, Signal from PySide6.QtGui import QColor from PySide6.QtWidgets import ( QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, @@ -21,6 +21,20 @@ from plugin_manager import PluginManager, parse_version from src.shared.shareddata import APP_NAME, CURRENT_VERSION, PLUGINS_URL +class RemoteFetchWorker(QThread): + """Background thread to fetch remote repository data without lagging the main UI.""" + fetched = Signal(list, bool) # (plugins_data, all_failed) + + def __init__(self, manager: PluginManager, urls: list[str]) -> None: + super().__init__() + self.manager = manager + self.urls = urls + + def run(self) -> None: + plugins, all_failed = self.manager.fetch_remote_repositories(self.urls) + self.fetched.emit(plugins, all_failed) + + class PluginsWindow(QWidget): def __init__(self, parent: QWidget | None, plugin_manager: PluginManager) -> None: @@ -29,12 +43,13 @@ class PluginsWindow(QWidget): self.main_app_window = parent self.manager: PluginManager = plugin_manager - self.setWindowTitle(f"{APP_NAME.upper()} - Plugins") + self.setWindowTitle(f"Plugins - {APP_NAME.upper()}") self.resize(750, 500) self.repository_urls: list[str] = [PLUGINS_URL] self.remote_plugins_data: list[dict[str, Any]] = [] self._has_fetched_remote: bool = False + self._fetch_worker: RemoteFetchWorker | None = None # Refresh UI automatically if manager updates state self.manager.plugins_changed.connect(self.refresh_installed_plugins) @@ -74,6 +89,10 @@ class PluginsWindow(QWidget): self.installed_list.addItem("No plugins installed.") self._clear_details_panel() + # Re-populate browser list if remote data was already fetched to reflect newly installed/uninstalled plugins + if self._has_fetched_remote and self.remote_plugins_data: + self._populate_browser_list() + def _on_installed_item_changed(self, current: QListWidgetItem | None, _: Any) -> None: """Updates the right-hand details panel when a plugin is selected.""" if not current: @@ -136,13 +155,25 @@ class PluginsWindow(QWidget): self.manager.uninstall_plugin(info["path"]) def fetch_remote_plugins(self) -> None: + """Asynchronously fetches remote plugins on a background thread.""" + if self._fetch_worker is not None and self._fetch_worker.isRunning(): + return # Fetch operation already in progress + self.browser_list.clear() self.browser_list.addItem("Fetching remote repositories...") self.btn_install.setEnabled(False) + self.btn_fetch.setEnabled(False) - plugins, all_failed = self.manager.fetch_remote_repositories(self.repository_urls) + # Defer network call to background worker to avoid UI freeze + self._fetch_worker = RemoteFetchWorker(self.manager, self.repository_urls) + self._fetch_worker.fetched.connect(self._on_remote_fetched) + self._fetch_worker.start() + + def _on_remote_fetched(self, plugins: list[dict[str, Any]], all_failed: bool) -> None: + """Callback executed on the main UI thread when remote fetching finishes.""" self.remote_plugins_data = plugins self._has_fetched_remote = True + self.btn_fetch.setEnabled(True) if plugins: self._populate_browser_list() @@ -154,9 +185,17 @@ class PluginsWindow(QWidget): self.browser_list.addItem("No plugins found across configured repositories.") def _populate_browser_list(self) -> None: + """Populates the browser tab list and checks if each plugin is already installed.""" self.browser_list.clear() + + # Gather set of currently installed identifiers (by ID and Name) + installed_info = self.manager.get_installed_plugins_info() + installed_ids = {p.get("id") for p in installed_info if p.get("id")} + installed_names = {p.get("name") for p in installed_info if p.get("name")} + for plugin in self.remote_plugins_data: name = plugin.get("name", "Unknown") + p_id = plugin.get("id", "") version = plugin.get("version", "v0.0") desc = plugin.get("description", "") platforms = plugin.get("platforms", []) @@ -166,16 +205,24 @@ class PluginsWindow(QWidget): is_version_ok = self.manager.current_app_version >= parse_version(min_v_str) is_compatible = is_platform_ok and is_version_ok + # Check if plugin is already installed locally + is_installed = (bool(p_id) and p_id in installed_ids) or (bool(name) and name in installed_names) + display_text = f"{name} (v{version}) - {desc}" - if not is_compatible: + if is_installed: + display_text += " [Installed]" + elif not is_compatible: display_text += " [Incompatible]" item = QListWidgetItem(display_text, self.browser_list) item.setData(Qt.ItemDataRole.UserRole, plugin) item.setData(Qt.ItemDataRole.UserRole + 1, is_compatible) + item.setData(Qt.ItemDataRole.UserRole + 2, is_installed) - if not is_compatible: - item.setForeground(QColor("#d32f2f")) + if is_installed: + item.setForeground(QColor("#2e7d32")) # Green for installed + elif not is_compatible: + item.setForeground(QColor("#d32f2f")) # Red for incompatible def install_selected_plugin(self) -> None: selected = self.browser_list.currentItem() @@ -200,7 +247,21 @@ class PluginsWindow(QWidget): def _on_browser_item_changed(self, current: QListWidgetItem | None, _: Any) -> None: if current: - self.btn_install.setEnabled(bool(current.data(Qt.ItemDataRole.UserRole + 1))) + is_compatible = bool(current.data(Qt.ItemDataRole.UserRole + 1)) + is_installed = bool(current.data(Qt.ItemDataRole.UserRole + 2)) + + # Disable install button if plugin is incompatible OR ALREADY INSTALLED + self.btn_install.setEnabled(is_compatible and not is_installed) + else: + self.btn_install.setEnabled(False) + + def _on_add_repo(self) -> None: + """Adds a custom repository URL and re-fetches plugins.""" + url = self.repo_input.text().strip() + if url and url not in self.repository_urls: + self.repository_urls.append(url) + self.repo_input.clear() + self.fetch_remote_plugins() def _create_installed_tab(self) -> QWidget: tab = QWidget() @@ -284,6 +345,7 @@ class PluginsWindow(QWidget): self.repo_input = QLineEdit(tab) self.repo_input.setPlaceholderText("Enter custom plugins.json URL...") btn_add = QPushButton("Add Repo", tab) + btn_add.clicked.connect(self._on_add_repo) repo_layout.addWidget(self.repo_input) repo_layout.addWidget(btn_add) @@ -292,14 +354,14 @@ class PluginsWindow(QWidget): self.browser_list.currentItemChanged.connect(self._on_browser_item_changed) btn_layout = QHBoxLayout() - btn_fetch = QPushButton("Fetch Remote Lists", tab) + self.btn_fetch = QPushButton("Fetch Remote Lists", tab) self.btn_install = QPushButton("Install Plugin", tab) self.btn_install.setEnabled(False) - btn_fetch.clicked.connect(self.fetch_remote_plugins) + self.btn_fetch.clicked.connect(self.fetch_remote_plugins) self.btn_install.clicked.connect(self.install_selected_plugin) - btn_layout.addWidget(btn_fetch) + btn_layout.addWidget(self.btn_fetch) btn_layout.addStretch() btn_layout.addWidget(self.btn_install)