2025-12-10 10:57:16 -05:00
|
|
|
#include "MainUIBackend.h"
|
2026-04-06 11:00:30 -03:00
|
|
|
#include "PluginLoader.h"
|
2026-03-18 14:17:05 +01:00
|
|
|
#include "LogosBasecampPaths.h"
|
2025-12-10 10:57:16 -05:00
|
|
|
#include <QDebug>
|
|
|
|
|
#include <QJsonObject>
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QJsonDocument>
|
2026-03-27 10:57:53 -03:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
#include <QCoreApplication>
|
|
|
|
|
#include <QDir>
|
|
|
|
|
#include <QFile>
|
|
|
|
|
#include <QFileInfo>
|
2026-01-02 12:31:56 -05:00
|
|
|
#include <QLibraryInfo>
|
|
|
|
|
#include <QTimer>
|
2026-01-02 10:40:56 -05:00
|
|
|
#include <QQmlContext>
|
2026-01-02 10:10:54 -05:00
|
|
|
#include <QQuickWidget>
|
|
|
|
|
#include <QQmlEngine>
|
|
|
|
|
#include <QQmlError>
|
|
|
|
|
#include <QUrl>
|
2026-02-17 15:14:58 +01:00
|
|
|
#include <QIcon>
|
2026-01-13 16:35:49 -05:00
|
|
|
#include <QStandardPaths>
|
2026-03-27 10:57:53 -03:00
|
|
|
#include <QPointer>
|
2026-01-16 13:42:23 -05:00
|
|
|
#include <QFileDialog>
|
2026-04-07 16:45:09 +02:00
|
|
|
#include <memory>
|
|
|
|
|
#include <ViewModuleHost.h>
|
2025-12-10 10:57:16 -05:00
|
|
|
#include "logos_sdk.h"
|
|
|
|
|
#include "token_manager.h"
|
|
|
|
|
|
|
|
|
|
extern "C" {
|
|
|
|
|
char* logos_core_get_module_stats();
|
2026-01-09 16:36:48 -05:00
|
|
|
char* logos_core_process_plugin(const char* plugin_path);
|
2026-03-18 10:53:38 -04:00
|
|
|
char** logos_core_get_known_plugins();
|
|
|
|
|
char** logos_core_get_loaded_plugins();
|
|
|
|
|
int logos_core_load_plugin_with_dependencies(const char* plugin_name);
|
|
|
|
|
int logos_core_unload_plugin(const char* plugin_name);
|
2026-03-27 10:57:53 -03:00
|
|
|
void logos_core_refresh_plugins();
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MainUIBackend::MainUIBackend(LogosAPI* logosAPI, QObject* parent)
|
|
|
|
|
: QObject(parent)
|
2026-02-02 18:05:41 +05:30
|
|
|
, m_currentActiveSectionIndex(0)
|
2025-12-10 10:57:16 -05:00
|
|
|
, m_logosAPI(logosAPI)
|
|
|
|
|
, m_ownsLogosAPI(false)
|
|
|
|
|
, m_statsTimer(nullptr)
|
2026-02-27 15:05:08 +01:00
|
|
|
, m_currentVisibleApp("")
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
|
|
|
|
if (!m_logosAPI) {
|
|
|
|
|
m_logosAPI = new LogosAPI("core", this);
|
|
|
|
|
m_ownsLogosAPI = true;
|
|
|
|
|
}
|
2026-04-06 11:00:30 -03:00
|
|
|
|
|
|
|
|
m_pluginLoader = new PluginLoader(m_logosAPI, this);
|
|
|
|
|
connect(m_pluginLoader, &PluginLoader::pluginLoaded,
|
|
|
|
|
this, &MainUIBackend::onPluginLoaded);
|
|
|
|
|
connect(m_pluginLoader, &PluginLoader::pluginLoadFailed,
|
|
|
|
|
this, &MainUIBackend::onPluginLoadFailed);
|
|
|
|
|
connect(m_pluginLoader, &PluginLoader::loadingChanged,
|
|
|
|
|
this, &MainUIBackend::loadingModulesChanged);
|
2025-12-10 10:57:16 -05:00
|
|
|
|
2026-02-02 18:05:41 +05:30
|
|
|
initializeSections();
|
2025-12-10 10:57:16 -05:00
|
|
|
|
|
|
|
|
m_statsTimer = new QTimer(this);
|
|
|
|
|
connect(m_statsTimer, &QTimer::timeout, this, &MainUIBackend::updateModuleStats);
|
|
|
|
|
m_statsTimer->start(2000);
|
|
|
|
|
|
|
|
|
|
refreshCoreModules();
|
2026-03-27 10:57:53 -03:00
|
|
|
|
2026-01-13 16:42:24 -05:00
|
|
|
subscribeToPackageInstallationEvents();
|
2026-03-27 10:57:53 -03:00
|
|
|
|
|
|
|
|
// Initial async fetch of UI plugin metadata (emits uiModulesChanged + launcherAppsChanged when done)
|
|
|
|
|
fetchUiPluginMetadata();
|
2026-01-13 16:42:24 -05:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
qDebug() << "MainUIBackend created";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MainUIBackend::~MainUIBackend()
|
|
|
|
|
{
|
|
|
|
|
QStringList moduleNames = m_loadedUiModules.keys();
|
2026-01-02 10:10:54 -05:00
|
|
|
for (const QString& name : m_qmlPluginWidgets.keys()) {
|
|
|
|
|
if (!moduleNames.contains(name)) {
|
|
|
|
|
moduleNames.append(name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
for (const QString& name : moduleNames) {
|
|
|
|
|
unloadUiModule(name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-13 16:42:24 -05:00
|
|
|
void MainUIBackend::subscribeToPackageInstallationEvents()
|
|
|
|
|
{
|
|
|
|
|
if (!m_logosAPI) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosAPIClient* client = m_logosAPI->getClient("package_manager");
|
|
|
|
|
if (!client || !client->isConnected()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosModules logos(m_logosAPI);
|
2026-03-27 10:57:53 -03:00
|
|
|
|
|
|
|
|
// Configure the package_manager module's directories so it knows where to install
|
|
|
|
|
logos.package_manager.setEmbeddedModulesDirectory(LogosBasecampPaths::embeddedModulesDirectory());
|
|
|
|
|
logos.package_manager.setUserModulesDirectory(LogosBasecampPaths::modulesDirectory());
|
|
|
|
|
logos.package_manager.setEmbeddedUiPluginsDirectory(LogosBasecampPaths::embeddedPluginsDirectory());
|
|
|
|
|
logos.package_manager.setUserUiPluginsDirectory(LogosBasecampPaths::pluginsDirectory());
|
|
|
|
|
|
|
|
|
|
logos.package_manager.on("corePluginFileInstalled", [this](const QVariantList& data) {
|
|
|
|
|
if (data.isEmpty()) return;
|
|
|
|
|
qDebug() << "Core module file installed:" << data[0].toString();
|
|
|
|
|
QTimer::singleShot(100, this, [this]() {
|
|
|
|
|
refreshCoreModules();
|
|
|
|
|
});
|
2026-01-13 16:42:24 -05:00
|
|
|
});
|
2026-03-18 10:37:20 -04:00
|
|
|
|
2026-03-27 10:57:53 -03:00
|
|
|
logos.package_manager.on("uiPluginFileInstalled", [this](const QVariantList& data) {
|
2026-03-18 10:37:20 -04:00
|
|
|
if (data.isEmpty()) return;
|
2026-03-27 10:57:53 -03:00
|
|
|
qDebug() << "UI plugin file installed:" << data[0].toString();
|
|
|
|
|
QTimer::singleShot(100, this, [this]() {
|
|
|
|
|
fetchUiPluginMetadata();
|
|
|
|
|
});
|
2026-03-18 10:37:20 -04:00
|
|
|
});
|
2026-01-13 16:42:24 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 18:05:41 +05:30
|
|
|
void MainUIBackend::initializeSections()
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
2026-02-02 18:05:41 +05:30
|
|
|
auto makeSection = [](const QString& name, const QString& iconPath, const QString& type) {
|
|
|
|
|
QVariantMap section;
|
|
|
|
|
section["name"] = name;
|
|
|
|
|
section["iconPath"] = iconPath;
|
|
|
|
|
section["type"] = type;
|
|
|
|
|
return section;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
m_sections = QVariantList{
|
|
|
|
|
makeSection("Apps", "qrc:/icons/tent.png", "workspace"),
|
|
|
|
|
makeSection("Dashboard", "qrc:/icons/dashboard.png", "view"),
|
|
|
|
|
makeSection("Modules", "qrc:/icons/module.png", "view"),
|
|
|
|
|
makeSection("Settings", "qrc:/icons/settings.png", "view")
|
|
|
|
|
};
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 18:05:41 +05:30
|
|
|
int MainUIBackend::currentActiveSectionIndex() const
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
2026-02-02 18:05:41 +05:30
|
|
|
return m_currentActiveSectionIndex;
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 18:05:41 +05:30
|
|
|
void MainUIBackend::setCurrentActiveSectionIndex(int index)
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
2026-02-02 18:05:41 +05:30
|
|
|
// Valid indices: 0-3 (Apps, Dashboard, Modules, Settings)
|
|
|
|
|
if (m_currentActiveSectionIndex != index && index >= 0 && index < m_sections.size()) {
|
|
|
|
|
m_currentActiveSectionIndex = index;
|
|
|
|
|
emit currentActiveSectionIndexChanged();
|
|
|
|
|
|
|
|
|
|
// Check if we're navigating to Modules view
|
|
|
|
|
const QVariantMap section = m_sections[index].toMap();
|
|
|
|
|
const QString name = section.value("name").toString();
|
|
|
|
|
if (name == "Modules") {
|
2026-03-27 10:57:53 -03:00
|
|
|
fetchUiPluginMetadata();
|
2025-12-10 10:57:16 -05:00
|
|
|
refreshCoreModules();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 18:05:41 +05:30
|
|
|
QVariantList MainUIBackend::sections() const
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
2026-02-02 18:05:41 +05:30
|
|
|
return m_sections;
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantList MainUIBackend::uiModules() const
|
|
|
|
|
{
|
|
|
|
|
QVariantList modules;
|
|
|
|
|
QStringList availablePlugins = findAvailableUiPlugins();
|
|
|
|
|
|
|
|
|
|
for (const QString& pluginName : availablePlugins) {
|
|
|
|
|
QVariantMap module;
|
|
|
|
|
module["name"] = pluginName;
|
2026-01-02 10:10:54 -05:00
|
|
|
module["isLoaded"] = m_loadedUiModules.contains(pluginName) || m_qmlPluginWidgets.contains(pluginName);
|
2025-12-10 10:57:16 -05:00
|
|
|
module["isMainUi"] = (pluginName == "main_ui");
|
2026-02-26 11:05:34 -03:00
|
|
|
module["iconPath"] = getPluginIconPath(pluginName);
|
2025-12-10 10:57:16 -05:00
|
|
|
|
|
|
|
|
modules.append(module);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return modules;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::loadUiModule(const QString& moduleName)
|
|
|
|
|
{
|
|
|
|
|
qDebug() << "Loading UI module:" << moduleName;
|
2026-04-06 11:00:30 -03:00
|
|
|
|
2026-01-02 10:10:54 -05:00
|
|
|
if (m_loadedUiModules.contains(moduleName) || m_qmlPluginWidgets.contains(moduleName)) {
|
2025-12-10 10:57:16 -05:00
|
|
|
qDebug() << "Module" << moduleName << "is already loaded";
|
|
|
|
|
activateApp(moduleName);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-01-02 10:10:54 -05:00
|
|
|
|
2026-04-07 16:45:09 +02:00
|
|
|
if (isQmlPlugin(moduleName)) {
|
2026-04-13 12:39:17 -03:00
|
|
|
const QVariantMap& meta = m_uiPluginMetadata.value(moduleName);
|
|
|
|
|
|
|
|
|
|
PluginLoadRequest request;
|
|
|
|
|
request.name = moduleName;
|
|
|
|
|
request.type = UIPluginType::UiQml;
|
|
|
|
|
request.installDir = meta.value("installDir").toString();
|
|
|
|
|
request.qmlViewPath = resolveQmlViewPath(meta);
|
|
|
|
|
request.iconPath = getPluginIconPath(moduleName, true);
|
|
|
|
|
if (hasBackendPlugin(moduleName))
|
|
|
|
|
request.mainFilePath = meta.value("mainFilePath").toString();
|
|
|
|
|
request.coreDependencies = meta.value("dependencies").toList();
|
|
|
|
|
|
|
|
|
|
m_pluginLoader->load(request);
|
2026-04-06 11:00:30 -03:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-07 16:45:09 +02:00
|
|
|
loadLegacyUiModule(moduleName);
|
2026-04-06 11:00:30 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::onPluginLoaded(const QString& name, QWidget* widget,
|
2026-04-13 12:39:17 -03:00
|
|
|
IComponent* component, UIPluginType type,
|
|
|
|
|
ViewModuleHost* viewHost)
|
2026-04-06 11:00:30 -03:00
|
|
|
{
|
|
|
|
|
if (component)
|
|
|
|
|
m_loadedUiModules[name] = component;
|
2026-04-13 12:39:17 -03:00
|
|
|
if (type != UIPluginType::Legacy)
|
2026-04-06 11:00:30 -03:00
|
|
|
m_qmlPluginWidgets[name] = qobject_cast<QQuickWidget*>(widget);
|
2026-04-13 12:39:17 -03:00
|
|
|
if (viewHost)
|
|
|
|
|
m_viewModuleHosts[name] = viewHost;
|
2026-04-06 11:00:30 -03:00
|
|
|
m_uiModuleWidgets[name] = widget;
|
|
|
|
|
m_loadedApps.insert(name);
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
emit uiModulesChanged();
|
|
|
|
|
emit launcherAppsChanged();
|
2026-04-06 11:00:30 -03:00
|
|
|
emit pluginWindowRequested(widget, name);
|
2025-12-10 10:57:16 -05:00
|
|
|
emit navigateToApps();
|
2026-04-06 11:00:30 -03:00
|
|
|
|
|
|
|
|
qDebug() << "Successfully loaded UI module:" << name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::onPluginLoadFailed(const QString& name, const QString& error)
|
|
|
|
|
{
|
|
|
|
|
qWarning() << "Failed to load UI module" << name << ":" << error;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QStringList MainUIBackend::loadingModules() const
|
|
|
|
|
{
|
2026-04-13 12:39:17 -03:00
|
|
|
return m_pluginLoader->loadingPlugins();
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::unloadUiModule(const QString& moduleName)
|
|
|
|
|
{
|
|
|
|
|
qDebug() << "Unloading UI module:" << moduleName;
|
|
|
|
|
|
2026-01-02 10:10:54 -05:00
|
|
|
bool isQml = m_qmlPluginWidgets.contains(moduleName);
|
|
|
|
|
bool isCpp = m_loadedUiModules.contains(moduleName);
|
|
|
|
|
|
|
|
|
|
if (!isQml && !isCpp) {
|
2025-12-10 10:57:16 -05:00
|
|
|
qDebug() << "Module" << moduleName << "is not loaded";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QWidget* widget = m_uiModuleWidgets.value(moduleName);
|
|
|
|
|
IComponent* component = m_loadedUiModules.value(moduleName);
|
|
|
|
|
|
|
|
|
|
if (widget) {
|
|
|
|
|
emit pluginWindowRemoveRequested(widget);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (component && widget) {
|
|
|
|
|
component->destroyWidget(widget);
|
|
|
|
|
}
|
2026-01-02 10:10:54 -05:00
|
|
|
|
|
|
|
|
if (isQml && widget) {
|
|
|
|
|
widget->deleteLater();
|
|
|
|
|
}
|
2025-12-10 10:57:16 -05:00
|
|
|
|
2026-04-07 16:45:09 +02:00
|
|
|
// Stop view module host process if this was a view module
|
|
|
|
|
if (m_viewModuleHosts.contains(moduleName)) {
|
|
|
|
|
m_viewModuleHosts[moduleName]->stop();
|
|
|
|
|
delete m_viewModuleHosts.take(moduleName);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
m_loadedUiModules.remove(moduleName);
|
|
|
|
|
m_uiModuleWidgets.remove(moduleName);
|
2026-01-02 10:10:54 -05:00
|
|
|
m_qmlPluginWidgets.remove(moduleName);
|
2025-12-10 10:57:16 -05:00
|
|
|
m_loadedApps.remove(moduleName);
|
2026-04-07 16:45:09 +02:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
emit uiModulesChanged();
|
|
|
|
|
emit launcherAppsChanged();
|
2026-04-07 16:45:09 +02:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
qDebug() << "Successfully unloaded UI module:" << moduleName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::activateApp(const QString& appName)
|
|
|
|
|
{
|
|
|
|
|
QWidget* widget = m_uiModuleWidgets.value(appName);
|
|
|
|
|
if (widget) {
|
|
|
|
|
emit pluginWindowActivateRequested(widget);
|
|
|
|
|
emit navigateToApps();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-27 15:05:08 +01:00
|
|
|
void MainUIBackend::setCurrentVisibleApp(const QString& pluginName)
|
|
|
|
|
{
|
|
|
|
|
if (m_currentVisibleApp != pluginName) {
|
|
|
|
|
m_currentVisibleApp = pluginName;
|
|
|
|
|
emit currentVisibleAppChanged();
|
|
|
|
|
emit launcherAppsChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString MainUIBackend::currentVisibleApp() const
|
|
|
|
|
{
|
|
|
|
|
return m_currentVisibleApp;
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
void MainUIBackend::onPluginWindowClosed(const QString& pluginName)
|
|
|
|
|
{
|
|
|
|
|
qDebug() << "Plugin window closed:" << pluginName;
|
2026-01-29 10:35:00 +01:00
|
|
|
|
|
|
|
|
// Called when user closes the plugin window (tab X or subwindow close). The MDI
|
|
|
|
|
// subwindow and plugin widget are already destroyed
|
2025-12-10 10:57:16 -05:00
|
|
|
if (m_loadedUiModules.contains(pluginName)) {
|
|
|
|
|
m_loadedUiModules.remove(pluginName);
|
|
|
|
|
m_uiModuleWidgets.remove(pluginName);
|
|
|
|
|
m_loadedApps.remove(pluginName);
|
2026-01-29 10:35:00 +01:00
|
|
|
|
2026-01-02 10:10:54 -05:00
|
|
|
emit uiModulesChanged();
|
|
|
|
|
emit launcherAppsChanged();
|
|
|
|
|
} else if (m_qmlPluginWidgets.contains(pluginName)) {
|
2026-04-07 16:45:09 +02:00
|
|
|
// Stop view module host process if applicable
|
|
|
|
|
if (m_viewModuleHosts.contains(pluginName)) {
|
|
|
|
|
m_viewModuleHosts[pluginName]->stop();
|
|
|
|
|
delete m_viewModuleHosts.take(pluginName);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-02 10:10:54 -05:00
|
|
|
m_qmlPluginWidgets.remove(pluginName);
|
|
|
|
|
m_uiModuleWidgets.remove(pluginName);
|
|
|
|
|
m_loadedApps.remove(pluginName);
|
2026-01-29 10:35:00 +01:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
emit uiModulesChanged();
|
|
|
|
|
emit launcherAppsChanged();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantList MainUIBackend::coreModules() const
|
|
|
|
|
{
|
|
|
|
|
QVariantList modules;
|
2026-03-18 10:53:38 -04:00
|
|
|
|
|
|
|
|
// Build the set of loaded plugins for status checking
|
|
|
|
|
QStringList loadedPlugins;
|
|
|
|
|
char** loaded = logos_core_get_loaded_plugins();
|
|
|
|
|
if (loaded) {
|
|
|
|
|
for (char** p = loaded; *p != nullptr; ++p) {
|
|
|
|
|
loadedPlugins << QString::fromUtf8(*p);
|
|
|
|
|
delete[] *p;
|
|
|
|
|
}
|
|
|
|
|
delete[] loaded;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
char** known = logos_core_get_known_plugins();
|
|
|
|
|
if (!known) {
|
2025-12-10 10:57:16 -05:00
|
|
|
return modules;
|
|
|
|
|
}
|
2026-03-18 10:53:38 -04:00
|
|
|
|
|
|
|
|
for (char** p = known; *p != nullptr; ++p) {
|
|
|
|
|
QString name = QString::fromUtf8(*p);
|
|
|
|
|
delete[] *p;
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
QVariantMap module;
|
|
|
|
|
module["name"] = name;
|
2026-03-18 10:53:38 -04:00
|
|
|
module["isLoaded"] = loadedPlugins.contains(name);
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
if (m_moduleStats.contains(name)) {
|
|
|
|
|
module["cpu"] = m_moduleStats[name]["cpu"];
|
|
|
|
|
module["memory"] = m_moduleStats[name]["memory"];
|
|
|
|
|
} else {
|
|
|
|
|
module["cpu"] = "0.0";
|
|
|
|
|
module["memory"] = "0.0";
|
|
|
|
|
}
|
2026-03-18 10:53:38 -04:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
modules.append(module);
|
|
|
|
|
}
|
2026-03-18 10:53:38 -04:00
|
|
|
delete[] known;
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
return modules;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::loadCoreModule(const QString& moduleName)
|
|
|
|
|
{
|
|
|
|
|
qDebug() << "Loading core module:" << moduleName;
|
2026-03-18 10:53:38 -04:00
|
|
|
|
|
|
|
|
bool success = logos_core_load_plugin_with_dependencies(moduleName.toUtf8().constData()) == 1;
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
if (success) {
|
|
|
|
|
qDebug() << "Successfully loaded core module:" << moduleName;
|
|
|
|
|
emit coreModulesChanged();
|
|
|
|
|
} else {
|
|
|
|
|
qDebug() << "Failed to load core module:" << moduleName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::unloadCoreModule(const QString& moduleName)
|
|
|
|
|
{
|
|
|
|
|
qDebug() << "Unloading core module:" << moduleName;
|
2026-03-18 10:53:38 -04:00
|
|
|
|
|
|
|
|
bool success = logos_core_unload_plugin(moduleName.toUtf8().constData()) == 1;
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
if (success) {
|
|
|
|
|
qDebug() << "Successfully unloaded core module:" << moduleName;
|
|
|
|
|
emit coreModulesChanged();
|
|
|
|
|
} else {
|
|
|
|
|
qDebug() << "Failed to unload core module:" << moduleName;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::refreshCoreModules()
|
|
|
|
|
{
|
2026-03-27 10:57:53 -03:00
|
|
|
// Re-scan all plugin directories via logos_core C API
|
|
|
|
|
logos_core_refresh_plugins();
|
2026-02-26 11:05:34 -03:00
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
emit coreModulesChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString MainUIBackend::getCoreModuleMethods(const QString& moduleName)
|
|
|
|
|
{
|
|
|
|
|
if (!m_logosAPI) {
|
|
|
|
|
return "[]";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosAPIClient* client = m_logosAPI->getClient(moduleName);
|
|
|
|
|
if (!client || !client->isConnected()) {
|
|
|
|
|
return "[]";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant result = client->invokeRemoteMethod(moduleName, "getMethods");
|
|
|
|
|
if (result.canConvert<QJsonArray>()) {
|
|
|
|
|
QJsonArray methods = result.toJsonArray();
|
|
|
|
|
QJsonDocument doc(methods);
|
|
|
|
|
return doc.toJson(QJsonDocument::Compact);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return "[]";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString MainUIBackend::callCoreModuleMethod(const QString& moduleName, const QString& methodName, const QString& argsJson)
|
|
|
|
|
{
|
|
|
|
|
if (!m_logosAPI) {
|
|
|
|
|
return "{\"error\": \"LogosAPI not available\"}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LogosAPIClient* client = m_logosAPI->getClient(moduleName);
|
|
|
|
|
if (!client || !client->isConnected()) {
|
|
|
|
|
return "{\"error\": \"Module not connected\"}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonDocument argsDoc = QJsonDocument::fromJson(argsJson.toUtf8());
|
|
|
|
|
QJsonArray argsArray = argsDoc.array();
|
|
|
|
|
|
|
|
|
|
QVariantList args;
|
|
|
|
|
for (const QJsonValue& val : argsArray) {
|
|
|
|
|
args.append(val.toVariant());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariant result;
|
|
|
|
|
if (args.isEmpty()) {
|
|
|
|
|
result = client->invokeRemoteMethod(moduleName, methodName);
|
|
|
|
|
} else if (args.size() == 1) {
|
|
|
|
|
result = client->invokeRemoteMethod(moduleName, methodName, args[0]);
|
|
|
|
|
} else if (args.size() == 2) {
|
|
|
|
|
result = client->invokeRemoteMethod(moduleName, methodName, args[0], args[1]);
|
|
|
|
|
} else if (args.size() == 3) {
|
|
|
|
|
result = client->invokeRemoteMethod(moduleName, methodName, args[0], args[1], args[2]);
|
|
|
|
|
} else {
|
|
|
|
|
return "{\"error\": \"Too many arguments\"}";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonObject wrapper;
|
|
|
|
|
wrapper["result"] = QJsonValue::fromVariant(result);
|
|
|
|
|
QJsonDocument resultDoc(wrapper);
|
|
|
|
|
return resultDoc.toJson(QJsonDocument::Compact);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantList MainUIBackend::launcherApps() const
|
|
|
|
|
{
|
|
|
|
|
QVariantList apps;
|
|
|
|
|
QStringList availablePlugins = findAvailableUiPlugins();
|
|
|
|
|
|
|
|
|
|
for (const QString& pluginName : availablePlugins) {
|
|
|
|
|
if (pluginName == "main_ui") {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QVariantMap app;
|
|
|
|
|
app["name"] = pluginName;
|
|
|
|
|
app["isLoaded"] = m_loadedApps.contains(pluginName);
|
2026-02-26 11:05:34 -03:00
|
|
|
app["iconPath"] = getPluginIconPath(pluginName);
|
2025-12-10 10:57:16 -05:00
|
|
|
|
|
|
|
|
apps.append(app);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return apps;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::onAppLauncherClicked(const QString& appName)
|
|
|
|
|
{
|
|
|
|
|
qDebug() << "App launcher clicked:" << appName;
|
2026-02-27 15:05:08 +01:00
|
|
|
|
|
|
|
|
setCurrentVisibleApp(appName);
|
2025-12-10 10:57:16 -05:00
|
|
|
if (m_loadedApps.contains(appName)) {
|
|
|
|
|
activateApp(appName);
|
|
|
|
|
} else {
|
|
|
|
|
loadUiModule(appName);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 11:38:01 -05:00
|
|
|
|
2026-01-16 13:42:23 -05:00
|
|
|
void MainUIBackend::openInstallPluginDialog()
|
|
|
|
|
{
|
2026-02-26 11:05:34 -03:00
|
|
|
QString filter = "LGX Package (*.lgx);;All Files (*)";
|
|
|
|
|
|
2026-01-16 13:42:23 -05:00
|
|
|
QString filePath = QFileDialog::getOpenFileName(nullptr, tr("Select Plugin to Install"), QString(), filter);
|
2026-02-26 11:05:34 -03:00
|
|
|
|
2026-01-16 13:42:23 -05:00
|
|
|
if (!filePath.isEmpty()) {
|
|
|
|
|
installPluginFromPath(filePath);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-14 11:38:01 -05:00
|
|
|
void MainUIBackend::installPluginFromPath(const QString& filePath)
|
|
|
|
|
{
|
2026-02-26 11:05:34 -03:00
|
|
|
LogosModules logos(m_logosAPI);
|
|
|
|
|
|
2026-03-27 10:57:53 -03:00
|
|
|
QPointer<MainUIBackend> self(this);
|
|
|
|
|
logos.package_manager.installPluginAsync(filePath, false, [self](QVariant result) {
|
|
|
|
|
if (!self) return;
|
|
|
|
|
Q_UNUSED(result);
|
|
|
|
|
self->refreshCoreModules();
|
|
|
|
|
self->fetchUiPluginMetadata();
|
|
|
|
|
});
|
2026-01-14 11:38:01 -05:00
|
|
|
}
|
2026-01-16 13:50:55 -05:00
|
|
|
|
2026-02-04 10:11:55 -05:00
|
|
|
|
2026-02-26 11:05:34 -03:00
|
|
|
QString MainUIBackend::getPluginType(const QString& name) const
|
|
|
|
|
{
|
2026-03-27 10:57:53 -03:00
|
|
|
const auto it = m_uiPluginMetadata.constFind(name);
|
|
|
|
|
if (it != m_uiPluginMetadata.cend()) {
|
|
|
|
|
return it->value("type").toString();
|
2026-02-26 11:05:34 -03:00
|
|
|
}
|
2026-03-27 10:57:53 -03:00
|
|
|
return QString();
|
2026-02-26 11:05:34 -03:00
|
|
|
}
|
|
|
|
|
|
2026-01-02 10:10:54 -05:00
|
|
|
bool MainUIBackend::isQmlPlugin(const QString& name) const
|
|
|
|
|
{
|
2026-04-07 16:45:09 +02:00
|
|
|
return getPluginType(name) == QStringLiteral("ui_qml");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString MainUIBackend::resolveQmlViewPath(const QVariantMap& meta) const
|
|
|
|
|
{
|
|
|
|
|
// ui_qml contract: "view" is the QML entry point, relative to installDir.
|
|
|
|
|
const QString installDir = meta.value("installDir").toString();
|
|
|
|
|
const QString viewField = meta.value("view").toString();
|
|
|
|
|
if (viewField.isEmpty()) return QString();
|
|
|
|
|
return QDir(installDir).filePath(viewField);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::loadLegacyUiModule(const QString& moduleName)
|
|
|
|
|
{
|
|
|
|
|
if (m_pluginLoader->isLoading(moduleName)) {
|
|
|
|
|
qDebug() << "Module" << moduleName << "is already loading";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
PluginLoadRequest request;
|
|
|
|
|
request.name = moduleName;
|
2026-04-13 12:39:17 -03:00
|
|
|
request.type = UIPluginType::Legacy;
|
2026-04-07 16:45:09 +02:00
|
|
|
request.pluginPath = getPluginPath(moduleName);
|
|
|
|
|
request.iconPath = getPluginIconPath(moduleName, true);
|
|
|
|
|
if (m_uiPluginMetadata.contains(moduleName)) {
|
|
|
|
|
request.coreDependencies = m_uiPluginMetadata[moduleName].value("dependencies").toList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
m_pluginLoader->load(request);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
bool MainUIBackend::hasBackendPlugin(const QString& name) const
|
|
|
|
|
{
|
|
|
|
|
// True iff the ui_qml plugin ships a backend Qt plugin lib alongside its
|
|
|
|
|
// QML view. For QML-only ui_qml modules, mainFilePath is empty (no
|
|
|
|
|
// backend). When a backend is present, mainFilePath points at the .so/.dylib.
|
|
|
|
|
if (!isQmlPlugin(name)) return false;
|
|
|
|
|
const QString mainPath = m_uiPluginMetadata.value(name).value("mainFilePath").toString();
|
|
|
|
|
if (mainPath.isEmpty()) return false;
|
|
|
|
|
return mainPath.endsWith(QStringLiteral(".so"), Qt::CaseInsensitive)
|
|
|
|
|
|| mainPath.endsWith(QStringLiteral(".dylib"), Qt::CaseInsensitive)
|
|
|
|
|
|| mainPath.endsWith(QStringLiteral(".dll"), Qt::CaseInsensitive);
|
2026-01-02 10:10:54 -05:00
|
|
|
}
|
|
|
|
|
|
2025-12-10 10:57:16 -05:00
|
|
|
QStringList MainUIBackend::findAvailableUiPlugins() const
|
|
|
|
|
{
|
2026-03-27 10:57:53 -03:00
|
|
|
return m_uiPluginMetadata.keys();
|
|
|
|
|
}
|
2026-01-02 10:10:54 -05:00
|
|
|
|
2026-03-27 10:57:53 -03:00
|
|
|
void MainUIBackend::fetchUiPluginMetadata()
|
|
|
|
|
{
|
|
|
|
|
if (!m_logosAPI) return;
|
2026-01-02 10:10:54 -05:00
|
|
|
|
2026-03-27 10:57:53 -03:00
|
|
|
LogosModules logos(m_logosAPI);
|
|
|
|
|
QPointer<MainUIBackend> self(this);
|
|
|
|
|
logos.package_manager.getInstalledUiPluginsAsync([self](QVariantList uiPlugins) {
|
|
|
|
|
if (!self) return;
|
|
|
|
|
self->m_uiPluginMetadata.clear();
|
|
|
|
|
for (const QVariant& item : uiPlugins) {
|
|
|
|
|
QVariantMap pluginInfo = item.toMap();
|
|
|
|
|
QString name = pluginInfo.value("name").toString();
|
2026-04-07 16:45:09 +02:00
|
|
|
if (name.isEmpty()) continue;
|
|
|
|
|
|
|
|
|
|
// ui_qml requires "view" (the QML entry point); "main" is optional.
|
|
|
|
|
// Other types require "mainFilePath" (the backend lib).
|
|
|
|
|
const QString type = pluginInfo.value("type").toString();
|
|
|
|
|
if (type == QStringLiteral("ui_qml")) {
|
|
|
|
|
if (pluginInfo.value("view").toString().isEmpty()) continue;
|
|
|
|
|
} else {
|
|
|
|
|
if (pluginInfo.value("mainFilePath").toString().isEmpty()) continue;
|
2026-01-13 16:35:49 -05:00
|
|
|
}
|
2026-04-07 16:45:09 +02:00
|
|
|
self->m_uiPluginMetadata[name] = pluginInfo;
|
2026-01-02 10:10:54 -05:00
|
|
|
}
|
2026-03-27 10:57:53 -03:00
|
|
|
emit self->uiModulesChanged();
|
|
|
|
|
emit self->launcherAppsChanged();
|
|
|
|
|
});
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString MainUIBackend::getPluginPath(const QString& name) const
|
|
|
|
|
{
|
2026-04-07 16:45:09 +02:00
|
|
|
// Only used by loadLegacyUiModule (type "ui"); ui_qml uses installDir + view.
|
2026-03-27 10:57:53 -03:00
|
|
|
const auto it = m_uiPluginMetadata.constFind(name);
|
|
|
|
|
if (it != m_uiPluginMetadata.cend()) {
|
2026-04-07 16:45:09 +02:00
|
|
|
return it->value("mainFilePath").toString();
|
2026-01-13 16:35:49 -05:00
|
|
|
}
|
2026-03-27 10:57:53 -03:00
|
|
|
return QString();
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
2026-02-26 11:05:34 -03:00
|
|
|
QString MainUIBackend::getPluginIconPath(const QString& pluginName, bool forWidgetIcon) const
|
2025-12-10 10:57:16 -05:00
|
|
|
{
|
2026-03-27 10:57:53 -03:00
|
|
|
if (!m_uiPluginMetadata.contains(pluginName)) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const QVariantMap& meta = m_uiPluginMetadata[pluginName];
|
|
|
|
|
QString iconPath = meta.value("icon").toString();
|
|
|
|
|
QString installDir = meta.value("installDir").toString();
|
|
|
|
|
|
2026-02-17 15:14:58 +01:00
|
|
|
if (iconPath.isEmpty()) {
|
2026-01-02 10:10:54 -05:00
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-27 10:57:53 -03:00
|
|
|
QDir pluginDir(installDir);
|
2026-02-26 11:05:34 -03:00
|
|
|
QString filePath = pluginDir.filePath(iconPath.startsWith(":/") ? iconPath.mid(2) : iconPath);
|
2026-02-17 15:14:58 +01:00
|
|
|
bool exists = QFile::exists(filePath);
|
|
|
|
|
|
|
|
|
|
if (forWidgetIcon) {
|
|
|
|
|
if (exists) {
|
|
|
|
|
return filePath;
|
2026-01-02 10:10:54 -05:00
|
|
|
}
|
2026-02-17 15:14:58 +01:00
|
|
|
if (iconPath.startsWith(":/")) {
|
|
|
|
|
qWarning() << "Plugin icon not on disk, using resource path; expected:" << filePath;
|
|
|
|
|
return iconPath;
|
|
|
|
|
}
|
|
|
|
|
qWarning() << "Plugin icon not found, expected:" << filePath;
|
|
|
|
|
return QString();
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
2026-02-17 15:14:58 +01:00
|
|
|
return exists ? QUrl::fromLocalFile(filePath).toString() : (iconPath.startsWith(":/") ? "qrc" + iconPath : QString());
|
2025-12-10 10:57:16 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void MainUIBackend::updateModuleStats()
|
|
|
|
|
{
|
|
|
|
|
char* stats_json = logos_core_get_module_stats();
|
|
|
|
|
if (!stats_json) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString jsonStr = QString::fromUtf8(stats_json);
|
|
|
|
|
QJsonDocument doc = QJsonDocument::fromJson(stats_json);
|
|
|
|
|
free(stats_json);
|
|
|
|
|
|
|
|
|
|
if (doc.isNull()) {
|
|
|
|
|
qWarning() << "Failed to parse module stats JSON";
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QJsonArray modulesArray;
|
|
|
|
|
if (doc.isArray()) {
|
|
|
|
|
modulesArray = doc.array();
|
|
|
|
|
} else if (doc.isObject()) {
|
|
|
|
|
QJsonObject root = doc.object();
|
|
|
|
|
modulesArray = root["modules"].toArray();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const QJsonValue& val : modulesArray) {
|
|
|
|
|
QJsonObject moduleObj = val.toObject();
|
|
|
|
|
QString name = moduleObj["name"].toString();
|
|
|
|
|
|
|
|
|
|
if (!name.isEmpty()) {
|
|
|
|
|
QVariantMap stats;
|
|
|
|
|
double cpu = moduleObj["cpu_percent"].toDouble();
|
|
|
|
|
if (cpu == 0) cpu = moduleObj["cpu"].toDouble();
|
|
|
|
|
|
|
|
|
|
double memory = moduleObj["memory_mb"].toDouble();
|
|
|
|
|
if (memory == 0) memory = moduleObj["memory"].toDouble();
|
|
|
|
|
if (memory == 0) memory = moduleObj["memory_MB"].toDouble();
|
|
|
|
|
|
|
|
|
|
stats["cpu"] = QString::number(cpu, 'f', 1);
|
|
|
|
|
stats["memory"] = QString::number(memory, 'f', 1);
|
|
|
|
|
m_moduleStats[name] = stats;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emit coreModulesChanged();
|
|
|
|
|
}
|
2026-01-20 13:28:27 -05:00
|
|
|
|