feat: integrate ui with data api
Much of this was ai generated.
This commit is contained in:
+3
-2
@@ -29,8 +29,9 @@ qt_add_executable(llink
|
||||
src/authdialog.ui
|
||||
src/authdialog.cpp
|
||||
src/settingsdialog.h src/settingsdialog.cpp src/settingsdialog.ui
|
||||
src/data.h
|
||||
src/data.cpp
|
||||
src/models.h
|
||||
src/store.h
|
||||
src/store.cpp
|
||||
src/networkstreammodel.h
|
||||
src/networkstreammodel.cpp
|
||||
src/particlelistmodel.h
|
||||
|
||||
+49
-31
@@ -19,7 +19,7 @@
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), m_trayIcon(nullptr), m_authManager(new AuthManager(this)), m_settingsDialog(nullptr),
|
||||
m_authDialog(nullptr), m_data(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr),
|
||||
m_authDialog(nullptr), m_store(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr),
|
||||
m_selectedParticleRow(-1), m_isDragging(false)
|
||||
{
|
||||
m_authDialog = new AuthDialog(this);
|
||||
@@ -95,6 +95,12 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
connect(m_authManager, &AuthManager::signedIn, m_authDialog, &AuthDialog::reset);
|
||||
connect(m_authManager, &AuthManager::signedOut, m_authDialog, &AuthDialog::reset);
|
||||
connect(m_authManager, &AuthManager::signedOut, m_authDialog, &AuthDialog::show);
|
||||
connect(m_authManager, &AuthManager::signedIn, this, [this]() {
|
||||
m_store->loadStartupData();
|
||||
});
|
||||
connect(m_store, &Store::startupDataLoaded, this, [this]() {
|
||||
ui->topLevelParticlesView->expandAll();
|
||||
});
|
||||
|
||||
m_authManager->tryRestoreSession();
|
||||
|
||||
@@ -143,11 +149,11 @@ void MainWindow::on_settingsButton_clicked()
|
||||
|
||||
void MainWindow::setupModels()
|
||||
{
|
||||
// Create data with fake data
|
||||
m_data = new Data(this);
|
||||
// Create store (data loaded after sign-in via loadStartupData)
|
||||
m_store = new Store(this);
|
||||
|
||||
// Create and set tree model
|
||||
m_networkStreamModel = new NetworkStreamModel(m_data, this);
|
||||
m_networkStreamModel = new NetworkStreamModel(m_store, this);
|
||||
ui->topLevelParticlesView->setModel(m_networkStreamModel);
|
||||
ui->topLevelParticlesView->setIndentation(5);
|
||||
ui->topLevelParticlesView->setHeaderHidden(true);
|
||||
@@ -160,7 +166,7 @@ void MainWindow::setupModels()
|
||||
ui->topLevelParticlesView->setColumnWidth(1, 40);
|
||||
|
||||
// Create and set list model
|
||||
m_particleListModel = new ParticleListModel(m_data, this);
|
||||
m_particleListModel = new ParticleListModel(m_store, this);
|
||||
ui->particlesView->setModel(m_particleListModel);
|
||||
|
||||
// Configure particle list view to use ellipses for long text
|
||||
@@ -175,8 +181,6 @@ void MainWindow::setupModels()
|
||||
connect(ui->particlesView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||
&MainWindow::onParticleSelectionChanged);
|
||||
|
||||
// setup human presence list
|
||||
ui->presenceList->addItems(QStringList{"agnes", "katie", "stephen", "viet", "anthony"});
|
||||
}
|
||||
|
||||
void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
|
||||
@@ -186,6 +190,7 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI
|
||||
if (selected.indexes().isEmpty())
|
||||
{
|
||||
m_particleListModel->setStreamId(QString());
|
||||
ui->membersList->clear();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,6 +207,17 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI
|
||||
QString networkId = m_networkStreamModel->networkIdFromIndex(parentIndex);
|
||||
m_selectedNetworkName = networkId;
|
||||
|
||||
// Show stream members
|
||||
ui->membersList->clear();
|
||||
const Stream *stream = m_store->streamById(streamId);
|
||||
if (stream)
|
||||
{
|
||||
for (const QString &email : stream->memberEmails)
|
||||
{
|
||||
ui->membersList->addItem(email.split("@")[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select first unseen particle (or first particle if all seen)
|
||||
int unseenRow = m_particleListModel->firstUnseenRow();
|
||||
if (unseenRow >= 0)
|
||||
@@ -220,6 +236,7 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI
|
||||
m_particleListModel->setStreamId(QString());
|
||||
m_selectedStreamId.clear();
|
||||
m_selectedParticleRow = -1;
|
||||
ui->membersList->clear();
|
||||
|
||||
QString networkId = m_networkStreamModel->networkIdFromIndex(index);
|
||||
m_selectedNetworkName = networkId;
|
||||
@@ -247,9 +264,17 @@ void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, cons
|
||||
const Particle *particle = m_particleListModel->particleAtRow(index.row());
|
||||
if (particle)
|
||||
{
|
||||
// ui->label->setText(particle->summary);
|
||||
ui->label_2->setText(particle->createdBy);
|
||||
ui->label_2->setText(particle->createdByEmail);
|
||||
m_selectedParticleId = particle->id;
|
||||
|
||||
if (particle->type == "text")
|
||||
{
|
||||
ui->label->setText(particle->data.value("content").toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->label->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusBar();
|
||||
@@ -262,41 +287,30 @@ void MainWindow::updateStatusBar()
|
||||
// Add network if selected
|
||||
if (!m_selectedNetworkName.isEmpty())
|
||||
{
|
||||
for (const auto &network : m_data->networks())
|
||||
const Network *network = m_store->networkById(m_selectedNetworkName);
|
||||
if (network)
|
||||
{
|
||||
if (network.id == m_selectedNetworkName)
|
||||
{
|
||||
pathComponents << network.name;
|
||||
break;
|
||||
}
|
||||
pathComponents << network->name;
|
||||
}
|
||||
}
|
||||
|
||||
// Add stream if selected
|
||||
if (!m_selectedStreamId.isEmpty() && !m_selectedNetworkName.isEmpty())
|
||||
if (!m_selectedStreamId.isEmpty())
|
||||
{
|
||||
const QList<Stream> &streams = m_data->streamsForNetwork(m_selectedNetworkName);
|
||||
for (const auto &stream : streams)
|
||||
const Stream *stream = m_store->streamById(m_selectedStreamId);
|
||||
if (stream)
|
||||
{
|
||||
if (stream.id == m_selectedStreamId)
|
||||
{
|
||||
pathComponents << stream.name;
|
||||
break;
|
||||
}
|
||||
pathComponents << stream->name;
|
||||
}
|
||||
}
|
||||
|
||||
// Add particle if selected
|
||||
if (!m_selectedParticleId.isEmpty() && !m_selectedStreamId.isEmpty())
|
||||
if (!m_selectedParticleId.isEmpty())
|
||||
{
|
||||
const QList<Particle> &particles = m_data->particlesForStream(m_selectedStreamId);
|
||||
for (const auto &particle : particles)
|
||||
const Particle *particle = m_store->particleById(m_selectedParticleId);
|
||||
if (particle)
|
||||
{
|
||||
if (particle.id == m_selectedParticleId)
|
||||
{
|
||||
pathComponents << particle.id;
|
||||
break;
|
||||
}
|
||||
pathComponents << particle->id;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,9 +365,13 @@ void MainWindow::updatePingIndicator()
|
||||
int variation = (QRandomGenerator::global()->bounded(21)) - 10; // -10 to +10
|
||||
int currentPing = basePing + variation;
|
||||
if (currentPing < 15)
|
||||
{
|
||||
currentPing = 15;
|
||||
}
|
||||
if (currentPing > 35)
|
||||
{
|
||||
currentPing = 35;
|
||||
}
|
||||
|
||||
m_pingLabel->setText("connected " + QString::number(currentPing) + "ms");
|
||||
}
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@
|
||||
#ifndef LLINK_MAINWINDOW_H
|
||||
#define LLINK_MAINWINDOW_H
|
||||
|
||||
#include "data.h"
|
||||
#include "store.h"
|
||||
#include "networkstreammodel.h"
|
||||
#include "particlelistmodel.h"
|
||||
#include <QMainWindow>
|
||||
@@ -60,7 +60,7 @@ private:
|
||||
SettingsDialog *m_settingsDialog;
|
||||
|
||||
// Data and models
|
||||
Data *m_data;
|
||||
Store *m_store;
|
||||
NetworkStreamModel *m_networkStreamModel;
|
||||
ParticleListModel *m_particleListModel;
|
||||
|
||||
|
||||
-160
@@ -1,160 +0,0 @@
|
||||
#include "data.h"
|
||||
|
||||
Data::Data(QObject *parent) : QObject(parent)
|
||||
{
|
||||
initializeFakeData();
|
||||
}
|
||||
|
||||
void Data::initializeFakeData()
|
||||
{
|
||||
// Network 1: Private
|
||||
Network net1;
|
||||
net1.id = "net-private";
|
||||
net1.name = "Private";
|
||||
net1.openStreamCount = 2;
|
||||
net1.openStreamCapacity = 5;
|
||||
net1.countHumans = 1;
|
||||
m_networks.append(net1);
|
||||
|
||||
// Stream: Personal Notes
|
||||
Stream stream1;
|
||||
stream1.id = "stream-notes";
|
||||
stream1.name = "Personal Notes";
|
||||
stream1.status = Stream::OPEN;
|
||||
stream1.unseenCount = 3;
|
||||
m_streams[net1.id].append(stream1);
|
||||
|
||||
// Particles for Personal Notes
|
||||
m_particles[stream1.id].append({"p1", "text", "viet@flowy.com", "Meeting notes from standup", true});
|
||||
m_particles[stream1.id].append({"p2", "link", "viet@flowy.com", "API documentation for new endpoint", true});
|
||||
m_particles[stream1.id].append({"p3", "image", "stephen@flowy.com", "Screenshot of bug in login flow", false});
|
||||
m_particles[stream1.id].append({"p4", "text", "stephen@flowy.com", "Quick thought about architecture refactor", false});
|
||||
m_particles[stream1.id].append({"p5", "link", "anthony@flowy.com", "Design inspiration board from Dribbble", false});
|
||||
|
||||
// Stream: Quick Captures
|
||||
Stream stream2;
|
||||
stream2.id = "stream-captures";
|
||||
stream2.name = "Quick Captures";
|
||||
stream2.status = Stream::OPEN;
|
||||
stream2.unseenCount = 0;
|
||||
m_streams[net1.id].append(stream2);
|
||||
|
||||
// Particles for Quick Captures
|
||||
m_particles[stream2.id].append({"p6", "text", "alice@flowy.com", "Remember to follow up on client feedback", true});
|
||||
m_particles[stream2.id].append({"p7", "image", "bob@flowy.com", "Mockup for new feature dashboard", true});
|
||||
|
||||
// Network 2: Flowy Labs, Inc
|
||||
Network net2;
|
||||
net2.id = "net-flowy";
|
||||
net2.name = "Flowy Labs, Inc";
|
||||
net2.openStreamCount = 3;
|
||||
net2.openStreamCapacity = 10;
|
||||
net2.countHumans = 12;
|
||||
m_networks.append(net2);
|
||||
|
||||
// Stream: Engineering
|
||||
Stream stream3;
|
||||
stream3.id = "stream-eng";
|
||||
stream3.name = "wild-shape";
|
||||
stream3.status = Stream::OPEN;
|
||||
stream3.unseenCount = 10;
|
||||
m_streams[net2.id].append(stream3);
|
||||
|
||||
// Particles for Engineering
|
||||
m_particles[stream3.id].append({"p8", "text", "charlie@flowy.com", "hey team, anyone seeing issues with the auth service?", true});
|
||||
m_particles[stream3.id].append({"p9", "text", "dave@flowy.com", "yeah, getting 401s on the staging server", true});
|
||||
m_particles[stream3.id].append({"p10", "text", "eve@flowy.com", "i think it's the JWT expiration. checking now", true});
|
||||
m_particles[stream3.id].append({"p11", "link", "eve@flowy.com", "https://github.com/flowy/api/pull/234", true});
|
||||
m_particles[stream3.id].append({"p12", "text", "eve@flowy.com", "^ PR ready for review. fixed the token refresh logic", false});
|
||||
m_particles[stream3.id].append({"p13", "text", "frank@flowy.com", "looking at it now", false});
|
||||
m_particles[stream3.id].append({"p14", "text", "grace@flowy.com", "nice catch! left a few comments about error handling", false});
|
||||
m_particles[stream3.id].append({"p15", "text", "eve@flowy.com", "updated, should be good to merge", false});
|
||||
m_particles[stream3.id].append({"p16", "text", "charlie@flowy.com", "merged! deploying to staging now", false});
|
||||
m_particles[stream3.id].append({"p17", "text", "henry@flowy.com", "btw, we should probably add monitoring for token expiration", false});
|
||||
m_particles[stream3.id].append({"p18", "text", "dave@flowy.com", "good idea, i can set that up after lunch", false});
|
||||
m_particles[stream3.id].append({"p19", "text", "ivy@flowy.com", "also, can someone review my database migration PR?", false});
|
||||
m_particles[stream3.id].append({"p20", "image", "ivy@flowy.com", "here's the schema changes", false});
|
||||
m_particles[stream3.id].append({"p21", "text", "jack@flowy.com", "looks good to me. just run it by charlie for final approval", false});
|
||||
|
||||
// Stream: Design
|
||||
Stream stream4;
|
||||
stream4.id = "stream-design";
|
||||
stream4.name = "arch-rival";
|
||||
stream4.status = Stream::OPEN;
|
||||
stream4.unseenCount = 4;
|
||||
m_streams[net2.id].append(stream4);
|
||||
|
||||
// Particles for Design
|
||||
m_particles[stream4.id].append({"p22", "text", "karen@flowy.com", "starting on the mobile dashboard redesign", true});
|
||||
m_particles[stream4.id].append({"p23", "image", "karen@flowy.com", "initial mockup - thoughts?", true});
|
||||
m_particles[stream4.id].append({"p24", "text", "leo@flowy.com", "love the direction! maybe try a darker header?", true});
|
||||
m_particles[stream4.id].append({"p25", "text", "maya@flowy.com", "agreed, and can we use the new color palette from the design system?", true});
|
||||
m_particles[stream4.id].append({"p26", "link", "maya@flowy.com", "https://figma.com/design-system-v2", true});
|
||||
m_particles[stream4.id].append({"p27", "text", "karen@flowy.com", "oh perfect! i'll update it", true});
|
||||
m_particles[stream4.id].append({"p28", "text", "nathan@flowy.com", "hey, should the settings page follow the same style?", false});
|
||||
m_particles[stream4.id].append({"p29", "text", "leo@flowy.com", "yes definitely, keeping it consistent across all pages", false});
|
||||
m_particles[stream4.id].append({"p30", "image", "nathan@flowy.com", "cool, here's my wireframe draft", false});
|
||||
m_particles[stream4.id].append({"p31", "text", "maya@flowy.com", "looks great! can you add that to the figma file?", false});
|
||||
|
||||
// Stream: Archive
|
||||
Stream stream5;
|
||||
stream5.id = "stream-archive";
|
||||
stream5.name = "anthem-project";
|
||||
stream5.status = Stream::CLOSED;
|
||||
stream5.unseenCount = 0;
|
||||
m_streams[net2.id].append(stream5);
|
||||
|
||||
// Particles for Archive
|
||||
m_particles[stream5.id].append({"p32", "text", "oliver@flowy.com", "so we're officially deprecating the old API next week", true});
|
||||
m_particles[stream5.id].append({"p33", "text", "patricia@flowy.com", "yep, all clients have migrated to v2", true});
|
||||
m_particles[stream5.id].append({"p34", "text", "quinn@flowy.com", "great work everyone! this project was a long journey", true});
|
||||
m_particles[stream5.id].append({"p35", "link", "quinn@flowy.com", "https://docs.flowy.com/api/v1-migration-guide", true});
|
||||
m_particles[stream5.id].append({"p36", "text", "rachel@flowy.com", "should we keep the docs around for a bit?", true});
|
||||
m_particles[stream5.id].append({"p37", "text", "sam@flowy.com", "yeah i'd say 6 months, then archive everything", true});
|
||||
m_particles[stream5.id].append({"p38", "text", "tina@flowy.com", "agreed. i'll set a reminder", true});
|
||||
m_particles[stream5.id].append({"p39", "text", "oliver@flowy.com", "also documented all the lessons learned", true});
|
||||
m_particles[stream5.id].append({"p40", "text", "patricia@flowy.com", "nice! that'll be super helpful for future projects", true});
|
||||
m_particles[stream5.id].append({"p41", "text", "quinn@flowy.com", "alright, marking this stream as closed. good job team!", true});
|
||||
}
|
||||
|
||||
const QList<Network>& Data::networks() const
|
||||
{
|
||||
return m_networks;
|
||||
}
|
||||
|
||||
const QList<Stream>& Data::streamsForNetwork(const QString &networkId) const
|
||||
{
|
||||
static QList<Stream> emptyList;
|
||||
auto it = m_streams.find(networkId);
|
||||
if (it == m_streams.end())
|
||||
{
|
||||
return emptyList;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
const QList<Particle>& Data::particlesForStream(const QString &streamId) const
|
||||
{
|
||||
static QList<Particle> emptyList;
|
||||
auto it = m_particles.find(streamId);
|
||||
if (it == m_particles.end())
|
||||
{
|
||||
return emptyList;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
int Data::networkCount() const
|
||||
{
|
||||
return m_networks.size();
|
||||
}
|
||||
|
||||
int Data::streamCount(const QString &networkId) const
|
||||
{
|
||||
return m_streams.value(networkId).size();
|
||||
}
|
||||
|
||||
int Data::particleCount(const QString &streamId) const
|
||||
{
|
||||
return m_particles.value(streamId).size();
|
||||
}
|
||||
-68
@@ -1,68 +0,0 @@
|
||||
#ifndef DATA_H
|
||||
#define DATA_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
|
||||
struct Network
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
int openStreamCount;
|
||||
int openStreamCapacity;
|
||||
int countHumans;
|
||||
};
|
||||
|
||||
struct Stream
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
enum StreamStatus
|
||||
{
|
||||
OPEN,
|
||||
CLOSED,
|
||||
};
|
||||
StreamStatus status;
|
||||
int unseenCount;
|
||||
};
|
||||
|
||||
struct Particle
|
||||
{
|
||||
QString id;
|
||||
QString type;
|
||||
QString createdBy;
|
||||
QString summary;
|
||||
bool seen;
|
||||
};
|
||||
|
||||
class Data : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Data(QObject *parent = nullptr);
|
||||
|
||||
// Accessors for models
|
||||
const QList<Network>& networks() const;
|
||||
const QList<Stream>& streamsForNetwork(const QString &networkId) const;
|
||||
const QList<Particle>& particlesForStream(const QString &streamId) const;
|
||||
|
||||
int networkCount() const;
|
||||
int streamCount(const QString &networkId) const;
|
||||
int particleCount(const QString &streamId) const;
|
||||
|
||||
signals:
|
||||
void networksChanged();
|
||||
void streamsChanged(const QString &networkId);
|
||||
void particlesChanged(const QString &streamId);
|
||||
|
||||
private:
|
||||
void initializeFakeData();
|
||||
QList<Network> m_networks;
|
||||
// Network -> Streams
|
||||
QMap<QString, QList<Stream>> m_streams;
|
||||
// Stream -> Particles
|
||||
QMap<QString, QList<Particle>> m_particles;
|
||||
};
|
||||
|
||||
#endif // DATA_H
|
||||
+27
-14
@@ -205,6 +205,19 @@
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="sizePolicy">
|
||||
@@ -247,6 +260,19 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="font">
|
||||
@@ -273,19 +299,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -346,7 +359,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QListWidget" name="presenceList">
|
||||
<widget class="QListWidget" name="membersList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#ifndef MODELS_H
|
||||
#define MODELS_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QMap>
|
||||
#include <QVariant>
|
||||
#include <vector>
|
||||
|
||||
struct Human
|
||||
{
|
||||
// Empty if not a registered human
|
||||
QString id;
|
||||
QString email;
|
||||
QString emailPrefix;
|
||||
};
|
||||
|
||||
struct Stream
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
QString description;
|
||||
bool isOpen;
|
||||
std::vector<QString> memberEmails;
|
||||
int unseenCount;
|
||||
};
|
||||
|
||||
struct Particle
|
||||
{
|
||||
QString id;
|
||||
QMap<QString, QVariant> data;
|
||||
QString type;
|
||||
QString createdByEmail;
|
||||
bool seen;
|
||||
std::vector<QString> ackedByEmails;
|
||||
QDateTime updatedAt;
|
||||
QDateTime createdAt;
|
||||
};
|
||||
|
||||
struct Network
|
||||
{
|
||||
QString id;
|
||||
QString name;
|
||||
Human admin;
|
||||
std::vector<Human> members;
|
||||
int openStreamCount;
|
||||
int openStreamCapacity;
|
||||
std::vector<Stream> streams;
|
||||
};
|
||||
|
||||
#endif //MODELS_H
|
||||
@@ -67,6 +67,37 @@ QNetworkReply *NetworkManager::post(const QString &path, const QJsonDocument &do
|
||||
return m_qnam->post(request, doc.toJson());
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkManager::put(const QString &path, const QJsonDocument &doc)
|
||||
{
|
||||
QNetworkRequest request = QNetworkRequest(getFullUrl(path));
|
||||
setAuthHeader(request);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
return m_qnam->put(request, doc.toJson());
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkManager::patch(const QString &path, const QJsonDocument &doc)
|
||||
{
|
||||
QNetworkRequest request = QNetworkRequest(getFullUrl(path));
|
||||
setAuthHeader(request);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
return m_qnam->sendCustomRequest(request, "PATCH", doc.toJson());
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkManager::del(const QString &path)
|
||||
{
|
||||
QNetworkRequest request = QNetworkRequest(getFullUrl(path));
|
||||
setAuthHeader(request);
|
||||
return m_qnam->deleteResource(request);
|
||||
}
|
||||
|
||||
QNetworkReply *NetworkManager::del(const QString &path, const QJsonDocument &doc)
|
||||
{
|
||||
QNetworkRequest request = QNetworkRequest(getFullUrl(path));
|
||||
setAuthHeader(request);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
|
||||
return m_qnam->sendCustomRequest(request, "DELETE", doc.toJson());
|
||||
}
|
||||
|
||||
QString NetworkManager::sessionToken() const
|
||||
{
|
||||
return m_sessionToken;
|
||||
|
||||
@@ -27,8 +27,10 @@ public slots:
|
||||
// Network manager discard replies internally, in cases where a consumer callsite prefers to not manage QNetworkReply.
|
||||
QNetworkReply *get(const QString &path);
|
||||
QNetworkReply *post(const QString &path, const QJsonDocument &data);
|
||||
// QNetworkReply *put(QNetworkRequest &request);
|
||||
// QNetworkReply *del(QNetworkRequest &request);
|
||||
QNetworkReply *put(const QString &path, const QJsonDocument &data);
|
||||
QNetworkReply *patch(const QString &path, const QJsonDocument &data);
|
||||
QNetworkReply *del(const QString &path);
|
||||
QNetworkReply *del(const QString &path, const QJsonDocument &data);
|
||||
|
||||
/// @brief sets session token which will is injected into every request's headers
|
||||
void setSessionToken(const QString &token);
|
||||
|
||||
+22
-17
@@ -2,8 +2,16 @@
|
||||
#include <QBrush>
|
||||
#include <QColor>
|
||||
|
||||
NetworkStreamModel::NetworkStreamModel(Data *data, QObject *parent) : QAbstractItemModel(parent), m_data(data)
|
||||
NetworkStreamModel::NetworkStreamModel(Store *store, QObject *parent) : QAbstractItemModel(parent), m_store(store)
|
||||
{
|
||||
connect(m_store, &Store::networksChanged, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
});
|
||||
connect(m_store, &Store::streamsChanged, this, [this]() {
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
});
|
||||
}
|
||||
|
||||
QModelIndex NetworkStreamModel::index(int row, int column, const QModelIndex &parent) const
|
||||
@@ -59,7 +67,7 @@ int NetworkStreamModel::rowCount(const QModelIndex &parent) const
|
||||
if (!parent.isValid())
|
||||
{
|
||||
// Root level - return number of networks
|
||||
return m_data->networkCount();
|
||||
return m_store->networkCount();
|
||||
}
|
||||
|
||||
quintptr id = parent.internalId();
|
||||
@@ -69,10 +77,9 @@ int NetworkStreamModel::rowCount(const QModelIndex &parent) const
|
||||
if (networkIndex == 0xFFFFFFFF)
|
||||
{
|
||||
int actualNetworkIndex = static_cast<int>(id & 0xFFFFFFFF);
|
||||
if (actualNetworkIndex >= 0 && actualNetworkIndex < m_data->networks().size())
|
||||
if (actualNetworkIndex >= 0 && actualNetworkIndex < m_store->networks().size())
|
||||
{
|
||||
const QString &networkId = m_data->networks().at(actualNetworkIndex).id;
|
||||
return m_data->streamCount(networkId);
|
||||
return static_cast<int>(m_store->networks().at(actualNetworkIndex).streams.size());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,9 +109,9 @@ QVariant NetworkStreamModel::data(const QModelIndex &index, int role) const
|
||||
if (role == Qt::DisplayRole && index.column() == 0)
|
||||
{
|
||||
int actualNetworkIndex = static_cast<int>(id & 0xFFFFFFFF);
|
||||
if (actualNetworkIndex >= 0 && actualNetworkIndex < m_data->networks().size())
|
||||
if (actualNetworkIndex >= 0 && actualNetworkIndex < m_store->networks().size())
|
||||
{
|
||||
return m_data->networks().at(actualNetworkIndex).name.toUpper();
|
||||
return m_store->networks().at(actualNetworkIndex).name.toUpper();
|
||||
}
|
||||
}
|
||||
// Networks don't show unseen count in column 1
|
||||
@@ -114,12 +121,11 @@ QVariant NetworkStreamModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
// This is a stream item
|
||||
int streamIndex = static_cast<int>(id & 0xFFFFFFFF);
|
||||
if (networkIndex >= 0 && networkIndex < m_data->networks().size())
|
||||
if (networkIndex >= 0 && networkIndex < m_store->networks().size())
|
||||
{
|
||||
const QString &networkId = m_data->networks().at(networkIndex).id;
|
||||
const QList<Stream> &streams = m_data->streamsForNetwork(networkId);
|
||||
const auto &streams = m_store->networks().at(networkIndex).streams;
|
||||
|
||||
if (streamIndex >= 0 && streamIndex < streams.size())
|
||||
if (streamIndex >= 0 && streamIndex < static_cast<int>(streams.size()))
|
||||
{
|
||||
const Stream &stream = streams.at(streamIndex);
|
||||
|
||||
@@ -174,12 +180,11 @@ QString NetworkStreamModel::streamIdFromIndex(const QModelIndex &index) const
|
||||
if (networkIndex != 0xFFFFFFFF)
|
||||
{
|
||||
int streamIndex = static_cast<int>(id & 0xFFFFFFFF);
|
||||
if (networkIndex >= 0 && networkIndex < m_data->networks().size())
|
||||
if (networkIndex >= 0 && networkIndex < m_store->networks().size())
|
||||
{
|
||||
const QString &networkId = m_data->networks().at(networkIndex).id;
|
||||
const QList<Stream> &streams = m_data->streamsForNetwork(networkId);
|
||||
const auto &streams = m_store->networks().at(networkIndex).streams;
|
||||
|
||||
if (streamIndex >= 0 && streamIndex < streams.size())
|
||||
if (streamIndex >= 0 && streamIndex < static_cast<int>(streams.size()))
|
||||
{
|
||||
return streams.at(streamIndex).id;
|
||||
}
|
||||
@@ -203,9 +208,9 @@ QString NetworkStreamModel::networkIdFromIndex(const QModelIndex &index) const
|
||||
if (networkIndex == 0xFFFFFFFF)
|
||||
{
|
||||
int actualNetworkIndex = static_cast<int>(id & 0xFFFFFFFF);
|
||||
if (actualNetworkIndex >= 0 && actualNetworkIndex < m_data->networks().size())
|
||||
if (actualNetworkIndex >= 0 && actualNetworkIndex < m_store->networks().size())
|
||||
{
|
||||
return m_data->networks().at(actualNetworkIndex).id;
|
||||
return m_store->networks().at(actualNetworkIndex).id;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
#define NETWORKSTREAMMODEL_H
|
||||
|
||||
#include <QAbstractItemModel>
|
||||
#include "data.h"
|
||||
#include "store.h"
|
||||
|
||||
class NetworkStreamModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NetworkStreamModel(Data *data, QObject *parent = nullptr);
|
||||
explicit NetworkStreamModel(Store *store, QObject *parent = nullptr);
|
||||
|
||||
// Required QAbstractItemModel interface
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
@@ -24,7 +24,7 @@ public:
|
||||
QString networkIdFromIndex(const QModelIndex &index) const;
|
||||
|
||||
private:
|
||||
Data *m_data;
|
||||
Store *m_store;
|
||||
};
|
||||
|
||||
#endif // NETWORKSTREAMMODEL_H
|
||||
|
||||
+24
-23
@@ -5,9 +5,16 @@
|
||||
#include <QFont>
|
||||
#include <QStyle>
|
||||
|
||||
ParticleListModel::ParticleListModel(Data *data, QObject *parent)
|
||||
: QAbstractListModel(parent), m_data(data), m_currentStreamId()
|
||||
ParticleListModel::ParticleListModel(Store *store, QObject *parent)
|
||||
: QAbstractListModel(parent), m_store(store), m_currentStreamId()
|
||||
{
|
||||
connect(m_store, &Store::particlesChanged, this, [this](const QString &streamId) {
|
||||
if (streamId == m_currentStreamId)
|
||||
{
|
||||
beginResetModel();
|
||||
endResetModel();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int ParticleListModel::rowCount(const QModelIndex &parent) const
|
||||
@@ -22,7 +29,7 @@ int ParticleListModel::rowCount(const QModelIndex &parent) const
|
||||
return 0;
|
||||
}
|
||||
|
||||
return m_data->particleCount(m_currentStreamId);
|
||||
return m_store->particleCount(m_currentStreamId);
|
||||
}
|
||||
|
||||
QVariant ParticleListModel::data(const QModelIndex &index, int role) const
|
||||
@@ -37,7 +44,7 @@ QVariant ParticleListModel::data(const QModelIndex &index, int role) const
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const QList<Particle> &particles = m_data->particlesForStream(m_currentStreamId);
|
||||
const QList<Particle> &particles = m_store->particlesForStream(m_currentStreamId);
|
||||
|
||||
if (index.row() < 0 || index.row() >= particles.size())
|
||||
{
|
||||
@@ -48,8 +55,13 @@ QVariant ParticleListModel::data(const QModelIndex &index, int role) const
|
||||
|
||||
if (role == Qt::DisplayRole)
|
||||
{
|
||||
QString emailPrefix = particle.createdBy.split("@")[0];
|
||||
QString display = QString("[%1] - %2").arg(emailPrefix, particle.summary);
|
||||
QString emailPrefix = particle.createdByEmail.split("@")[0];
|
||||
QString summary = particle.data.value("content").toString();
|
||||
if (summary.isEmpty())
|
||||
{
|
||||
summary = particle.type;
|
||||
}
|
||||
QString display = QString("[%1] - %2").arg(emailPrefix, summary);
|
||||
if (!particle.seen)
|
||||
{
|
||||
display += " •";
|
||||
@@ -65,21 +77,6 @@ QVariant ParticleListModel::data(const QModelIndex &index, int role) const
|
||||
}
|
||||
// Unseen items use default foreground color
|
||||
}
|
||||
// else if (role == Qt::DecorationRole)
|
||||
// {
|
||||
// // Return icon based on type
|
||||
// QStyle::StandardPixmap pixmap;
|
||||
// if (particle.type == "text")
|
||||
// pixmap = QStyle::SP_DirIcon;
|
||||
// else if (particle.type == "link")
|
||||
// pixmap = QStyle::SP_CommandLink;
|
||||
// else if (particle.type == "image")
|
||||
// pixmap = QStyle::SP_MediaPlay;
|
||||
// else
|
||||
// return QVariant();
|
||||
//
|
||||
// return qApp->style()->standardIcon(pixmap);
|
||||
// }
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
@@ -98,7 +95,7 @@ int ParticleListModel::firstUnseenRow() const
|
||||
return -1;
|
||||
}
|
||||
|
||||
const QList<Particle> &particles = m_data->particlesForStream(m_currentStreamId);
|
||||
const QList<Particle> &particles = m_store->particlesForStream(m_currentStreamId);
|
||||
for (int i = 0; i < particles.size(); ++i)
|
||||
{
|
||||
if (!particles.at(i).seen)
|
||||
@@ -113,11 +110,15 @@ int ParticleListModel::firstUnseenRow() const
|
||||
const Particle *ParticleListModel::particleAtRow(int row) const
|
||||
{
|
||||
if (m_currentStreamId.isEmpty())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QList<Particle> &particles = m_data->particlesForStream(m_currentStreamId);
|
||||
const QList<Particle> &particles = m_store->particlesForStream(m_currentStreamId);
|
||||
if (row >= 0 && row < particles.size())
|
||||
{
|
||||
return &particles.at(row);
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
#define PARTICLELISTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include "data.h"
|
||||
#include "store.h"
|
||||
|
||||
class ParticleListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ParticleListModel(Data *data, QObject *parent = nullptr);
|
||||
explicit ParticleListModel(Store *store, QObject *parent = nullptr);
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
@@ -19,7 +19,7 @@ public:
|
||||
const Particle* particleAtRow(int row) const;
|
||||
|
||||
private:
|
||||
Data *m_data;
|
||||
Store *m_store;
|
||||
QString m_currentStreamId;
|
||||
};
|
||||
|
||||
|
||||
+879
@@ -0,0 +1,879 @@
|
||||
#include "store.h"
|
||||
#include "networkmanager.h"
|
||||
#include <QDebug>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QNetworkReply>
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Operation
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation::Operation(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Store — construction
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Store::Store(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Operation helpers
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation *Store::makeOperation()
|
||||
{
|
||||
auto *op = new Operation(this);
|
||||
connect(op, &Operation::finished, op, &QObject::deleteLater);
|
||||
return op;
|
||||
}
|
||||
|
||||
void Store::wireReply(QNetworkReply *reply, Operation *op,
|
||||
std::function<void(const QJsonDocument &)> successHandler)
|
||||
{
|
||||
connect(reply, &QNetworkReply::finished, this, [reply, op, successHandler = std::move(successHandler)]() {
|
||||
if (reply->error() != QNetworkReply::NoError)
|
||||
{
|
||||
QByteArray body = reply->readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(body);
|
||||
QString message = doc.isObject() ? doc.object().value("message").toString() : QString();
|
||||
if (message.isEmpty())
|
||||
{
|
||||
message = reply->errorString();
|
||||
}
|
||||
emit op->failed(message);
|
||||
emit op->finished();
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray body = reply->readAll();
|
||||
QJsonDocument doc = body.isEmpty() ? QJsonDocument() : QJsonDocument::fromJson(body);
|
||||
successHandler(doc);
|
||||
});
|
||||
}
|
||||
|
||||
void Store::wireReply(QNetworkReply *reply, Operation *op)
|
||||
{
|
||||
wireReply(reply, op, [op](const QJsonDocument &) {
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// JSON parsing
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Human Store::parseHuman(const QJsonObject &obj)
|
||||
{
|
||||
Human h;
|
||||
h.id = obj["id"].toString();
|
||||
h.email = obj["email"].toString();
|
||||
h.emailPrefix = obj["email_prefix"].toString();
|
||||
return h;
|
||||
}
|
||||
|
||||
Network Store::parseNetwork(const QJsonObject &obj)
|
||||
{
|
||||
Network n;
|
||||
n.id = obj["id"].toString();
|
||||
n.name = obj["name"].toString();
|
||||
n.admin = parseHuman(obj["admin_human"].toObject());
|
||||
n.openStreamCount = obj["open_stream_count"].toInt();
|
||||
n.openStreamCapacity = obj["open_stream_capacity"].toInt();
|
||||
|
||||
const QJsonArray humansArr = obj["humans"].toArray();
|
||||
for (const QJsonValue &v : humansArr)
|
||||
{
|
||||
n.members.push_back(parseHuman(v.toObject()));
|
||||
}
|
||||
|
||||
// Streams are parsed at a higher level (populateFromStartupData) so we can
|
||||
// also extract particles per stream. Individual fetch calls parse streams inline.
|
||||
return n;
|
||||
}
|
||||
|
||||
Stream Store::parseStream(const QJsonObject &obj)
|
||||
{
|
||||
Stream s;
|
||||
s.id = obj["id"].toString();
|
||||
s.name = obj["name"].toString();
|
||||
s.description = obj["description"].toString();
|
||||
s.isOpen = (obj["status"].toString() == "open");
|
||||
s.unseenCount = obj["unseen_count"].toInt();
|
||||
|
||||
const QJsonArray membersArr = obj["members"].toArray();
|
||||
for (const QJsonValue &v : membersArr)
|
||||
{
|
||||
s.memberEmails.push_back(v.toString());
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
Particle Store::parseParticle(const QJsonObject &obj)
|
||||
{
|
||||
Particle p;
|
||||
p.id = obj["id"].toString();
|
||||
p.type = obj["type"].toString();
|
||||
p.createdByEmail = obj["created_by_email"].toString();
|
||||
p.seen = obj["seen"].toBool();
|
||||
|
||||
const QJsonObject dataObj = obj["data"].toObject();
|
||||
for (auto it = dataObj.begin(); it != dataObj.end(); ++it)
|
||||
{
|
||||
p.data[it.key()] = it.value().toVariant();
|
||||
}
|
||||
|
||||
const QJsonArray acksArr = obj["acks"].toArray();
|
||||
for (const QJsonValue &v : acksArr)
|
||||
{
|
||||
// acks can be objects with { email, acked_at } or plain strings
|
||||
if (v.isObject())
|
||||
{
|
||||
p.ackedByEmails.push_back(v.toObject()["email"].toString());
|
||||
}
|
||||
else
|
||||
{
|
||||
p.ackedByEmails.push_back(v.toString());
|
||||
}
|
||||
}
|
||||
|
||||
p.updatedAt = QDateTime::fromString(obj["updated_at"].toString(), Qt::ISODate);
|
||||
p.createdAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate);
|
||||
return p;
|
||||
}
|
||||
|
||||
void Store::populateFromStartupData(const QJsonObject &root)
|
||||
{
|
||||
m_networks.clear();
|
||||
m_particles.clear();
|
||||
m_networkIndex.clear();
|
||||
m_streamToNetwork.clear();
|
||||
|
||||
const QJsonArray networksArr = root["networks"].toArray();
|
||||
for (const QJsonValue &netVal : networksArr)
|
||||
{
|
||||
QJsonObject netObj = netVal.toObject();
|
||||
Network network = parseNetwork(netObj);
|
||||
|
||||
const QJsonArray streamsArr = netObj["streams"].toArray();
|
||||
for (const QJsonValue &streamVal : streamsArr)
|
||||
{
|
||||
QJsonObject streamObj = streamVal.toObject();
|
||||
Stream stream = parseStream(streamObj);
|
||||
network.streams.push_back(stream);
|
||||
m_streamToNetwork[stream.id] = network.id;
|
||||
|
||||
// Parse particles embedded in stream
|
||||
QList<Particle> particleList;
|
||||
const QJsonArray particlesArr = streamObj["particles"].toArray();
|
||||
for (const QJsonValue &partVal : particlesArr)
|
||||
{
|
||||
particleList.append(parseParticle(partVal.toObject()));
|
||||
}
|
||||
m_particles[stream.id] = particleList;
|
||||
}
|
||||
|
||||
m_networkIndex[network.id] = m_networks.size();
|
||||
m_networks.append(network);
|
||||
}
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// State mutation helpers
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
void Store::upsertNetwork(const Network &network)
|
||||
{
|
||||
if (m_networkIndex.contains(network.id))
|
||||
{
|
||||
int idx = m_networkIndex[network.id];
|
||||
std::vector<Stream> existingStreams = m_networks[idx].streams;
|
||||
m_networks[idx] = network;
|
||||
if (network.streams.empty())
|
||||
{
|
||||
m_networks[idx].streams = existingStreams;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_networkIndex[network.id] = m_networks.size();
|
||||
m_networks.append(network);
|
||||
}
|
||||
}
|
||||
|
||||
void Store::upsertStream(const QString &networkId, const Stream &stream)
|
||||
{
|
||||
if (!m_networkIndex.contains(networkId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto &streams = m_networks[m_networkIndex[networkId]].streams;
|
||||
for (size_t i = 0; i < streams.size(); ++i)
|
||||
{
|
||||
if (streams[i].id == stream.id)
|
||||
{
|
||||
streams[i] = stream;
|
||||
m_streamToNetwork[stream.id] = networkId;
|
||||
return;
|
||||
}
|
||||
}
|
||||
streams.push_back(stream);
|
||||
m_streamToNetwork[stream.id] = networkId;
|
||||
}
|
||||
|
||||
void Store::upsertParticle(const QString &streamId, const Particle &particle)
|
||||
{
|
||||
QList<Particle> &list = m_particles[streamId];
|
||||
for (int i = 0; i < list.size(); ++i)
|
||||
{
|
||||
if (list[i].id == particle.id)
|
||||
{
|
||||
list[i] = particle;
|
||||
return;
|
||||
}
|
||||
}
|
||||
list.append(particle);
|
||||
}
|
||||
|
||||
void Store::removeParticleFromState(const QString &particleId)
|
||||
{
|
||||
for (auto it = m_particles.begin(); it != m_particles.end(); ++it)
|
||||
{
|
||||
QList<Particle> &list = it.value();
|
||||
for (int i = 0; i < list.size(); ++i)
|
||||
{
|
||||
if (list[i].id == particleId)
|
||||
{
|
||||
list.removeAt(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QString Store::findStreamForParticle(const QString &particleId) const
|
||||
{
|
||||
for (auto it = m_particles.constBegin(); it != m_particles.constEnd(); ++it)
|
||||
{
|
||||
for (const auto &p : it.value())
|
||||
{
|
||||
if (p.id == particleId)
|
||||
{
|
||||
return it.key();
|
||||
}
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Accessors
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
const QList<Network> &Store::networks() const { return m_networks; }
|
||||
|
||||
int Store::networkCount() const { return m_networks.size(); }
|
||||
|
||||
const Network *Store::networkById(const QString &id) const
|
||||
{
|
||||
auto it = m_networkIndex.find(id);
|
||||
if (it == m_networkIndex.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
return &m_networks[it.value()];
|
||||
}
|
||||
|
||||
QList<Stream> Store::streamsForNetwork(const QString &networkId) const
|
||||
{
|
||||
auto it = m_networkIndex.find(networkId);
|
||||
if (it == m_networkIndex.end())
|
||||
{
|
||||
return {};
|
||||
}
|
||||
const auto &streams = m_networks[it.value()].streams;
|
||||
QList<Stream> result;
|
||||
result.reserve(static_cast<int>(streams.size()));
|
||||
for (const auto &s : streams)
|
||||
{
|
||||
result.append(s);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int Store::streamCount(const QString &networkId) const
|
||||
{
|
||||
auto it = m_networkIndex.find(networkId);
|
||||
if (it == m_networkIndex.end())
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
return static_cast<int>(m_networks[it.value()].streams.size());
|
||||
}
|
||||
|
||||
const Stream *Store::streamById(const QString &id) const
|
||||
{
|
||||
auto netIt = m_streamToNetwork.find(id);
|
||||
if (netIt == m_streamToNetwork.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
auto idxIt = m_networkIndex.find(netIt.value());
|
||||
if (idxIt == m_networkIndex.end())
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
const auto &streams = m_networks[idxIt.value()].streams;
|
||||
for (const auto &s : streams)
|
||||
{
|
||||
if (s.id == id)
|
||||
{
|
||||
return &s;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const QList<Particle> &Store::particlesForStream(const QString &streamId) const
|
||||
{
|
||||
static const QList<Particle> empty;
|
||||
auto it = m_particles.find(streamId);
|
||||
if (it == m_particles.end())
|
||||
{
|
||||
return empty;
|
||||
}
|
||||
return it.value();
|
||||
}
|
||||
|
||||
int Store::particleCount(const QString &streamId) const
|
||||
{
|
||||
return m_particles.value(streamId).size();
|
||||
}
|
||||
|
||||
const Particle *Store::particleById(const QString &id) const
|
||||
{
|
||||
for (auto it = m_particles.constBegin(); it != m_particles.constEnd(); ++it)
|
||||
{
|
||||
for (const auto &p : it.value())
|
||||
{
|
||||
if (p.id == id)
|
||||
{
|
||||
return &p;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Bootstrap
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation *Store::loadStartupData()
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get("/startup");
|
||||
|
||||
wireReply(reply, op, [this, op](const QJsonDocument &doc) {
|
||||
if (!doc.isObject())
|
||||
{
|
||||
emit op->failed("Invalid startup data");
|
||||
emit op->finished();
|
||||
return;
|
||||
}
|
||||
populateFromStartupData(doc.object());
|
||||
emit startupDataLoaded();
|
||||
emit networksChanged();
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Network operations
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation *Store::createNetwork(const QString &name)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["name"] = name;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post("/networks", QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
upsertNetwork(parseNetwork(doc.object()));
|
||||
emit networksChanged();
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchNetworks()
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get("/networks");
|
||||
|
||||
wireReply(reply, op, [this, op](const QJsonDocument &doc) {
|
||||
const QJsonArray arr = doc.array();
|
||||
for (const QJsonValue &v : arr)
|
||||
{
|
||||
upsertNetwork(parseNetwork(v.toObject()));
|
||||
}
|
||||
emit networksChanged();
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchNetwork(const QString &networkId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get(QString("/networks/%1").arg(networkId));
|
||||
|
||||
wireReply(reply, op, [this, op, networkId](const QJsonDocument &doc) {
|
||||
QJsonObject obj = doc.object();
|
||||
Network network = parseNetwork(obj);
|
||||
|
||||
const QJsonArray streamsArr = obj["streams"].toArray();
|
||||
for (const QJsonValue &sv : streamsArr)
|
||||
{
|
||||
Stream stream = parseStream(sv.toObject());
|
||||
network.streams.push_back(stream);
|
||||
m_streamToNetwork[stream.id] = network.id;
|
||||
}
|
||||
upsertNetwork(network);
|
||||
emit networksChanged();
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::addNetworkMembers(const QString &networkId, const QStringList &emails)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray arr;
|
||||
for (const QString &e : emails)
|
||||
{
|
||||
arr.append(e);
|
||||
}
|
||||
body["email_addresses"] = arr;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/networks/%1/members").arg(networkId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::removeNetworkMember(const QString &networkId, const QString &email)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().del(
|
||||
QString("/networks/%1/members/%2").arg(networkId, email));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::setStreamCapacity(const QString &networkId, int capacity)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["capacity"] = capacity;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().put(
|
||||
QString("/networks/%1/capacity").arg(networkId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Stream operations
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation *Store::createStream(const QString &networkId, const QString &name,
|
||||
const QString &description, const QString &visibility,
|
||||
const QStringList &members)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["name"] = name;
|
||||
body["description"] = description;
|
||||
body["visibility"] = visibility;
|
||||
QJsonArray membersArr;
|
||||
for (const QString &m : members)
|
||||
{
|
||||
membersArr.append(m);
|
||||
}
|
||||
body["members"] = membersArr;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/networks/%1/streams").arg(networkId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, networkId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Stream stream = parseStream(doc.object());
|
||||
upsertStream(networkId, stream);
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchStream(const QString &streamId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get(QString("/streams/%1").arg(streamId));
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &doc) {
|
||||
QJsonObject obj = doc.object();
|
||||
Stream stream = parseStream(obj);
|
||||
|
||||
QString networkId = m_streamToNetwork.value(streamId);
|
||||
if (!networkId.isEmpty())
|
||||
{
|
||||
upsertStream(networkId, stream);
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
|
||||
// Parse particles if present
|
||||
const QJsonArray particlesArr = obj["particles"].toArray();
|
||||
QList<Particle> particleList;
|
||||
for (const QJsonValue &pv : particlesArr)
|
||||
{
|
||||
particleList.append(parseParticle(pv.toObject()));
|
||||
}
|
||||
m_particles[streamId] = particleList;
|
||||
emit particlesChanged(streamId);
|
||||
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::updateStream(const QString &streamId, const QString &name, const QString &description)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
if (!name.isEmpty())
|
||||
{
|
||||
body["name"] = name;
|
||||
}
|
||||
if (!description.isEmpty())
|
||||
{
|
||||
body["description"] = description;
|
||||
}
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().patch(
|
||||
QString("/streams/%1").arg(streamId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Stream stream = parseStream(doc.object());
|
||||
QString networkId = m_streamToNetwork.value(streamId);
|
||||
if (!networkId.isEmpty())
|
||||
{
|
||||
upsertStream(networkId, stream);
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::openStream(const QString &streamId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/open").arg(streamId), QJsonDocument());
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &) {
|
||||
QString networkId = m_streamToNetwork.value(streamId);
|
||||
if (m_networkIndex.contains(networkId))
|
||||
{
|
||||
auto &streams = m_networks[m_networkIndex[networkId]].streams;
|
||||
for (auto &s : streams)
|
||||
{
|
||||
if (s.id == streamId)
|
||||
{
|
||||
s.isOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::closeStream(const QString &streamId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/close").arg(streamId), QJsonDocument());
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &) {
|
||||
QString networkId = m_streamToNetwork.value(streamId);
|
||||
if (m_networkIndex.contains(networkId))
|
||||
{
|
||||
auto &streams = m_networks[m_networkIndex[networkId]].streams;
|
||||
for (auto &s : streams)
|
||||
{
|
||||
if (s.id == streamId)
|
||||
{
|
||||
s.isOpen = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
emit streamsChanged(networkId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::addStreamMembers(const QString &streamId, const QStringList &emails)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray arr;
|
||||
for (const QString &e : emails)
|
||||
{
|
||||
arr.append(e);
|
||||
}
|
||||
body["emails"] = arr;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/members").arg(streamId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::removeStreamMembers(const QString &streamId, const QStringList &emails)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray arr;
|
||||
for (const QString &e : emails)
|
||||
arr.append(e);
|
||||
body["emails"] = arr;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().del(
|
||||
QString("/streams/%1/members").arg(streamId), QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
// Particle operations
|
||||
// ────────────────────────────────────────────────────────────────
|
||||
|
||||
Operation *Store::createParticle(const QString &streamId, const QString &type,
|
||||
const QMap<QString, QVariant> &data)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
body["type"] = type;
|
||||
QJsonObject dataObj;
|
||||
for (auto it = data.constBegin(); it != data.constEnd(); ++it)
|
||||
dataObj[it.key()] = QJsonValue::fromVariant(it.value());
|
||||
body["data"] = dataObj;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/streams/%1/particles").arg(streamId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, streamId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Particle particle = parseParticle(doc.object());
|
||||
upsertParticle(streamId, particle);
|
||||
emit particlesChanged(streamId);
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::fetchParticle(const QString &particleId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().get(QString("/particles/%1").arg(particleId));
|
||||
|
||||
wireReply(reply, op, [this, op, particleId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Particle particle = parseParticle(doc.object());
|
||||
QString streamId = findStreamForParticle(particleId);
|
||||
if (!streamId.isEmpty())
|
||||
{
|
||||
upsertParticle(streamId, particle);
|
||||
emit particlesChanged(streamId);
|
||||
}
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::updateParticle(const QString &particleId, const QMap<QString, QVariant> &data)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonObject dataObj;
|
||||
for (auto it = data.constBegin(); it != data.constEnd(); ++it)
|
||||
dataObj[it.key()] = QJsonValue::fromVariant(it.value());
|
||||
body["data"] = dataObj;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().patch(
|
||||
QString("/particles/%1").arg(particleId), QJsonDocument(body));
|
||||
|
||||
wireReply(reply, op, [this, op, particleId](const QJsonDocument &doc) {
|
||||
if (doc.isObject())
|
||||
{
|
||||
Particle particle = parseParticle(doc.object());
|
||||
QString streamId = findStreamForParticle(particleId);
|
||||
if (!streamId.isEmpty())
|
||||
{
|
||||
upsertParticle(streamId, particle);
|
||||
emit particlesChanged(streamId);
|
||||
}
|
||||
}
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::deleteParticle(const QString &particleId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().del(QString("/particles/%1").arg(particleId));
|
||||
|
||||
wireReply(reply, op, [this, op, particleId](const QJsonDocument &) {
|
||||
QString streamId = findStreamForParticle(particleId);
|
||||
removeParticleFromState(particleId);
|
||||
if (!streamId.isEmpty())
|
||||
emit particlesChanged(streamId);
|
||||
emit op->success();
|
||||
emit op->finished();
|
||||
});
|
||||
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::markParticleSeen(const QString &particleId)
|
||||
{
|
||||
// Optimistic local update
|
||||
for (auto it = m_particles.begin(); it != m_particles.end(); ++it)
|
||||
{
|
||||
for (int i = 0; i < it.value().size(); ++i)
|
||||
{
|
||||
if (it.value()[i].id == particleId)
|
||||
{
|
||||
it.value()[i].seen = true;
|
||||
emit particlesChanged(it.key());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/particles/%1/seen").arg(particleId), QJsonDocument());
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::markParticlesSeen(const QStringList &particleIds)
|
||||
{
|
||||
// Optimistic local update
|
||||
QSet<QString> affectedStreams;
|
||||
for (const QString &pid : particleIds)
|
||||
{
|
||||
for (auto it = m_particles.begin(); it != m_particles.end(); ++it)
|
||||
{
|
||||
for (int i = 0; i < it.value().size(); ++i)
|
||||
{
|
||||
if (it.value()[i].id == pid)
|
||||
{
|
||||
it.value()[i].seen = true;
|
||||
affectedStreams.insert(it.key());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const QString &streamId : affectedStreams)
|
||||
emit particlesChanged(streamId);
|
||||
|
||||
Operation *op = makeOperation();
|
||||
|
||||
QJsonObject body;
|
||||
QJsonArray ids;
|
||||
for (const QString &id : particleIds)
|
||||
ids.append(id);
|
||||
body["particle_ids"] = ids;
|
||||
|
||||
QNetworkReply *reply = NetworkManager::instance().post("/particles/seen", QJsonDocument(body));
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
|
||||
Operation *Store::ackParticle(const QString &particleId)
|
||||
{
|
||||
Operation *op = makeOperation();
|
||||
QNetworkReply *reply = NetworkManager::instance().post(
|
||||
QString("/particles/%1/ack").arg(particleId), QJsonDocument());
|
||||
wireReply(reply, op);
|
||||
return op;
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
#ifndef STORE_H
|
||||
#define STORE_H
|
||||
|
||||
#include "models.h"
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
class QNetworkReply;
|
||||
QT_END_NAMESPACE
|
||||
|
||||
/// Represents an in-flight API operation.
|
||||
/// Callers connect to success/failed for one-off feedback.
|
||||
/// Automatically deletes itself after finished() fires.
|
||||
class Operation : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Operation(QObject *parent = nullptr);
|
||||
|
||||
signals:
|
||||
void success();
|
||||
void failed(const QString &message);
|
||||
void finished();
|
||||
};
|
||||
|
||||
/// Central data store backed by the Orion API.
|
||||
/// Holds cached state and exposes read-only accessors + mutating operations.
|
||||
class Store : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit Store(QObject *parent = nullptr);
|
||||
|
||||
// ── Read-only accessors ──
|
||||
|
||||
const QList<Network> &networks() const;
|
||||
int networkCount() const;
|
||||
const Network *networkById(const QString &id) const;
|
||||
|
||||
QList<Stream> streamsForNetwork(const QString &networkId) const;
|
||||
int streamCount(const QString &networkId) const;
|
||||
const Stream *streamById(const QString &id) const;
|
||||
|
||||
const QList<Particle> &particlesForStream(const QString &streamId) const;
|
||||
int particleCount(const QString &streamId) const;
|
||||
const Particle *particleById(const QString &id) const;
|
||||
|
||||
// ── Bootstrap ──
|
||||
|
||||
Operation *loadStartupData();
|
||||
|
||||
// ── Network operations ──
|
||||
|
||||
Operation *createNetwork(const QString &name);
|
||||
Operation *fetchNetworks();
|
||||
Operation *fetchNetwork(const QString &networkId);
|
||||
Operation *addNetworkMembers(const QString &networkId, const QStringList &emails);
|
||||
Operation *removeNetworkMember(const QString &networkId, const QString &email);
|
||||
Operation *setStreamCapacity(const QString &networkId, int capacity);
|
||||
|
||||
// ── Stream operations ──
|
||||
|
||||
Operation *createStream(const QString &networkId, const QString &name,
|
||||
const QString &description, const QString &visibility = "network_all",
|
||||
const QStringList &members = {});
|
||||
Operation *fetchStream(const QString &streamId);
|
||||
Operation *updateStream(const QString &streamId, const QString &name, const QString &description);
|
||||
Operation *openStream(const QString &streamId);
|
||||
Operation *closeStream(const QString &streamId);
|
||||
Operation *addStreamMembers(const QString &streamId, const QStringList &emails);
|
||||
Operation *removeStreamMembers(const QString &streamId, const QStringList &emails);
|
||||
|
||||
// ── Particle operations ──
|
||||
|
||||
Operation *createParticle(const QString &streamId, const QString &type,
|
||||
const QMap<QString, QVariant> &data);
|
||||
Operation *fetchParticle(const QString &particleId);
|
||||
Operation *updateParticle(const QString &particleId, const QMap<QString, QVariant> &data);
|
||||
Operation *deleteParticle(const QString &particleId);
|
||||
Operation *markParticleSeen(const QString &particleId);
|
||||
Operation *markParticlesSeen(const QStringList &particleIds);
|
||||
Operation *ackParticle(const QString &particleId);
|
||||
|
||||
signals:
|
||||
// ── Data-change signals ──
|
||||
|
||||
void networksChanged();
|
||||
void streamsChanged(const QString &networkId);
|
||||
void particlesChanged(const QString &streamId);
|
||||
|
||||
void startupDataLoaded();
|
||||
void errorOccurred(const QString &message);
|
||||
|
||||
private:
|
||||
// ── Internal state ──
|
||||
|
||||
QList<Network> m_networks;
|
||||
QMap<QString, QList<Particle>> m_particles; // streamId -> particles
|
||||
QMap<QString, int> m_networkIndex; // networkId -> index in m_networks
|
||||
QMap<QString, QString> m_streamToNetwork; // streamId -> networkId
|
||||
|
||||
// ── JSON parsing ──
|
||||
|
||||
static Human parseHuman(const QJsonObject &obj);
|
||||
static Network parseNetwork(const QJsonObject &obj);
|
||||
static Stream parseStream(const QJsonObject &obj);
|
||||
static Particle parseParticle(const QJsonObject &obj);
|
||||
void populateFromStartupData(const QJsonObject &root);
|
||||
|
||||
// ── State mutation helpers ──
|
||||
|
||||
void upsertNetwork(const Network &network);
|
||||
void upsertStream(const QString &networkId, const Stream &stream);
|
||||
void upsertParticle(const QString &streamId, const Particle &particle);
|
||||
void removeParticleFromState(const QString &particleId);
|
||||
|
||||
/// Returns the streamId that contains the given particle, or empty QString.
|
||||
QString findStreamForParticle(const QString &particleId) const;
|
||||
|
||||
// ── Operation helpers ──
|
||||
|
||||
/// Creates an Operation parented to this Store, wired to self-destruct after finished().
|
||||
Operation *makeOperation();
|
||||
|
||||
/// Connects reply->finished to a handler. On error, emits op->failed + op->finished.
|
||||
/// On success, calls successHandler with the parsed QJsonDocument.
|
||||
/// successHandler is responsible for emitting op->success() and op->finished().
|
||||
void wireReply(QNetworkReply *reply, Operation *op,
|
||||
std::function<void(const QJsonDocument &)> successHandler);
|
||||
|
||||
/// Simplified wireReply for operations that don't need to parse a response body
|
||||
/// (e.g. 204 No Content). Emits op->success() and op->finished() automatically.
|
||||
void wireReply(QNetworkReply *reply, Operation *op);
|
||||
};
|
||||
|
||||
#endif // STORE_H
|
||||
Reference in New Issue
Block a user