// // Created by arjun-flowylabs on 1/10/2026. // #include "MainWindow.h" #include "authdialog.h" #include "authmanager.h" #include "settingsdialog.h" #include "ui_mainwindow.h" #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_data(nullptr), m_networkStreamModel(nullptr), m_particleListModel(nullptr), m_selectedParticleRow(-1), m_isDragging(false) { m_authDialog = new AuthDialog(m_authManager, this); ui = new Ui::MainWindow; ui->setupUi(this); QWidget *topBar = ui->widget; 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); ui->statusbar->addWidget(m_currentPathLabel); // Add spacer to push right-aligned items to the right ui->statusbar->addPermanentWidget(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); ui->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); ui->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); ui->statusbar->addPermanentWidget(m_storageLabel); // Setup ping timer to oscillate every 500ms m_pingTimer = new QTimer(this); connect(m_pingTimer, &QTimer::timeout, this, &MainWindow::updatePingIndicator); m_pingTimer->start(500); updatePingIndicator(); ui->splitter->setStretchFactor(0, 1); ui->splitter->setStretchFactor(1, 3); ui->splitter->setStretchFactor(2, 1); setupModels(); m_authDialog->setModal(true); QTimer::singleShot(3000, this, [this]() { if (!m_authManager->isSignedIn()) { m_authDialog->reset(); m_authDialog->show(); } }); // Connect signal so dialog hides if valid session found, shows and resets on sign out connect(m_authManager, &AuthManager::isSignedInChanged, this, [this]() { if (m_authManager->isSignedIn()) { m_authDialog->hide(); } else { m_authDialog->reset(); m_authDialog->show(); } }); // Check for cached session (async) m_authManager->init(); setupTrayIcon(); } 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() { delete ui; } void MainWindow::on_exitButton_clicked() { qApp->exit(0); } void MainWindow::on_settingsButton_clicked() { if (!m_settingsDialog) { m_settingsDialog = new SettingsDialog(m_authManager, this); } m_settingsDialog->show(); } void MainWindow::setupModels() { // Create data with fake data m_data = new Data(this); // Create and set tree model m_networkStreamModel = new NetworkStreamModel(m_data, this); ui->topLevelParticlesView->setModel(m_networkStreamModel); ui->topLevelParticlesView->setIndentation(5); ui->topLevelParticlesView->setHeaderHidden(true); ui->topLevelParticlesView->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); // Create and set list model m_particleListModel = new ParticleListModel(m_data, this); ui->particlesView->setModel(m_particleListModel); // Configure particle list view to use ellipses for long text ui->particlesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ui->particlesView->setTextElideMode(Qt::ElideRight); // Connect selection changes connect(ui->topLevelParticlesView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::onTreeSelectionChanged); // Connect particle selection changes 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) { Q_UNUSED(deselected); if (selected.indexes().isEmpty()) { m_particleListModel->setStreamId(QString()); return; } QModelIndex index = selected.indexes().first(); if (m_networkStreamModel->isStreamItem(index)) { 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_selectedNetworkName = networkId; // Auto-select first unseen particle (or first particle if all seen) int unseenRow = m_particleListModel->firstUnseenRow(); if (unseenRow >= 0) { ui->particlesView->setCurrentIndex(m_particleListModel->index(unseenRow, 0)); ui->particlesView->setFocus(); } else if (m_particleListModel->rowCount() > 0) { ui->particlesView->setCurrentIndex(m_particleListModel->index(m_particleListModel->rowCount() - 1, 0)); ui->particlesView->setFocus(); } } else // network selected { m_particleListModel->setStreamId(QString()); m_selectedStreamId.clear(); m_selectedParticleRow = -1; QString networkId = m_networkStreamModel->networkIdFromIndex(index); m_selectedNetworkName = networkId; } updateStatusBar(); } void MainWindow::onParticleSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected) { Q_UNUSED(deselected); if (selected.indexes().isEmpty()) { ui->label->setText(""); ui->label_2->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) { // ui->label->setText(particle->summary); ui->label_2->setText(particle->createdBy); m_selectedParticleId = particle->id; } updateStatusBar(); } void MainWindow::updateStatusBar() { QStringList pathComponents; // Add network if selected if (!m_selectedNetworkName.isEmpty()) { for (const auto &network : m_data->networks()) { if (network.id == m_selectedNetworkName) { pathComponents << network.name; break; } } } // Add stream if selected if (!m_selectedStreamId.isEmpty() && !m_selectedNetworkName.isEmpty()) { const QList &streams = m_data->streamsForNetwork(m_selectedNetworkName); for (const auto &stream : streams) { if (stream.id == m_selectedStreamId) { pathComponents << stream.name; break; } } } // Add particle if selected if (!m_selectedParticleId.isEmpty() && !m_selectedStreamId.isEmpty()) { const QList &particles = m_data->particlesForStream(m_selectedStreamId); for (const auto &particle : particles) { if (particle.id == m_selectedParticleId) { pathComponents << particle.id; break; } } } 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 storage indicator with pseudo-random size based on stream ID uint hash = qHash(m_selectedStreamId); int sizeKB = 50 + (hash % 950); // Random size between 50-1000 kB m_storageLabel->setText(QString::number(sizeKB) + " kB"); } else { m_positionLabel->setText(""); m_storageLabel->setText(""); } } void MainWindow::updatePingIndicator() { // Oscillate ping between 15-35ms static int basePing = 23; 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"); } 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())) { 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); }