From 36cd59b08d8f372b5ea32ffaf2186c61b7bde93f Mon Sep 17 00:00:00 2001 From: ItsLemmy Date: Fri, 5 Dec 2025 17:25:46 -0500 Subject: [PATCH] PluginSystem: first pass on auto update --- Assets/Translations/en.json | 12 +- Services/Noctalia/PluginService.qml | 187 ++++++++++++++++++++++++++++ shell.qml | 14 +++ 3 files changed, 212 insertions(+), 1 deletion(-) diff --git a/Assets/Translations/en.json b/Assets/Translations/en.json index 8b14986b8..72539e4c3 100644 --- a/Assets/Translations/en.json +++ b/Assets/Translations/en.json @@ -458,6 +458,7 @@ "add": "Add", "apply": "Apply", "cancel": "Cancel", + "check-settings": "Check Settings for details", "close": "Close", "save": "Save" }, @@ -1808,7 +1809,16 @@ "uninstall-error": "Failed to uninstall: {error}", "uninstall-success": "Successfully uninstalled {plugin}", "uninstall.tooltip": "Uninstall plugin", - "uninstalling": "Uninstalling {plugin}..." + "uninstalling": "Uninstalling {plugin}...", + "update": "Update", + "update-all": "Update All ({count})", + "update-all-success": "All plugins updated successfully", + "update-available": "{count} update(s) available", + "update-error": "Failed to update {plugin}: {error}", + "update-incompatible": "Requires Noctalia v{version} or higher", + "update-success": "Updated {plugin} to v{version}", + "update-version": "v{current} → v{new}", + "updating": "Updating {plugin}..." }, "screen-recorder": { "audio": { diff --git a/Services/Noctalia/PluginService.qml b/Services/Noctalia/PluginService.qml index 679970ca3..9d6b43b6f 100644 --- a/Services/Noctalia/PluginService.qml +++ b/Services/Noctalia/PluginService.qml @@ -17,12 +17,23 @@ Singleton { signal availablePluginsUpdated signal allPluginsLoaded + // When available plugins are updated, check if we should perform update check + onAvailablePluginsUpdated: { + if (shouldCheckUpdatesAfterFetch && Object.keys(activeFetches).length === 0) { + Logger.d("PluginService", "All registry fetches complete, performing update check"); + performUpdateCheck(); + } + } + // Loaded plugin instances property var loadedPlugins: ({}) // { pluginId: { component, instance, api } } // Available plugins from all sources (fetched from registries) property var availablePlugins: ([]) // Array of plugin metadata from all sources + // Plugin updates available: { pluginId: { currentVersion, availableVersion } } + property var pluginUpdates: ({}) + // Track active fetches property var activeFetches: ({}) @@ -131,6 +142,12 @@ Singleton { // Refresh available plugins from all sources function refreshAvailablePlugins() { + // If fetches are already in progress, don't start new ones + if (Object.keys(activeFetches).length > 0) { + Logger.d("PluginService", "Refresh already in progress, skipping duplicate refresh"); + return; + } + Logger.i("PluginService", "Refreshing available plugins"); root.availablePlugins = []; @@ -837,6 +854,176 @@ Singleton { } } + // Find available plugin by ID + function findAvailablePlugin(pluginId) { + for (var i = 0; i < root.availablePlugins.length; i++) { + if (root.availablePlugins[i].id === pluginId) { + return root.availablePlugins[i]; + } + } + return null; + } + + // Internal flag to track if we should check for updates after registry fetch + property bool shouldCheckUpdatesAfterFetch: false + + // Check for plugin updates (call this after availablePlugins are loaded) + function checkForUpdates() { + Logger.i("PluginService", "Checking for plugin updates"); + + // If we have available plugins, check immediately regardless of active fetches + if (root.availablePlugins.length > 0) { + Logger.d("PluginService", "Available plugins already loaded, checking now"); + performUpdateCheck(); + return; + } + + // No plugins yet - check if fetch is in progress + if (Object.keys(activeFetches).length > 0) { + Logger.d("PluginService", "Registry fetch in progress, will check after fetch completes"); + shouldCheckUpdatesAfterFetch = true; + return; + } + + // No plugins and no fetches - trigger refresh + Logger.d("PluginService", "No available plugins yet, triggering refresh"); + shouldCheckUpdatesAfterFetch = true; + refreshAvailablePlugins(); + } + + // Perform the actual update check + function performUpdateCheck() { + var updates = {}; + var installedIds = PluginRegistry.getAllInstalledPluginIds(); + + Logger.d("PluginService", "Checking", installedIds.length, "installed plugins against", root.availablePlugins.length, "available plugins"); + + for (var i = 0; i < installedIds.length; i++) { + var pluginId = installedIds[i]; + var installedManifest = PluginRegistry.getPluginManifest(pluginId); + var availablePlugin = findAvailablePlugin(pluginId); + + if (installedManifest && availablePlugin) { + var currentVersion = installedManifest.version; + var availableVersion = availablePlugin.version; + + Logger.d("PluginService", "Comparing", pluginId + ":", currentVersion, "vs", availableVersion); + + // Compare versions + if (compareVersions(availableVersion, currentVersion) > 0) { + updates[pluginId] = { + currentVersion: currentVersion, + availableVersion: availableVersion + }; + Logger.i("PluginService", "Update available for", pluginId + ":", currentVersion, "→", availableVersion); + } + } else if (installedManifest && !availablePlugin) { + Logger.d("PluginService", "Plugin", pluginId, "not found in available plugins (might be from disabled source)"); + } + } + + root.pluginUpdates = updates; + var updateCount = Object.keys(updates).length; + + if (updateCount > 0) { + Logger.i("PluginService", updateCount, "plugin update(s) available"); + ToastService.showNotice(I18n.tr("settings.plugins.update-available", { + "count": updateCount + }), I18n.tr("common.check-settings")); + } else { + Logger.i("PluginService", "All plugins are up to date"); + } + + shouldCheckUpdatesAfterFetch = false; + } + + // Simple version comparison (semantic versioning x.y.z) + function compareVersions(a, b) { + var aParts = a.split('.').map(function (x) { + return parseInt(x) || 0; + }); + var bParts = b.split('.').map(function (x) { + return parseInt(x) || 0; + }); + + for (var i = 0; i < 3; i++) { + var aNum = aParts[i] || 0; + var bNum = bParts[i] || 0; + if (aNum > bNum) + return 1; + if (aNum < bNum) + return -1; + } + return 0; + } + + // Update a plugin to the latest version + function updatePlugin(pluginId, callback) { + Logger.i("PluginService", "Updating plugin:", pluginId); + + // Find available plugin metadata + var availablePlugin = findAvailablePlugin(pluginId); + if (!availablePlugin) { + Logger.e("PluginService", "Plugin not found in available plugins:", pluginId); + if (callback) + callback(false, "Plugin not found"); + return; + } + + // Check Noctalia compatibility + if (availablePlugin.minNoctaliaVersion) { + // Simple check: just warn, don't block (UpdateService would have more sophisticated logic) + Logger.d("PluginService", "Plugin requires Noctalia v" + availablePlugin.minNoctaliaVersion); + } + + // Backup entire bar layout + var barBackup = { + left: JSON.parse(JSON.stringify(Settings.data.bar.widgets.left || [])), + center: JSON.parse(JSON.stringify(Settings.data.bar.widgets.center || [])), + right: JSON.parse(JSON.stringify(Settings.data.bar.widgets.right || [])) + }; + Logger.d("PluginService", "Backed up bar layout"); + + // Disable plugin (this removes widgets and unloads code) + if (PluginRegistry.isPluginEnabled(pluginId)) { + disablePlugin(pluginId); + } + + // Now install the new version (reuse installPlugin logic) + installPlugin(availablePlugin, function (success, error) { + if (success) { + Logger.i("PluginService", "Plugin updated successfully:", pluginId); + + // Restore bar layout + Settings.data.bar.widgets.left = barBackup.left; + Settings.data.bar.widgets.center = barBackup.center; + Settings.data.bar.widgets.right = barBackup.right; + Logger.d("PluginService", "Restored bar layout"); + + // Re-enable the plugin + enablePlugin(pluginId); + + // Remove from updates list + var updates = Object.assign({}, root.pluginUpdates); + delete updates[pluginId]; + root.pluginUpdates = updates; + + if (callback) + callback(true, null); + } else { + Logger.e("PluginService", "Failed to update plugin:", pluginId, error); + + // Restore bar layout even on failure + Settings.data.bar.widgets.left = barBackup.left; + Settings.data.bar.widgets.center = barBackup.center; + Settings.data.bar.widgets.right = barBackup.right; + + if (callback) + callback(false, error); + } + }); + } + // Get plugin API for a loaded plugin function getPluginAPI(pluginId) { return root.loadedPlugins[pluginId]?.api || null; diff --git a/shell.qml b/shell.qml index 164a8e07d..c825117e6 100644 --- a/shell.qml +++ b/shell.qml @@ -128,6 +128,20 @@ ShellRoot { PluginService.pluginContainer = pluginContainer; } } + + // Listen for when available plugins are fetched, then check for updates + Connections { + target: PluginService + property bool hasCheckedOnStartup: false + + function onAvailablePluginsUpdated() { + // Only check once on startup, after first plugin list is fetched + if (!hasCheckedOnStartup && Object.keys(PluginService.activeFetches).length === 0) { + hasCheckedOnStartup = true; + PluginService.checkForUpdates(); + } + } + } } }