From 87fe792f333c125c62c00ad3cc72ce1852cb7796 Mon Sep 17 00:00:00 2001 From: talksik Date: Fri, 6 Feb 2026 17:11:24 -0800 Subject: [PATCH] feat: show network detail view Includes refactor to not using mainwindow.ui form. More flexible layout management with programmatic initialization. --- CMakeLists.txt | 7 +- src/MainWindow.cpp | 338 +++++++++++++++++++---- src/MainWindow.h | 37 ++- src/mainwindow.ui | 520 ------------------------------------ src/networkdetailwidget.cpp | 125 +++++++++ src/networkdetailwidget.h | 37 +++ src/networkstreammodel.cpp | 9 +- src/settingsdialog.cpp | 27 +- src/settingsdialog.h | 5 +- src/settingsdialog.ui | 144 ++++++---- src/store.cpp | 2 + src/store.h | 1 + 12 files changed, 604 insertions(+), 648 deletions(-) delete mode 100644 src/mainwindow.ui create mode 100644 src/networkdetailwidget.cpp create mode 100644 src/networkdetailwidget.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 36cb21e..bcc2f9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,12 +23,13 @@ qt_add_executable(llink src/MainWindow.h src/VerticalLabel.cpp src/VerticalLabel.h - src/mainwindow.ui src/authmanager.cpp src/networkmanager.cpp src/authdialog.ui src/authdialog.cpp - src/settingsdialog.h src/settingsdialog.cpp src/settingsdialog.ui + src/settingsdialog.ui + src/settingsdialog.h + src/settingsdialog.cpp src/models.h src/store.h src/store.cpp @@ -42,6 +43,8 @@ qt_add_executable(llink src/createstreamdialog.cpp src/memberpickerdialog.h src/memberpickerdialog.cpp + src/networkdetailwidget.h + src/networkdetailwidget.cpp ) qt_add_resources(llink diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index e541ff4..450d16e 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -7,37 +7,44 @@ #include "authmanager.h" #include "createstreamdialog.h" #include "memberpickerdialog.h" +#include "networkdetailwidget.h" #include "networkmanager.h" #include "settingsdialog.h" #include "textinputdialog.h" -#include "ui_mainwindow.h" #include #include #include #include #include +#include +#include #include #include #include #include +#include +#include +#include #include +#include #include #include +#include +#include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), m_trayIcon(nullptr), m_authManager(new AuthManager(this)), m_settingsDialog(nullptr), m_authDialog(nullptr), m_store(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr), - m_selectedParticleRow(-1), m_isDragging(false), m_textInputDialog(nullptr), m_createStreamDialog(nullptr) + m_selectedParticleRow(-1), m_isDragging(false), m_textInputDialog(nullptr), m_createStreamDialog(nullptr), + m_networkDetailWidget(nullptr) { m_authDialog = new AuthDialog(this); m_textInputDialog = new TextInputDialog(this); connect(m_textInputDialog, &TextInputDialog::textSubmitted, this, &MainWindow::onTextMessageSubmitted); - ui = new Ui::MainWindow; - ui->setupUi(this); + buildUi(); - QWidget *topBar = ui->widget; - topBar->setCursor(Qt::OpenHandCursor); + m_topBar->setCursor(Qt::OpenHandCursor); // Enable frameless window for edge-to-edge experience setWindowFlags(Qt::FramelessWindowHint | Qt::Window); @@ -53,10 +60,10 @@ MainWindow::MainWindow(QWidget *parent) QFont font = QFont(); font.setStyle(QFont::StyleItalic); m_currentPathLabel->setFont(font); - ui->statusbar->addWidget(m_currentPathLabel); + m_statusBar->addWidget(m_currentPathLabel); // Add spacer to push right-aligned items to the right - ui->statusbar->addPermanentWidget(new QLabel(""), 1); + m_statusBar->addPermanentWidget(new QLabel(""), 1); // Ping indicator (green text) m_pingLabel = new QLabel(this); @@ -64,7 +71,7 @@ MainWindow::MainWindow(QWidget *parent) m_pingLabel->setMinimumWidth(100); m_pingLabel->setAlignment(Qt::AlignCenter); m_pingLabel->setStyleSheet(pingLabelStyle); - ui->statusbar->addPermanentWidget(m_pingLabel); + m_statusBar->addPermanentWidget(m_pingLabel); // Position indicator (vim-style) m_positionLabel = new QLabel(this); @@ -72,7 +79,7 @@ MainWindow::MainWindow(QWidget *parent) m_positionLabel->setMinimumWidth(50); m_positionLabel->setAlignment(Qt::AlignCenter); m_positionLabel->setStyleSheet(statusBarLabelStyle); - ui->statusbar->addPermanentWidget(m_positionLabel); + m_statusBar->addPermanentWidget(m_positionLabel); // Storage indicator m_storageLabel = new QLabel(this); @@ -80,7 +87,7 @@ MainWindow::MainWindow(QWidget *parent) m_storageLabel->setMinimumWidth(60); m_storageLabel->setAlignment(Qt::AlignCenter); m_storageLabel->setStyleSheet(statusBarLabelStyle); - ui->statusbar->addPermanentWidget(m_storageLabel); + m_statusBar->addPermanentWidget(m_storageLabel); // Setup ping timer for periodic server connectivity check m_pingTimer = new QTimer(this); @@ -98,9 +105,10 @@ MainWindow::MainWindow(QWidget *parent) m_pingTimer->start(10000); NetworkManager::instance().ping(); - ui->splitter->setStretchFactor(0, 1); - ui->splitter->setStretchFactor(1, 4); - ui->splitter->setStretchFactor(2, 1); + m_splitter->setStretchFactor(0, 1); + m_splitter->setStretchFactor(1, 4); + m_splitter->setStretchFactor(2, 1); + m_splitter->setStretchFactor(3, 5); setupModels(); @@ -116,10 +124,11 @@ MainWindow::MainWindow(QWidget *parent) connect(m_authManager, &AuthManager::signedOut, m_authDialog, &AuthDialog::show); connect(m_authManager, &AuthManager::signedIn, this, [this]() { m_store->setCurrentUserEmail(m_authManager->sessionData()->email); + m_networkDetailWidget->setCurrentUserEmail(m_authManager->sessionData()->email); m_store->loadStartupData(); }); connect(m_store, &Store::startupDataLoaded, this, [this]() { - ui->topLevelParticlesView->expandAll(); + m_treeView->expandAll(); }); connect(m_store, &Store::particleUpdated, this, [this](const QString &, const QString &particleId) { if (particleId == m_selectedParticleId) @@ -130,21 +139,25 @@ MainWindow::MainWindow(QWidget *parent) QStringList acked; for (const auto &email : particle->ackedByEmails) acked.append(email); - ui->label_3->setText("Acked by: " + acked.join(", ")); + m_ackLabel->setText("Acked by: " + acked.join(", ")); } else { - ui->label_3->setText(""); + m_ackLabel->setText(""); } } }); // DocumentSend toolbar button opens text input dialog - connect(ui->toolButton_3, &QToolButton::clicked, this, &MainWindow::showTextInputDialog); + connect(m_sendTextButton, &QToolButton::clicked, this, &MainWindow::showTextInputDialog); + + // Explicit connects for buttons (no auto-connect without .ui) + connect(m_settingsButton, &QToolButton::clicked, this, &MainWindow::on_settingsButton_clicked); + connect(m_exitButton, &QToolButton::clicked, this, &MainWindow::on_exitButton_clicked); // "New stream" button m_createStreamDialog = new CreateStreamDialog(this); - connect(ui->pushButton, &QPushButton::clicked, this, [this]() { + connect(m_newStreamButton, &QPushButton::clicked, this, [this]() { if (m_selectedNetworkName.isEmpty()) return; const Network *network = m_store->networkById(m_selectedNetworkName); @@ -160,7 +173,7 @@ MainWindow::MainWindow(QWidget *parent) }); // "Add members" toolbar button (ListAdd icon in right sidebar) - connect(ui->toolButton_7, &QToolButton::clicked, this, [this]() { + connect(m_addMembersButton, &QToolButton::clicked, this, [this]() { if (m_selectedStreamId.isEmpty() || m_selectedNetworkName.isEmpty()) return; const Network *network = m_store->networkById(m_selectedNetworkName); @@ -180,12 +193,12 @@ MainWindow::MainWindow(QWidget *parent) connect(op, &Operation::success, this, [this](const QJsonDocument &) { auto *fetchOp = m_store->fetchStream(m_selectedStreamId); connect(fetchOp, &Operation::success, this, [this](const QJsonDocument &) { - ui->membersList->clear(); + m_membersList->clear(); const Stream *s = m_store->streamById(m_selectedStreamId); if (s) { for (const QString &email : s->memberEmails) - ui->membersList->addItem(email.split("@")[0]); + m_membersList->addItem(email.split("@")[0]); } }); }); @@ -198,6 +211,196 @@ MainWindow::MainWindow(QWidget *parent) setupTrayIcon(); } +void MainWindow::buildUi() +{ + resize(650, 500); + setWindowTitle("Flowy.llink"); + + auto *centralWidget = new QWidget(this); + setCentralWidget(centralWidget); + auto *mainLayout = new QVBoxLayout(centralWidget); + mainLayout->setContentsMargins(0, 0, 0, 0); + + m_splitter = new QSplitter(Qt::Horizontal); + mainLayout->addWidget(m_splitter); + + // ── Left sidebar ── + m_leftSidebar = new QWidget(); + m_leftSidebar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + auto *leftLayout = new QVBoxLayout(m_leftSidebar); + leftLayout->setSpacing(5); + leftLayout->setContentsMargins(2, 2, 2, 2); + + // Top bar (drag area) + m_topBar = new QWidget(); + m_topBar->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + m_topBar->setMinimumHeight(20); + auto *topBarLayout = new QHBoxLayout(m_topBar); + topBarLayout->setSpacing(0); + topBarLayout->setContentsMargins(0, 0, 0, 0); + + m_settingsButton = new QToolButton(); + m_settingsButton->setText("..."); + m_settingsButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::HelpAbout)); + topBarLayout->addWidget(m_settingsButton); + + topBarLayout->addStretch(); + + m_exitButton = new QToolButton(); + m_exitButton->setText("..."); + m_exitButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ApplicationExit)); + topBarLayout->addWidget(m_exitButton); + + leftLayout->addWidget(m_topBar); + + // Tree view + m_treeView = new QTreeView(); + m_treeView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + leftLayout->addWidget(m_treeView); + + // New stream button + m_newStreamButton = new QPushButton("New stream"); + m_newStreamButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListAdd)); + leftLayout->addWidget(m_newStreamButton); + + m_splitter->addWidget(m_leftSidebar); + + // ── Content view ── + m_contentView = new QWidget(); + m_contentView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + m_contentView->setMinimumWidth(300); + auto *contentLayout = new QVBoxLayout(m_contentView); + contentLayout->setSpacing(2); + contentLayout->setContentsMargins(0, 0, 0, 2); + + // Content container with spacers and labels + auto *contentContainer = new QWidget(); + contentContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + auto *contentContainerLayout = new QVBoxLayout(contentContainer); + contentContainerLayout->setSpacing(2); + contentContainerLayout->setContentsMargins(0, 0, 0, 0); + + contentContainerLayout->addStretch(); + + m_particleContentLabel = new QLabel(); + QFont contentFont; + contentFont.setPointSize(36); + m_particleContentLabel->setFont(contentFont); + m_particleContentLabel->setMaximumSize(500, 300); + m_particleContentLabel->setAlignment(Qt::AlignCenter); + m_particleContentLabel->setWordWrap(true); + m_particleContentLabel->setScaledContents(true); + contentContainerLayout->addWidget(m_particleContentLabel); + + contentContainerLayout->addStretch(); + + m_creatorLabel = new QLabel(); + QFont creatorFont; + creatorFont.setPointSize(15); + m_creatorLabel->setFont(creatorFont); + m_creatorLabel->setAlignment(Qt::AlignCenter); + contentContainerLayout->addWidget(m_creatorLabel); + + m_ackLabel = new QLabel(); + m_ackLabel->setAlignment(Qt::AlignCenter); + contentContainerLayout->addWidget(m_ackLabel); + + contentLayout->addWidget(contentContainer); + + // Content toolbar (empty, matches widget_4 from .ui) + auto *contentToolbar = new QWidget(); + auto *contentToolbarLayout = new QHBoxLayout(contentToolbar); + contentToolbarLayout->setSpacing(2); + contentToolbarLayout->setContentsMargins(8, 0, 0, 0); + contentLayout->addWidget(contentToolbar); + + m_splitter->addWidget(m_contentView); + + // ── Right sidebar ── + m_rightSidebar = new QWidget(); + m_rightSidebar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + auto *rightLayout = new QVBoxLayout(m_rightSidebar); + rightLayout->setSpacing(2); + rightLayout->setContentsMargins(2, 2, 2, 2); + + // Particle list view + m_particlesView = new QListView(); + m_particlesView->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + auto sp1 = m_particlesView->sizePolicy(); + sp1.setVerticalStretch(2); + m_particlesView->setSizePolicy(sp1); + rightLayout->addWidget(m_particlesView); + + // Members list + m_membersList = new QListWidget(); + m_membersList->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + auto sp2 = m_membersList->sizePolicy(); + sp2.setVerticalStretch(1); + m_membersList->setSizePolicy(sp2); + rightLayout->addWidget(m_membersList); + + // "Hold space to speak" label + auto *holdSpaceLabel = new QLabel("Hold space to speak"); + holdSpaceLabel->setAlignment(Qt::AlignCenter); + QPalette holdPalette = holdSpaceLabel->palette(); + QColor holdColor(153, 156, 145, 217); + holdPalette.setColor(QPalette::WindowText, holdColor); + holdSpaceLabel->setPalette(holdPalette); + rightLayout->addWidget(holdSpaceLabel); + + // Empty toolbar row (widget_5) + auto *emptyToolbar = new QWidget(); + auto *emptyToolbarLayout = new QHBoxLayout(emptyToolbar); + emptyToolbarLayout->setSpacing(2); + emptyToolbarLayout->setContentsMargins(0, 0, 0, 0); + rightLayout->addWidget(emptyToolbar); + + // RTL toolbar row (widget_3) + auto *rtlToolbar = new QWidget(); + rtlToolbar->setLayoutDirection(Qt::RightToLeft); + auto *rtlToolbarLayout = new QHBoxLayout(rtlToolbar); + rtlToolbarLayout->setSpacing(2); + rtlToolbarLayout->setContentsMargins(0, 0, 0, 0); + + auto *editClearButton = new QToolButton(); + editClearButton->setText("..."); + editClearButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::EditClear)); + rtlToolbarLayout->addWidget(editClearButton); + + m_addMembersButton = new QToolButton(); + m_addMembersButton->setText("..."); + m_addMembersButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListAdd)); + rtlToolbarLayout->addWidget(m_addMembersButton); + + auto *insertImageButton = new QToolButton(); + insertImageButton->setText("..."); + insertImageButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::InsertImage)); + rtlToolbarLayout->addWidget(insertImageButton); + + m_sendTextButton = new QToolButton(); + m_sendTextButton->setText("..."); + m_sendTextButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::DocumentSend)); + rtlToolbarLayout->addWidget(m_sendTextButton); + + auto *cameraVideoButton = new QToolButton(); + cameraVideoButton->setText("..."); + cameraVideoButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::CameraVideo)); + rtlToolbarLayout->addWidget(cameraVideoButton); + + rightLayout->addWidget(rtlToolbar); + + m_splitter->addWidget(m_rightSidebar); + + // ── Network detail widget (hidden by default) ── + // Store is created in setupModels, so we pass nullptr now and set store later + // Actually, store is created in setupModels which is called after buildUi. + // We need to create it after store exists. We'll create it in the constructor after setupModels. + // For now just reserve the splitter slot — we'll add the widget after setupModels(). + + m_statusBar = new QStatusBar(); + setStatusBar(m_statusBar); +} + void MainWindow::setupTrayIcon() { m_trayIcon = new QSystemTrayIcon(this); @@ -221,7 +424,6 @@ void MainWindow::setupTrayIcon() MainWindow::~MainWindow() { - delete ui; } void MainWindow::on_exitButton_clicked() @@ -233,7 +435,7 @@ void MainWindow::on_settingsButton_clicked() { if (!m_settingsDialog) { - m_settingsDialog = new SettingsDialog(m_authManager, this); + m_settingsDialog = new SettingsDialog(m_authManager, m_store, this); } m_settingsDialog->show(); } @@ -243,38 +445,43 @@ void MainWindow::setupModels() // Create store (data loaded after sign-in via loadStartupData) m_store = new Store(this); + // Now create the network detail widget (needs store) + m_networkDetailWidget = new NetworkDetailWidget(m_store); + m_splitter->addWidget(m_networkDetailWidget); + m_networkDetailWidget->hide(); + // Create and set tree model m_networkStreamModel = new NetworkStreamModel(m_store, this); - ui->topLevelParticlesView->setModel(m_networkStreamModel); - ui->topLevelParticlesView->setIndentation(5); - ui->topLevelParticlesView->setHeaderHidden(true); - ui->topLevelParticlesView->expandAll(); + m_treeView->setModel(m_networkStreamModel); + m_treeView->setIndentation(5); + m_treeView->setHeaderHidden(true); + m_treeView->expandAll(); // Configure column behavior: name stretches, unseen count is fixed width - ui->topLevelParticlesView->header()->setStretchLastSection(false); - ui->topLevelParticlesView->header()->setSectionResizeMode(0, QHeaderView::Stretch); - ui->topLevelParticlesView->header()->setSectionResizeMode(1, QHeaderView::Fixed); - ui->topLevelParticlesView->setColumnWidth(1, 40); + m_treeView->header()->setStretchLastSection(false); + m_treeView->header()->setSectionResizeMode(0, QHeaderView::Stretch); + m_treeView->header()->setSectionResizeMode(1, QHeaderView::Fixed); + m_treeView->setColumnWidth(1, 40); // Create and set list model m_particleListModel = new ParticleListModel(m_store, this); - ui->particlesView->setModel(m_particleListModel); + m_particlesView->setModel(m_particleListModel); // Configure particle list view to use ellipses for long text - ui->particlesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - ui->particlesView->setTextElideMode(Qt::ElideRight); + m_particlesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + m_particlesView->setTextElideMode(Qt::ElideRight); // Connect selection changes - connect(ui->topLevelParticlesView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::onTreeSelectionChanged); // Connect particle selection changes - connect(ui->particlesView->selectionModel(), &QItemSelectionModel::selectionChanged, this, + connect(m_particlesView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::onParticleSelectionChanged); // Install event filter to capture key presses from child views - ui->topLevelParticlesView->installEventFilter(this); - ui->particlesView->installEventFilter(this); + m_treeView->installEventFilter(this); + m_particlesView->installEventFilter(this); } void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) @@ -284,7 +491,10 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI if (selected.indexes().isEmpty()) { m_particleListModel->setStreamId(QString()); - ui->membersList->clear(); + m_membersList->clear(); + m_networkDetailWidget->hide(); + m_contentView->show(); + m_rightSidebar->show(); return; } @@ -292,6 +502,11 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI if (m_networkStreamModel->isStreamItem(index)) { + // Show normal 3-pane view + m_networkDetailWidget->hide(); + m_contentView->show(); + m_rightSidebar->show(); + QString streamId = m_networkStreamModel->streamIdFromIndex(index); m_particleListModel->setStreamId(streamId); m_selectedStreamId = streamId; @@ -302,13 +517,13 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI m_selectedNetworkName = networkId; // Show stream members - ui->membersList->clear(); + m_membersList->clear(); const Stream *stream = m_store->streamById(streamId); if (stream) { for (const QString &email : stream->memberEmails) { - ui->membersList->addItem(email.split("@")[0]); + m_membersList->addItem(email.split("@")[0]); } } @@ -316,13 +531,13 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI int unseenRow = m_particleListModel->firstUnseenRow(); if (unseenRow >= 0) { - ui->particlesView->setCurrentIndex(m_particleListModel->index(unseenRow, 0)); - ui->particlesView->setFocus(); + m_particlesView->setCurrentIndex(m_particleListModel->index(unseenRow, 0)); + m_particlesView->setFocus(); } else if (m_particleListModel->rowCount() > 0) { - ui->particlesView->setCurrentIndex(m_particleListModel->index(m_particleListModel->rowCount() - 1, 0)); - ui->particlesView->setFocus(); + m_particlesView->setCurrentIndex(m_particleListModel->index(m_particleListModel->rowCount() - 1, 0)); + m_particlesView->setFocus(); } } else // network selected @@ -330,10 +545,16 @@ void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QI m_particleListModel->setStreamId(QString()); m_selectedStreamId.clear(); m_selectedParticleRow = -1; - ui->membersList->clear(); + m_membersList->clear(); QString networkId = m_networkStreamModel->networkIdFromIndex(index); m_selectedNetworkName = networkId; + + // Show network detail widget + m_networkDetailWidget->setNetwork(networkId); + m_contentView->hide(); + m_rightSidebar->hide(); + m_networkDetailWidget->show(); } updateStatusBar(); @@ -345,9 +566,9 @@ void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, cons if (selected.indexes().isEmpty()) { - ui->label->setText(""); - ui->label_2->setText(""); - ui->label_3->setText(""); + m_particleContentLabel->setText(""); + m_creatorLabel->setText(""); + m_ackLabel->setText(""); m_selectedParticleId.clear(); m_selectedParticleRow = -1; updateStatusBar(); @@ -359,16 +580,16 @@ void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, cons const Particle *particle = m_particleListModel->particleAtRow(index.row()); if (particle) { - ui->label_2->setText(particle->createdByEmail); + m_creatorLabel->setText(particle->createdByEmail); m_selectedParticleId = particle->id; if (particle->type == "text") { - ui->label->setText(particle->data.value("content").toString()); + m_particleContentLabel->setText(particle->data.value("content").toString()); } else { - ui->label->setText(""); + m_particleContentLabel->setText(""); } // Show ack info @@ -377,11 +598,11 @@ void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, cons QStringList acked; for (const auto &email : particle->ackedByEmails) acked.append(email); - ui->label_3->setText("Acked by: " + acked.join(", ")); + m_ackLabel->setText("Acked by: " + acked.join(", ")); } else { - ui->label_3->setText(""); + m_ackLabel->setText(""); } // Mark seen on selection @@ -532,8 +753,8 @@ void MainWindow::onTextMessageSubmitted(const QString &text) int lastRow = m_particleListModel->rowCount() - 1; if (lastRow >= 0) { - ui->particlesView->setCurrentIndex(m_particleListModel->index(lastRow, 0)); - ui->particlesView->setFocus(); + m_particlesView->setCurrentIndex(m_particleListModel->index(lastRow, 0)); + m_particlesView->setFocus(); } }); } @@ -543,8 +764,7 @@ void MainWindow::mousePressEvent(QMouseEvent *event) if (event->button() == Qt::LeftButton) { // Check if the click is within the top bar widget area - QWidget *topBar = ui->widget; // The top bar widget from the UI - if (topBar && topBar->geometry().contains(event->pos())) + if (m_topBar && m_topBar->geometry().contains(event->pos())) { m_isDragging = true; setCursor(Qt::ClosedHandCursor); diff --git a/src/MainWindow.h b/src/MainWindow.h index 7e0f506..32e4d01 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -15,19 +15,22 @@ class QSystemTrayIcon; class AuthManager; class QItemSelection; class QLabel; +class QSplitter; +class QTreeView; +class QListView; +class QListWidget; +class QToolButton; +class QPushButton; +class QStatusBar; QT_END_NAMESPACE class AuthDialog; class CreateStreamDialog; class MemberPickerDialog; +class NetworkDetailWidget; class SettingsDialog; class TextInputDialog; -namespace Ui -{ -class MainWindow; -} - class MainWindow : public QMainWindow { Q_OBJECT @@ -53,12 +56,34 @@ protected: void mouseReleaseEvent(QMouseEvent *event) override; private: + void buildUi(); void setupTrayIcon(); void setupModels(); QSystemTrayIcon *m_trayIcon; - Ui::MainWindow *ui; + // ── UI widgets ── + QWidget *m_topBar; + QSplitter *m_splitter; + QToolButton *m_settingsButton; + QToolButton *m_exitButton; + QTreeView *m_treeView; + QPushButton *m_newStreamButton; + QLabel *m_particleContentLabel; + QLabel *m_creatorLabel; + QLabel *m_ackLabel; + QListView *m_particlesView; + QListWidget *m_membersList; + QToolButton *m_sendTextButton; + QToolButton *m_addMembersButton; + QStatusBar *m_statusBar; + + QWidget *m_leftSidebar; + QWidget *m_contentView; + QWidget *m_rightSidebar; + NetworkDetailWidget *m_networkDetailWidget; + + // ── Auth & dialogs ── AuthManager *m_authManager; AuthDialog *m_authDialog; diff --git a/src/mainwindow.ui b/src/mainwindow.ui deleted file mode 100644 index 1ffc0a2..0000000 --- a/src/mainwindow.ui +++ /dev/null @@ -1,520 +0,0 @@ - - - MainWindow - - - - 0 - 0 - 650 - 500 - - - - Flowy.llink - - - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Orientation::Horizontal - - - - - 0 - 0 - - - - - 5 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - true - - - - 0 - 0 - - - - - 0 - 20 - - - - - 0 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - ... - - - - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - ... - - - - - - - - - - - - - - 0 - 0 - - - - - - - - New stream - - - - - - false - - - false - - - - - - - - - 0 - 0 - - - - - 300 - 0 - - - - - 2 - - - 0 - - - 0 - - - 0 - - - 2 - - - - - - 0 - 0 - - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 500 - 300 - - - - - 36 - - - - 1 - - - - - - true - - - Qt::AlignmentFlag::AlignCenter - - - true - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - 15 - false - - - - - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - - - - 2 - - - 8 - - - 0 - - - 0 - - - 0 - - - - - - - - - - 0 - 0 - - - - - 2 - - - 2 - - - 2 - - - 2 - - - 2 - - - - - - 0 - 2 - - - - - - - - - 0 - 1 - - - - - - - - - - - - - 153 - 156 - 145 - - - - - - - - - 153 - 156 - 145 - - - - - - - - - Hold space to speak - - - Qt::AlignmentFlag::AlignCenter - - - - - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - - - - Qt::LayoutDirection::RightToLeft - - - - 2 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - ... - - - - - - - - - - ... - - - - - - - - - - ... - - - - - - - - - - ... - - - - - - - - - - ... - - - - - - - - - - - - - - - - - - - 0 - 0 - - - - - - - diff --git a/src/networkdetailwidget.cpp b/src/networkdetailwidget.cpp new file mode 100644 index 0000000..cf27178 --- /dev/null +++ b/src/networkdetailwidget.cpp @@ -0,0 +1,125 @@ +#include "networkdetailwidget.h" +#include "store.h" +#include +#include +#include +#include +#include +#include +#include + +NetworkDetailWidget::NetworkDetailWidget(Store *store, QWidget *parent) + : QWidget(parent), m_store(store) +{ + auto *layout = new QVBoxLayout(this); + layout->setContentsMargins(16, 16, 16, 16); + + // Network name + m_nameLabel = new QLabel(this); + QFont nameFont; + nameFont.setPointSize(24); + m_nameLabel->setFont(nameFont); + layout->addWidget(m_nameLabel); + + // Admin label + m_adminLabel = new QLabel(this); + layout->addWidget(m_adminLabel); + + // Stream capacity progress bar + m_streamBar = new QProgressBar(this); + m_streamBar->setMinimum(0); + m_streamBar->setTextVisible(false); + layout->addWidget(m_streamBar); + + m_streamBarLabel = new QLabel(this); + layout->addWidget(m_streamBarLabel); + + layout->addStretch(); + + // Members header + auto *membersHeader = new QLabel("Members", this); + QFont headerFont; + headerFont.setBold(true); + membersHeader->setFont(headerFont); + layout->addWidget(membersHeader); + + // Members list + m_membersList = new QListWidget(this); + layout->addWidget(m_membersList, 1); + + // Button row + auto *buttonRow = new QHBoxLayout; + m_addMemberBtn = new QPushButton("Add Member", this); + buttonRow->addWidget(m_addMemberBtn); + buttonRow->addStretch(); + layout->addLayout(buttonRow); + + // Admin actions + connect(m_addMemberBtn, &QPushButton::clicked, this, [this]() { + bool ok; + QString email = QInputDialog::getText(this, "Add Member", + "Email address:", QLineEdit::Normal, + QString(), &ok); + if (ok && !email.isEmpty()) + { + auto *op = m_store->addNetworkMembers(m_networkId, {email}); + connect(op, &Operation::success, this, [this](const QJsonDocument &) { + m_store->fetchNetwork(m_networkId); + }); + } + }); + + // Refresh when networks change + connect(m_store, &Store::networksChanged, this, [this]() { + if (!m_networkId.isEmpty()) + refresh(); + }); +} + +void NetworkDetailWidget::setNetwork(const QString &networkId) +{ + m_networkId = networkId; + refresh(); +} + +void NetworkDetailWidget::setCurrentUserEmail(const QString &email) +{ + m_currentUserEmail = email; + if (!m_networkId.isEmpty()) + refresh(); +} + +void NetworkDetailWidget::refresh() +{ + const Network *network = m_store->networkById(m_networkId); + if (!network) + { + m_nameLabel->setText(""); + m_adminLabel->setText(""); + m_streamBar->setValue(0); + m_streamBarLabel->setText(""); + m_membersList->clear(); + m_addMemberBtn->hide(); + return; + } + + m_nameLabel->setText(network->name); + m_adminLabel->setText("Admin: " + network->admin.emailPrefix); + + m_streamBar->setMaximum(network->openStreamCapacity > 0 ? network->openStreamCapacity : 1); + m_streamBar->setValue(network->openStreamCount); + m_streamBarLabel->setText(QString("%1 / %2 open streams") + .arg(network->openStreamCount) + .arg(network->openStreamCapacity)); + + m_membersList->clear(); + for (const auto &member : network->members) + { + m_membersList->addItem(member.email); + } + + // Show admin controls only if current user is admin + bool isAdmin = (!m_currentUserEmail.isEmpty() && + m_currentUserEmail == network->admin.email); + m_addMemberBtn->setVisible(isAdmin); +} diff --git a/src/networkdetailwidget.h b/src/networkdetailwidget.h new file mode 100644 index 0000000..3c1fd20 --- /dev/null +++ b/src/networkdetailwidget.h @@ -0,0 +1,37 @@ +#ifndef NETWORKDETAILWIDGET_H +#define NETWORKDETAILWIDGET_H + +#include + +class Store; +class QLabel; +class QProgressBar; +class QListWidget; +class QPushButton; + +class NetworkDetailWidget : public QWidget +{ + Q_OBJECT + +public: + explicit NetworkDetailWidget(Store *store, QWidget *parent = nullptr); + + void setNetwork(const QString &networkId); + void setCurrentUserEmail(const QString &email); + +private: + void refresh(); + + Store *m_store; + QString m_networkId; + QString m_currentUserEmail; + + QLabel *m_nameLabel; + QLabel *m_adminLabel; + QProgressBar *m_streamBar; + QLabel *m_streamBarLabel; + QListWidget *m_membersList; + QPushButton *m_addMemberBtn; +}; + +#endif // NETWORKDETAILWIDGET_H diff --git a/src/networkstreammodel.cpp b/src/networkstreammodel.cpp index 7a43291..dc929bd 100644 --- a/src/networkstreammodel.cpp +++ b/src/networkstreammodel.cpp @@ -125,14 +125,21 @@ QVariant NetworkStreamModel::data(const QModelIndex &index, int role) const if (networkIndex == 0xFFFFFFFF) { + int actualNetworkIndex = static_cast(id & 0xFFFFFFFF); // This is a network item if (role == Qt::DisplayRole && index.column() == 0) { - int actualNetworkIndex = static_cast(id & 0xFFFFFFFF); if (actualNetworkIndex >= 0 && actualNetworkIndex < m_store->networks().size()) { return m_store->networks().at(actualNetworkIndex).name.toUpper(); } + } else if (role == Qt::DisplayRole && index.column() == 1) + { + if (actualNetworkIndex >= 0 && actualNetworkIndex < m_store->networks().size()) + { + auto net = m_store->networks().at(actualNetworkIndex); + return QString("%1/%2").arg(net.openStreamCount).arg(net.openStreamCapacity); + } } // Networks don't show unseen count in column 1 return QVariant(); diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index ef4af7d..8e694d7 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -1,15 +1,28 @@ #include "settingsdialog.h" #include "authmanager.h" +#include "store.h" #include "ui_settingsdialog.h" -SettingsDialog::SettingsDialog(AuthManager *authManager, QWidget *parent) - : QDialog(parent), m_authManager(authManager), ui(new Ui::SettingsDialog) +SettingsDialog::SettingsDialog(AuthManager *authManager, Store *store, QWidget *parent) + : QDialog(parent), m_authManager(authManager), m_store(store), ui(new Ui::SettingsDialog) { ui->setupUi(this); this->setModal(true); + // Populate list widget with tab items + ui->listWidget->addItem("Account"); + ui->listWidget->addItem("Create Network"); + + // Wire list navigation to stacked widget + connect(ui->listWidget, &QListWidget::currentRowChanged, + ui->stackedWidget, &QStackedWidget::setCurrentIndex); + + // Select first item by default + ui->listWidget->setCurrentRow(0); + connect(ui->signOutButton, &QPushButton::clicked, this, &SettingsDialog::on_signOutButton_clicked); + connect(ui->createNetworkButton, &QPushButton::clicked, this, &SettingsDialog::onCreateNetworkClicked); } SettingsDialog::~SettingsDialog() @@ -22,3 +35,13 @@ void SettingsDialog::on_signOutButton_clicked() m_authManager->signOut(); close(); } + +void SettingsDialog::onCreateNetworkClicked() +{ + QString name = ui->networkNameEdit->text().trimmed(); + if (name.isEmpty()) + return; + + m_store->createNetwork(name); + ui->networkNameEdit->clear(); +} diff --git a/src/settingsdialog.h b/src/settingsdialog.h index e84f495..ba678ad 100644 --- a/src/settingsdialog.h +++ b/src/settingsdialog.h @@ -4,6 +4,7 @@ #include class AuthManager; +class Store; namespace Ui { @@ -15,15 +16,17 @@ class SettingsDialog : public QDialog Q_OBJECT public: - explicit SettingsDialog(AuthManager *, QWidget *parent = nullptr); + explicit SettingsDialog(AuthManager *, Store *, QWidget *parent = nullptr); ~SettingsDialog(); private slots: void on_signOutButton_clicked(); + void onCreateNetworkClicked(); private: Ui::SettingsDialog *ui; AuthManager *m_authManager; + Store *m_store; }; #endif // SETTINGSDIALOG_H diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 74dcdf7..5894b0a 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -11,66 +11,96 @@ - Dialog + Settings - - - - 0 - 0 - 221 - 701 - - - - - - - 220 - 0 - 561 - 701 - - - - - - - 220 - 310 - 85 - 64 - + + + + + + 220 + 16777215 + - - - - - Hello, - - - Qt::AlignmentFlag::AlignCenter - - - - - - - Sign Out - - - true - - - false - - - - - - - + + + + + + + + + Qt::Orientation::Vertical + + + + + + + Hello, + + + Qt::AlignmentFlag::AlignCenter + + + + + + + Sign Out + + + + + + + Qt::Orientation::Vertical + + + + + + + + + + + Create a new network + + + + 16 + + + + + + + + Network name + + + + + + + Create + + + + + + + Qt::Orientation::Vertical + + + + + + + + diff --git a/src/store.cpp b/src/store.cpp index ca50496..a172dec 100644 --- a/src/store.cpp +++ b/src/store.cpp @@ -793,6 +793,8 @@ Operation *Store::markParticlesSeen(const QStringList &particleIds) void Store::setCurrentUserEmail(const QString &email) { m_currentUserEmail = email; } +QString Store::currentUserEmail() const { return m_currentUserEmail; } + Operation *Store::ackParticle(const QString &particleId) { // Optimistic local update diff --git a/src/store.h b/src/store.h index e3ab598..9da2975 100644 --- a/src/store.h +++ b/src/store.h @@ -86,6 +86,7 @@ public: Operation *ackParticle(const QString &particleId); void setCurrentUserEmail(const QString &email); + QString currentUserEmail() const; signals: // ── Data-change signals ──