PluginSystem: general improvements to the way we load plugins and the pluginApi.

This commit is contained in:
Lemmy
2025-12-21 17:15:40 -05:00
parent 4eeb361647
commit 9856899e89
6 changed files with 141 additions and 184 deletions
+1 -25
View File
@@ -29,23 +29,6 @@ Item {
return (item && item.visible) ? Math.round(item[prop]) : 0;
}
// Create a dummy pluginApi that returns empty strings to avoid undefined warnings
property var _dummyApi: QtObject {
property var pluginSettings: ({})
property var manifest: ({
metadata: {
defaultSettings: {}
}
})
function tr(key) {
return "";
}
function trp(key, count) {
return "";
}
}
// Only load if widget exists in registry
function checkWidgetExists(): bool {
return root.widgetId !== "" && BarWidgetRegistry.hasWidget(root.widgetId);
@@ -84,13 +67,6 @@ Item {
if (!item)
return;
// Inject dummy API immediately for plugin widgets before any other code runs
if (BarWidgetRegistry.isPluginWidget(widgetId) && item.hasOwnProperty("pluginApi")) {
if (!item.pluginApi) {
item.pluginApi = root._dummyApi;
}
}
Logger.d("BarWidgetLoader", "Loading widget", widgetId, "on screen:", widgetScreen.name);
// Apply properties to loaded widget
@@ -113,11 +89,11 @@ Item {
}
// Inject plugin API for plugin widgets
// The API is fully populated (settings/translations already loaded) by PluginService
if (BarWidgetRegistry.isPluginWidget(widgetId)) {
var pluginId = widgetId.replace("plugin:", "");
var api = PluginService.getPluginAPI(pluginId);
if (api && item.hasOwnProperty("pluginApi")) {
// Inject API into widget
item.pluginApi = api;
Logger.d("BarWidgetLoader", "Injected plugin API for", widgetId);
}
+1 -24
View File
@@ -64,34 +64,11 @@ SmartPanel {
id: pluginContentItem
anchors.fill: parent
// Plugin content loader, pluginApi is injected synchronously in loadPluginPanel()
Loader {
id: pluginContentLoader
anchors.fill: parent
active: false
// Create a dummy pluginApi that returns empty strings to avoid undefined warnings
property var _dummyApi: QtObject {
property var pluginSettings: ({})
property var manifest: ({
metadata: {
defaultSettings: {}
}
})
function tr(key) {
return "";
}
function trp(key, count) {
return "";
}
}
onLoaded: {
// Inject the dummy API immediately to prevent undefined warnings
if (item && item.hasOwnProperty("pluginApi") && !item.pluginApi) {
item.pluginApi = _dummyApi;
}
}
}
}
@@ -600,6 +600,7 @@ ColumnLayout {
// Add source dialog
Popup {
id: addSourceDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
@@ -673,6 +674,7 @@ ColumnLayout {
// Uninstall confirmation dialog
Popup {
id: uninstallDialog
parent: Overlay.overlay
modal: true
dim: false
anchors.centerIn: parent
+136 -94
View File
@@ -115,6 +115,9 @@ Singleton {
}
}
// Track pending plugin loads for init completion
property int _pendingPluginLoads: 0
function init() {
if (root.initialized) {
Logger.d("PluginService", "Already initialized, skipping");
@@ -133,12 +136,12 @@ Singleton {
var enabledIds = PluginRegistry.getEnabledPluginIds();
Logger.i("PluginService", "Found", enabledIds.length, "enabled plugins:", JSON.stringify(enabledIds));
// Count plugins that need to be loaded
var pluginsToLoad = [];
for (var i = 0; i < enabledIds.length; i++) {
Logger.d("PluginService", "Attempting to load plugin:", enabledIds[i]);
var manifest = PluginRegistry.getPluginManifest(enabledIds[i]);
if (manifest) {
Logger.d("PluginService", "Manifest found for", enabledIds[i]);
loadPlugin(enabledIds[i]);
pluginsToLoad.push(enabledIds[i]);
} else {
Logger.w("PluginService", "Plugin", enabledIds[i], "is enabled but not found on disk - cleaning up");
// Plugin was deleted from disk but still marked as enabled
@@ -149,13 +152,38 @@ Singleton {
}
}
// Mark plugins as fully loaded
root.pluginsFullyLoaded = true;
Logger.i("PluginService", "All plugins loaded");
root.allPluginsLoaded();
// If no plugins to load, mark as complete immediately
if (pluginsToLoad.length === 0) {
root.pluginsFullyLoaded = true;
Logger.i("PluginService", "No plugins to load");
root.allPluginsLoaded();
refreshAvailablePlugins();
return;
}
// Fetch available plugins from all sources
refreshAvailablePlugins();
// Track pending loads
root._pendingPluginLoads = pluginsToLoad.length;
// Load all plugins (async - they will call _onPluginLoadComplete when done)
for (var j = 0; j < pluginsToLoad.length; j++) {
Logger.d("PluginService", "Attempting to load plugin:", pluginsToLoad[j]);
loadPlugin(pluginsToLoad[j]);
}
}
// Called when a plugin finishes loading (success or failure)
function _onPluginLoadComplete() {
root._pendingPluginLoads--;
if (root._pendingPluginLoads <= 0) {
// All plugins finished loading
root.pluginsFullyLoaded = true;
Logger.i("PluginService", "All plugins loaded");
root.allPluginsLoaded();
// Fetch available plugins from all sources
refreshAvailablePlugins();
}
}
// Refresh available plugins from all sources
@@ -223,13 +251,20 @@ Singleton {
}
Logger.i("PluginService", "Loaded", registry.plugins.length, "plugins from", source.name);
// Remove from active fetches BEFORE emitting signal so handler sees correct count
delete activeFetches[source.url];
fetchProcess.destroy();
root.availablePluginsUpdated();
return;
}
} catch (e) {
Logger.e("PluginService", "Failed to parse registry from", source.name, ":", e);
Logger.e("PluginService", "Response was:", response ? response.substring(0, 200) : "null");
}
// Clean up on error or empty response
delete activeFetches[source.url];
fetchProcess.destroy();
});
@@ -487,6 +522,19 @@ Singleton {
return changed;
}
// Load plugin settings and translations before instantiating components
// This ensures pluginApi is fully populated before being passed to createObject()
function loadPluginData(pluginId, manifest, callback) {
// Load settings first
loadPluginSettings(pluginId, function (settings) {
// Then load translations
loadPluginTranslationsAsync(pluginId, manifest, I18n.langCode, function (translations) {
// Both ready - call back with complete data
callback(settings, translations);
});
});
}
// Load a plugin
function loadPlugin(pluginId) {
if (root.loadedPlugins[pluginId]) {
@@ -504,97 +552,98 @@ Singleton {
Logger.i("PluginService", "Loading plugin:", pluginId);
// Create plugin API object
var pluginApi = createPluginAPI(pluginId, manifest);
// Load settings and translations FIRST, then create API and instantiate components
loadPluginData(pluginId, manifest, function (settings, translations) {
// Create plugin API object with pre-loaded data
var pluginApi = createPluginAPI(pluginId, manifest, settings, translations);
// Initialize plugin entry with API and manifest
root.loadedPlugins[pluginId] = {
barWidget: null,
desktopWidget: null,
mainInstance: null,
api: pluginApi,
manifest: manifest
};
// Initialize plugin entry with API and manifest
root.loadedPlugins[pluginId] = {
barWidget: null,
desktopWidget: null,
mainInstance: null,
api: pluginApi,
manifest: manifest
};
// Clear any previous errors for this plugin
root.clearPluginError(pluginId);
// Clear any previous errors for this plugin
root.clearPluginError(pluginId);
// Load Main.qml entry point if it exists
if (manifest.entryPoints && manifest.entryPoints.main) {
var mainPath = pluginDir + "/" + manifest.entryPoints.main;
var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
var mainComponent = Qt.createComponent("file://" + mainPath + "?v=" + loadVersion);
// Load Main.qml entry point if it exists
if (manifest.entryPoints && manifest.entryPoints.main) {
var mainPath = pluginDir + "/" + manifest.entryPoints.main;
var loadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
var mainComponent = Qt.createComponent("file://" + mainPath + "?v=" + loadVersion);
if (mainComponent.status === Component.Ready) {
// Get the plugin container from shell.qml (must be in graphics scene)
if (!root.pluginContainer) {
Logger.e("PluginService", "Plugin container not set. Shell must set PluginService.pluginContainer.");
return;
}
// Instantiate Main.qml with container as parent (places it in graphics scene)
var mainInstance = mainComponent.createObject(root.pluginContainer);
if (mainInstance) {
// Set pluginApi property after creation
if (mainInstance.hasOwnProperty('pluginApi')) {
mainInstance.pluginApi = pluginApi;
} else {
Logger.w("PluginService", "Main.qml for", pluginId, "should declare 'property var pluginApi: null'");
if (mainComponent.status === Component.Ready) {
// Get the plugin container from shell.qml (must be in graphics scene)
if (!root.pluginContainer) {
Logger.e("PluginService", "Plugin container not set. Shell must set PluginService.pluginContainer.");
return;
}
root.loadedPlugins[pluginId].mainInstance = mainInstance;
pluginApi.mainInstance = mainInstance;
Logger.i("PluginService", "Loaded Main.qml for plugin:", pluginId);
} else {
root.recordPluginError(pluginId, "main", "Failed to instantiate Main.qml");
// Instantiate Main.qml with pluginApi passed directly in createObject
var mainInstance = mainComponent.createObject(root.pluginContainer, {
pluginApi: pluginApi
});
if (mainInstance) {
root.loadedPlugins[pluginId].mainInstance = mainInstance;
pluginApi.mainInstance = mainInstance;
Logger.i("PluginService", "Loaded Main.qml for plugin:", pluginId);
} else {
root.recordPluginError(pluginId, "main", "Failed to instantiate Main.qml");
}
} else if (mainComponent.status === Component.Error) {
root.recordPluginError(pluginId, "main", mainComponent.errorString());
}
} else if (mainComponent.status === Component.Error) {
root.recordPluginError(pluginId, "main", mainComponent.errorString());
}
}
// Load bar widget component if provided (don't instantiate - BarWidgetRegistry will do that)
if (manifest.entryPoints && manifest.entryPoints.barWidget) {
var widgetPath = pluginDir + "/" + manifest.entryPoints.barWidget;
var widgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
var widgetComponent = Qt.createComponent("file://" + widgetPath + "?v=" + widgetLoadVersion);
// Load bar widget component if provided (don't instantiate - BarWidgetRegistry will do that)
if (manifest.entryPoints && manifest.entryPoints.barWidget) {
var widgetPath = pluginDir + "/" + manifest.entryPoints.barWidget;
var widgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
var widgetComponent = Qt.createComponent("file://" + widgetPath + "?v=" + widgetLoadVersion);
if (widgetComponent.status === Component.Ready) {
root.loadedPlugins[pluginId].barWidget = widgetComponent;
pluginApi.barWidget = widgetComponent;
if (widgetComponent.status === Component.Ready) {
root.loadedPlugins[pluginId].barWidget = widgetComponent;
pluginApi.barWidget = widgetComponent;
// Register with BarWidgetRegistry
BarWidgetRegistry.registerPluginWidget(pluginId, widgetComponent, manifest.metadata);
Logger.i("PluginService", "Loaded bar widget for plugin:", pluginId);
} else if (widgetComponent.status === Component.Error) {
root.recordPluginError(pluginId, "barWidget", widgetComponent.errorString());
// Register with BarWidgetRegistry
BarWidgetRegistry.registerPluginWidget(pluginId, widgetComponent, manifest.metadata);
Logger.i("PluginService", "Loaded bar widget for plugin:", pluginId);
} else if (widgetComponent.status === Component.Error) {
root.recordPluginError(pluginId, "barWidget", widgetComponent.errorString());
}
}
}
// Load desktop widget component if provided (don't instantiate - DesktopWidgetRegistry will do that)
if (manifest.entryPoints && manifest.entryPoints.desktopWidget) {
var desktopWidgetPath = pluginDir + "/" + manifest.entryPoints.desktopWidget;
var desktopWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
var desktopWidgetComponent = Qt.createComponent("file://" + desktopWidgetPath + "?v=" + desktopWidgetLoadVersion);
// Load desktop widget component if provided (don't instantiate - DesktopWidgetRegistry will do that)
if (manifest.entryPoints && manifest.entryPoints.desktopWidget) {
var desktopWidgetPath = pluginDir + "/" + manifest.entryPoints.desktopWidget;
var desktopWidgetLoadVersion = PluginRegistry.pluginLoadVersions[pluginId] || 0;
var desktopWidgetComponent = Qt.createComponent("file://" + desktopWidgetPath + "?v=" + desktopWidgetLoadVersion);
if (desktopWidgetComponent.status === Component.Ready) {
root.loadedPlugins[pluginId].desktopWidget = desktopWidgetComponent;
pluginApi.desktopWidget = desktopWidgetComponent;
if (desktopWidgetComponent.status === Component.Ready) {
root.loadedPlugins[pluginId].desktopWidget = desktopWidgetComponent;
pluginApi.desktopWidget = desktopWidgetComponent;
// Register with DesktopWidgetRegistry
DesktopWidgetRegistry.registerPluginWidget(pluginId, desktopWidgetComponent, manifest.metadata);
Logger.i("PluginService", "Loaded desktop widget for plugin:", pluginId);
} else if (desktopWidgetComponent.status === Component.Error) {
root.recordPluginError(pluginId, "desktopWidget", desktopWidgetComponent.errorString());
// Register with DesktopWidgetRegistry
DesktopWidgetRegistry.registerPluginWidget(pluginId, desktopWidgetComponent, manifest.metadata);
Logger.i("PluginService", "Loaded desktop widget for plugin:", pluginId);
} else if (desktopWidgetComponent.status === Component.Error) {
root.recordPluginError(pluginId, "desktopWidget", desktopWidgetComponent.errorString());
}
}
}
Logger.i("PluginService", "Plugin loaded:", pluginId);
root.pluginLoaded(pluginId);
Logger.i("PluginService", "Plugin loaded:", pluginId);
root.pluginLoaded(pluginId);
// Set up hot reload watcher if enabled
setupPluginFileWatcher(pluginId);
// Set up hot reload watcher if enabled
setupPluginFileWatcher(pluginId);
// Notify that this plugin finished loading (for init tracking)
root._onPluginLoadComplete();
});
}
// Unload a plugin
@@ -635,10 +684,9 @@ Singleton {
Logger.i("PluginService", "Unloaded plugin:", pluginId);
}
// Create plugin API object
function createPluginAPI(pluginId, manifest) {
// Create plugin API object with pre-loaded settings and translations
function createPluginAPI(pluginId, manifest, settings, translations) {
var pluginDir = PluginRegistry.getPluginDir(pluginId);
var settingsFile = PluginRegistry.getPluginSettingsFile(pluginId);
var api = Qt.createQmlObject(`
import QtQuick
@@ -679,15 +727,9 @@ Singleton {
// Set current language (can't use binding in Qt.createQmlObject string)
api.currentLanguage = I18n.langCode;
// Load plugin settings
loadPluginSettings(pluginId, function (settings) {
api.pluginSettings = settings;
});
// Load plugin translations for current language
loadPluginTranslationsAsync(pluginId, manifest, I18n.langCode, function (translations) {
api.pluginTranslations = translations;
});
// Set pre-loaded settings and translations (available immediately!)
api.pluginSettings = settings || {};
api.pluginTranslations = translations || {};
// ----------------------------------------
// Helper function to get nested property by dot notation
+1 -29
View File
@@ -62,38 +62,10 @@ Popup {
color: Color.mOutline
}
// Settings loader
// Settings loader - pluginApi is passed via setSource() in openPluginSettings()
Loader {
id: settingsLoader
Layout.fillWidth: true
// Create a dummy pluginApi that returns empty strings to avoid undefined warnings
property var _dummyApi: QtObject {
property var pluginSettings: ({})
property var manifest: ({
metadata: {
defaultSettings: {}
}
})
function tr(key) {
return "";
}
function trp(key, count) {
return "";
}
function saveSettings() {
}
}
onLoaded: {
// Inject dummy API immediately to prevent undefined warnings during initialization
if (item && item.hasOwnProperty("pluginApi") && !item.pluginApi) {
item.pluginApi = _dummyApi;
}
}
}
// Action buttons
-12
View File
@@ -143,18 +143,6 @@ ShellRoot {
PluginService.screenDetector = screenDetector;
}
}
// Listen for when available plugins are fetched, then check for updates
Connections {
target: PluginService
property bool hasCheckedOnStartup: false
function onAvailablePluginsUpdated() {
if (!hasCheckedOnStartup) {
hasCheckedOnStartup = true;
PluginService.checkForUpdates();
}
}
}
}
}