feat: email grouping by date + stability fixes

- EmailTreeModel: hierarchical model for grouped email view (QTreeView)
- DateGroupProxyModel: proxy model for date-based grouping logic
- MailListView: migrated from QTableView to QTreeView with grouping support
- ReaderView: subject frame with shadow, avatar moved to header, body inside header
- MainWindow: frameless with rounded corners, 1px border, edge resize, no status bar

- Stability fixes for grouping mode switch:
  * Proper tree cleanup (no double-delete of root)
  * Bounds checking on all array accesses
  * Null pointer checks in index/parent/data methods
  * Date validity checks before grouping
  * Index validation before accessing m_emails
This commit is contained in:
2026-08-24 01:04:00 +02:00
parent e28ab35fa9
commit 2281b01f49
19 changed files with 1998 additions and 219 deletions
+56 -23
View File
@@ -20,6 +20,7 @@
#include <QFileDialog>
#include <QDir>
#include <functional>
#include <QGraphicsDropShadowEffect>
#include "db/dao/mailitemdao.h"
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
@@ -29,7 +30,7 @@ ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
void ReaderView::setupUI() {
QVBoxLayout *mainLayout = new QVBoxLayout(this);
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setContentsMargins(12, 0, 12, 0);
mainLayout->setSpacing(0);
// ===== Toolbar (zoom, find, images) =====
@@ -182,23 +183,31 @@ void ReaderView::setupUI() {
mainLayout->addWidget(m_toolbar);
mainLayout->addWidget(m_findBar);
// ===== Header Section =====
m_headerWidget = new QWidget();
m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }");
QVBoxLayout *headerLayout = new QVBoxLayout(m_headerWidget);
headerLayout->setContentsMargins(16, 12, 16, 12);
headerLayout->setSpacing(8);
// ===== Subject Section (independent, with shadow) =====
QFrame *subjectFrame = new QFrame();
subjectFrame->setObjectName("SubjectFrame");
subjectFrame->setStyleSheet(
"QFrame#SubjectFrame {"
" background: white;"
" border: none;"
" border: 1px solid #e0e0e0;"
" border-radius: 10px;"
"}"
);
// Add shadow effect
QGraphicsDropShadowEffect *shadowEffect = new QGraphicsDropShadowEffect(this);
shadowEffect->setBlurRadius(8);
shadowEffect->setOffset(0, 3);
shadowEffect->setColor(QColor(0, 0, 0, 40));
subjectFrame->setGraphicsEffect(shadowEffect);
QVBoxLayout *subjectLayout = new QVBoxLayout(subjectFrame);
subjectLayout->setContentsMargins(16, 16, 16, 16);
subjectLayout->setSpacing(8);
// Subject row
QHBoxLayout *subjectRow = new QHBoxLayout();
subjectRow->setSpacing(12);
m_avatarLabel = new QLabel();
m_avatarLabel->setFixedSize(40, 40);
m_avatarLabel->setAlignment(Qt::AlignCenter);
m_avatarLabel->setStyleSheet("QLabel { background: #1976D2; color: white; border-radius: 20px; font-weight: bold; font-size: 14px; }");
subjectRow->addWidget(m_avatarLabel);
m_subjectLabel = new QLabel();
m_subjectLabel->setText("(Sin asunto)");
QFont subjectFont = m_subjectLabel->font();
@@ -210,12 +219,33 @@ void ReaderView::setupUI() {
m_subjectLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
subjectRow->addWidget(m_subjectLabel, 1);
headerLayout->addLayout(subjectRow);
subjectLayout->addLayout(subjectRow);
mainLayout->addWidget(subjectFrame);
// Add spacing after subject frame so shadow is visible
mainLayout->addSpacing(12);
// From / To / Date row
// ===== Header Section =====
m_headerWidget = new QWidget();
m_headerWidget->setStyleSheet("QWidget { background: white; border-bottom: 1px solid #e0e0e0; }");
QVBoxLayout *headerLayout = new QVBoxLayout(m_headerWidget);
headerLayout->setContentsMargins(16, 12, 16, 12);
headerLayout->setSpacing(8);
// Avatar + From / To / Date row
QHBoxLayout *metaRow = new QHBoxLayout();
metaRow->setSpacing(16);
// Avatar on the left
m_avatarLabel = new QLabel();
m_avatarLabel->setFixedSize(40, 40);
m_avatarLabel->setAlignment(Qt::AlignCenter);
m_avatarLabel->setStyleSheet("QLabel { background: #1976D2; color: white; border-radius: 20px; font-weight: bold; font-size: 14px; }");
metaRow->addWidget(m_avatarLabel, 0, Qt::AlignTop);
// From / To / Date on the right of avatar
QVBoxLayout *metaRightLayout = new QVBoxLayout();
metaRightLayout->setSpacing(2);
m_fromLabel = new QLabel();
m_fromLabel->setStyleSheet("color: #333; font-size: 13px;");
m_fromLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
@@ -223,17 +253,19 @@ void ReaderView::setupUI() {
connect(m_fromLabel, &QLabel::linkActivated, [this](const QString &link) {
QDesktopServices::openUrl(QUrl(link));
});
metaRow->addWidget(m_fromLabel, 1);
metaRightLayout->addWidget(m_fromLabel);
m_toLabel = new QLabel();
m_toLabel->setStyleSheet("color: #666; font-size: 12px;");
m_toLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
m_toLabel->setWordWrap(true);
metaRow->addWidget(m_toLabel, 2);
metaRightLayout->addWidget(m_toLabel);
m_dateLabel = new QLabel();
m_dateLabel->setStyleSheet("color: #888; font-size: 12px;");
metaRow->addWidget(m_dateLabel);
metaRightLayout->addWidget(m_dateLabel);
metaRow->addLayout(metaRightLayout, 1);
headerLayout->addLayout(metaRow);
@@ -310,16 +342,17 @@ void ReaderView::setupUI() {
actionsLayout->addWidget(m_moreButton);
headerLayout->addLayout(actionsLayout);
// ===== Body Viewer (inside header widget) =====
setupBodyViewer();
headerLayout->addWidget(m_scrollArea, 1);
mainLayout->addWidget(m_headerWidget);
// ===== Attachments Area =====
setupAttachmentsArea();
mainLayout->addWidget(m_attachmentsFrame);
// ===== Body Viewer =====
setupBodyViewer();
mainLayout->addWidget(m_scrollArea, 1);
// Shortcuts
QShortcut *findShortcut = new QShortcut(QKeySequence(Qt::ControlModifier | Qt::Key_F), this);
connect(findShortcut, &QShortcut::activated, [this]() {