403 lines
11 KiB
C++
403 lines
11 KiB
C++
//
|
|
// 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 <QApplication>
|
|
#include <QHeaderView>
|
|
#include <QItemSelectionModel>
|
|
#include <QLabel>
|
|
#include <QMenu>
|
|
#include <QMouseEvent>
|
|
#include <QRandomGenerator>
|
|
#include <QSystemTrayIcon>
|
|
#include <QTimer>
|
|
|
|
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);
|
|
|
|
// 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, 1);
|
|
ui->splitter->setStretchFactor(2, 2);
|
|
|
|
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->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);
|
|
}
|
|
|
|
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<Stream> &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<Particle> &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;
|
|
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;
|
|
}
|
|
QMainWindow::mouseReleaseEvent(event);
|
|
}
|