mirror of
https://github.com/logos-co/logos-basecamp.git
synced 2026-08-27 14:51:07 +00:00
fix: add application modal state is wrong when clicked in between an ongoing download
This commit is contained in:
@@ -99,6 +99,17 @@ int AppsFilterProxy::alreadyInstalledCount() const
|
||||
return c;
|
||||
}
|
||||
|
||||
int AppsFilterProxy::installingCount() const
|
||||
{
|
||||
int c = 0;
|
||||
const int n = rowCount();
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (data(index(i, 0), AppsModel::ActionRole).toString()
|
||||
== QStringLiteral("installing")) ++c;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
int AppsFilterProxy::errorCount() const
|
||||
{
|
||||
int c = 0;
|
||||
|
||||
@@ -23,6 +23,7 @@ class AppsFilterProxy : public QSortFilterProxyModel {
|
||||
Q_PROPERTY(int upgradeCount READ upgradeCount NOTIFY breakdownChanged)
|
||||
Q_PROPERTY(int reinstallCount READ reinstallCount NOTIFY breakdownChanged)
|
||||
Q_PROPERTY(int alreadyInstalledCount READ alreadyInstalledCount NOTIFY breakdownChanged)
|
||||
Q_PROPERTY(int installingCount READ installingCount NOTIFY breakdownChanged)
|
||||
Q_PROPERTY(int errorCount READ errorCount NOTIFY breakdownChanged)
|
||||
Q_PROPERTY(bool hasResolutionErrors READ hasResolutionErrors NOTIFY hasResolutionErrorsChanged)
|
||||
Q_PROPERTY(int visibleCount READ visibleCount NOTIFY visibleCountChanged)
|
||||
@@ -52,6 +53,7 @@ public:
|
||||
int upgradeCount() const;
|
||||
int reinstallCount() const;
|
||||
int alreadyInstalledCount() const;
|
||||
int installingCount() const;
|
||||
int errorCount() const;
|
||||
bool hasResolutionErrors() const;
|
||||
int visibleCount() const { return rowCount(); }
|
||||
|
||||
+60
-24
@@ -1,5 +1,7 @@
|
||||
#include "AppsModel.h"
|
||||
|
||||
#include "InstallRegistry.h"
|
||||
|
||||
#include <QSet>
|
||||
|
||||
namespace {
|
||||
@@ -46,12 +48,23 @@ QVariant AppsModel::data(const QModelIndex& index, int role) const
|
||||
case MissingDepsRole: return r.missingDeps;
|
||||
case InstallStatusRole: return static_cast<int>(r.installStatus);
|
||||
case InstallTypeRole: return r.installType;
|
||||
case ActionRole: return r.action;
|
||||
case ActionRole: {
|
||||
if (m_installRegistry) {
|
||||
if (m_installRegistry->isInFlight(r.name))
|
||||
return QStringLiteral("installing");
|
||||
if (m_installRegistry->stage(r.name) == InstallStage::Installed)
|
||||
return QStringLiteral("installed");
|
||||
}
|
||||
return r.action;
|
||||
}
|
||||
case ToVersionRole: return r.toVersion;
|
||||
case IsTopLevelRole: return r.isTopLevel;
|
||||
case ResolverErrorRole: return r.resolverError;
|
||||
case InstallStageRole: return static_cast<int>(r.installStage);
|
||||
case InstallErrorRole: return r.installError;
|
||||
case InstallStageRole:
|
||||
return m_installRegistry ? m_installRegistry->stage(r.name)
|
||||
: static_cast<int>(InstallStage::None);
|
||||
case InstallErrorRole:
|
||||
return m_installRegistry ? m_installRegistry->error(r.name) : QString();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -302,6 +315,30 @@ void AppsModel::markInstalled(const QString& name,
|
||||
}
|
||||
}
|
||||
|
||||
void AppsModel::replaceInstalledSet(const QHash<QString, QString>& versionByName,
|
||||
const QHash<QString, QString>& hashByName)
|
||||
{
|
||||
const bool wasBulk = m_inBulkInstalledUpdate;
|
||||
if (!wasBulk) beginBulkInstalledUpdate();
|
||||
|
||||
for (int idx = 0; idx < m_rows.size(); ++idx) {
|
||||
Row& r = m_rows[idx];
|
||||
const auto it = versionByName.find(r.name);
|
||||
const bool isInstalled = (it != versionByName.end());
|
||||
const QString newVersion = isInstalled ? it.value() : QString();
|
||||
const QString newHash = isInstalled ? hashByName.value(r.name) : QString();
|
||||
if (r.installedVersion == newVersion && r.installedHash == newHash) continue;
|
||||
r.installedVersion = newVersion;
|
||||
r.installedHash = newHash;
|
||||
recomputeInstallStatus(r);
|
||||
const QModelIndex mi = index(idx);
|
||||
emit dataChanged(mi, mi, {InstalledVersionRole, HasUpdateRole,
|
||||
IsInstalledRole, InstallStatusRole});
|
||||
}
|
||||
|
||||
if (!wasBulk) endBulkInstalledUpdate();
|
||||
}
|
||||
|
||||
void AppsModel::beginBulkInstalledUpdate()
|
||||
{
|
||||
m_inBulkInstalledUpdate = true;
|
||||
@@ -357,20 +394,26 @@ void AppsModel::setMissingDeps(const QString& name, const QStringList& missing)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mutation: live install stage ──────────────────────────────────────────
|
||||
// ── Wiring: live install state ────────────────────────────────────────────
|
||||
|
||||
void AppsModel::setInstallStage(const QString& name,
|
||||
InstallStage::Value stage,
|
||||
const QString& error)
|
||||
void AppsModel::setInstallRegistry(InstallRegistry* installRegistry)
|
||||
{
|
||||
for (int idx : m_indicesByName.values(name)) {
|
||||
Row& r = m_rows[idx];
|
||||
if (r.installStage == stage && r.installError == error) continue;
|
||||
r.installStage = stage;
|
||||
r.installError = stage == InstallStage::Failed ? error : QString();
|
||||
const QModelIndex mi = index(idx);
|
||||
emit dataChanged(mi, mi, {InstallStageRole, InstallErrorRole});
|
||||
}
|
||||
if (m_installRegistry == installRegistry) return;
|
||||
if (m_installRegistry) m_installRegistry->disconnect(this);
|
||||
m_installRegistry = installRegistry;
|
||||
if (!m_installRegistry) return;
|
||||
|
||||
auto refresh = [this](const QString& name) {
|
||||
const QList<int> roles{InstallStageRole, InstallErrorRole, ActionRole};
|
||||
for (int idx : m_indicesByName.values(name)) {
|
||||
const QModelIndex mi = index(idx);
|
||||
emit dataChanged(mi, mi, roles);
|
||||
}
|
||||
};
|
||||
connect(m_installRegistry, &InstallRegistry::stageChanged, this,
|
||||
[refresh](const QString& name, InstallStage::Value) { refresh(name); });
|
||||
connect(m_installRegistry, &InstallRegistry::errorChanged, this,
|
||||
[refresh](const QString& name, const QString&) { refresh(name); });
|
||||
}
|
||||
|
||||
// ── Mutation: resolver overlay ─────────────────────────────────────────────
|
||||
@@ -386,16 +429,9 @@ void AppsModel::setResolverOverlay(const QList<ResolverRow>& rows)
|
||||
r.toVersion = src.toVersion;
|
||||
r.isTopLevel = src.isTopLevel;
|
||||
r.resolverError = src.resolverError;
|
||||
// Wipe any sticky pipeline state from a previous session
|
||||
QList<int> changedRoles{ActionRole, ToVersionRole, IsTopLevelRole, ResolverErrorRole};
|
||||
if (r.installStage != InstallStage::None
|
||||
|| !r.installError.isEmpty()) {
|
||||
r.installStage = InstallStage::None;
|
||||
r.installError.clear();
|
||||
changedRoles << InstallStageRole << InstallErrorRole;
|
||||
}
|
||||
const QModelIndex mi = index(idx);
|
||||
emit dataChanged(mi, mi, changedRoles);
|
||||
emit dataChanged(mi, mi,
|
||||
{ActionRole, ToVersionRole, IsTopLevelRole, ResolverErrorRole});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -4,10 +4,13 @@
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <QString>
|
||||
#include <QVariantList>
|
||||
#include <QVariantMap>
|
||||
|
||||
class InstallRegistry;
|
||||
|
||||
// AppsModel — the single source of truth for every package the App Manager
|
||||
// (and Modules tab) cares about.
|
||||
class AppsModel : public QAbstractListModel {
|
||||
@@ -65,12 +68,13 @@ public:
|
||||
void markInstalled(const QString& name,
|
||||
const QString& installedVersion,
|
||||
const QString& installedHash = {});
|
||||
void replaceInstalledSet(const QHash<QString, QString>& versionByName,
|
||||
const QHash<QString, QString>& hashByName);
|
||||
|
||||
void setInstallType(const QString& name, const QString& installType);
|
||||
void setIconUrl(const QString& name, const QString& iconUrl);
|
||||
void setMissingDeps(const QString& name, const QStringList& missing);
|
||||
void setInstallStage(const QString& name,
|
||||
InstallStage::Value stage,
|
||||
const QString& error = {});
|
||||
void setInstallRegistry(InstallRegistry* installRegistry);
|
||||
|
||||
void beginBulkInstalledUpdate();
|
||||
void endBulkInstalledUpdate();
|
||||
@@ -116,11 +120,9 @@ private:
|
||||
QStringList missingDeps;
|
||||
InstallStatus::Value installStatus = InstallStatus::NotInstalled;
|
||||
|
||||
// Live install pipeline state
|
||||
InstallStage::Value installStage = InstallStage::None;
|
||||
QString installError;
|
||||
|
||||
// Resolver overlay (per dialog session)
|
||||
// Resolver overlay (per dialog session). Live install state lives
|
||||
// on m_installRegistry — see setInstallRegistry. Per-row InstallStageRole /
|
||||
// InstallErrorRole / ActionRole derive from there at read time.
|
||||
QString action;
|
||||
QString toVersion;
|
||||
bool isTopLevel = false;
|
||||
@@ -136,4 +138,7 @@ private:
|
||||
QHash<QString, int> m_indexByKey; // (repo + "\n" + name) → row index
|
||||
QMultiHash<QString, int> m_indicesByName;
|
||||
bool m_inBulkInstalledUpdate = false;
|
||||
|
||||
// Source of truth for in-flight install state
|
||||
QPointer<InstallRegistry> m_installRegistry;
|
||||
};
|
||||
|
||||
@@ -112,6 +112,7 @@ Dialog {
|
||||
readonly property bool actionEnabled:
|
||||
d.targetName.length > 0
|
||||
&& !d.installing
|
||||
&& d.installingBuckets === 0
|
||||
&& (d.actionMode === "launch" || !d.hasResolutionErrors)
|
||||
|
||||
readonly property int totalDeps:
|
||||
@@ -131,6 +132,8 @@ Dialog {
|
||||
root.requiredPackagesModel ? root.requiredPackagesModel.reinstallCount : 0
|
||||
readonly property int alreadyInstalledBuckets:
|
||||
root.requiredPackagesModel ? root.requiredPackagesModel.alreadyInstalledCount : 0
|
||||
readonly property int installingBuckets:
|
||||
root.requiredPackagesModel ? root.requiredPackagesModel.installingCount : 0
|
||||
readonly property int errorBuckets:
|
||||
root.requiredPackagesModel ? root.requiredPackagesModel.errorCount : 0
|
||||
readonly property int changingBuckets:
|
||||
@@ -144,6 +147,12 @@ Dialog {
|
||||
return d.stageLabel
|
||||
}
|
||||
|
||||
if (d.installingBuckets > 0 && d.changingBuckets === 0) {
|
||||
return qsTr("%1 of %2's required package(s) are currently being installed. "
|
||||
+ "Wait for the active install to finish before launching %2.")
|
||||
.arg(d.installingBuckets).arg(d.targetDisplayName)
|
||||
}
|
||||
|
||||
// Top-level + deps all match disk → nothing to do. The action
|
||||
// button reads "Launch"; hide the footer entirely.
|
||||
if (d.changingBuckets === 0) return ""
|
||||
|
||||
@@ -24,6 +24,8 @@ ItemDelegate {
|
||||
readonly property string action: root.appRow ? (root.appRow.action || "") : ""
|
||||
readonly property string toVersion: root.appRow ? (root.appRow.toVersion || "") : ""
|
||||
readonly property bool isError: d.action === "error"
|
||||
readonly property bool isInstalled:
|
||||
root.appRow ? (root.appRow.isInstalled === true) : false
|
||||
readonly property int rowStage:
|
||||
root.appRow && root.appRow.installStage !== undefined
|
||||
? root.appRow.installStage
|
||||
@@ -128,8 +130,9 @@ ItemDelegate {
|
||||
case "upgrade": return qsTr("Upgrade")
|
||||
case "downgrade": return qsTr("Downgrade")
|
||||
case "reinstall": return qsTr("Reinstall")
|
||||
default: return qsTr("Installed")
|
||||
case "installed": return qsTr("Installed")
|
||||
}
|
||||
return d.isInstalled ? qsTr("Installed") : qsTr("Install")
|
||||
}
|
||||
color: {
|
||||
if (d.isError) return Theme.palette.error
|
||||
@@ -145,8 +148,9 @@ ItemDelegate {
|
||||
case "upgrade": return Theme.palette.info
|
||||
case "downgrade": return Theme.palette.info
|
||||
case "reinstall": return Theme.palette.info
|
||||
default: return Theme.palette.textTertiary
|
||||
case "installed": return Theme.palette.textTertiary
|
||||
}
|
||||
return d.isInstalled ? Theme.palette.textTertiary : Theme.palette.primary
|
||||
}
|
||||
radius: Theme.spacing.radiusLarge
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ Item {
|
||||
AddApplicationDialog {
|
||||
id: addApplicationDialog
|
||||
requiredPackagesModel: backend.requiredPackagesModel
|
||||
onClosed: backend.notifyAddApplicationDialogClosed()
|
||||
onInstallRequested: function(name, repositoryUrl, versionPins) {
|
||||
addApplicationDialog.installStage = InstallStage.Downloading
|
||||
backend.confirmCatalogInstall(name, repositoryUrl, versionPins)
|
||||
@@ -195,15 +196,23 @@ Item {
|
||||
backend.onAppLauncherClicked(name);
|
||||
}
|
||||
|
||||
function onAddApplicationRequested(metadata, requiredPackages) {
|
||||
if (addApplicationDialog.visible) {
|
||||
function onRequestOpenAddApplicationDialog(metadata) {
|
||||
if (!addApplicationDialog.visible) {
|
||||
addApplicationDialog.openWith(metadata);
|
||||
} else if (addApplicationDialog.metadata.name === metadata.name) {
|
||||
// Version re-resolve while the same app's dialog is already open.
|
||||
addApplicationDialog.metadata = metadata;
|
||||
addApplicationDialog.installStage = metadata.installStage || InstallStage.None;
|
||||
} else {
|
||||
addApplicationDialog.openWith(metadata);
|
||||
}
|
||||
}
|
||||
|
||||
function onAddApplicationDataUpdated(metadata) {
|
||||
if (!addApplicationDialog.visible) return;
|
||||
if (addApplicationDialog.metadata.name !== metadata.name) return;
|
||||
addApplicationDialog.metadata = metadata;
|
||||
addApplicationDialog.installStage = metadata.installStage || InstallStage.None;
|
||||
}
|
||||
|
||||
function onCatalogInstallStageChanged(name, stage) {
|
||||
if (!addApplicationDialog.visible) return;
|
||||
if (addApplicationDialog.metadata.name !== name) return;
|
||||
|
||||
@@ -118,6 +118,7 @@ set(SOURCES
|
||||
CoreModuleManager.cpp
|
||||
UIPluginManager.cpp
|
||||
PackageCoordinator.cpp
|
||||
InstallRegistry.cpp
|
||||
AppsModel.cpp
|
||||
PluginLoader.cpp
|
||||
restricted/DenyAllReply.cpp
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
#include "InstallRegistry.h"
|
||||
|
||||
InstallRegistry::InstallRegistry(QObject* parent) : QObject(parent) {}
|
||||
|
||||
int InstallRegistry::stage(const QString& name) const
|
||||
{
|
||||
const auto it = m_ops.constFind(name);
|
||||
return it == m_ops.cend() ? InstallStage::None
|
||||
: static_cast<int>(it->stage);
|
||||
}
|
||||
|
||||
bool InstallRegistry::isInFlight(const QString& name) const
|
||||
{
|
||||
const auto it = m_ops.constFind(name);
|
||||
if (it == m_ops.cend()) return false;
|
||||
switch (it->stage) {
|
||||
case InstallStage::Downloading:
|
||||
case InstallStage::Queued:
|
||||
case InstallStage::Installing:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QString InstallRegistry::error(const QString& name) const
|
||||
{
|
||||
const auto it = m_ops.constFind(name);
|
||||
return it == m_ops.cend() ? QString() : it->error;
|
||||
}
|
||||
|
||||
QString InstallRegistry::targetVersion(const QString& name) const
|
||||
{
|
||||
const auto it = m_ops.constFind(name);
|
||||
return it == m_ops.cend() ? QString() : it->targetVersion;
|
||||
}
|
||||
|
||||
QString InstallRegistry::targetHash(const QString& name) const
|
||||
{
|
||||
const auto it = m_ops.constFind(name);
|
||||
return it == m_ops.cend() ? QString() : it->targetHash;
|
||||
}
|
||||
|
||||
void InstallRegistry::begin(const QString& name,
|
||||
const QString& targetVersion,
|
||||
const QString& targetHash,
|
||||
const QString& startedByTopLevel)
|
||||
{
|
||||
if (name.isEmpty()) return;
|
||||
const bool added = !m_ops.contains(name);
|
||||
Entry& e = m_ops[name];
|
||||
e.name = name;
|
||||
e.targetVersion = targetVersion;
|
||||
e.targetHash = targetHash;
|
||||
e.stage = InstallStage::Downloading;
|
||||
e.error.clear();
|
||||
e.startedByTopLevel = startedByTopLevel;
|
||||
emit stageChanged(name, e.stage);
|
||||
if (added) emit activeNamesChanged();
|
||||
}
|
||||
|
||||
void InstallRegistry::setStage(const QString& name, InstallStage::Value stage)
|
||||
{
|
||||
auto it = m_ops.find(name);
|
||||
if (it == m_ops.end()) return;
|
||||
if (it->stage == stage) return;
|
||||
it->stage = stage;
|
||||
emit stageChanged(name, stage);
|
||||
}
|
||||
|
||||
void InstallRegistry::fail(const QString& name, const QString& error)
|
||||
{
|
||||
auto it = m_ops.find(name);
|
||||
if (it == m_ops.end()) return;
|
||||
const bool stageChanged_ = it->stage != InstallStage::Failed;
|
||||
it->stage = InstallStage::Failed;
|
||||
it->error = error;
|
||||
if (stageChanged_) emit stageChanged(name, InstallStage::Failed);
|
||||
emit errorChanged(name, error);
|
||||
}
|
||||
|
||||
void InstallRegistry::finish(const QString& name)
|
||||
{
|
||||
setStage(name, InstallStage::Installed);
|
||||
}
|
||||
|
||||
void InstallRegistry::clear(const QString& name)
|
||||
{
|
||||
if (!m_ops.contains(name)) return;
|
||||
m_ops.remove(name);
|
||||
emit stageChanged(name, InstallStage::None);
|
||||
emit errorChanged(name, QString());
|
||||
emit activeNamesChanged();
|
||||
}
|
||||
|
||||
void InstallRegistry::clearByTopLevel(const QString& topLevelName)
|
||||
{
|
||||
if (topLevelName.isEmpty()) return;
|
||||
QStringList toRemove;
|
||||
for (auto it = m_ops.cbegin(); it != m_ops.cend(); ++it) {
|
||||
if (it->startedByTopLevel == topLevelName) toRemove.append(it.key());
|
||||
}
|
||||
if (toRemove.isEmpty()) return;
|
||||
for (const QString& name : toRemove) m_ops.remove(name);
|
||||
for (const QString& name : toRemove) {
|
||||
emit stageChanged(name, InstallStage::None);
|
||||
emit errorChanged(name, QString());
|
||||
}
|
||||
emit activeNamesChanged();
|
||||
}
|
||||
|
||||
void InstallRegistry::beginOrTrack(const QString& name,
|
||||
const QString& targetVersion,
|
||||
const QString& targetHash,
|
||||
const QString& startedByTopLevel)
|
||||
{
|
||||
if (name.isEmpty()) return;
|
||||
if (!m_ops.contains(name)) {
|
||||
begin(name, targetVersion, targetHash, startedByTopLevel);
|
||||
return;
|
||||
}
|
||||
Entry& e = m_ops[name];
|
||||
if (!targetVersion.isEmpty()) e.targetVersion = targetVersion;
|
||||
if (!targetHash.isEmpty()) e.targetHash = targetHash;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
#pragma once
|
||||
|
||||
#include "InstallEnums.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
// Registry of in-flight install operations. One entry per package name
|
||||
class InstallRegistry : public QObject {
|
||||
Q_OBJECT
|
||||
Q_PROPERTY(QStringList activeNames READ activeNames NOTIFY activeNamesChanged)
|
||||
|
||||
public:
|
||||
struct Entry {
|
||||
QString name;
|
||||
QString targetVersion;
|
||||
QString targetHash;
|
||||
InstallStage::Value stage = InstallStage::None;
|
||||
QString error;
|
||||
QString startedByTopLevel;
|
||||
};
|
||||
|
||||
explicit InstallRegistry(QObject* parent = nullptr);
|
||||
|
||||
Q_INVOKABLE bool has(const QString& name) const { return m_ops.contains(name); }
|
||||
Q_INVOKABLE int stage(const QString& name) const;
|
||||
Q_INVOKABLE bool isInFlight(const QString& name) const;
|
||||
QString error(const QString& name) const;
|
||||
QString targetVersion(const QString& name) const;
|
||||
QString targetHash(const QString& name) const;
|
||||
QStringList activeNames() const { return m_ops.keys(); }
|
||||
|
||||
void beginOrTrack(const QString& name,
|
||||
const QString& targetVersion,
|
||||
const QString& targetHash,
|
||||
const QString& startedByTopLevel);
|
||||
void begin(const QString& name,
|
||||
const QString& targetVersion,
|
||||
const QString& targetHash,
|
||||
const QString& startedByTopLevel);
|
||||
void setStage(const QString& name, InstallStage::Value stage);
|
||||
void fail(const QString& name, const QString& error);
|
||||
void finish(const QString& name);
|
||||
void clear(const QString& name);
|
||||
void clearByTopLevel(const QString& topLevelName);
|
||||
|
||||
signals:
|
||||
void activeNamesChanged();
|
||||
void stageChanged(const QString& name, InstallStage::Value stage);
|
||||
void errorChanged(const QString& name, const QString& error);
|
||||
|
||||
private:
|
||||
QHash<QString, Entry> m_ops;
|
||||
};
|
||||
@@ -47,6 +47,7 @@ MainUIBackend::MainUIBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
m_uiPluginManager = new UIPluginManager(m_logosAPI, m_coreModuleManager, this);
|
||||
m_packageCoordinator = new PackageCoordinator(m_logosAPI, m_coreModuleManager, m_uiPluginManager, m_appsModel, this);
|
||||
m_packageCoordinator->setRequiredPackagesModel(m_requiredPackagesModel);
|
||||
m_appsModel->setInstallRegistry(m_packageCoordinator->installRegistry());
|
||||
|
||||
// Setter-injection closes the cycle — UIPluginManager queries
|
||||
// PackageCoordinator for installType / missing-deps when building its
|
||||
@@ -99,8 +100,10 @@ MainUIBackend::MainUIBackend(LogosAPI* logosAPI, QObject* parent)
|
||||
this, &MainUIBackend::upgradeCascadeConfirmationRequested);
|
||||
connect(m_packageCoordinator, &PackageCoordinator::uninstallMultiCascadeConfirmationRequested,
|
||||
this, &MainUIBackend::uninstallMultiCascadeConfirmationRequested);
|
||||
connect(m_packageCoordinator, &PackageCoordinator::addApplicationRequested,
|
||||
this, &MainUIBackend::addApplicationRequested);
|
||||
connect(m_packageCoordinator, &PackageCoordinator::requestOpenAddApplicationDialog,
|
||||
this, &MainUIBackend::requestOpenAddApplicationDialog);
|
||||
connect(m_packageCoordinator, &PackageCoordinator::addApplicationDataUpdated,
|
||||
this, &MainUIBackend::addApplicationDataUpdated);
|
||||
connect(m_packageCoordinator, &PackageCoordinator::launchAppRequested,
|
||||
this, &MainUIBackend::launchAppRequested);
|
||||
connect(m_packageCoordinator, &PackageCoordinator::catalogInstallStageChanged,
|
||||
@@ -241,6 +244,8 @@ void MainUIBackend::openApp(const QString& name, const QString& repositoryUrl, c
|
||||
{ m_packageCoordinator->openApp(name, repositoryUrl, versionPins, allowFastLaunch); }
|
||||
void MainUIBackend::confirmCatalogInstall(const QString& name, const QString& repositoryUrl, const QVariantMap& versionPins)
|
||||
{ m_packageCoordinator->confirmCatalogInstall(name, repositoryUrl, versionPins); }
|
||||
void MainUIBackend::notifyAddApplicationDialogClosed()
|
||||
{ m_packageCoordinator->notifyAddApplicationDialogClosed(); }
|
||||
|
||||
// cancelPendingAction is the one slot that doesn't route to a single manager:
|
||||
// a pending action lives on either UIPluginManager (local unload cascade) or
|
||||
|
||||
+3
-1
@@ -175,6 +175,7 @@ public slots:
|
||||
Q_INVOKABLE void confirmCatalogInstall(const QString& name,
|
||||
const QString& repositoryUrl,
|
||||
const QVariantMap& versionPins = QVariantMap());
|
||||
Q_INVOKABLE void notifyAddApplicationDialogClosed();
|
||||
|
||||
// Core Module operations — routing rule: cascade-aware (load/unload)
|
||||
// goes through UIPluginManager so it can run the pre-flight dependent
|
||||
@@ -209,7 +210,8 @@ signals:
|
||||
|
||||
// App-Manager dialog + install lifecycle. See PackageCoordinator for
|
||||
// the contract — these are pure re-emits.
|
||||
void addApplicationRequested(const QVariantMap& metadata);
|
||||
void requestOpenAddApplicationDialog(const QVariantMap& metadata);
|
||||
void addApplicationDataUpdated(const QVariantMap& metadata);
|
||||
void launchAppRequested(const QString& name);
|
||||
void catalogInstallStageChanged(const QString& name, InstallStage::Value stage);
|
||||
void catalogInstallFinished(const QString& name);
|
||||
|
||||
+149
-74
@@ -1,4 +1,5 @@
|
||||
#include "PackageCoordinator.h"
|
||||
#include "InstallRegistry.h"
|
||||
#include "AppsFilterProxy.h"
|
||||
#include "AppsModel.h"
|
||||
#include "CoreModuleManager.h"
|
||||
@@ -31,6 +32,7 @@ PackageCoordinator::PackageCoordinator(LogosAPI* logosAPI,
|
||||
, m_coreModuleManager(coreModuleManager)
|
||||
, m_uiPluginManager(uiPluginManager)
|
||||
, m_appsModel(appsModel)
|
||||
, m_installRegistry(new InstallRegistry(this))
|
||||
{
|
||||
subscribeToPackageInstallationEvents();
|
||||
subscribeToPackageDownloaderEvents();
|
||||
@@ -995,11 +997,6 @@ void PackageCoordinator::refreshDependencyInfo()
|
||||
[self](QVariantList packages) {
|
||||
if (!self) return;
|
||||
self->m_installedPackagesCache = packages;
|
||||
|
||||
// Snapshot the previous installed set BEFORE the wholesale assignments
|
||||
// below overwrite it.
|
||||
const QSet<QString> previouslyInstalled = self->m_installedNameSet;
|
||||
|
||||
QMap<QString, QString> typeMap;
|
||||
QSet<QString> nameSet;
|
||||
QHash<QString, QString> versionByName;
|
||||
@@ -1026,35 +1023,14 @@ void PackageCoordinator::refreshDependencyInfo()
|
||||
if (!version.isEmpty()) versionByName.insert(lookupName, version);
|
||||
if (!rootHash.isEmpty()) hashByName.insert(lookupName, rootHash);
|
||||
}
|
||||
self->m_installTypeByModule = typeMap;
|
||||
self->m_installTypeByModule = std::move(typeMap);
|
||||
self->m_displayNameByModule = std::move(displayNameMap);
|
||||
self->m_installedNameSet = std::move(nameSet);
|
||||
self->m_installedVersionByName = std::move(versionByName);
|
||||
for (auto it = hashByName.cbegin(); it != hashByName.cend(); ++it) {
|
||||
self->m_installedHashByName.insert(it.key(), it.value());
|
||||
}
|
||||
for (const QString& name : previouslyInstalled) {
|
||||
if (!self->m_installedNameSet.contains(name)) {
|
||||
self->m_installedHashByName.remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
// Replay markInstalled across the full package set — fetchUiPluginMetadata
|
||||
// only saw UI plugins. Idempotent on (version, hash).
|
||||
self->m_installedHashByName = std::move(hashByName);
|
||||
if (self->m_appsModel) {
|
||||
self->m_appsModel->beginBulkInstalledUpdate();
|
||||
for (const QString& name : previouslyInstalled) {
|
||||
if (!self->m_installedNameSet.contains(name)) {
|
||||
self->m_appsModel->markInstalled(name, QString(), QString());
|
||||
}
|
||||
}
|
||||
for (auto it = self->m_installedVersionByName.cbegin();
|
||||
it != self->m_installedVersionByName.cend(); ++it) {
|
||||
self->m_appsModel->markInstalled(
|
||||
it.key(), it.value(),
|
||||
self->m_installedHashByName.value(it.key()));
|
||||
}
|
||||
self->m_appsModel->endBulkInstalledUpdate();
|
||||
self->m_appsModel->replaceInstalledSet(
|
||||
self->m_installedVersionByName, self->m_installedHashByName);
|
||||
}
|
||||
|
||||
// Second pass — per-module missing/dependents queries. Dispatched
|
||||
@@ -1213,6 +1189,34 @@ QVariantMap nameAndRepo(const QString& name, const QString& repo)
|
||||
};
|
||||
}
|
||||
|
||||
QVariantList PackageCoordinator::collectCatalogRequired(const QString& name,
|
||||
const QString& repositoryUrl) const
|
||||
{
|
||||
QVariantList out;
|
||||
QSet<QString> seen;
|
||||
out.append(nameAndRepo(name, repositoryUrl));
|
||||
seen.insert(name);
|
||||
|
||||
if (repositoryUrl.isEmpty() || !m_appsModel) return out;
|
||||
|
||||
QStringList queue;
|
||||
queue << name;
|
||||
for (int head = 0; head < queue.size(); ++head) {
|
||||
const QVariantMap row = m_appsModel->rowDataByName(queue[head], repositoryUrl);
|
||||
if (row.isEmpty()) continue;
|
||||
const QVariantList deps = row.value("dependencies").toList();
|
||||
for (const QVariant& d : deps) {
|
||||
const QString depName = d.toMap().value("name").toString();
|
||||
if (depName.isEmpty() || seen.contains(depName)) continue;
|
||||
if (m_appsModel->rowDataByName(depName, repositoryUrl).isEmpty()) continue;
|
||||
seen.insert(depName);
|
||||
out.append(nameAndRepo(depName, repositoryUrl));
|
||||
queue << depName;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
QString PackageCoordinator::depAction(const QString& installedVersion,
|
||||
const QString& resolvedVersion,
|
||||
const QString& installedHash,
|
||||
@@ -1280,12 +1284,11 @@ QVariantList PackageCoordinator::computeDepChanges(
|
||||
return out;
|
||||
}
|
||||
|
||||
void PackageCoordinator::setSessionStage(const QString& name, InstallStage::Value stage)
|
||||
void PackageCoordinator::setOpStage(const QString& name, InstallStage::Value stage)
|
||||
{
|
||||
auto it = m_installSessions.find(name);
|
||||
if (it == m_installSessions.end()) return;
|
||||
if (it->stage == stage) return;
|
||||
it->stage = stage;
|
||||
if (!m_installRegistry->has(name)) return;
|
||||
if (m_installRegistry->stage(name) == static_cast<int>(stage)) return;
|
||||
m_installRegistry->setStage(name, stage);
|
||||
emit catalogInstallStageChanged(name, stage);
|
||||
}
|
||||
|
||||
@@ -1317,6 +1320,13 @@ void PackageCoordinator::openApp(const QString& name,
|
||||
runResolverAndOpenDialog(name, repositoryUrl, versionPins);
|
||||
}
|
||||
|
||||
void PackageCoordinator::notifyAddApplicationDialogClosed()
|
||||
{
|
||||
if (m_activeAddDialogName.isEmpty()) return;
|
||||
++m_dialogResolveEpoch[m_activeAddDialogName];
|
||||
m_activeAddDialogName.clear();
|
||||
}
|
||||
|
||||
void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
|
||||
const QString& repositoryUrl,
|
||||
const QVariantMap& versionPins)
|
||||
@@ -1327,6 +1337,7 @@ void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
|
||||
const QString targetVersion = versionPins.value(name).toString();
|
||||
|
||||
const int epoch = ++m_dialogResolveEpoch[name];
|
||||
m_activeAddDialogName = name;
|
||||
|
||||
const QString depsJson = buildResolverDepsJson(name, repositoryUrl, versionPins);
|
||||
|
||||
@@ -1334,7 +1345,12 @@ void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
|
||||
<< "repo=" << repositoryUrl << "targetVersion=" << targetVersion
|
||||
<< "pins=" << versionPins.size() << "epoch=" << epoch;
|
||||
|
||||
emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, /*changes=*/{});
|
||||
QVariantList initialChanges;
|
||||
if (m_installRegistry->isInFlight(name))
|
||||
initialChanges = m_lastResolvedChangesByName.value(name);
|
||||
// Sync stack frame only — QML may open the modal from this signal.
|
||||
emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, initialChanges,
|
||||
/*requestOpen=*/true);
|
||||
|
||||
LogosModules logos(m_logosAPI);
|
||||
QPointer<PackageCoordinator> self(this);
|
||||
@@ -1349,7 +1365,13 @@ void PackageCoordinator::runResolverAndOpenDialog(const QString& name,
|
||||
}
|
||||
const QVariantList changes =
|
||||
self->computeDepChanges(resolved, self->m_installedVersionByName);
|
||||
self->emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, changes);
|
||||
if (!resolved.isEmpty())
|
||||
self->m_lastResolvedRawByName.insert(name, resolved);
|
||||
if (!changes.isEmpty())
|
||||
self->m_lastResolvedChangesByName.insert(name, changes);
|
||||
// Async refresh only — never reopens the modal.
|
||||
self->emitDialogMetadata(name, repositoryUrl, targetVersion, catalogRow, changes,
|
||||
/*requestOpen=*/false);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1357,8 +1379,12 @@ void PackageCoordinator::emitDialogMetadata(const QString& name,
|
||||
const QString& repositoryUrl,
|
||||
const QString& targetVersion,
|
||||
const QVariantMap& catalogRow,
|
||||
const QVariantList& changes)
|
||||
const QVariantList& changes,
|
||||
bool requestOpen)
|
||||
{
|
||||
if (name != m_activeAddDialogName)
|
||||
return;
|
||||
|
||||
QVariantMap metadata;
|
||||
metadata["name"] = name;
|
||||
metadata["repositoryUrl"] = repositoryUrl;
|
||||
@@ -1388,10 +1414,7 @@ void PackageCoordinator::emitDialogMetadata(const QString& name,
|
||||
? QString()
|
||||
: versionsList.first().toMap().value("manifest").toMap().value("version").toString();
|
||||
|
||||
metadata["installStage"] = static_cast<int>(
|
||||
m_installSessions.contains(name)
|
||||
? m_installSessions.value(name).stage
|
||||
: InstallStage::None);
|
||||
metadata["installStage"] = m_installRegistry->stage(name);
|
||||
|
||||
// {name, repo} entries so the filter pins each row to the resolver's
|
||||
// chosen repo and multi-repo names don't duplicate. Always at least the
|
||||
@@ -1424,10 +1447,43 @@ void PackageCoordinator::emitDialogMetadata(const QString& name,
|
||||
m_appsModel->setResolverOverlay(overlay);
|
||||
}
|
||||
|
||||
// Union in the catalog-derived dependency set.
|
||||
for (const QVariant& v : collectCatalogRequired(name, repositoryUrl)) {
|
||||
const QString depName = v.toMap().value("name").toString();
|
||||
if (depName.isEmpty() || seen.contains(depName)) continue;
|
||||
seen.insert(depName);
|
||||
requiredEntries.append(v);
|
||||
}
|
||||
|
||||
if (m_requiredPackagesModel)
|
||||
m_requiredPackagesModel->setRequiredPackages(requiredEntries);
|
||||
|
||||
emit addApplicationRequested(metadata);
|
||||
if (requestOpen)
|
||||
emit requestOpenAddApplicationDialog(metadata);
|
||||
else
|
||||
emit addApplicationDataUpdated(metadata);
|
||||
}
|
||||
|
||||
void PackageCoordinator::refreshOverlayAfterInstall(const QString& topLevelName)
|
||||
{
|
||||
if (!m_appsModel || topLevelName.isEmpty()) return;
|
||||
|
||||
const QVariantList raw = m_lastResolvedRawByName.value(topLevelName);
|
||||
if (raw.isEmpty()) return;
|
||||
|
||||
const QVariantList changes =
|
||||
computeDepChanges(raw, m_installedVersionByName);
|
||||
if (!changes.isEmpty())
|
||||
m_lastResolvedChangesByName.insert(topLevelName, changes);
|
||||
|
||||
// Only push UI updates while this app's dialog is still the active session.
|
||||
if (topLevelName != m_activeAddDialogName) return;
|
||||
|
||||
const QString repositoryUrl = m_repoByName.value(topLevelName);
|
||||
const QVariantMap catalogRow =
|
||||
m_appsModel->rowDataByName(topLevelName, repositoryUrl);
|
||||
emitDialogMetadata(topLevelName, repositoryUrl, QString(), catalogRow, changes,
|
||||
/*requestOpen=*/false);
|
||||
}
|
||||
|
||||
void PackageCoordinator::confirmCatalogInstall(const QString& name,
|
||||
@@ -1436,26 +1492,28 @@ void PackageCoordinator::confirmCatalogInstall(const QString& name,
|
||||
{
|
||||
if (!m_logosAPI || name.isEmpty()) return;
|
||||
|
||||
if (m_installSessions.contains(name)) {
|
||||
if (m_installRegistry->has(name)) {
|
||||
qDebug() << "confirmCatalogInstall: session for" << name
|
||||
<< "already in progress, ignoring";
|
||||
return;
|
||||
}
|
||||
|
||||
InstallSession s;
|
||||
s.name = name;
|
||||
s.stage = InstallStage::Downloading;
|
||||
m_installSessions.insert(name, s);
|
||||
|
||||
if (m_appsModel) m_appsModel->setInstallStage(name, InstallStage::Downloading);
|
||||
m_installRegistry->begin(name, /*targetVersion=*/{}, /*targetHash=*/{},
|
||||
/*startedByTopLevel=*/name);
|
||||
emit catalogInstallStageChanged(name, InstallStage::Downloading);
|
||||
|
||||
const QString depsJson = buildResolverDepsJson(name, repositoryUrl, versionPins);
|
||||
|
||||
LogosModules logos(m_logosAPI);
|
||||
QPointer<PackageCoordinator> self(this);
|
||||
// Default IPC deadline (20s) is too tight when the catalog blob is many
|
||||
// MB or the user is on a slow connection
|
||||
constexpr int kDownloadIpcDeadlineMs = 5 * 60 * 1000;
|
||||
logos.package_downloader.downloadResolvedDependenciesAsync(depsJson,
|
||||
[self, name](QVariantList results) {
|
||||
if (!self) return;
|
||||
if (!results.isEmpty())
|
||||
self->m_lastResolvedRawByName.insert(name, results);
|
||||
|
||||
QVariantList toInstall;
|
||||
for (const QVariant& v : results) {
|
||||
@@ -1478,9 +1536,9 @@ void PackageCoordinator::confirmCatalogInstall(const QString& name,
|
||||
|| installedHash.isEmpty()
|
||||
|| resolvedHash == installedHash;
|
||||
if (versionMatches && hashMatches) {
|
||||
if (self->m_appsModel)
|
||||
self->m_appsModel->setInstallStage(rowName,
|
||||
InstallStage::Installed);
|
||||
self->m_installRegistry->beginOrTrack(rowName, resolvedVersion,
|
||||
resolvedHash, name);
|
||||
self->m_installRegistry->setStage(rowName, InstallStage::Installed);
|
||||
continue;
|
||||
}
|
||||
toInstall.append(v);
|
||||
@@ -1489,25 +1547,31 @@ void PackageCoordinator::confirmCatalogInstall(const QString& name,
|
||||
if (toInstall.isEmpty()) {
|
||||
// Nothing left to do after the skip-already-installed
|
||||
// filter; treat as a successful no-op rather than Failed.
|
||||
self->setSessionStage(name, InstallStage::Installed);
|
||||
self->setOpStage(name, InstallStage::Installed);
|
||||
emit self->catalogInstallFinished(name);
|
||||
self->refreshOverlayAfterInstall(name);
|
||||
QTimer::singleShot(1500, self.data(), [self, name]() {
|
||||
if (!self) return;
|
||||
self->m_installSessions.remove(name);
|
||||
self->m_installRegistry->clearByTopLevel(name);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const QVariant& v : toInstall) {
|
||||
const QString rowName = v.toMap().value("name").toString();
|
||||
if (!rowName.isEmpty() && self->m_appsModel)
|
||||
self->m_appsModel->setInstallStage(rowName,
|
||||
InstallStage::Queued);
|
||||
const QVariantMap m = v.toMap();
|
||||
const QString rowName = m.value("name").toString();
|
||||
if (rowName.isEmpty()) continue;
|
||||
self->m_installRegistry->beginOrTrack(rowName,
|
||||
m.value("version").toString(),
|
||||
m.value("rootHash").toString(),
|
||||
name);
|
||||
self->m_installRegistry->setStage(rowName, InstallStage::Queued);
|
||||
}
|
||||
|
||||
self->setSessionStage(name, InstallStage::Installing);
|
||||
self->setOpStage(name, InstallStage::Installing);
|
||||
self->installResultsSequential(toInstall, name, 0);
|
||||
});
|
||||
},
|
||||
Timeout(kDownloadIpcDeadlineMs));
|
||||
}
|
||||
|
||||
void PackageCoordinator::installOnePackage(const QVariantMap& dl,
|
||||
@@ -1547,24 +1611,34 @@ void PackageCoordinator::installResultsSequential(const QVariantList& results,
|
||||
<< "of" << results.size()
|
||||
<< "rowName=" << rowName
|
||||
<< "topLevel=" << topLevelName;
|
||||
if (!rowName.isEmpty() && m_appsModel)
|
||||
m_appsModel->setInstallStage(rowName, InstallStage::Installing);
|
||||
if (!rowName.isEmpty()) {
|
||||
m_installRegistry->beginOrTrack(rowName, dl.value("version").toString(),
|
||||
dl.value("rootHash").toString(), topLevelName);
|
||||
m_installRegistry->setStage(rowName, InstallStage::Installing);
|
||||
}
|
||||
|
||||
QPointer<PackageCoordinator> self(this);
|
||||
installOnePackage(dl,
|
||||
[self, results, topLevelName, rowName, index, failures]
|
||||
[self, results, topLevelName, rowName, index, failures, dl]
|
||||
(bool success, const QString& err) mutable {
|
||||
qDebug() << "installOnePackage callback rowName=" << rowName
|
||||
<< "success=" << success << "err=" << err;
|
||||
if (!self) return;
|
||||
|
||||
if (!rowName.isEmpty()) {
|
||||
const InstallStage::Value stage = success
|
||||
? InstallStage::Installed
|
||||
: InstallStage::Failed;
|
||||
if (self->m_appsModel)
|
||||
self->m_appsModel->setInstallStage(rowName, stage,
|
||||
success ? QString() : err);
|
||||
if (success) {
|
||||
const QString ver = dl.value("version").toString();
|
||||
const QString hash = dl.value("rootHash").toString();
|
||||
if (!ver.isEmpty())
|
||||
self->m_installedVersionByName.insert(rowName, ver);
|
||||
if (!hash.isEmpty())
|
||||
self->m_installedHashByName.insert(rowName, hash);
|
||||
if (self->m_appsModel)
|
||||
self->m_appsModel->markInstalled(rowName, ver, hash);
|
||||
self->m_installRegistry->finish(rowName);
|
||||
} else {
|
||||
self->m_installRegistry->fail(rowName, err);
|
||||
}
|
||||
}
|
||||
if (!success) {
|
||||
failures.append(rowName.isEmpty()
|
||||
@@ -1584,21 +1658,22 @@ void PackageCoordinator::installResultsSequential(const QVariantList& results,
|
||||
if (!failures.isEmpty()) {
|
||||
qDebug() << " install loop complete with failures for"
|
||||
<< topLevelName << ":" << failures.size();
|
||||
self->setSessionStage(topLevelName, InstallStage::Failed);
|
||||
self->setOpStage(topLevelName, InstallStage::Failed);
|
||||
emit self->catalogInstallFailed(
|
||||
topLevelName, failures.join(QStringLiteral("; ")));
|
||||
QTimer::singleShot(2500, self.data(), [self, topLevelName]() {
|
||||
if (!self) return;
|
||||
self->m_installSessions.remove(topLevelName);
|
||||
self->m_installRegistry->clearByTopLevel(topLevelName);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
self->setSessionStage(topLevelName, InstallStage::Installed);
|
||||
self->setOpStage(topLevelName, InstallStage::Installed);
|
||||
emit self->catalogInstallFinished(topLevelName);
|
||||
self->refreshOverlayAfterInstall(topLevelName);
|
||||
QTimer::singleShot(1500, self.data(), [self, topLevelName]() {
|
||||
if (!self) return;
|
||||
self->m_installSessions.remove(topLevelName);
|
||||
self->m_installRegistry->clearByTopLevel(topLevelName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
class AppsFilterProxy;
|
||||
class AppsModel;
|
||||
class InstallRegistry;
|
||||
class CoreModuleManager;
|
||||
class UIPluginManager;
|
||||
|
||||
@@ -133,6 +134,13 @@ public slots:
|
||||
Q_INVOKABLE void removeRepository(const QString& url);
|
||||
Q_INVOKABLE void setRepositoryEnabled(const QString& url, bool enabled);
|
||||
|
||||
// Called by QML when the Add Application dialog closes so stale async
|
||||
// resolver callbacks for previously-opened apps don't mutate the shared
|
||||
// required-packages model or reopen the dialog.
|
||||
Q_INVOKABLE void notifyAddApplicationDialogClosed();
|
||||
|
||||
InstallRegistry* installRegistry() const { return m_installRegistry; }
|
||||
|
||||
signals:
|
||||
// Tells MainUIBackend to refresh the uiModules / launcherApps / coreModules
|
||||
// properties — their values compose installType/missing-deps from here with
|
||||
@@ -148,7 +156,12 @@ signals:
|
||||
// changed.
|
||||
void uiPluginsFetched(const QVariantList& uiPlugins);
|
||||
|
||||
void addApplicationRequested(const QVariantMap& metadata);
|
||||
// Sync entry from openApp() only — QML opens the modal if not already
|
||||
// visible (or refreshes in place when re-resolving the same app).
|
||||
void requestOpenAddApplicationDialog(const QVariantMap& metadata);
|
||||
// Passive refresh — never opens the modal; stale callbacks are dropped in
|
||||
// emitDialogMetadata before this is emitted.
|
||||
void addApplicationDataUpdated(const QVariantMap& metadata);
|
||||
void catalogInstallStageChanged(const QString& name, InstallStage::Value stage);
|
||||
void catalogInstallFinished(const QString& name);
|
||||
void catalogInstallFailed(const QString& name, const QString& error);
|
||||
@@ -267,6 +280,10 @@ private:
|
||||
QString buildResolverDepsJson(const QString& name,
|
||||
const QString& repositoryUrl,
|
||||
const QVariantMap& versionPins) const;
|
||||
// Transitive required-package set ({name, repositoryUrl}) computed purely
|
||||
// from the local catalog dependency graph — no async resolver.
|
||||
QVariantList collectCatalogRequired(const QString& name,
|
||||
const QString& repositoryUrl) const;
|
||||
QString buildInstalledPackagesJson() const;
|
||||
QVariantList computeDepChanges(const QVariantList& resolved,
|
||||
const QHash<QString, QString>& installedByName) const;
|
||||
@@ -286,7 +303,11 @@ private:
|
||||
const QString& repositoryUrl,
|
||||
const QString& targetVersion,
|
||||
const QVariantMap& catalogRow,
|
||||
const QVariantList& changes);
|
||||
const QVariantList& changes,
|
||||
bool requestOpen);
|
||||
// Recompute resolver overlay from cached raw resolve + current disk state.
|
||||
// Keeps dep badges correct after the install registry is cleared.
|
||||
void refreshOverlayAfterInstall(const QString& topLevelName);
|
||||
void installResultsSequential(const QVariantList& results,
|
||||
const QString& topLevelName,
|
||||
int index,
|
||||
@@ -294,8 +315,9 @@ private:
|
||||
void installOnePackage(const QVariantMap& downloadResult,
|
||||
std::function<void(bool, const QString&)> onDone);
|
||||
|
||||
// Move a session's stage and emit catalogInstallStageChanged.
|
||||
void setSessionStage(const QString& name, InstallStage::Value stage);
|
||||
// Drive the in-flight registry. setOpStage updates the InstallRegistry entry
|
||||
// and emits catalogInstallStageChanged.
|
||||
void setOpStage(const QString& name, InstallStage::Value stage);
|
||||
|
||||
// Wiring (not owned — see ctor comment).
|
||||
LogosAPI* m_logosAPI;
|
||||
@@ -326,11 +348,13 @@ private:
|
||||
// to feed AppsModel's
|
||||
// DifferentHash detection.
|
||||
QHash<QString, int> m_dialogResolveEpoch;
|
||||
struct InstallSession {
|
||||
QString name;
|
||||
InstallStage::Value stage = InstallStage::None;
|
||||
};
|
||||
QHash<QString, InstallSession> m_installSessions;
|
||||
QString m_activeAddDialogName;
|
||||
|
||||
// Last resolver output per top-level: raw IPC rows and derived changes.
|
||||
QHash<QString, QVariantList> m_lastResolvedRawByName;
|
||||
QHash<QString, QVariantList> m_lastResolvedChangesByName;
|
||||
|
||||
InstallRegistry* m_installRegistry = nullptr;
|
||||
|
||||
QVariantList m_repositories;
|
||||
int m_repositoriesLoadingCount = 0;
|
||||
|
||||
Reference in New Issue
Block a user