diff --git a/CMakeLists.txt b/CMakeLists.txt index 7883e91..f7890ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,7 +20,21 @@ qt_add_executable(adhd service.cpp service.h mainwindow.ui - sessionslistmodel.h sessionslistmodel.cpp + sessionslistmodel.h + sessionslistmodel.cpp + sessionitemdelegate.h + sessionitemdelegate.cpp + createsessiondialog.h + createsessiondialog.cpp + createsessiondialog.ui + focusoverlay.h + focusoverlay.cpp + focusoverlay.ui + endsessiondialog.h + endsessiondialog.cpp + endsessiondialog.ui + namegenerator.h + namegenerator.cpp ) # Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1. diff --git a/createsessiondialog.cpp b/createsessiondialog.cpp new file mode 100644 index 0000000..db05854 --- /dev/null +++ b/createsessiondialog.cpp @@ -0,0 +1,40 @@ +#include "createsessiondialog.h" +#include "ui_createsessiondialog.h" +#include + +CreateSessionDialog::CreateSessionDialog(QWidget *parent) + : QDialog(parent), + ui(new Ui::CreateSessionDialog) +{ + ui->setupUi(this); + + setWindowTitle("Create New Session"); + + // Disable OK button initially + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + // Connect oneliner field to validation + connect(ui->onelinerEdit, &QLineEdit::textChanged, + this, &CreateSessionDialog::onOnelinerChanged); +} + +CreateSessionDialog::~CreateSessionDialog() +{ + delete ui; +} + +QString CreateSessionDialog::getOneliner() const +{ + return ui->onelinerEdit->text().trimmed(); +} + +QString CreateSessionDialog::getContent() const +{ + return ui->contentEdit->toPlainText().trimmed(); +} + +void CreateSessionDialog::onOnelinerChanged(const QString &text) +{ + bool isValid = !text.trimmed().isEmpty(); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(isValid); +} diff --git a/createsessiondialog.h b/createsessiondialog.h new file mode 100644 index 0000000..fe07bf8 --- /dev/null +++ b/createsessiondialog.h @@ -0,0 +1,28 @@ +#ifndef CREATESESSIONDIALOG_H +#define CREATESESSIONDIALOG_H + +#include + +namespace Ui { +class CreateSessionDialog; +} + +class CreateSessionDialog : public QDialog +{ + Q_OBJECT + +public: + explicit CreateSessionDialog(QWidget *parent = nullptr); + ~CreateSessionDialog(); + + QString getOneliner() const; + QString getContent() const; + +private slots: + void onOnelinerChanged(const QString &text); + +private: + Ui::CreateSessionDialog *ui; +}; + +#endif // CREATESESSIONDIALOG_H diff --git a/createsessiondialog.ui b/createsessiondialog.ui new file mode 100644 index 0000000..dea1ee9 --- /dev/null +++ b/createsessiondialog.ui @@ -0,0 +1,96 @@ + + + CreateSessionDialog + + + + 0 + 0 + 500 + 300 + + + + Create Session + + + + + + + + Oneliner: + + + + + + + What will you focus on? + + + + + + + Initial Content: + + + + + + + Optional: Add initial notes or context... + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + CreateSessionDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + CreateSessionDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/endsessiondialog.cpp b/endsessiondialog.cpp new file mode 100644 index 0000000..8efc133 --- /dev/null +++ b/endsessiondialog.cpp @@ -0,0 +1,68 @@ +#include "endsessiondialog.h" +#include "ui_endsessiondialog.h" + +EndSessionDialog::EndSessionDialog(const QString &oneliner, int elapsedMs, QWidget *parent) + : QDialog(parent), + ui(new Ui::EndSessionDialog), + m_selectedResult(Session::UNSPECIFIED) +{ + ui->setupUi(this); + + setWindowTitle("End Session"); + + // Set oneliner and elapsed time + ui->onelinerLabel->setText(oneliner); + + int seconds = elapsedMs / 1000; + int hours = seconds / 3600; + int minutes = (seconds % 3600) / 60; + int secs = seconds % 60; + + QString timeStr = QString("%1:%2:%3") + .arg(hours, 2, 10, QChar('0')) + .arg(minutes, 2, 10, QChar('0')) + .arg(secs, 2, 10, QChar('0')); + + ui->elapsedLabel->setText("Duration: " + timeStr); + + // Connect buttons + connect(ui->winButton, &QPushButton::clicked, + this, &EndSessionDialog::onWinClicked); + connect(ui->lossButton, &QPushButton::clicked, + this, &EndSessionDialog::onLossClicked); + connect(ui->neutralButton, &QPushButton::clicked, + this, &EndSessionDialog::onNeutralClicked); +} + +EndSessionDialog::~EndSessionDialog() +{ + delete ui; +} + +Session::Result EndSessionDialog::getResult() const +{ + return m_selectedResult; +} + +QString EndSessionDialog::getAdditionalNotes() const +{ + return ui->notesEdit->toPlainText().trimmed(); +} + +void EndSessionDialog::onWinClicked() +{ + m_selectedResult = Session::WIN; + accept(); +} + +void EndSessionDialog::onLossClicked() +{ + m_selectedResult = Session::LOSS; + accept(); +} + +void EndSessionDialog::onNeutralClicked() +{ + m_selectedResult = Session::NEUTRAL; + accept(); +} diff --git a/endsessiondialog.h b/endsessiondialog.h new file mode 100644 index 0000000..0f55c5e --- /dev/null +++ b/endsessiondialog.h @@ -0,0 +1,32 @@ +#ifndef ENDSESSIONDIALOG_H +#define ENDSESSIONDIALOG_H + +#include +#include "service.h" + +namespace Ui { +class EndSessionDialog; +} + +class EndSessionDialog : public QDialog +{ + Q_OBJECT + +public: + explicit EndSessionDialog(const QString &oneliner, int elapsedMs, QWidget *parent = nullptr); + ~EndSessionDialog(); + + Session::Result getResult() const; + QString getAdditionalNotes() const; + +private slots: + void onWinClicked(); + void onLossClicked(); + void onNeutralClicked(); + +private: + Ui::EndSessionDialog *ui; + Session::Result m_selectedResult; +}; + +#endif // ENDSESSIONDIALOG_H diff --git a/endsessiondialog.ui b/endsessiondialog.ui new file mode 100644 index 0000000..bd84603 --- /dev/null +++ b/endsessiondialog.ui @@ -0,0 +1,221 @@ + + + EndSessionDialog + + + + 0 + 0 + 500 + 400 + + + + End Session + + + + + + + 16 + 75 + true + + + + How did your session go? + + + Qt::AlignCenter + + + + + + + + 14 + + + + Session Oneliner + + + Qt::AlignCenter + + + true + + + + + + + Duration: 00:00:00 + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + + + 0 + 60 + + + + + 14 + 75 + true + + + + QPushButton { + background-color: #2ecc71; + color: white; + border-radius: 5px; +} +QPushButton:hover { + background-color: #27ae60; +} +QPushButton:pressed { + background-color: #229954; +} + + + WIN + + + + + + + + 0 + 60 + + + + + 14 + 75 + true + + + + QPushButton { + background-color: #e74c3c; + color: white; + border-radius: 5px; +} +QPushButton:hover { + background-color: #c0392b; +} +QPushButton:pressed { + background-color: #a93226; +} + + + LOSS + + + + + + + + 0 + 60 + + + + + 14 + 75 + true + + + + QPushButton { + background-color: #95a5a6; + color: white; + border-radius: 5px; +} +QPushButton:hover { + background-color: #7f8c8d; +} +QPushButton:pressed { + background-color: #707b7c; +} + + + NEUTRAL + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + Optional: Add final notes + + + + + + + + 16777215 + 100 + + + + Any final thoughts or notes about this session... + + + + + + + + diff --git a/focusoverlay.cpp b/focusoverlay.cpp new file mode 100644 index 0000000..cd2fa62 --- /dev/null +++ b/focusoverlay.cpp @@ -0,0 +1,71 @@ +#include "focusoverlay.h" +#include "ui_focusoverlay.h" +#include +#include + +FocusOverlay::FocusOverlay(QWidget *parent) + : QWidget(parent), + ui(new Ui::FocusOverlay) +{ + ui->setupUi(this); + + // Set window flags for always-on-top frameless window + setWindowFlags(Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::Tool); + setAttribute(Qt::WA_TranslucentBackground); + + // Connect buttons + connect(ui->resetButton, &QPushButton::clicked, + this, &FocusOverlay::onResetButtonClicked); + connect(ui->endButton, &QPushButton::clicked, + this, &FocusOverlay::onEndButtonClicked); + + // Position window + positionAtBottomCenter(); +} + +FocusOverlay::~FocusOverlay() +{ + delete ui; +} + +void FocusOverlay::setOneliner(const QString &oneliner) +{ + ui->onelinerLabel->setText(oneliner); +} + +void FocusOverlay::setElapsedTime(int elapsedMs) +{ + int seconds = elapsedMs / 1000; + int hours = seconds / 3600; + int minutes = (seconds % 3600) / 60; + int secs = seconds % 60; + + QString timeStr = QString("%1:%2:%3") + .arg(hours, 2, 10, QChar('0')) + .arg(minutes, 2, 10, QChar('0')) + .arg(secs, 2, 10, QChar('0')); + + ui->elapsedLabel->setText("Elapsed: " + timeStr); +} + +void FocusOverlay::onResetButtonClicked() +{ + emit resetClicked(); +} + +void FocusOverlay::onEndButtonClicked() +{ + emit endSessionClicked(); +} + +void FocusOverlay::positionAtBottomCenter() +{ + QScreen *screen = QGuiApplication::primaryScreen(); + if (!screen) return; + + QRect screenGeometry = screen->geometry(); + int x = (screenGeometry.width() - width()) / 2; + int y = screenGeometry.height() - height() - 300; // 300px from bottom + + move(x, y); +} diff --git a/focusoverlay.h b/focusoverlay.h new file mode 100644 index 0000000..58862ca --- /dev/null +++ b/focusoverlay.h @@ -0,0 +1,34 @@ +#ifndef FOCUSOVERLAY_H +#define FOCUSOVERLAY_H + +#include + +namespace Ui { +class FocusOverlay; +} + +class FocusOverlay : public QWidget +{ + Q_OBJECT + +public: + explicit FocusOverlay(QWidget *parent = nullptr); + ~FocusOverlay(); + + void setOneliner(const QString &oneliner); + void setElapsedTime(int elapsedMs); + +signals: + void resetClicked(); + void endSessionClicked(); + +private slots: + void onResetButtonClicked(); + void onEndButtonClicked(); + +private: + Ui::FocusOverlay *ui; + void positionAtBottomCenter(); +}; + +#endif // FOCUSOVERLAY_H diff --git a/focusoverlay.ui b/focusoverlay.ui new file mode 100644 index 0000000..c7cc5e8 --- /dev/null +++ b/focusoverlay.ui @@ -0,0 +1,114 @@ + + + FocusOverlay + + + + 0 + 0 + 400 + 120 + + + + Focus Session + + + QWidget#FocusOverlay { + background-color: rgba(50, 50, 50, 200); + border-radius: 10px; + border: 2px solid rgba(100, 100, 100, 255); +} + +QLabel { + color: white; +} + +QPushButton { + background-color: rgba(80, 80, 80, 255); + color: white; + border: 1px solid rgba(120, 120, 120, 255); + border-radius: 5px; + padding: 5px 10px; + min-width: 60px; +} + +QPushButton:hover { + background-color: rgba(100, 100, 100, 255); +} + +QPushButton:pressed { + background-color: rgba(60, 60, 60, 255); +} + + + + 10 + + + 15 + + + 15 + + + 15 + + + 15 + + + + + + 14 + 75 + true + + + + Session Oneliner + + + Qt::AlignCenter + + + + + + + + 12 + + + + Elapsed: 00:00:00 + + + Qt::AlignCenter + + + + + + + + + Reset + + + + + + + End Session + + + + + + + + + + diff --git a/mainwindow.cpp b/mainwindow.cpp index b694bda..541bab2 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -1,19 +1,51 @@ #include "mainwindow.h" #include "service.h" #include "sessionslistmodel.h" +#include "sessionitemdelegate.h" +#include "createsessiondialog.h" +#include "focusoverlay.h" +#include "endsessiondialog.h" #include #include #include +#include +#include #include "ui_mainwindow.h" MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent), m_service(new Service(this)) + : QMainWindow(parent), + m_service(new Service(this)), + m_focusOverlay(nullptr), + m_updatingActiveFields(false) { ui = new Ui::MainWindow; ui->setupUi(this); m_listModel = new SessionsListModel(m_service, this); ui->listView->setModel(m_listModel); + ui->listView->setItemDelegate(new SessionItemDelegate(this)); + + // Connect Service signals + connect(m_service, &Service::activeSessionChanged, + this, &MainWindow::onActiveSessionChanged); + connect(m_service, &Service::sessionTimerTicked, + this, &MainWindow::onSessionTimerTicked); + + // Connect Active tab field changes + connect(ui->oneLineEdit, &QLineEdit::editingFinished, + this, &MainWindow::onActiveOnelinerChanged); + + // Debounce content text changes + QTimer *contentDebounce = new QTimer(this); + contentDebounce->setSingleShot(true); + contentDebounce->setInterval(500); + connect(ui->contentTextEdit, &QTextEdit::textChanged, + [contentDebounce]() { contentDebounce->start(); }); + connect(contentDebounce, &QTimer::timeout, + this, &MainWindow::onActiveContentChanged); + + // Set initial state: no active session + ui->activeStackedWidget->setCurrentIndex(0); } MainWindow::~MainWindow() {} @@ -27,6 +59,190 @@ void MainWindow::on_selectVaultButton_clicked() if (!dir.isEmpty()) { m_service->setVaultDirectory(dir); ui->tabWidget->setCurrentIndex(1); - ui->vaultLabel->setText(m_service->vaultDirectory()); + ui->vaultLabel->setText(dir); } } + +void MainWindow::on_newSessionButton_clicked() +{ + CreateSessionDialog dialog(this); + if (dialog.exec() == QDialog::Accepted) { + QString oneliner = dialog.getOneliner(); + QString content = dialog.getContent(); + + Session session = m_service->createSession(oneliner, content); + m_service->startSession(session); + } +} + +void MainWindow::on_startSessionButton_clicked() +{ + QModelIndex index = ui->listView->currentIndex(); + if (!index.isValid()) { + QMessageBox::warning(this, "No Selection", "Please select a session to start."); + return; + } + + Session session = m_listModel->getSessionByIndex(index); + m_service->startSession(session); +} + +void MainWindow::on_endSessionButton_clicked() +{ + auto activeSession = m_service->activeSession(); + if (!activeSession) { + return; + } + + EndSessionDialog dialog(activeSession->oneliner, m_service->getElapsedMs(), this); + if (dialog.exec() == QDialog::Accepted) { + Session::Result result = dialog.getResult(); + QString notes = dialog.getAdditionalNotes(); + + // Append notes to content if provided + if (!notes.isEmpty()) { + Session updated = *activeSession; + if (!updated.content.isEmpty()) { + updated.content += "\n\n"; + } + updated.content += notes; + m_service->updateSession(updated); + } + + m_service->endSession(result); + } +} + +void MainWindow::onActiveSessionChanged() +{ + auto activeSession = m_service->activeSession(); + + if (activeSession) { + // Session started + ui->activeStackedWidget->setCurrentIndex(1); + updateActiveTab(); + + // Create and show overlay + if (!m_focusOverlay) { + m_focusOverlay = new FocusOverlay(); + connect(m_focusOverlay, &FocusOverlay::resetClicked, + this, &MainWindow::onOverlayResetClicked); + connect(m_focusOverlay, &FocusOverlay::endSessionClicked, + this, &MainWindow::onOverlayEndSessionClicked); + } + + m_focusOverlay->setOneliner(activeSession->oneliner); + m_focusOverlay->setElapsedTime(0); + m_focusOverlay->show(); + + // Switch to Active tab + ui->tabWidget->setCurrentIndex(2); + } else { + // Session ended + ui->activeStackedWidget->setCurrentIndex(0); + + // Hide and delete overlay + if (m_focusOverlay) { + m_focusOverlay->hide(); + m_focusOverlay->deleteLater(); + m_focusOverlay = nullptr; + } + } +} + +void MainWindow::onSessionTimerTicked(int elapsedMs) +{ + ui->elapsedLabel->setText(formatDuration(elapsedMs)); + + if (m_focusOverlay) { + m_focusOverlay->setElapsedTime(elapsedMs); + } +} + +void MainWindow::onActiveOnelinerChanged() +{ + if (m_updatingActiveFields) return; + + auto activeSession = m_service->activeSession(); + if (!activeSession) return; + + Session updated = *activeSession; + updated.oneliner = ui->oneLineEdit->text(); + updated.lastModifiedAt = QDateTime::currentDateTime(); + + m_updatingActiveFields = true; + m_service->updateSession(updated); + m_service->updateActiveSession(updated); + + // Update overlay + if (m_focusOverlay) { + m_focusOverlay->setOneliner(updated.oneliner); + } + + m_updatingActiveFields = false; +} + +void MainWindow::onActiveContentChanged() +{ + if (m_updatingActiveFields) return; + + auto activeSession = m_service->activeSession(); + if (!activeSession) return; + + Session updated = *activeSession; + updated.content = ui->contentTextEdit->toPlainText(); + updated.lastModifiedAt = QDateTime::currentDateTime(); + + m_updatingActiveFields = true; + m_service->updateSession(updated); + m_service->updateActiveSession(updated); + m_updatingActiveFields = false; +} + +void MainWindow::onOverlayResetClicked() +{ + QMessageBox::StandardButton reply = QMessageBox::question( + this, + "Reset Timer", + "Are you sure you want to reset the timer to 00:00:00?", + QMessageBox::Yes | QMessageBox::No); + + if (reply == QMessageBox::Yes) { + m_service->resetSessionTimer(); + } +} + +void MainWindow::onOverlayEndSessionClicked() +{ + on_endSessionButton_clicked(); +} + +void MainWindow::updateActiveTab() +{ + auto activeSession = m_service->activeSession(); + if (!activeSession) return; + + m_updatingActiveFields = true; + + ui->oneLineEdit->setText(activeSession->oneliner); + ui->elapsedLabel->setText(formatDuration(m_service->getElapsedMs())); + + ui->createdLabel->setText(activeSession->createdAt.toString("yyyy-MM-dd hh:mm:ss")); + ui->modifiedLabel->setText(activeSession->lastModifiedAt.toString("yyyy-MM-dd hh:mm:ss")); + ui->contentTextEdit->setPlainText(activeSession->content); + + m_updatingActiveFields = false; +} + +QString MainWindow::formatDuration(int ms) const +{ + int seconds = ms / 1000; + int hours = seconds / 3600; + int minutes = (seconds % 3600) / 60; + int secs = seconds % 60; + + return QString("%1:%2:%3") + .arg(hours, 2, 10, QChar('0')) + .arg(minutes, 2, 10, QChar('0')) + .arg(secs, 2, 10, QChar('0')); +} diff --git a/mainwindow.h b/mainwindow.h index e62dba7..9030128 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -6,6 +6,8 @@ class Service; class SessionsListModel; +class FocusOverlay; +class QTimer; QT_BEGIN_NAMESPACE QT_END_NAMESPACE @@ -21,13 +23,34 @@ public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); - private slots: +private slots: void on_selectVaultButton_clicked(); + void on_newSessionButton_clicked(); + void on_startSessionButton_clicked(); + void on_endSessionButton_clicked(); - private: + // Service signal handlers + void onActiveSessionChanged(); + void onSessionTimerTicked(int elapsedMs); + + // Active tab field editing + void onActiveOnelinerChanged(); + void onActiveContentChanged(); + + // Overlay handlers + void onOverlayResetClicked(); + void onOverlayEndSessionClicked(); + +private: Ui::MainWindow *ui; Service *m_service; SessionsListModel *m_listModel; + FocusOverlay *m_focusOverlay; + + bool m_updatingActiveFields; + + void updateActiveTab(); + QString formatDuration(int ms) const; }; #endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui index 0fd782c..7dbef64 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -65,6 +65,37 @@ Sessions + + + + + + New Session + + + + + + + Start Selected + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -74,6 +105,168 @@ Active + + + + + 0 + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + No session running. Start a session to begin. + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + true + + + + + 0 + 0 + 358 + 457 + + + + + + + + + Oneliner: + + + + + + + + + + Elapsed Time: + + + + + + + 00:00:00 + + + + + + + Created: + + + + + + + - + + + + + + + Modified: + + + + + + + - + + + + + + + Content: + + + + + + + Add notes, context, or anything related to this session... + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + End Session + + + + + + + + + + + + diff --git a/namegenerator.cpp b/namegenerator.cpp index 8e307ad..93886d3 100644 --- a/namegenerator.cpp +++ b/namegenerator.cpp @@ -1,9 +1,5 @@ -#include "namegenerator.cpp" - -namespace { - constexpr char data[] -} +#include "namegenerator.h" QString generate() { - return; + return QString("generated-name-placeholder"); } diff --git a/service.cpp b/service.cpp index e3fd559..2f21323 100644 --- a/service.cpp +++ b/service.cpp @@ -8,7 +8,12 @@ bool Session::isComplete() const { return result != UNSPECIFIED; } -Service::Service(QObject *parent) : QObject(parent) { +Service::Service(QObject *parent) + : QObject(parent), + m_tickTimer(new QTimer(this)) +{ + m_tickTimer->setInterval(1000); // Fire every second + connect(m_tickTimer, &QTimer::timeout, this, &Service::onTickTimerFired); } Service::~Service() {} @@ -161,7 +166,7 @@ std::optional Service::parseFile(const QFileInfo &fileInfo) { } Session session = {}; - session.id = file.fileName(); + session.id = fileInfo.baseName(); // Get filename without extension session.content = ""; session.oneliner = ""; session.durationMs = 0; @@ -268,3 +273,77 @@ void Service::overwriteSessionFile(const Session &session) out << session.content << "\n"; file.close(); } + +// Session lifecycle methods + +void Service::startSession(const Session &session) { + if (m_activeSession.has_value()) { + qWarning() << "A session is already active. End it before starting a new one."; + return; + } + + m_activeSession = session; + m_sessionTimer.start(); + m_tickTimer->start(); + + emit activeSessionChanged(); + emit sessionTimerTicked(0); +} + +void Service::updateActiveSession(const Session &session) { + if (!m_activeSession.has_value()) { + qWarning() << "No active session to update."; + return; + } + + m_activeSession = session; + // Don't emit activeSessionChanged - just update the session data + // Timer keeps running +} + +int Service::getElapsedMs() const { + if (!m_activeSession) { + return 0; + } + return m_sessionTimer.elapsed(); +} + +void Service::resetSessionTimer() { + if (!m_activeSession) { + return; + } + + m_sessionTimer.restart(); + emit sessionTimerTicked(0); +} + +void Service::endSession(Session::Result result) { + if (!m_activeSession) { + return; + } + + // Update session with final values + Session updated = *m_activeSession; + updated.durationMs = getElapsedMs(); + updated.result = result; + updated.lastModifiedAt = QDateTime::currentDateTime(); + + // Save to file and update in list + updateSession(updated); + + // Clear active state + m_activeSession.reset(); + m_tickTimer->stop(); + + emit activeSessionChanged(); +} + +std::optional Service::activeSession() const { + return m_activeSession; +} + +void Service::onTickTimerFired() { + if (m_activeSession) { + emit sessionTimerTicked(getElapsedMs()); + } +} diff --git a/service.h b/service.h index 011bbad..393eb55 100644 --- a/service.h +++ b/service.h @@ -6,6 +6,8 @@ #include #include #include +#include +#include #include struct Session @@ -65,18 +67,38 @@ public slots: void deleteSession(const QString &sessionId); + // Session lifecycle methods + void startSession(const Session &session); + void updateActiveSession(const Session &session); // Update without restarting timer + int getElapsedMs() const; + void resetSessionTimer(); + void endSession(Session::Result result); + std::optional activeSession() const; + signals: void sessionsChanged(); + void activeSessionChanged(); + void sessionTimerTicked(int elapsedMs); private: QString m_vaultDirectory; QList m_sessions; + // Active session tracking + std::optional m_activeSession; + QElapsedTimer m_sessionTimer; + QTimer *m_tickTimer; + /// @returns null Session if not a valid session markdown document std::optional parseFile(const QFileInfo &fileInfo); Session::Result parseResult(const QString &resultStr); void overwriteSessionFile(const Session &session); + +private slots: + void onTickTimerFired(); }; +Q_DECLARE_METATYPE(Session) + #endif //SERVICE_H diff --git a/sessionitemdelegate.cpp b/sessionitemdelegate.cpp new file mode 100644 index 0000000..183f543 --- /dev/null +++ b/sessionitemdelegate.cpp @@ -0,0 +1,139 @@ +#include "sessionitemdelegate.h" +#include "service.h" +#include +#include + +SessionItemDelegate::SessionItemDelegate(QObject *parent) + : QStyledItemDelegate(parent) +{ +} + +void SessionItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + if (!index.isValid()) { + QStyledItemDelegate::paint(painter, option, index); + return; + } + + painter->save(); + + // Get session data + Session session = index.data(Qt::UserRole).value(); + QString oneliner = session.oneliner; + + // Format duration + int seconds = session.durationMs / 1000; + int hours = seconds / 3600; + int minutes = (seconds % 3600) / 60; + int secs = seconds % 60; + QString durationStr = QString("%1:%2:%3") + .arg(hours, 2, 10, QChar('0')) + .arg(minutes, 2, 10, QChar('0')) + .arg(secs, 2, 10, QChar('0')); + + // Format result + QString resultStr; + switch (session.result) { + case Session::WIN: + resultStr = "WIN"; + break; + case Session::LOSS: + resultStr = "LOSS"; + break; + case Session::NEUTRAL: + resultStr = "NEUTRAL"; + break; + default: + resultStr = "UNSPECIFIED"; + break; + } + + // Format relative time + QDateTime now = QDateTime::currentDateTime(); + qint64 secsSinceModified = session.lastModifiedAt.secsTo(now); + QString relativeTime; + if (secsSinceModified < 60) { + relativeTime = "just now"; + } else if (secsSinceModified < 3600) { + relativeTime = QString("%1 min ago").arg(secsSinceModified / 60); + } else if (secsSinceModified < 86400) { + relativeTime = QString("%1 hr ago").arg(secsSinceModified / 3600); + } else { + relativeTime = QString("%1 days ago").arg(secsSinceModified / 86400); + } + + QString metadata = QString("Duration: %1 | Result: %2 | Modified: %3") + .arg(durationStr) + .arg(resultStr) + .arg(relativeTime); + + // Draw background + if (option.state & QStyle::State_Selected) { + painter->fillRect(option.rect, option.palette.highlight()); + } else { + painter->fillRect(option.rect, option.palette.base()); + } + + // Draw result indicator bar on the left + QColor indicatorColor; + switch (session.result) { + case Session::WIN: + indicatorColor = QColor(46, 204, 113); // Green + break; + case Session::LOSS: + indicatorColor = QColor(231, 76, 60); // Red + break; + case Session::NEUTRAL: + indicatorColor = QColor(149, 165, 166); // Gray + break; + default: + indicatorColor = QColor(0, 0, 0, 0); // Transparent + break; + } + + if (session.result != Session::UNSPECIFIED) { + QRect indicatorRect(option.rect.left(), option.rect.top(), 4, option.rect.height()); + painter->fillRect(indicatorRect, indicatorColor); + } + + // Set up text drawing + QRect textRect = option.rect.adjusted(10, 5, -5, -5); + QColor textColor = (option.state & QStyle::State_Selected) + ? option.palette.highlightedText().color() + : option.palette.text().color(); + + // Draw oneliner (bold, larger font) + painter->setPen(textColor); + QFont onelineFont = option.font; + onelineFont.setPointSize(14); + onelineFont.setBold(true); + painter->setFont(onelineFont); + + QRect onelineRect = textRect; + onelineRect.setHeight(24); + QString elidedOneliner = painter->fontMetrics().elidedText(oneliner, Qt::ElideRight, onelineRect.width()); + painter->drawText(onelineRect, Qt::AlignLeft | Qt::AlignVCenter, elidedOneliner); + + // Draw metadata (smaller font, lighter color) + QFont metadataFont = option.font; + metadataFont.setPointSize(11); + painter->setFont(metadataFont); + + QColor metadataColor = textColor; + metadataColor.setAlpha(180); // Make it slightly transparent + painter->setPen(metadataColor); + + QRect metadataRect = textRect; + metadataRect.setTop(onelineRect.bottom() + 2); + QString elidedMetadata = painter->fontMetrics().elidedText(metadata, Qt::ElideRight, metadataRect.width()); + painter->drawText(metadataRect, Qt::AlignLeft | Qt::AlignVCenter, elidedMetadata); + + painter->restore(); +} + +QSize SessionItemDelegate::sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + return QSize(option.rect.width(), 65); +} diff --git a/sessionitemdelegate.h b/sessionitemdelegate.h new file mode 100644 index 0000000..e89b75e --- /dev/null +++ b/sessionitemdelegate.h @@ -0,0 +1,20 @@ +#ifndef SESSIONITEMDELEGATE_H +#define SESSIONITEMDELEGATE_H + +#include + +class SessionItemDelegate : public QStyledItemDelegate +{ + Q_OBJECT + +public: + explicit SessionItemDelegate(QObject *parent = nullptr); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + + QSize sizeHint(const QStyleOptionViewItem &option, + const QModelIndex &index) const override; +}; + +#endif // SESSIONITEMDELEGATE_H diff --git a/sessionslistmodel.cpp b/sessionslistmodel.cpp index 77239b8..cf13129 100644 --- a/sessionslistmodel.cpp +++ b/sessionslistmodel.cpp @@ -27,8 +27,19 @@ QVariant SessionsListModel::data(const QModelIndex &index, int role) const { const Session session = m_service->sessions().at(index.row()); switch (role) { case Qt::DisplayRole: + // Simple display for fallback - delegate will handle rich rendering return session.oneliner; + case Qt::UserRole: + // Store full session object for delegate access + return QVariant::fromValue(session); default: return QVariant(); } } + +Session SessionsListModel::getSessionByIndex(const QModelIndex &index) const { + if (!index.isValid() || index.row() < 0 || index.row() >= rowCount()) { + return Session(); // Return default-constructed session + } + return m_service->sessions().at(index.row()); +} diff --git a/sessionslistmodel.h b/sessionslistmodel.h index 1419bf4..fb56763 100644 --- a/sessionslistmodel.h +++ b/sessionslistmodel.h @@ -13,7 +13,9 @@ public: int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; - QVariant data(const QModelIndex &index, int role) const; + QVariant data(const QModelIndex &index, int role) const override; + + Session getSessionByIndex(const QModelIndex &index) const; private: Service *m_service;