setup electron app with boilerplate
This commit is contained in:
@@ -0,0 +1,952 @@
|
||||
//
|
||||
// Created by arjun-flowylabs on 1/10/2026.
|
||||
//
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "dialogs/authdialog.h"
|
||||
#include "authmanager.h"
|
||||
#include "dialogs/createstreamdialog.h"
|
||||
#include "dialogs/memberpickerdialog.h"
|
||||
#include "widgets/networkdetailwidget.h"
|
||||
#include "networkmanager.h"
|
||||
#include "dialogs/settingsdialog.h"
|
||||
#include "dialogs/textinputdialog.h"
|
||||
#include <QApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QHeaderView>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QLabel>
|
||||
#include <QListView>
|
||||
#include <QListWidget>
|
||||
#include <QMenu>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QJsonObject>
|
||||
#include <QPushButton>
|
||||
#include <QSplitter>
|
||||
#include <QStatusBar>
|
||||
#include <QToolButton>
|
||||
#include <QTreeView>
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QTimer>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include "media/mediahandler.h"
|
||||
#include "dialogs/mediapreviewdialog.h"
|
||||
#include "mediaparticlegenerator.h"
|
||||
#include "widgets/textparticlewidget.h"
|
||||
#include "widgets/mediaparticlewidget.h"
|
||||
#include <QStackedWidget>
|
||||
#include <QUrl>
|
||||
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), m_trayIcon(nullptr), m_networkDetailWidget(nullptr),
|
||||
m_authManager(new AuthManager(this)), m_authDialog(nullptr), m_settingsDialog(nullptr),
|
||||
m_textInputDialog(nullptr), m_createStreamDialog(nullptr),
|
||||
m_store(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr),
|
||||
m_selectedParticleRow(-1), m_isDragging(false), m_media{new MediaHandler{this}},
|
||||
m_spaceHeld(false)
|
||||
{
|
||||
m_authDialog = new AuthDialog(this);
|
||||
m_textInputDialog = new TextInputDialog(this);
|
||||
connect(m_textInputDialog, &TextInputDialog::textSubmitted, this, &MainWindow::onTextMessageSubmitted);
|
||||
|
||||
buildUi();
|
||||
|
||||
m_topBar->setCursor(Qt::OpenHandCursor);
|
||||
|
||||
// Enable frameless window for edge-to-edge experience
|
||||
setWindowFlags(Qt::FramelessWindowHint | Qt::Window);
|
||||
setAttribute(Qt::WA_TranslucentBackground, false);
|
||||
|
||||
// Style for grey status bar text
|
||||
QString statusBarLabelStyle = "QLabel { color: #808080; }";
|
||||
QString pingLabelStyle = "QLabel { color: #00AA00; }"; // Green for ping
|
||||
|
||||
m_currentPathLabel = new QLabel(this);
|
||||
m_currentPathLabel->setMargin(5);
|
||||
m_currentPathLabel->setStyleSheet(statusBarLabelStyle);
|
||||
QFont font = QFont();
|
||||
font.setStyle(QFont::StyleItalic);
|
||||
m_currentPathLabel->setFont(font);
|
||||
m_statusBar->addWidget(m_currentPathLabel);
|
||||
|
||||
// Add spacer to push right-aligned items to the right
|
||||
m_statusBar->addWidget(new QLabel(""), 1);
|
||||
|
||||
// Ping indicator (green text)
|
||||
m_pingLabel = new QLabel(this);
|
||||
m_pingLabel->setMargin(5);
|
||||
m_pingLabel->setMinimumWidth(100);
|
||||
m_pingLabel->setAlignment(Qt::AlignCenter);
|
||||
m_pingLabel->setStyleSheet(pingLabelStyle);
|
||||
m_statusBar->addPermanentWidget(m_pingLabel);
|
||||
|
||||
// Position indicator (vim-style)
|
||||
m_positionLabel = new QLabel(this);
|
||||
m_positionLabel->setMargin(5);
|
||||
m_positionLabel->setMinimumWidth(50);
|
||||
m_positionLabel->setAlignment(Qt::AlignCenter);
|
||||
m_positionLabel->setStyleSheet(statusBarLabelStyle);
|
||||
m_statusBar->addPermanentWidget(m_positionLabel);
|
||||
|
||||
// Storage indicator
|
||||
m_storageLabel = new QLabel(this);
|
||||
m_storageLabel->setMargin(5);
|
||||
m_storageLabel->setMinimumWidth(60);
|
||||
m_storageLabel->setAlignment(Qt::AlignCenter);
|
||||
m_storageLabel->setStyleSheet(statusBarLabelStyle);
|
||||
m_statusBar->addPermanentWidget(m_storageLabel);
|
||||
|
||||
// Setup ping timer for periodic server connectivity check
|
||||
m_pingTimer = new QTimer(this);
|
||||
connect(m_pingTimer, &QTimer::timeout, this, []() {
|
||||
NetworkManager::instance().ping();
|
||||
});
|
||||
connect(&NetworkManager::instance(), &NetworkManager::pingResult, this, [this](int ms) {
|
||||
m_pingLabel->setText("connected " + QString::number(ms) + "ms");
|
||||
m_pingLabel->setStyleSheet("QLabel { color: #00AA00; }");
|
||||
});
|
||||
connect(&NetworkManager::instance(), &NetworkManager::pingFailed, this, [this]() {
|
||||
m_pingLabel->setText("disconnected");
|
||||
m_pingLabel->setStyleSheet("QLabel { color: #AA0000; }");
|
||||
});
|
||||
m_pingTimer->start(10000);
|
||||
NetworkManager::instance().ping();
|
||||
|
||||
m_splitter->setStretchFactor(0, 1);
|
||||
m_splitter->setStretchFactor(1, 4);
|
||||
m_splitter->setStretchFactor(2, 1);
|
||||
|
||||
setupModels();
|
||||
|
||||
m_authDialog->setModal(true);
|
||||
|
||||
connect(m_authManager, &AuthManager::codeSentToEmail, m_authDialog, &AuthDialog::onCodeSentToEmail);
|
||||
connect(m_authManager, &AuthManager::errorOccurred, m_authDialog, &AuthDialog::showError);
|
||||
connect(m_authManager, &AuthManager::signedIn, m_authDialog, &AuthDialog::hide);
|
||||
connect(m_authDialog, &AuthDialog::codeRequested, m_authManager, &AuthManager::requestSignInCode);
|
||||
connect(m_authDialog, &AuthDialog::signInRequested, m_authManager, &AuthManager::signIn);
|
||||
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, &MainWindow::onSignedIn);
|
||||
connect(m_store, &Store::startupDataLoaded, m_treeView, &QTreeView::expandAll);
|
||||
connect(m_store, &Store::particleUpdated, this, &MainWindow::onParticleUpdated);
|
||||
|
||||
// DocumentSend toolbar button opens text input dialog
|
||||
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(m_newStreamButton, &QPushButton::clicked, this, &MainWindow::onNewStreamClicked);
|
||||
connect(m_createStreamDialog, &CreateStreamDialog::streamRequested, this, &MainWindow::onCreateStreamRequested);
|
||||
|
||||
// "Add members" toolbar button (ListAdd icon in right sidebar)
|
||||
connect(m_addMembersButton, &QToolButton::clicked, this, &MainWindow::onAddMembersClicked);
|
||||
|
||||
m_authManager->tryRestoreSession();
|
||||
|
||||
connect(m_media, &MediaHandler::errorOccurred, this, [this](const QString &message) {
|
||||
m_statusBar->showMessage(message);
|
||||
});
|
||||
|
||||
m_mediaPreviewDialog = new MediaPreviewDialog(this);
|
||||
m_mediaParticleGenerator = new MediaParticleGenerator(m_store, this);
|
||||
|
||||
connect(m_media, &MediaHandler::recordingComplete, this,
|
||||
[this](const QUrl &url, qint64 durationMs, const QString &mimeType) {
|
||||
m_media->stop();
|
||||
m_recordedFileUrl = url;
|
||||
m_recordedDurationMs = durationMs;
|
||||
m_recordedMimeType = mimeType;
|
||||
m_mediaPreviewDialog->showPreview(url);
|
||||
});
|
||||
|
||||
connect(m_mediaPreviewDialog, &MediaPreviewDialog::spaceReleased, this, [this]() {
|
||||
m_spaceHeld = false;
|
||||
m_media->recordStop();
|
||||
});
|
||||
|
||||
connect(m_mediaPreviewDialog, &QDialog::accepted, this, [this]() {
|
||||
m_mediaPreviewDialog->reset();
|
||||
if (m_selectedStreamId.isEmpty() || m_selectedNetworkId.isEmpty()) return;
|
||||
m_mediaParticleGenerator->generate(
|
||||
m_recordedFileUrl, m_recordedMimeType,
|
||||
m_selectedNetworkId, m_recordedDurationMs, m_selectedStreamId);
|
||||
});
|
||||
|
||||
connect(m_mediaPreviewDialog, &QDialog::rejected, this, [this]() {
|
||||
m_mediaPreviewDialog->reset();
|
||||
});
|
||||
|
||||
connect(m_mediaParticleGenerator, &MediaParticleGenerator::errorOccurred, this,
|
||||
[this](const QString &msg) { m_statusBar->showMessage(msg); });
|
||||
|
||||
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(350);
|
||||
auto *contentLayout = new QVBoxLayout(m_contentView);
|
||||
contentLayout->setSpacing(2);
|
||||
contentLayout->setContentsMargins(0, 0, 0, 2);
|
||||
|
||||
// Stacked widget for particle type-specific content
|
||||
m_contentStack = new QStackedWidget();
|
||||
m_contentStack->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
|
||||
m_textParticleWidget = new TextParticleWidget();
|
||||
m_mediaParticleWidget = new MediaParticleWidget();
|
||||
|
||||
m_contentStack->addWidget(m_textParticleWidget);
|
||||
m_contentStack->addWidget(m_mediaParticleWidget);
|
||||
|
||||
contentLayout->addWidget(m_contentStack);
|
||||
|
||||
m_creatorLabel = new QLabel();
|
||||
QFont creatorFont;
|
||||
creatorFont.setPointSize(15);
|
||||
m_creatorLabel->setFont(creatorFont);
|
||||
m_creatorLabel->setAlignment(Qt::AlignCenter);
|
||||
contentLayout->addWidget(m_creatorLabel);
|
||||
|
||||
m_ackLabel = new QLabel();
|
||||
m_ackLabel->setAlignment(Qt::AlignCenter);
|
||||
contentLayout->addWidget(m_ackLabel);
|
||||
|
||||
// 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));
|
||||
editClearButton->setToolTip(QStringLiteral("Close stream"));
|
||||
rtlToolbarLayout->addWidget(editClearButton);
|
||||
|
||||
m_addMembersButton = new QToolButton();
|
||||
m_addMembersButton->setText("...");
|
||||
m_addMembersButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::ListAdd));
|
||||
m_addMembersButton->setToolTip(QStringLiteral("Add members to stream"));
|
||||
rtlToolbarLayout->addWidget(m_addMembersButton);
|
||||
|
||||
auto *insertImageButton = new QToolButton();
|
||||
insertImageButton->setText("...");
|
||||
insertImageButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::InsertImage));
|
||||
insertImageButton->setToolTip(QStringLiteral("Attach files"));
|
||||
rtlToolbarLayout->addWidget(insertImageButton);
|
||||
|
||||
m_sendTextButton = new QToolButton();
|
||||
m_sendTextButton->setText("...");
|
||||
m_sendTextButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::DocumentSend));
|
||||
m_sendTextButton->setToolTip(QStringLiteral("Send text message"));
|
||||
rtlToolbarLayout->addWidget(m_sendTextButton);
|
||||
|
||||
auto *cameraVideoButton = new QToolButton();
|
||||
cameraVideoButton->setText("...");
|
||||
cameraVideoButton->setIcon(QIcon::fromTheme(QIcon::ThemeIcon::CameraVideo));
|
||||
cameraVideoButton->setToolTip(QStringLiteral("Toggle camera mode"));
|
||||
rtlToolbarLayout->addWidget(cameraVideoButton);
|
||||
|
||||
rightLayout->addWidget(rtlToolbar);
|
||||
|
||||
m_splitter->addWidget(m_rightSidebar);
|
||||
|
||||
QFrame* hSeparator = new QFrame();
|
||||
hSeparator->setFrameShape(QFrame::HLine);
|
||||
hSeparator->setFrameShadow(QFrame::Plain);
|
||||
hSeparator->setStyleSheet("color: rgba(128, 128, 128, 0.3);");
|
||||
hSeparator->setFixedHeight(1);
|
||||
mainLayout->addWidget(hSeparator);
|
||||
|
||||
// ── Network detail widget (hidden by default) ──
|
||||
|
||||
m_statusBar = new QStatusBar();
|
||||
setStatusBar(m_statusBar);
|
||||
}
|
||||
|
||||
void MainWindow::setupTrayIcon()
|
||||
{
|
||||
m_trayIcon = new QSystemTrayIcon(this);
|
||||
m_trayIcon->setIcon(QIcon(":/assets/logo.png"));
|
||||
m_trayIcon->setToolTip("llink");
|
||||
|
||||
QMenu *trayMenu = new QMenu(this);
|
||||
trayMenu->addAction("Show", this, &MainWindow::show);
|
||||
trayMenu->addAction("Quit", qApp, &QApplication::quit);
|
||||
m_trayIcon->setContextMenu(trayMenu);
|
||||
|
||||
connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) {
|
||||
if (reason == QSystemTrayIcon::Trigger)
|
||||
{
|
||||
isVisible() ? hide() : show();
|
||||
}
|
||||
});
|
||||
|
||||
m_trayIcon->show();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
if (m_spaceHeld)
|
||||
{
|
||||
m_media->stop();
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_exitButton_clicked()
|
||||
{
|
||||
qApp->exit(0);
|
||||
}
|
||||
|
||||
void MainWindow::on_settingsButton_clicked()
|
||||
{
|
||||
if (!m_settingsDialog)
|
||||
{
|
||||
m_settingsDialog = new SettingsDialog(this);
|
||||
connect(m_settingsDialog, &SettingsDialog::signOutRequested, m_authManager, &AuthManager::signOut);
|
||||
connect(m_settingsDialog, &SettingsDialog::createNetworkRequested, this, [this](const QString &name) {
|
||||
m_store->createNetwork(name);
|
||||
});
|
||||
}
|
||||
if (m_authManager->sessionData())
|
||||
{
|
||||
m_settingsDialog->setAuthLabel("Hello, " + m_authManager->sessionData()->emailPrefix);
|
||||
}
|
||||
m_settingsDialog->show();
|
||||
}
|
||||
|
||||
void MainWindow::setupModels()
|
||||
{
|
||||
// Create store (data loaded after sign-in via loadStartupData)
|
||||
m_store = new Store(this);
|
||||
|
||||
m_networkDetailWidget = new NetworkDetailWidget(this);
|
||||
m_splitter->addWidget(m_networkDetailWidget);
|
||||
m_splitter->setStretchFactor(3, 4);
|
||||
m_networkDetailWidget->hide();
|
||||
|
||||
connect(m_store, &Store::networksChanged, this, &MainWindow::onNetworksChanged);
|
||||
connect(m_networkDetailWidget, &NetworkDetailWidget::addMemberRequested, this, &MainWindow::onAddNetworkMemberRequested);
|
||||
|
||||
// Create and set tree model
|
||||
m_networkStreamModel = new NetworkStreamModel(m_store, this);
|
||||
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
|
||||
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);
|
||||
m_particlesView->setModel(m_particleListModel);
|
||||
|
||||
// Configure particle list view to use ellipses for long text
|
||||
m_particlesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
m_particlesView->setTextElideMode(Qt::ElideRight);
|
||||
|
||||
// Connect selection changes
|
||||
connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||
&MainWindow::onTreeSelectionChanged);
|
||||
|
||||
// Connect particle selection changes
|
||||
connect(m_particlesView->selectionModel(), &QItemSelectionModel::selectionChanged, this,
|
||||
&MainWindow::onParticleSelectionChanged);
|
||||
|
||||
// Install event filter to capture key presses from child views
|
||||
m_treeView->installEventFilter(this);
|
||||
m_particlesView->installEventFilter(this);
|
||||
}
|
||||
|
||||
void MainWindow::onSignedIn()
|
||||
{
|
||||
m_store->setCurrentUserEmail(m_authManager->sessionData()->email);
|
||||
m_store->loadStartupData();
|
||||
}
|
||||
|
||||
void MainWindow::onParticleUpdated(const QString &streamId, const QString &particleId)
|
||||
{
|
||||
Q_UNUSED(streamId);
|
||||
|
||||
if (particleId != m_selectedParticleId)
|
||||
return;
|
||||
|
||||
const Particle *particle = m_store->particleById(particleId);
|
||||
if (particle && !particle->ackedByEmails.empty())
|
||||
{
|
||||
QStringList acked;
|
||||
for (const auto &email : particle->ackedByEmails)
|
||||
acked.append(email);
|
||||
m_ackLabel->setText("Acked by: " + acked.join(", "));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ackLabel->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onNewStreamClicked()
|
||||
{
|
||||
if (m_selectedNetworkId.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const Network *network = m_store->networkById(m_selectedNetworkId);
|
||||
if (network)
|
||||
{
|
||||
m_createStreamDialog->setNetworkMembers(network->members);
|
||||
}
|
||||
m_createStreamDialog->show();
|
||||
m_createStreamDialog->raise();
|
||||
m_createStreamDialog->activateWindow();
|
||||
}
|
||||
|
||||
void MainWindow::onCreateStreamRequested(const QString &name, const QString &visibility, const QStringList &members)
|
||||
{
|
||||
m_store->createStream(m_selectedNetworkId, name, "", visibility, members);
|
||||
}
|
||||
|
||||
void MainWindow::onAddMembersClicked()
|
||||
{
|
||||
if (m_selectedStreamId.isEmpty() || m_selectedNetworkId.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
const Network *network = m_store->networkById(m_selectedNetworkId);
|
||||
const Stream *stream = m_store->streamById(m_selectedStreamId);
|
||||
if (!network || !stream)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
MemberPickerDialog picker("Add Members", this);
|
||||
picker.setMembers(network->members, stream->memberEmails);
|
||||
|
||||
if (picker.exec() == QDialog::Accepted)
|
||||
{
|
||||
QStringList emails = picker.selectedEmails();
|
||||
if (!emails.isEmpty())
|
||||
{
|
||||
auto *op = m_store->addStreamMembers(m_selectedStreamId, emails);
|
||||
connect(op, &Operation::success, this, [this](const QJsonDocument &) {
|
||||
auto *fetchOp = m_store->fetchStream(m_selectedStreamId);
|
||||
connect(fetchOp, &Operation::success, this, [this](const QJsonDocument &) {
|
||||
m_membersList->clear();
|
||||
const Stream *s = m_store->streamById(m_selectedStreamId);
|
||||
if (s)
|
||||
{
|
||||
for (const QString &email : s->memberEmails)
|
||||
{
|
||||
m_membersList->addItem(email.split("@")[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onNetworksChanged()
|
||||
{
|
||||
if (!m_selectedNetworkId.isEmpty() && m_networkDetailWidget->isVisible())
|
||||
{
|
||||
const Network *network = m_store->networkById(m_selectedNetworkId);
|
||||
if (network)
|
||||
{
|
||||
m_networkDetailWidget->setNetwork(*network, m_authManager->sessionData() ? m_authManager->sessionData()->email : QString());
|
||||
}
|
||||
else
|
||||
{
|
||||
m_networkDetailWidget->clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MainWindow::onAddNetworkMemberRequested(const QString &networkId, const QString &email)
|
||||
{
|
||||
auto *op = m_store->addNetworkMembers(networkId, {email});
|
||||
connect(op, &Operation::success, this, [this, networkId](const QJsonDocument &) {
|
||||
m_store->fetchNetwork(networkId);
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::onTreeSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
|
||||
{
|
||||
Q_UNUSED(deselected);
|
||||
|
||||
if (selected.indexes().isEmpty())
|
||||
{
|
||||
m_particleListModel->setStreamId(QString());
|
||||
m_membersList->clear();
|
||||
m_networkDetailWidget->hide();
|
||||
m_contentView->show();
|
||||
m_rightSidebar->show();
|
||||
return;
|
||||
}
|
||||
|
||||
QModelIndex index = selected.indexes().first();
|
||||
|
||||
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;
|
||||
|
||||
// Also track the parent network
|
||||
QModelIndex parentIndex = m_networkStreamModel->parent(index);
|
||||
QString networkId = m_networkStreamModel->networkIdFromIndex(parentIndex);
|
||||
m_selectedNetworkId = networkId;
|
||||
|
||||
// Show stream members
|
||||
m_membersList->clear();
|
||||
const Stream *stream = m_store->streamById(streamId);
|
||||
if (stream)
|
||||
{
|
||||
for (const QString &email : stream->memberEmails)
|
||||
{
|
||||
m_membersList->addItem(email.split("@")[0]);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-select first unseen particle (or first particle if all seen)
|
||||
int unseenRow = m_particleListModel->firstUnseenRow();
|
||||
if (unseenRow >= 0)
|
||||
{
|
||||
m_particlesView->setCurrentIndex(m_particleListModel->index(unseenRow, 0));
|
||||
m_particlesView->setFocus();
|
||||
}
|
||||
else if (m_particleListModel->rowCount() > 0)
|
||||
{
|
||||
m_particlesView->setCurrentIndex(m_particleListModel->index(m_particleListModel->rowCount() - 1, 0));
|
||||
m_particlesView->setFocus();
|
||||
}
|
||||
}
|
||||
else // network selected
|
||||
{
|
||||
m_particleListModel->setStreamId(QString());
|
||||
m_selectedStreamId.clear();
|
||||
m_selectedParticleRow = -1;
|
||||
m_membersList->clear();
|
||||
|
||||
QString networkId = m_networkStreamModel->networkIdFromIndex(index);
|
||||
m_selectedNetworkId = networkId;
|
||||
|
||||
// Show network detail widget
|
||||
const Network *network = m_store->networkById(networkId);
|
||||
if (network)
|
||||
{
|
||||
m_networkDetailWidget->setNetwork(*network, m_authManager->sessionData() ? m_authManager->sessionData()->email : QString());
|
||||
}
|
||||
else
|
||||
{
|
||||
m_networkDetailWidget->clear();
|
||||
}
|
||||
m_contentView->hide();
|
||||
m_rightSidebar->hide();
|
||||
m_networkDetailWidget->show();
|
||||
}
|
||||
|
||||
updateStatusBar();
|
||||
}
|
||||
|
||||
void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
|
||||
{
|
||||
Q_UNUSED(deselected);
|
||||
|
||||
if (selected.indexes().isEmpty())
|
||||
{
|
||||
m_textParticleWidget->clear();
|
||||
m_mediaParticleWidget->clear();
|
||||
m_creatorLabel->setText("");
|
||||
m_ackLabel->setText("");
|
||||
m_selectedParticleId.clear();
|
||||
m_selectedParticleRow = -1;
|
||||
updateStatusBar();
|
||||
return;
|
||||
}
|
||||
|
||||
QModelIndex index = selected.indexes().first();
|
||||
m_selectedParticleRow = index.row();
|
||||
const Particle *particle = m_particleListModel->particleAtRow(index.row());
|
||||
if (particle)
|
||||
{
|
||||
m_creatorLabel->setText(particle->createdByEmail);
|
||||
m_selectedParticleId = particle->id;
|
||||
|
||||
if (particle->type == "text")
|
||||
{
|
||||
m_mediaParticleWidget->clear();
|
||||
m_textParticleWidget->setParticle(particle);
|
||||
m_contentStack->setCurrentWidget(m_textParticleWidget);
|
||||
}
|
||||
else if (particle->type == "media")
|
||||
{
|
||||
m_textParticleWidget->clear();
|
||||
m_mediaParticleWidget->setParticle(particle);
|
||||
m_contentStack->setCurrentWidget(m_mediaParticleWidget);
|
||||
}
|
||||
|
||||
// Show ack info
|
||||
if (!particle->ackedByEmails.empty())
|
||||
{
|
||||
QStringList acked;
|
||||
for (const auto &email : particle->ackedByEmails)
|
||||
{
|
||||
acked.append(email);
|
||||
}
|
||||
m_ackLabel->setText("Acked by: " + acked.join(", "));
|
||||
}
|
||||
else
|
||||
{
|
||||
m_ackLabel->setText("");
|
||||
}
|
||||
|
||||
// Mark seen on selection
|
||||
if (!particle->seen)
|
||||
{
|
||||
m_store->markParticleSeen(particle->id);
|
||||
}
|
||||
}
|
||||
|
||||
updateStatusBar();
|
||||
}
|
||||
|
||||
void MainWindow::updateStatusBar()
|
||||
{
|
||||
QStringList pathComponents;
|
||||
|
||||
// Add network if selected
|
||||
if (!m_selectedNetworkId.isEmpty())
|
||||
{
|
||||
const Network *network = m_store->networkById(m_selectedNetworkId);
|
||||
if (network)
|
||||
{
|
||||
pathComponents << network->name;
|
||||
}
|
||||
}
|
||||
|
||||
// Add stream if selected
|
||||
if (!m_selectedStreamId.isEmpty())
|
||||
{
|
||||
const Stream *stream = m_store->streamById(m_selectedStreamId);
|
||||
if (stream)
|
||||
{
|
||||
pathComponents << stream->name;
|
||||
}
|
||||
}
|
||||
|
||||
// Add particle if selected
|
||||
if (!m_selectedParticleId.isEmpty())
|
||||
{
|
||||
const Particle *particle = m_store->particleById(m_selectedParticleId);
|
||||
if (particle)
|
||||
{
|
||||
pathComponents << "p_" + particle->id.right(5);
|
||||
}
|
||||
}
|
||||
|
||||
m_currentPathLabel->setText(pathComponents.join(" > "));
|
||||
|
||||
// Update position indicator (vim-style)
|
||||
if (!m_selectedStreamId.isEmpty())
|
||||
{
|
||||
int totalCount = m_particleListModel->rowCount();
|
||||
if (totalCount == 0)
|
||||
{
|
||||
m_positionLabel->setText("");
|
||||
}
|
||||
else if (totalCount == 1)
|
||||
{
|
||||
m_positionLabel->setText("All");
|
||||
}
|
||||
else if (m_selectedParticleRow == 0)
|
||||
{
|
||||
m_positionLabel->setText("Top");
|
||||
}
|
||||
else if (m_selectedParticleRow == totalCount - 1)
|
||||
{
|
||||
m_positionLabel->setText("Bot");
|
||||
}
|
||||
else if (m_selectedParticleRow >= 0)
|
||||
{
|
||||
int percent = (m_selectedParticleRow * 100) / (totalCount - 1);
|
||||
m_positionLabel->setText(QString::number(percent) + "%");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_positionLabel->setText("");
|
||||
}
|
||||
|
||||
// Update data size indicator for selected particle
|
||||
if (!m_selectedParticleId.isEmpty())
|
||||
{
|
||||
const Particle *particle = m_store->particleById(m_selectedParticleId);
|
||||
if (particle)
|
||||
{
|
||||
QJsonObject dataObj;
|
||||
for (auto it = particle->data.constBegin(); it != particle->data.constEnd(); ++it)
|
||||
{
|
||||
dataObj.insert(it.key(), QJsonValue::fromVariant(it.value()));
|
||||
}
|
||||
int bytes = QJsonDocument(dataObj).toJson(QJsonDocument::Compact).size();
|
||||
if (bytes < 1024)
|
||||
m_storageLabel->setText(QString::number(bytes) + " B");
|
||||
else
|
||||
m_storageLabel->setText(QString::number(bytes / 1024) + " kB");
|
||||
}
|
||||
else
|
||||
{
|
||||
m_storageLabel->setText("");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_storageLabel->setText("");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
m_positionLabel->setText("");
|
||||
m_storageLabel->setText("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void MainWindow::showTextInputDialog()
|
||||
{
|
||||
if (m_selectedStreamId.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
m_textInputDialog->show();
|
||||
m_textInputDialog->raise();
|
||||
m_textInputDialog->activateWindow();
|
||||
}
|
||||
|
||||
bool MainWindow::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress)
|
||||
{
|
||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
if (keyEvent->key() == Qt::Key_Space && !keyEvent->isAutoRepeat() && !m_spaceHeld)
|
||||
{
|
||||
if (!m_selectedNetworkId.isEmpty() && !m_selectedStreamId.isEmpty())
|
||||
{
|
||||
m_spaceHeld = true;
|
||||
m_mediaPreviewDialog->showRecording();
|
||||
m_media->startRecording(m_mediaPreviewDialog->videoSink());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (keyEvent->key() == Qt::Key_T && keyEvent->modifiers() == Qt::NoModifier && !m_selectedStreamId.isEmpty())
|
||||
{
|
||||
showTextInputDialog();
|
||||
return true;
|
||||
}
|
||||
if (keyEvent->key() == Qt::Key_A && keyEvent->modifiers() == Qt::NoModifier && !m_selectedParticleId.isEmpty())
|
||||
{
|
||||
m_store->ackParticle(m_selectedParticleId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (event->type() == QEvent::KeyRelease)
|
||||
{
|
||||
auto *keyEvent = static_cast<QKeyEvent *>(event);
|
||||
if (keyEvent->key() == Qt::Key_Space && !keyEvent->isAutoRepeat() && m_spaceHeld)
|
||||
{
|
||||
m_spaceHeld = false;
|
||||
m_media->recordStop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return QMainWindow::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void MainWindow::onTextMessageSubmitted(const QString &text)
|
||||
{
|
||||
if (m_selectedStreamId.isEmpty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
auto *op = m_store->createParticle(m_selectedStreamId, "text", {{"content", text}});
|
||||
connect(op, &Operation::success, this, [this](const QJsonDocument &) {
|
||||
int lastRow = m_particleListModel->rowCount() - 1;
|
||||
if (lastRow >= 0)
|
||||
{
|
||||
m_particlesView->setCurrentIndex(m_particleListModel->index(lastRow, 0));
|
||||
m_particlesView->setFocus();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::keyPressEvent(QKeyEvent *event)
|
||||
{
|
||||
QMainWindow::keyPressEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::keyReleaseEvent(QKeyEvent *event)
|
||||
{
|
||||
QMainWindow::keyReleaseEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
// Check if the click is within the top bar widget area
|
||||
if (m_topBar && m_topBar->geometry().contains(event->pos()))
|
||||
{
|
||||
m_isDragging = true;
|
||||
setCursor(Qt::ClosedHandCursor);
|
||||
m_dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
}
|
||||
QMainWindow::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
if (m_isDragging && (event->buttons() & Qt::LeftButton))
|
||||
{
|
||||
move(event->globalPosition().toPoint() - m_dragPosition);
|
||||
event->accept();
|
||||
return;
|
||||
}
|
||||
QMainWindow::mouseMoveEvent(event);
|
||||
}
|
||||
|
||||
void MainWindow::mouseReleaseEvent(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::LeftButton)
|
||||
{
|
||||
m_isDragging = false;
|
||||
unsetCursor();
|
||||
}
|
||||
QMainWindow::mouseReleaseEvent(event);
|
||||
}
|
||||
Reference in New Issue
Block a user