PluginSystem: first pass on auto update

This commit is contained in:
ItsLemmy
2025-12-05 17:25:46 -05:00
parent 08fc3d4f8c
commit 36cd59b08d
3 changed files with 212 additions and 1 deletions
+11 -1
View File
@@ -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": {
+187
View File
@@ -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;
+14
View File
@@ -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();
}
}
}
}
}