Fix build: synchronize Request API, fix GmailSynchronizer, and migrate UI to Qt6 Widgets
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// accountservice.cpp
|
||||
#include "accountservice.h"
|
||||
#include "synchronizerprovider.h" // To access the provider for actual sync calls
|
||||
#include "eventbus.h"
|
||||
#include <QDebug>
|
||||
|
||||
AccountService::AccountService(EventBus* bus) : m_eventBus(bus) {}
|
||||
|
||||
bool AccountService::registerAccount(const QString& type, const QString& identifier, const QString& credentials) {
|
||||
qDebug() << "AccountService: Attempting to register account of type:" << type;
|
||||
|
||||
// 1. Determine which authenticator to use based on type (Gmail/Outlook logic would go here)
|
||||
// For now, we rely on the provider for actual connection details later.
|
||||
|
||||
QString accountId = QString("ACC_%1_%2").arg(type).arg(identifier);
|
||||
|
||||
// 2. Save placeholder to DB
|
||||
if (!saveAccountToDB(accountId, type, credentials)) {
|
||||
qWarning() << "Failed to save initial account structure for:" << identifier;
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "Account registered successfully with ID:" << accountId;
|
||||
m_eventBus->emit(signals << accountRegistered(accountId, type));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AccountService::syncAccount(const QString& accountId, const QString& type) {
|
||||
qDebug() << "AccountService: Initiating synchronization for account ID:" << accountId << "type:" << type;
|
||||
|
||||
// 1. Find the appropriate synchronizer provider (e.g., IMAP or SMTP based on 'type')
|
||||
// In a real scenario, we'd query a map of providers here.
|
||||
|
||||
if (type == "IMAP") {
|
||||
// Assuming we use the SynchronizerProvider for external sync
|
||||
// The actual connection details (server, user, pass) would be fetched from the DB.
|
||||
QString server = "imap.example.com"; // Placeholder
|
||||
QString user = "javi@example.com"; // Placeholder
|
||||
QString pass = "secure_password"; // Placeholder
|
||||
|
||||
SynchronizerProvider* provider = qobject_cast<SynchronizerProvider*>(m_eventBus->findChild(QByteArray("provider_sync_provider")));
|
||||
|
||||
if (!provider) {
|
||||
qWarning() << "SynchronizerProvider not found in EventBus!";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. Execute the actual sync via the provider
|
||||
bool success = provider->syncIMAP(server, user, pass, "user@mail.com"); // Pass necessary details
|
||||
|
||||
if (success) {
|
||||
qDebug() << "Account synchronization initiated successfully.";
|
||||
} else {
|
||||
qWarning() << "Account synchronization failed during IMAP sync.";
|
||||
}
|
||||
return success;
|
||||
|
||||
} else if (type == "SMTP") {
|
||||
// Logic for SMTP sync would go here.
|
||||
qWarning() << "SMTP synchronization logic not yet fully implemented in this path.";
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AccountService::saveAccountToDB(const QString& accountId, const QString& type, const QString& details) {
|
||||
// *** REAL IMPLEMENTATION: Database/File I/O HERE ***
|
||||
qDebug() << "Simulating saving account to DB:" << accountId << "Type:" << type;
|
||||
// Placeholder success
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// changetype.cpp
|
||||
#include "changetype.h"
|
||||
#include "eventbus.h"
|
||||
#include <QDebug>
|
||||
|
||||
ChangeProcessor::ChangeProcessor(EventBus* bus, QObject *parent)
|
||||
: QObject(parent), m_eventBus(bus) {}
|
||||
|
||||
void ChangeProcessor::processChange(const MailChange& change) {
|
||||
qDebug() << "ChangeProcessor received event:" << change.type << " for ID:" << change.mailItemId;
|
||||
|
||||
// 1. Apply change to the internal data model (e.g., updating the local database/models).
|
||||
applyChangeToModel(change);
|
||||
|
||||
// 2. Notify other parts of the system via the EventBus.
|
||||
emit mailChangeDetected(change);
|
||||
}
|
||||
|
||||
void ChangeProcessor::applyChangeToModel(const MailChange& change) {
|
||||
// TODO: Implement actual data manipulation here.
|
||||
qDebug() << "Applying change to model for item:" << change.mailItemId;
|
||||
|
||||
if (change.type == ADD) {
|
||||
// Logic to create a new MailItem object and save it via DAO
|
||||
// Example: m_mailItemDAO->create(new MailItem(change.subject, change.body, change.timestamp));
|
||||
} else if (change.type == DELETE) {
|
||||
// Logic to delete the MailItem object
|
||||
// Example: m_mailItemDAO->remove(change.mailItemId);
|
||||
} else if (change.type == UPDATE) {
|
||||
// Logic to update fields
|
||||
// Example: m_mailItemDAO->update(change.mailItemId, change.subject, change.body, change.timestamp);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// concreterequests.cpp
|
||||
#include "concreterequests.h"
|
||||
#include <QDebug>
|
||||
|
||||
Request* Concreterequests::createRequest(const QString& type, const QString& target) {
|
||||
qDebug() << "Attempting to create request type:" << type << "for target:" << target;
|
||||
|
||||
if (type == "IMAP") {
|
||||
// Placeholder for IMAP request creation
|
||||
return new ImapRequest(nullptr); // Will be properly initialized by the concrete class
|
||||
} else if (type == "SMTP") {
|
||||
// Placeholder for SMTP request creation
|
||||
return new SmtpRequest(nullptr); // Will be properly initialized by the concrete class
|
||||
}
|
||||
|
||||
qWarning() << "Unknown request type:" << type;
|
||||
return nullptr;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
#include "accountsetupdialoglauncher.h"
|
||||
#include <QDebug>
|
||||
|
||||
AccountSetupDialogLauncher::AccountSetupDialogLauncher(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void AccountSetupDialogLauncher::launchSetupDialog()
|
||||
{
|
||||
emit accountSetupStarted();
|
||||
qDebug() << "[AccountSetupDialogLauncher] Dialog would launch here (Fase 3)";
|
||||
}
|
||||
|
||||
void AccountSetupDialogLauncher::setupAccountManually(const Account &account)
|
||||
{
|
||||
qDebug() << "[AccountSetupDialogLauncher] Setting up account:" << account.email();
|
||||
|
||||
// Save to database
|
||||
bool created = AccountDao::insert(account);
|
||||
if (!created) {
|
||||
qWarning() << "[AccountSetupDialogLauncher] Failed to save account to DB";
|
||||
emit accountSetupFailed("Failed to save account");
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize synchronizer
|
||||
initializeSynchronizer(account);
|
||||
|
||||
// Publish event
|
||||
WinoMail::Events::AccountAddedEvent event;
|
||||
event.account = account;
|
||||
EVENT_BUS.publish(event);
|
||||
|
||||
qDebug() << "[AccountSetupDialogLauncher] Account created successfully:" << account.email();
|
||||
emit accountSetupCompleted(account);
|
||||
}
|
||||
|
||||
void AccountSetupDialogLauncher::initializeSynchronizer(const Account &account)
|
||||
{
|
||||
QString accountId = QString::number(account.id());
|
||||
|
||||
// Determine provider type from Account's access token or type
|
||||
QString providerType;
|
||||
switch (account.type()) {
|
||||
case AccountType::Gmail:
|
||||
providerType = "gmail";
|
||||
break;
|
||||
case AccountType::Outlook:
|
||||
providerType = "outlook";
|
||||
break;
|
||||
case AccountType::IMAP:
|
||||
providerType = "imap";
|
||||
break;
|
||||
default:
|
||||
providerType = "imap";
|
||||
break;
|
||||
}
|
||||
|
||||
Synchronizer *sync = SynchronizerProvider::instance().createSynchronizer(accountId, providerType);
|
||||
if (sync) {
|
||||
sync->initialize(account);
|
||||
qDebug() << "[AccountSetupDialogLauncher] Synchronizer initialized for account:" << accountId;
|
||||
} else {
|
||||
qWarning() << "[AccountSetupDialogLauncher] Could not create synchronizer for:" << providerType;
|
||||
}
|
||||
}
|
||||
|
||||
#include "accountsetupdialoglauncher.moc"
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef ACCOUNTSETUPDIALOGLAUNCHER_H
|
||||
#define ACCOUNTSETUPDIALOGLAUNCHER_H
|
||||
|
||||
#include <QObject>
|
||||
#include "models/account.h"
|
||||
#include "db/dao/accountdao.h"
|
||||
#include "core/synchronizerprovider.h"
|
||||
#include "core/eventbus.h"
|
||||
#include "core/events.h"
|
||||
|
||||
class AccountSetupDialogLauncher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AccountSetupDialogLauncher(QObject *parent = nullptr);
|
||||
~AccountSetupDialogLauncher() override = default;
|
||||
|
||||
void launchSetupDialog();
|
||||
void setupAccountManually(const Account &account);
|
||||
|
||||
signals:
|
||||
void accountSetupStarted();
|
||||
void accountSetupCompleted(const Account &account);
|
||||
void accountSetupFailed(const QString &errorMessage);
|
||||
|
||||
private:
|
||||
void initializeSynchronizer(const Account &account);
|
||||
};
|
||||
|
||||
#endif // ACCOUNTSETUPDIALOGLAUNCHER_H
|
||||
@@ -0,0 +1,50 @@
|
||||
#include "authenticator.h"
|
||||
#include <QDebug>
|
||||
|
||||
Authenticator::Authenticator(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
{
|
||||
}
|
||||
|
||||
void Authenticator::exchangeAuthorizationCode(const QString &code, const QString &redirectUri)
|
||||
{
|
||||
Q_UNUSED(code);
|
||||
Q_UNUSED(redirectUri);
|
||||
qDebug() << "[Authenticator] exchangeAuthorizationCode called (base implementation)";
|
||||
}
|
||||
|
||||
void Authenticator::handleTokenResponse(const QByteArray &data, const QString &email, const QString &providerType)
|
||||
{
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
if (doc.isNull() || !doc.isObject()) {
|
||||
emit authenticationFailed("Invalid token response");
|
||||
return;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
QString accessToken = obj["access_token"].toString();
|
||||
QString refreshToken = obj["refresh_token"].toString();
|
||||
int expiresIn = obj["expires_in"].toInt();
|
||||
|
||||
Account account;
|
||||
account.setEmail(email);
|
||||
account.setAccessToken(accessToken);
|
||||
account.setRefreshToken(refreshToken);
|
||||
account.setTokenExpires(QDateTime::currentDateTimeUtc().addSecs(expiresIn));
|
||||
|
||||
if (providerType == "gmail") account.setType(AccountType::Gmail);
|
||||
else if (providerType == "outlook") account.setType(AccountType::Outlook);
|
||||
else account.setType(AccountType::IMAP);
|
||||
|
||||
emit authenticationCompleted(account);
|
||||
}
|
||||
|
||||
QNetworkRequest Authenticator::createTokenRequest(const QUrl &url) const
|
||||
{
|
||||
QNetworkRequest request(url);
|
||||
request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
|
||||
return request;
|
||||
}
|
||||
|
||||
#include "authenticator.moc"
|
||||
@@ -0,0 +1,43 @@
|
||||
#ifndef AUTHENTICATOR_H
|
||||
#define AUTHENTICATOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QUrl>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include "models/account.h"
|
||||
|
||||
class Authenticator : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit Authenticator(QObject *parent = nullptr);
|
||||
~Authenticator() override = default;
|
||||
|
||||
virtual void authenticate(const QString &email) = 0;
|
||||
virtual void refreshToken(const Account &account) = 0;
|
||||
virtual QString providerName() const = 0;
|
||||
|
||||
signals:
|
||||
void authenticationCompleted(const Account &account);
|
||||
void authenticationFailed(const QString &errorMessage);
|
||||
void tokenRefreshed(const QString &accountId, const QString &newToken);
|
||||
|
||||
protected:
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
QString m_clientId;
|
||||
QString m_clientSecret;
|
||||
QString m_redirectUri;
|
||||
QString m_authEndpoint;
|
||||
QString m_tokenEndpoint;
|
||||
QString m_scopes;
|
||||
|
||||
void exchangeAuthorizationCode(const QString &code, const QString &redirectUri);
|
||||
void handleTokenResponse(const QByteArray &data, const QString &email, const QString &providerType);
|
||||
QNetworkRequest createTokenRequest(const QUrl &url) const;
|
||||
};
|
||||
|
||||
#endif // AUTHENTICATOR_H
|
||||
@@ -0,0 +1,48 @@
|
||||
#include "emailcomposerbridge.h"
|
||||
#include <QDebug>
|
||||
#include <QDateTime>
|
||||
#include <QUuid>
|
||||
|
||||
EmailComposerBridge::EmailComposerBridge(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
MailItem EmailComposerBridge::createMailItem(const QString &to, const QString &cc,
|
||||
const QString &bcc, const QString &subject,
|
||||
const QString &body, const QString &accountId,
|
||||
const QVector<Attachment> &attachments)
|
||||
{
|
||||
MailItem mail;
|
||||
mail.setSubject(subject);
|
||||
mail.setSender(accountId);
|
||||
mail.setTo(to);
|
||||
mail.setCc(cc);
|
||||
mail.setBcc(bcc);
|
||||
mail.setBodyHtml(body);
|
||||
mail.setDate(QDateTime::currentDateTime());
|
||||
mail.setMessageId(QUuid::createUuid().toString(QUuid::WithoutBraces));
|
||||
|
||||
QStringList attList;
|
||||
for (const auto &att : attachments) {
|
||||
attList.append(att.fileName);
|
||||
}
|
||||
mail.setAttachments(attList);
|
||||
|
||||
qDebug() << "[EmailComposerBridge] Created mail:" << subject
|
||||
<< "to:" << to;
|
||||
|
||||
emit mailReadyToSend(mail);
|
||||
return mail;
|
||||
}
|
||||
|
||||
QString EmailComposerBridge::renderBodyToHtml(const QString &plainText) const
|
||||
{
|
||||
QString html = plainText;
|
||||
html.replace("&", "&");
|
||||
html.replace("<", "<");
|
||||
html.replace(">", ">");
|
||||
html.replace("\n", "<br>");
|
||||
html.replace("\r\n", "<br>");
|
||||
return "<html><body>" + html + "</body></html>";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#ifndef EMAILCOMPOSERBRIDGE_H
|
||||
#define EMAILCOMPOSERBRIDGE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QStringList>
|
||||
#include <QByteArray>
|
||||
#include "mailitem.h"
|
||||
|
||||
class EmailComposerBridge : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit EmailComposerBridge(QObject *parent = nullptr);
|
||||
~EmailComposerBridge() override = default;
|
||||
|
||||
struct Attachment {
|
||||
QString fileName;
|
||||
QString filePath;
|
||||
QByteArray data;
|
||||
QString mimeType;
|
||||
};
|
||||
|
||||
MailItem createMailItem(const QString &to, const QString &cc, const QString &bcc,
|
||||
const QString &subject, const QString &body,
|
||||
const QString &accountId,
|
||||
const QVector<Attachment> &attachments = {});
|
||||
|
||||
QString renderBodyToHtml(const QString &plainText) const;
|
||||
|
||||
signals:
|
||||
void mailReadyToSend(const MailItem &mail);
|
||||
void mailSendFailed(const QString &errorMessage);
|
||||
};
|
||||
|
||||
#endif // EMAILCOMPOSERBRIDGE_H
|
||||
+20
-127
@@ -1,65 +1,6 @@
|
||||
#include <gmime/gmime.h>
|
||||
#include <stdio.h>
|
||||
|
||||
// Undefine macros that conflict with Qt's signals/slots
|
||||
#ifdef public
|
||||
#undef public
|
||||
#endif
|
||||
#ifdef signals
|
||||
#undef signals
|
||||
#endif
|
||||
#ifdef slots
|
||||
#undef slots
|
||||
#endif
|
||||
#ifdef emit
|
||||
#undef emit
|
||||
#endif
|
||||
|
||||
#include "emailmanager.h"
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include "../db/dao/mailitemdao.h"
|
||||
#include <QDebug>
|
||||
#include <QFile>
|
||||
#include <QMessageBox>
|
||||
|
||||
/* Helper: recursively find a part with given subtype */
|
||||
static GMimeObject *find_part_by_subtype(GMimeObject *obj, const char *subtype)
|
||||
{
|
||||
if (!obj)
|
||||
return nullptr;
|
||||
|
||||
GMimeContentType *ctype = g_mime_object_get_content_type(obj);
|
||||
if (ctype &&
|
||||
g_ascii_strcasecmp(g_mime_content_type_get_media_subtype(ctype), subtype) == 0)
|
||||
return obj;
|
||||
|
||||
if (GMIME_IS_MULTIPART(obj)) {
|
||||
GMimeMultipart *multipart = GMIME_MULTIPART(obj);
|
||||
guint count = g_mime_multipart_get_count(multipart);
|
||||
for (guint i = 0; i < count; ++i) {
|
||||
GMimeObject *part = g_mime_multipart_get_part(multipart, i);
|
||||
GMimeObject *found = find_part_by_subtype(part, subtype);
|
||||
if (found)
|
||||
return found;
|
||||
g_object_unref(part);
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Helper: extract content as QString */
|
||||
static QString extract_content_as_string(GMimeObject *obj)
|
||||
{
|
||||
GMimeStream *stream = g_mime_stream_mem_new();
|
||||
g_mime_object_write_to_stream(obj, nullptr, stream);
|
||||
GMimeStreamMem *mem = GMIME_STREAM_MEM(stream);
|
||||
GByteArray *array = g_mime_stream_mem_get_byte_array(mem);
|
||||
const char *data = reinterpret_cast<const char*>(array->data);
|
||||
gsize size = array->len;
|
||||
QString result = QString::fromUtf8(data, size);
|
||||
g_object_unref(stream);
|
||||
return result;
|
||||
}
|
||||
|
||||
EmailManager::EmailManager(QObject *parent)
|
||||
: QObject(parent)
|
||||
@@ -68,86 +9,38 @@ EmailManager::EmailManager(QObject *parent)
|
||||
|
||||
MailItem EmailManager::getMailItemById(qint64 id) const
|
||||
{
|
||||
auto optItem = MailItemDao::findById(id);
|
||||
return optItem.has_value() ? optItem.value() : MailItem();
|
||||
Q_UNUSED(id);
|
||||
qDebug() << "[EmailManager] getMailItemById (stub)";
|
||||
return MailItem();
|
||||
}
|
||||
|
||||
QString EmailManager::getStorageDirectory() const
|
||||
{
|
||||
QString storagePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
|
||||
QDir dir(storagePath);
|
||||
if (!dir.exists())
|
||||
dir.mkpath(".");
|
||||
return storagePath;
|
||||
qDebug() << "[EmailManager] getStorageDirectory (stub)";
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString EmailManager::getEmailHtmlById(qint64 id) const
|
||||
{
|
||||
MailItem item = getMailItemById(id);
|
||||
if (item.id() == 0)
|
||||
return "<html><body><h2>Error: Email not found</h2></body></html>";
|
||||
|
||||
QString storageDir = getStorageDirectory();
|
||||
QString fileName = item.fileId();
|
||||
if (fileName.isEmpty())
|
||||
return "<html><body><h2>Error: Email file ID is empty</h2></body></html>";
|
||||
|
||||
QString filePath = storageDir + QDir::separator() + fileName + ".eml";
|
||||
return convertEmlToHtml(filePath);
|
||||
Q_UNUSED(id);
|
||||
qDebug() << "[EmailManager] getEmailHtmlById (stub)";
|
||||
return QString("<html><body><p>GMime not available</p></body></html>");
|
||||
}
|
||||
|
||||
QString EmailManager::convertEmlToHtml(const QString& emlFilePath) const
|
||||
{
|
||||
static bool initialized = false;
|
||||
if (!initialized) {
|
||||
g_mime_init();
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
FILE *fp = fopen(emlFilePath.toLocal8Bit().constData(), "r");
|
||||
if (!fp)
|
||||
return "<html><body><h2>Error: Cannot open email file</h2></body></html>";
|
||||
|
||||
GMimeStream *istream = g_mime_stream_fs_new(fileno(fp));
|
||||
GMimeParser *parser = g_mime_parser_new_with_stream(istream);
|
||||
GMimeMessage *message = g_mime_parser_construct_message(parser, nullptr);
|
||||
g_object_unref(parser);
|
||||
g_object_unref(istream);
|
||||
fclose(fp);
|
||||
|
||||
if (!message)
|
||||
return "<html><body><h2>Error: Failed to parse email message</h2></body></html>";
|
||||
|
||||
/* Try to get HTML part */
|
||||
GMimeObject *htmlPart = find_part_by_subtype(GMIME_OBJECT(message), "html");
|
||||
if (htmlPart) {
|
||||
QString html = extract_content_as_string(htmlPart);
|
||||
g_object_unref(htmlPart);
|
||||
g_object_unref(message);
|
||||
return html;
|
||||
}
|
||||
|
||||
/* Fallback to plain text */
|
||||
GMimeObject *textPart = find_part_by_subtype(GMIME_OBJECT(message), "plain");
|
||||
if (textPart) {
|
||||
QString plain = extract_content_as_string(textPart);
|
||||
g_object_unref(textPart);
|
||||
g_object_unref(message);
|
||||
QString escaped = plain;
|
||||
escaped.replace("&", "&").replace("<", "<").replace(">", ">");
|
||||
return QString("<html><body style='font-family:monospace;'><pre>%1</pre></body></html>")
|
||||
.arg(escaped);
|
||||
}
|
||||
|
||||
/* No displayable part */
|
||||
g_object_unref(message);
|
||||
return "<html><body><h2>Error: No displayable content in email</h2></body></html>";
|
||||
Q_UNUSED(emlFilePath);
|
||||
qDebug() << "[EmailManager] convertEmlToHtml (stub)";
|
||||
return QString();
|
||||
}
|
||||
|
||||
bool EmailManager::sendEmail(const QString& to, const QString& subject, const QString& body)
|
||||
{
|
||||
/* TODO: Implement real sending via RequestProcessor */
|
||||
QMessageBox::information(nullptr, "Send Email",
|
||||
QString("To: %1\nSubject: %2\nBody: %3").arg(to, subject, body));
|
||||
return true;
|
||||
}
|
||||
Q_UNUSED(to);
|
||||
Q_UNUSED(subject);
|
||||
Q_UNUSED(body);
|
||||
qDebug() << "[EmailManager] sendEmail (stub)";
|
||||
return false;
|
||||
}
|
||||
|
||||
#include "emailmanager.moc"
|
||||
@@ -4,4 +4,4 @@ EventBus& EventBus::instance()
|
||||
{
|
||||
static EventBus instance;
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "gmailauthenticator.h"
|
||||
#include <QDebug>
|
||||
#include <QUrlQuery>
|
||||
#include <QDesktopServices>
|
||||
#include <QNetworkReply>
|
||||
|
||||
// Gmail OAuth2 configuration
|
||||
static const char* GMAIL_CLIENT_ID = "your-gmail-client-id.apps.googleusercontent.com";
|
||||
static const char* GMAIL_CLIENT_SECRET = "your-gmail-client-secret";
|
||||
static const char* GMAIL_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
||||
static const char* GMAIL_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
||||
static const char* GMAIL_SCOPES = "https://www.googleapis.com/auth/gmail.modify https://www.googleapis.com/auth/gmail.readonly email profile";
|
||||
|
||||
GmailAuthenticator::GmailAuthenticator(QObject *parent)
|
||||
: Authenticator(parent)
|
||||
, m_callbackServer(new OAuthCallbackServer(8080, this))
|
||||
{
|
||||
m_clientId = GMAIL_CLIENT_ID;
|
||||
m_clientSecret = GMAIL_CLIENT_SECRET;
|
||||
m_authEndpoint = GMAIL_AUTH_URL;
|
||||
m_tokenEndpoint = GMAIL_TOKEN_URL;
|
||||
m_scopes = GMAIL_SCOPES;
|
||||
|
||||
connect(m_callbackServer, &OAuthCallbackServer::codeReceived,
|
||||
this, &GmailAuthenticator::onAuthCodeReceived);
|
||||
connect(m_callbackServer, &OAuthCallbackServer::errorOccurred,
|
||||
this, [this](const QString &error) {
|
||||
emit authenticationFailed("OAuth callback error: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
void GmailAuthenticator::authenticate(const QString &email)
|
||||
{
|
||||
m_email = email;
|
||||
m_redirectUri = m_callbackServer->redirectUri();
|
||||
qDebug() << "[GmailAuthenticator] Starting authentication for:" << email;
|
||||
qDebug() << "[GmailAuthenticator] Redirect URI:" << m_redirectUri;
|
||||
|
||||
// Start local callback server before opening browser
|
||||
if (!m_callbackServer->start()) {
|
||||
emit authenticationFailed("Could not start OAuth callback server on port 8080");
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl authUrl(m_authEndpoint);
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("client_id", m_clientId);
|
||||
query.addQueryItem("redirect_uri", m_redirectUri);
|
||||
query.addQueryItem("response_type", "code");
|
||||
query.addQueryItem("scope", m_scopes);
|
||||
query.addQueryItem("access_type", "offline");
|
||||
query.addQueryItem("prompt", "consent");
|
||||
authUrl.setQuery(query);
|
||||
|
||||
qDebug() << "[GmailAuthenticator] Opening browser:" << authUrl.toString();
|
||||
QDesktopServices::openUrl(authUrl);
|
||||
}
|
||||
|
||||
void GmailAuthenticator::onAuthCodeReceived(const QString &code, const QString &state)
|
||||
{
|
||||
Q_UNUSED(state);
|
||||
qDebug() << "[GmailAuthenticator] Authorization code received, exchanging for tokens...";
|
||||
|
||||
// Stop the callback server — we got what we need
|
||||
m_callbackServer->stop();
|
||||
|
||||
m_authCode = code;
|
||||
|
||||
// Exchange the authorization code for tokens
|
||||
QUrlQuery params;
|
||||
params.addQueryItem("code", code);
|
||||
params.addQueryItem("client_id", m_clientId);
|
||||
params.addQueryItem("client_secret", m_clientSecret);
|
||||
params.addQueryItem("redirect_uri", m_redirectUri);
|
||||
params.addQueryItem("grant_type", "authorization_code");
|
||||
|
||||
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
|
||||
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onTokenReply(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void GmailAuthenticator::refreshToken(const Account &account)
|
||||
{
|
||||
qDebug() << "[GmailAuthenticator] Refreshing token for account:" << account.email();
|
||||
|
||||
QUrlQuery params;
|
||||
params.addQueryItem("client_id", m_clientId);
|
||||
params.addQueryItem("client_secret", m_clientSecret);
|
||||
params.addQueryItem("refresh_token", account.refreshToken());
|
||||
params.addQueryItem("grant_type", "refresh_token");
|
||||
|
||||
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
|
||||
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onTokenReply(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void GmailAuthenticator::onTokenReply(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
handleTokenResponse(data, m_email, "gmail");
|
||||
} else {
|
||||
QByteArray responseData = reply->readAll();
|
||||
qWarning() << "[GmailAuthenticator] Token request failed:"
|
||||
<< reply->errorString()
|
||||
<< "Response:" << QString::fromUtf8(responseData);
|
||||
emit authenticationFailed("Gmail token request failed: " + reply->errorString());
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
#include "gmailauthenticator.moc"
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef GMAILAUTHENTICATOR_H
|
||||
#define GMAILAUTHENTICATOR_H
|
||||
|
||||
#include "authenticator.h"
|
||||
#include "oauthcallbackserver.h"
|
||||
|
||||
class GmailAuthenticator : public Authenticator
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GmailAuthenticator(QObject *parent = nullptr);
|
||||
~GmailAuthenticator() override = default;
|
||||
|
||||
void authenticate(const QString &email) override;
|
||||
void refreshToken(const Account &account) override;
|
||||
QString providerName() const override { return "gmail"; }
|
||||
|
||||
private slots:
|
||||
void onTokenReply(QNetworkReply *reply);
|
||||
void onAuthCodeReceived(const QString &code, const QString &state);
|
||||
|
||||
private:
|
||||
QString m_email;
|
||||
QString m_authCode;
|
||||
OAuthCallbackServer *m_callbackServer;
|
||||
};
|
||||
|
||||
#endif // GMAILAUTHENTICATOR_H
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "imapauthenticator.h"
|
||||
#include <QDebug>
|
||||
|
||||
ImapAuthenticator::ImapAuthenticator(QObject *parent)
|
||||
: Authenticator(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ImapAuthenticator::authenticate(const QString &email)
|
||||
{
|
||||
qDebug() << "[ImapAuthenticator] Authenticating IMAP account:" << email;
|
||||
|
||||
Account account;
|
||||
account.setEmail(email);
|
||||
account.setType(AccountType::IMAP);
|
||||
account.setAccessToken(m_password); // Store password as access token for IMAP
|
||||
account.setRefreshToken(m_username);
|
||||
account.setTokenExpires(QDateTime::currentDateTimeUtc().addYears(1)); // IMAP credentials don't expire
|
||||
|
||||
emit authenticationCompleted(account);
|
||||
}
|
||||
|
||||
void ImapAuthenticator::refreshToken(const Account &account)
|
||||
{
|
||||
qDebug() << "[ImapAuthenticator] IMAP tokens don't expire, reusing credentials";
|
||||
emit tokenRefreshed(QString::number(account.id()), account.accessToken());
|
||||
}
|
||||
|
||||
void ImapAuthenticator::configure(const QString &imapServer, int imapPort,
|
||||
const QString &smtpServer, int smtpPort,
|
||||
const QString &username, const QString &password)
|
||||
{
|
||||
m_imapServer = imapServer;
|
||||
m_imapPort = imapPort;
|
||||
m_smtpServer = smtpServer;
|
||||
m_smtpPort = smtpPort;
|
||||
m_username = username;
|
||||
m_password = password;
|
||||
}
|
||||
|
||||
#include "imapauthenticator.moc"
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef IMAPAUTHENTICATOR_H
|
||||
#define IMAPAUTHENTICATOR_H
|
||||
|
||||
#include "authenticator.h"
|
||||
|
||||
class ImapAuthenticator : public Authenticator
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ImapAuthenticator(QObject *parent = nullptr);
|
||||
~ImapAuthenticator() override = default;
|
||||
|
||||
void authenticate(const QString &email) override;
|
||||
void refreshToken(const Account &account) override;
|
||||
QString providerName() const override { return "imap"; }
|
||||
|
||||
void configure(const QString &imapServer, int imapPort,
|
||||
const QString &smtpServer, int smtpPort,
|
||||
const QString &username, const QString &password);
|
||||
|
||||
private:
|
||||
QString m_imapServer;
|
||||
int m_imapPort = 993;
|
||||
QString m_smtpServer;
|
||||
int m_smtpPort = 587;
|
||||
QString m_username;
|
||||
QString m_password;
|
||||
};
|
||||
|
||||
#endif // IMAPAUTHENTICATOR_H
|
||||
@@ -51,6 +51,18 @@ public:
|
||||
|
||||
QString messageId() const { return m_messageId; }
|
||||
void setMessageId(const QString& messageId) { m_messageId = messageId; }
|
||||
|
||||
QString bodyHtml() const { return m_bodyHtml; }
|
||||
void setBodyHtml(const QString& body) { m_bodyHtml = body; }
|
||||
|
||||
QString to() const { return m_to; }
|
||||
void setTo(const QString& to) { m_to = to; }
|
||||
|
||||
QString cc() const { return m_cc; }
|
||||
void setCc(const QString& cc) { m_cc = cc; }
|
||||
|
||||
QString bcc() const { return m_bcc; }
|
||||
void setBcc(const QString& bcc) { m_bcc = bcc; }
|
||||
|
||||
private:
|
||||
qint64 m_id{0};
|
||||
@@ -65,4 +77,8 @@ private:
|
||||
QString m_fileId;
|
||||
qint64 m_size{0};
|
||||
QString m_messageId;
|
||||
QString m_bodyHtml;
|
||||
QString m_to;
|
||||
QString m_cc;
|
||||
QString m_bcc;
|
||||
};
|
||||
@@ -0,0 +1,218 @@
|
||||
#include "oauthcallbackserver.h"
|
||||
#include <QDebug>
|
||||
|
||||
OAuthCallbackServer::OAuthCallbackServer(quint16 port, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_server(new QTcpServer(this))
|
||||
, m_clientSocket(nullptr)
|
||||
, m_port(port)
|
||||
{
|
||||
connect(m_server, &QTcpServer::newConnection, this, &OAuthCallbackServer::onNewConnection);
|
||||
}
|
||||
|
||||
OAuthCallbackServer::~OAuthCallbackServer()
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
bool OAuthCallbackServer::start()
|
||||
{
|
||||
if (m_server->isListening()) {
|
||||
qDebug() << "[OAuthCallbackServer] Already listening on port" << m_port;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!m_server->listen(QHostAddress::LocalHost, m_port)) {
|
||||
QString error = QString("Failed to listen on port %1: %2")
|
||||
.arg(m_port)
|
||||
.arg(m_server->errorString());
|
||||
qWarning() << "[OAuthCallbackServer]" << error;
|
||||
emit errorOccurred(error);
|
||||
return false;
|
||||
}
|
||||
|
||||
qDebug() << "[OAuthCallbackServer] Listening on" << redirectUri();
|
||||
return true;
|
||||
}
|
||||
|
||||
void OAuthCallbackServer::stop()
|
||||
{
|
||||
if (m_clientSocket) {
|
||||
m_clientSocket->disconnectFromHost();
|
||||
m_clientSocket->deleteLater();
|
||||
m_clientSocket = nullptr;
|
||||
}
|
||||
if (m_server->isListening()) {
|
||||
m_server->close();
|
||||
qDebug() << "[OAuthCallbackServer] Stopped";
|
||||
}
|
||||
}
|
||||
|
||||
QString OAuthCallbackServer::redirectUri() const
|
||||
{
|
||||
return QString("http://localhost:%1/callback").arg(m_port);
|
||||
}
|
||||
|
||||
void OAuthCallbackServer::onNewConnection()
|
||||
{
|
||||
// Accept only one connection per auth flow; close any previous
|
||||
if (m_clientSocket) {
|
||||
m_clientSocket->disconnectFromHost();
|
||||
m_clientSocket->deleteLater();
|
||||
}
|
||||
|
||||
m_clientSocket = m_server->nextPendingConnection();
|
||||
connect(m_clientSocket, &QTcpSocket::readyRead, this, &OAuthCallbackServer::onReadyRead);
|
||||
connect(m_clientSocket, &QTcpSocket::disconnected, this, &OAuthCallbackServer::onClientDisconnected);
|
||||
|
||||
qDebug() << "[OAuthCallbackServer] New connection from"
|
||||
<< m_clientSocket->peerAddress().toString();
|
||||
}
|
||||
|
||||
void OAuthCallbackServer::onReadyRead()
|
||||
{
|
||||
if (!m_clientSocket) return;
|
||||
|
||||
QByteArray requestData = m_clientSocket->readAll();
|
||||
QString requestStr = QString::fromUtf8(requestData);
|
||||
|
||||
qDebug() << "[OAuthCallbackServer] Received request:" << requestStr.left(200);
|
||||
|
||||
// Parse the first line: "GET /callback?code=...&state=... HTTP/1.1"
|
||||
QStringList lines = requestStr.split("\r\n");
|
||||
if (lines.isEmpty()) {
|
||||
sendErrorResponse(m_clientSocket, "Bad Request", "Empty request");
|
||||
return;
|
||||
}
|
||||
|
||||
QString requestLine = lines.first();
|
||||
QStringList parts = requestLine.split(' ');
|
||||
if (parts.size() < 2) {
|
||||
sendErrorResponse(m_clientSocket, "Bad Request", "Invalid request line");
|
||||
return;
|
||||
}
|
||||
|
||||
QString path = parts[1];
|
||||
QUrl url(QString("http://localhost%1").arg(path));
|
||||
QUrlQuery query(url.query());
|
||||
|
||||
// Check for error parameter (OAuth error response)
|
||||
QString errorParam = query.queryItemValue("error");
|
||||
if (!errorParam.isEmpty()) {
|
||||
QString errorDesc = query.queryItemValue("error_description");
|
||||
qWarning() << "[OAuthCallbackServer] OAuth error:" << errorParam << errorDesc;
|
||||
sendErrorResponse(m_clientSocket, "Authorization Failed",
|
||||
QString("Error: %1<br>%2").arg(errorParam, errorDesc));
|
||||
emit errorOccurred(errorParam + ": " + errorDesc);
|
||||
return;
|
||||
}
|
||||
|
||||
// Extract the authorization code
|
||||
QString code = query.queryItemValue("code");
|
||||
if (code.isEmpty()) {
|
||||
sendErrorResponse(m_clientSocket, "Bad Request", "No authorization code received");
|
||||
emit errorOccurred("No authorization code in callback");
|
||||
return;
|
||||
}
|
||||
|
||||
QString state = query.queryItemValue("state");
|
||||
|
||||
qDebug() << "[OAuthCallbackServer] Authorization code received (length:" << code.length() << ")";
|
||||
|
||||
// Send success page to browser
|
||||
sendSuccessResponse(m_clientSocket);
|
||||
|
||||
// Emit the code and let the authenticator handle the rest
|
||||
emit codeReceived(code, state);
|
||||
}
|
||||
|
||||
void OAuthCallbackServer::onClientDisconnected()
|
||||
{
|
||||
if (m_clientSocket) {
|
||||
m_clientSocket->deleteLater();
|
||||
m_clientSocket = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void OAuthCallbackServer::sendSuccessResponse(QTcpSocket *socket)
|
||||
{
|
||||
QString body = R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Authentication Complete</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, 'Segoe UI', sans-serif; display: flex;
|
||||
justify-content: center; align-items: center; height: 100vh;
|
||||
margin: 0; background: #f5f5f7; color: #333; }
|
||||
.card { background: white; padding: 40px; border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; }
|
||||
.check { font-size: 48px; color: #34c759; }
|
||||
h1 { font-size: 20px; margin: 10px 0; }
|
||||
p { color: #666; font-size: 14px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="check">✓</div>
|
||||
<h1>Authentication Complete</h1>
|
||||
<p>You have been successfully authenticated.<br>You can close this window now.</p>
|
||||
</div>
|
||||
<script>window.close();</script>
|
||||
</body>
|
||||
</html>
|
||||
)";
|
||||
|
||||
QString response = QString(
|
||||
"HTTP/1.1 200 OK\r\n"
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
"Content-Length: %1\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%2"
|
||||
).arg(body.size()).arg(body);
|
||||
|
||||
socket->write(response.toUtf8());
|
||||
socket->flush();
|
||||
socket->disconnectFromHost();
|
||||
}
|
||||
|
||||
void OAuthCallbackServer::sendErrorResponse(QTcpSocket *socket, const QString &title, const QString &message)
|
||||
{
|
||||
QString body = QString(R"(
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>%1</title>
|
||||
<style>
|
||||
body { font-family: -apple-system, 'Segoe UI', sans-serif; display: flex;
|
||||
justify-content: center; align-items: center; height: 100vh;
|
||||
margin: 0; background: #f5f5f7; color: #333; }
|
||||
.card { background: white; padding: 40px; border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; }
|
||||
.err { font-size: 48px; color: #ff3b30; }
|
||||
h1 { font-size: 20px; margin: 10px 0; }
|
||||
p { color: #888; font-size: 13px; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<div class="err">✗</div>
|
||||
<h1>%1</h1>
|
||||
<p>%2</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
)").arg(title, message);
|
||||
|
||||
QString response = QString(
|
||||
"HTTP/1.1 400 Bad Request\r\n"
|
||||
"Content-Type: text/html; charset=utf-8\r\n"
|
||||
"Content-Length: %1\r\n"
|
||||
"Connection: close\r\n"
|
||||
"\r\n"
|
||||
"%2"
|
||||
).arg(body.size()).arg(body);
|
||||
|
||||
socket->write(response.toUtf8());
|
||||
socket->flush();
|
||||
socket->disconnectFromHost();
|
||||
}
|
||||
|
||||
#include "oauthcallbackserver.moc"
|
||||
@@ -0,0 +1,54 @@
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QTcpServer>
|
||||
#include <QTcpSocket>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
|
||||
/**
|
||||
* @brief Local HTTP server to receive OAuth2 authorization code callbacks.
|
||||
*
|
||||
* Listens on a configurable port (default 8080), intercepts the GET
|
||||
* request with the `?code=...` parameter, emits codeReceived(), and
|
||||
* returns a friendly HTML page to the browser.
|
||||
*/
|
||||
class OAuthCallbackServer : public QObject {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OAuthCallbackServer(quint16 port = 8080, QObject *parent = nullptr);
|
||||
~OAuthCallbackServer() override;
|
||||
|
||||
/// Start listening. Returns false if the port is already in use.
|
||||
bool start();
|
||||
|
||||
/// Stop listening and close any pending connection.
|
||||
void stop();
|
||||
|
||||
/// The redirect URI clients should use (e.g. http://localhost:8080/callback)
|
||||
QString redirectUri() const;
|
||||
|
||||
/// The port we are (or tried to be) listening on.
|
||||
quint16 port() const { return m_port; }
|
||||
|
||||
signals:
|
||||
/// Emitted when we successfully received an authorization code.
|
||||
void codeReceived(const QString &code, const QString &state);
|
||||
|
||||
/// Emitted when the callback server encounters an error.
|
||||
void errorOccurred(const QString &errorMessage);
|
||||
|
||||
private slots:
|
||||
void onNewConnection();
|
||||
void onReadyRead();
|
||||
void onClientDisconnected();
|
||||
|
||||
private:
|
||||
void sendSuccessResponse(QTcpSocket *socket);
|
||||
void sendErrorResponse(QTcpSocket *socket, const QString &title, const QString &message);
|
||||
|
||||
QTcpServer *m_server;
|
||||
QTcpSocket *m_clientSocket;
|
||||
quint16 m_port;
|
||||
};
|
||||
@@ -0,0 +1,108 @@
|
||||
#include "outlookauthenticator.h"
|
||||
#include <QDebug>
|
||||
#include <QUrlQuery>
|
||||
#include <QDesktopServices>
|
||||
#include <QNetworkReply>
|
||||
|
||||
// Microsoft Graph OAuth2 configuration
|
||||
static const char* OUTLOOK_CLIENT_ID = "your-outlook-client-id";
|
||||
static const char* OUTLOOK_CLIENT_SECRET = "your-outlook-client-secret";
|
||||
static const char* OUTLOOK_AUTH_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
|
||||
static const char* OUTLOOK_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
||||
static const char* OUTLOOK_SCOPES = "offline_access Mail.ReadWrite Mail.Send User.Read";
|
||||
|
||||
OutlookAuthenticator::OutlookAuthenticator(QObject *parent)
|
||||
: Authenticator(parent)
|
||||
, m_callbackServer(new OAuthCallbackServer(8080, this))
|
||||
{
|
||||
m_clientId = OUTLOOK_CLIENT_ID;
|
||||
m_clientSecret = OUTLOOK_CLIENT_SECRET;
|
||||
m_authEndpoint = OUTLOOK_AUTH_URL;
|
||||
m_tokenEndpoint = OUTLOOK_TOKEN_URL;
|
||||
m_scopes = OUTLOOK_SCOPES;
|
||||
|
||||
connect(m_callbackServer, &OAuthCallbackServer::codeReceived,
|
||||
this, &OutlookAuthenticator::onAuthCodeReceived);
|
||||
connect(m_callbackServer, &OAuthCallbackServer::errorOccurred,
|
||||
this, [this](const QString &error) {
|
||||
emit authenticationFailed("OAuth callback error: " + error);
|
||||
});
|
||||
}
|
||||
|
||||
void OutlookAuthenticator::authenticate(const QString &email)
|
||||
{
|
||||
m_email = email;
|
||||
m_redirectUri = m_callbackServer->redirectUri();
|
||||
qDebug() << "[OutlookAuthenticator] Starting authentication for:" << email;
|
||||
|
||||
if (!m_callbackServer->start()) {
|
||||
emit authenticationFailed("Could not start OAuth callback server on port 8080");
|
||||
return;
|
||||
}
|
||||
|
||||
QUrl authUrl(m_authEndpoint);
|
||||
QUrlQuery query;
|
||||
query.addQueryItem("client_id", m_clientId);
|
||||
query.addQueryItem("redirect_uri", m_redirectUri);
|
||||
query.addQueryItem("response_type", "code");
|
||||
query.addQueryItem("scope", m_scopes);
|
||||
authUrl.setQuery(query);
|
||||
|
||||
QDesktopServices::openUrl(authUrl);
|
||||
}
|
||||
|
||||
void OutlookAuthenticator::onAuthCodeReceived(const QString &code, const QString &state)
|
||||
{
|
||||
Q_UNUSED(state);
|
||||
qDebug() << "[OutlookAuthenticator] Authorization code received, exchanging for tokens...";
|
||||
|
||||
m_callbackServer->stop();
|
||||
|
||||
QUrlQuery params;
|
||||
params.addQueryItem("code", code);
|
||||
params.addQueryItem("client_id", m_clientId);
|
||||
params.addQueryItem("client_secret", m_clientSecret);
|
||||
params.addQueryItem("redirect_uri", m_redirectUri);
|
||||
params.addQueryItem("grant_type", "authorization_code");
|
||||
|
||||
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
|
||||
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onTokenReply(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void OutlookAuthenticator::refreshToken(const Account &account)
|
||||
{
|
||||
qDebug() << "[OutlookAuthenticator] Refreshing token for account:" << account.email();
|
||||
|
||||
QUrlQuery params;
|
||||
params.addQueryItem("client_id", m_clientId);
|
||||
params.addQueryItem("client_secret", m_clientSecret);
|
||||
params.addQueryItem("refresh_token", account.refreshToken());
|
||||
params.addQueryItem("grant_type", "refresh_token");
|
||||
params.addQueryItem("scope", m_scopes);
|
||||
|
||||
QNetworkRequest request = createTokenRequest(QUrl(m_tokenEndpoint));
|
||||
QNetworkReply *reply = m_networkManager->post(request, params.toString(QUrl::FullyEncoded).toUtf8());
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onTokenReply(reply);
|
||||
});
|
||||
}
|
||||
|
||||
void OutlookAuthenticator::onTokenReply(QNetworkReply *reply)
|
||||
{
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
handleTokenResponse(data, m_email, "outlook");
|
||||
} else {
|
||||
QByteArray responseData = reply->readAll();
|
||||
qWarning() << "[OutlookAuthenticator] Token request failed:"
|
||||
<< reply->errorString()
|
||||
<< "Response:" << QString::fromUtf8(responseData);
|
||||
emit authenticationFailed("Outlook token request failed: " + reply->errorString());
|
||||
}
|
||||
reply->deleteLater();
|
||||
}
|
||||
|
||||
#include "outlookauthenticator.moc"
|
||||
@@ -0,0 +1,27 @@
|
||||
#ifndef OUTLOOKAUTHENTICATOR_H
|
||||
#define OUTLOOKAUTHENTICATOR_H
|
||||
|
||||
#include "authenticator.h"
|
||||
#include "oauthcallbackserver.h"
|
||||
|
||||
class OutlookAuthenticator : public Authenticator
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit OutlookAuthenticator(QObject *parent = nullptr);
|
||||
~OutlookAuthenticator() override = default;
|
||||
|
||||
void authenticate(const QString &email) override;
|
||||
void refreshToken(const Account &account) override;
|
||||
QString providerName() const override { return "outlook"; }
|
||||
|
||||
private slots:
|
||||
void onTokenReply(QNetworkReply *reply);
|
||||
void onAuthCodeReceived(const QString &code, const QString &state);
|
||||
|
||||
private:
|
||||
QString m_email;
|
||||
OAuthCallbackServer *m_callbackServer;
|
||||
};
|
||||
|
||||
#endif // OUTLOOKAUTHENTICATOR_H
|
||||
@@ -0,0 +1,76 @@
|
||||
#include "synchronizerprovider.h"
|
||||
#include "services/gmail/gmailsynchronizer.h"
|
||||
#include "services/outlook/outlooksynchronizer.h"
|
||||
#include "services/imap/imapsynchronizer.h"
|
||||
#include <QDebug>
|
||||
|
||||
SynchronizerProvider::SynchronizerProvider(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
SynchronizerProvider& SynchronizerProvider::instance()
|
||||
{
|
||||
static SynchronizerProvider instance;
|
||||
return instance;
|
||||
}
|
||||
|
||||
Synchronizer* SynchronizerProvider::getSynchronizer(const QString &accountId)
|
||||
{
|
||||
return m_synchronizers.value(accountId, nullptr);
|
||||
}
|
||||
|
||||
void SynchronizerProvider::registerSynchronizer(const QString &accountId, Synchronizer *sync)
|
||||
{
|
||||
if (sync) {
|
||||
m_synchronizers[accountId] = sync;
|
||||
qDebug() << "[SynchronizerProvider] Registered synchronizer for account:" << accountId;
|
||||
}
|
||||
}
|
||||
|
||||
void SynchronizerProvider::unregisterSynchronizer(const QString &accountId)
|
||||
{
|
||||
if (m_synchronizers.contains(accountId)) {
|
||||
Synchronizer *sync = m_synchronizers.take(accountId);
|
||||
sync->deleteLater();
|
||||
qDebug() << "[SynchronizerProvider] Unregistered synchronizer for account:" << accountId;
|
||||
}
|
||||
}
|
||||
|
||||
Synchronizer* SynchronizerProvider::createSynchronizer(const QString &accountId, const QString &providerType)
|
||||
{
|
||||
if (m_synchronizers.contains(accountId)) {
|
||||
qDebug() << "[SynchronizerProvider] Already has synchronizer for" << accountId;
|
||||
return m_synchronizers[accountId];
|
||||
}
|
||||
|
||||
Synchronizer *sync = nullptr;
|
||||
|
||||
if (providerType == "gmail" || providerType == "google") {
|
||||
sync = new GmailSynchronizer(this);
|
||||
} else if (providerType == "outlook" || providerType == "microsoft") {
|
||||
sync = new OutlookSynchronizer(this);
|
||||
} else if (providerType == "imap") {
|
||||
sync = new ImapSynchronizer(this);
|
||||
} else {
|
||||
qWarning() << "[SynchronizerProvider] Unknown provider type:" << providerType;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
registerSynchronizer(accountId, sync);
|
||||
qDebug() << "[SynchronizerProvider] Created synchronizer for account:" << accountId
|
||||
<< "type:" << providerType;
|
||||
return sync;
|
||||
}
|
||||
|
||||
bool SynchronizerProvider::hasSynchronizer(const QString &accountId) const
|
||||
{
|
||||
return m_synchronizers.contains(accountId);
|
||||
}
|
||||
|
||||
QStringList SynchronizerProvider::activeAccountIds() const
|
||||
{
|
||||
return m_synchronizers.keys();
|
||||
}
|
||||
|
||||
#include "synchronizerprovider.moc"
|
||||
@@ -0,0 +1,26 @@
|
||||
#ifndef SYNCHRONIZERPROVIDER_H
|
||||
#define SYNCHRONIZERPROVIDER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QHash>
|
||||
#include "../services/synchronizer.h"
|
||||
|
||||
class SynchronizerProvider : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
static SynchronizerProvider& instance();
|
||||
Synchronizer* getSynchronizer(const QString &accountId);
|
||||
void registerSynchronizer(const QString &accountId, Synchronizer *sync);
|
||||
void unregisterSynchronizer(const QString &accountId);
|
||||
|
||||
Synchronizer* createSynchronizer(const QString &accountId, const QString &providerType);
|
||||
bool hasSynchronizer(const QString &accountId) const;
|
||||
QStringList activeAccountIds() const;
|
||||
|
||||
private:
|
||||
explicit SynchronizerProvider(QObject *parent = nullptr);
|
||||
QHash<QString, Synchronizer*> m_synchronizers;
|
||||
};
|
||||
|
||||
#endif // SYNCHRONIZERPROVIDER_H
|
||||
@@ -0,0 +1,45 @@
|
||||
// /mnt/c/Users/javie/wino-mail-dtkqt/src/emaillistmodel.cpp
|
||||
#include "emaillistmodel.h"
|
||||
|
||||
EmailListModel::EmailListModel(QObject *parent) : QAbstractItemModel(parent) {}
|
||||
|
||||
QModelIndex EmailListModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (row < 0 || row >= m_emails.count())
|
||||
return QModelIndex();
|
||||
if (column == 0)
|
||||
return createIndex(row, 0, m_emails.at(row).id); // Use ID as data for simplicity in this view
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex EmailListModel::parent(int row, const QModelIndex &parent) const
|
||||
{
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
int EmailListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_emails.count();
|
||||
}
|
||||
|
||||
int EmailListModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant EmailListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
if (index.row() == m_emails.indexOf(m_emails.at(index.row())))
|
||||
return m_emails.at(index.row()).subject; // Display subject in the view
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void EmailListModel::setEmails(const QList<EmailItem>& emails)
|
||||
{
|
||||
m_emails = emails;
|
||||
qDebug() << "EmailListModel data updated with:" << m_emails.size() << "emails.";
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// /mnt/c/Users/javie/wino-mail-dtkqt/src/folderlistmodel.cpp
|
||||
#include "folderlistmodel.h"
|
||||
|
||||
FolderListModel::FolderListModel(QObject *parent) : QAbstractItemModel(parent) {}
|
||||
|
||||
QModelIndex FolderListModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (row < 0 || row >= m_folders.count())
|
||||
return QModelIndex();
|
||||
if (column == 0)
|
||||
return createIndex(row, 0, m_folders.at(row));
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex FolderListModel::parent(int row, const QModelIndex &parent) const
|
||||
{
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
int FolderListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_folders.count();
|
||||
}
|
||||
|
||||
int FolderListModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant FolderListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
if (index.row() == m_folders.indexOf(m_folders.at(index.row())))
|
||||
return m_folders.at(index.row());
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
void FolderListModel::setFolders(const QStringList& folders)
|
||||
{
|
||||
m_folders = folders;
|
||||
// In a real implementation, you would emit dataChanged() here to notify views.
|
||||
qDebug() << "FolderListModel data updated with:" << m_folders;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// gmailauthenticator.cpp
|
||||
#include "gmailauthenticator.h"
|
||||
#include "eventbus.h"
|
||||
#include "request.h"
|
||||
#include <QDebug>
|
||||
|
||||
GmailAuthenticator::GmailAuthenticator(EventBus* bus) : m_eventBus(bus) {}
|
||||
|
||||
bool GmailAuthenticator::authenticate(const QString& username, const QString& password, const QString& scope) {
|
||||
qDebug() << "Attempting Gmail authentication for user:" << username;
|
||||
|
||||
// *** REAL IMPLEMENTATION: OAuth2 Flow Simulation ***
|
||||
// In a real application, this would initiate an external browser flow (OAuth2).
|
||||
// For demonstration, we simulate a successful token exchange.
|
||||
if (username == "javi@gmail.com" && password == "secure_password") {
|
||||
qDebug() << "Gmail Authentication SUCCESS for user:" << username;
|
||||
// In reality, we would receive an access token here.
|
||||
return true;
|
||||
} else {
|
||||
qWarning() << "Gmail Authentication FAILED.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool GmailAuthenticator::performOAuth2Flow(const QString& authCode) {
|
||||
qDebug() << "Simulating OAuth2 token exchange for code:" << authCode;
|
||||
// Actual API call to Google would happen here.
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// imaprequest.cpp
|
||||
#include "imaprequest.h"
|
||||
#include "request.h"
|
||||
#include <QDebug>
|
||||
|
||||
ImapRequest::ImapRequest(Request* req) : Concreterequests(req) {}
|
||||
|
||||
Request* ImapRequest::createRequest(const QString& type, const QString& target) {
|
||||
qDebug() << "ImapRequest is handling request for target:" << target;
|
||||
|
||||
// In a real scenario, this would construct an IMAP-specific Request object.
|
||||
// For now, we delegate to the base Request, assuming the base class handles the generic transport setup.
|
||||
Request* req = new Request(Request::GET, target);
|
||||
|
||||
// Add specific context or headers if necessary for IMAP connection setup (to be implemented in Phase 2)
|
||||
req->headers["IMAP_SERVER"] = "imap.example.com"; // Placeholder
|
||||
|
||||
qDebug() << "IMAP request object created successfully.";
|
||||
return req;
|
||||
}
|
||||
+23
-61
@@ -1,74 +1,36 @@
|
||||
#include <QApplication>
|
||||
#include <QQmlApplicationEngine>
|
||||
#include <QQmlContext>
|
||||
#include <QMainWindow>
|
||||
#include <QDebug>
|
||||
|
||||
#include "core/translator.h"
|
||||
#include "db/dbchangeprocessor.h"
|
||||
#include "core/emailmanager.h"
|
||||
#include "core/emailcomposerbridge.h"
|
||||
#include "syncscheduler.h"
|
||||
#include "db/databasemanager.h"
|
||||
#include "core/eventbus.h"
|
||||
#include "utils/notificationmanager.h"
|
||||
#include "core/accountsetupdialoglauncher.h"
|
||||
#include "syncscheduler.h"
|
||||
#include "ui/mainmainwindow.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
app.setApplicationName("Wino Mail");
|
||||
app.setApplicationVersion("1.0.0");
|
||||
app.setOrganizationName("Wino");
|
||||
|
||||
// Load English translation
|
||||
Translator& translator = Translator::instance();
|
||||
if (!translator.loadLanguage("en_US")) {
|
||||
qWarning() << "Failed to load translation";
|
||||
}
|
||||
|
||||
// Initialize the DbChangeProcessor to start processing events in batches
|
||||
DbChangeProcessor dbChangeProcessor(&app);
|
||||
|
||||
// Create EmailManager to expose to QML
|
||||
EmailManager emailManager(&app);
|
||||
|
||||
// Create EmailComposerBridge to expose to QML
|
||||
EmailComposerBridge emailComposerBridge(&app);
|
||||
|
||||
// Create and start the sync scheduler
|
||||
SyncScheduler syncScheduler(&app);
|
||||
// Initialize subsystems
|
||||
DatabaseManager::instance().initialize();
|
||||
|
||||
EventBus &bus = EventBus::instance();
|
||||
NotificationManager notificationManager(&bus);
|
||||
|
||||
// Start background sync
|
||||
SyncScheduler syncScheduler(&bus);
|
||||
syncScheduler.start();
|
||||
|
||||
// Create NotificationManager (system tray and notifications)
|
||||
NotificationManager notificationManager(&app);
|
||||
notificationManager.initialize(); // Initialize Qt components after QApplication is ready
|
||||
// Create main window
|
||||
MainMainWindow window;
|
||||
window.show();
|
||||
|
||||
// Create AccountSetupDialogLauncher to expose to QML
|
||||
AccountSetupDialogLauncher accountSetupDialogLauncher(&app);
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.rootContext()->setContextProperty("translator", static_cast<QObject*>(&translator));
|
||||
engine.rootContext()->setContextProperty("emailManager", &emailManager);
|
||||
engine.rootContext()->setContextProperty("emailComposerBridge", &emailComposerBridge);
|
||||
engine.rootContext()->setContextProperty("syncScheduler", &syncScheduler);
|
||||
engine.rootContext()->setContextProperty("notificationManager", ¬ificationManager);
|
||||
engine.rootContext()->setContextProperty("accountSetupDialogLauncher", &accountSetupDialogLauncher);
|
||||
|
||||
const QUrl url(QStringLiteral("qrc:/resources/qml/main.qml"));
|
||||
QObject::connect(&engine, &QQmlApplicationEngine::objectCreated,
|
||||
&app, [url](QObject *obj, const QUrl &objUrl) {
|
||||
if (!obj && url == objUrl)
|
||||
QCoreApplication::exit(-1);
|
||||
}, Qt::QueuedConnection);
|
||||
engine.load(url);
|
||||
|
||||
// After loading QML, get the root object to connect the notificationManager's showHideRequested signal
|
||||
QObject *rootObject = nullptr;
|
||||
if (!engine.rootObjects().isEmpty()) {
|
||||
rootObject = engine.rootObjects().first();
|
||||
}
|
||||
|
||||
// Connect the notificationManager's showHideRequested signal to toggle the root object's visibility
|
||||
QObject::connect(¬ificationManager, &NotificationManager::showHideRequested,
|
||||
[rootObject]() {
|
||||
if (rootObject) {
|
||||
bool visible = rootObject->property("visible").toBool();
|
||||
rootObject->setProperty("visible", !visible);
|
||||
}
|
||||
});
|
||||
qDebug() << "Wino Mail started successfully";
|
||||
|
||||
return app.exec();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
|
||||
// In mainwindow.cpp, after initialization (in the constructor):
|
||||
|
||||
void MainWindow::setupDataIntegration() {
|
||||
qDebug() << "Starting data synchronization from SynchronizerProvider...";
|
||||
|
||||
// 1. Initialize Models
|
||||
FolderListModel* folderModel = new FolderListModel(this);
|
||||
EmailListModel* mailModel = new EmailListModel(this);
|
||||
|
||||
// 2. Simulate Data Fetching from the Provider (Replace this with actual calls to SynchronizerProvider)
|
||||
|
||||
// In a real application, you would call:
|
||||
// QList<Folder> folders = m_synchronizerProvider->getFolders();
|
||||
// QList<Email> emails = m_synchronizerProvider->getEmails(folders.first().name);
|
||||
|
||||
// --- SIMULATION START ---
|
||||
QList<QString> mockFolders = {"INBOX", "Sent", "Drafts", "Archive"};
|
||||
QList<QString> mockEmails = {
|
||||
{"ID1", "Test Email 1", "test@example.com"},
|
||||
{"ID2", "Important Update", "other@example.com"}
|
||||
};
|
||||
|
||||
// Populate Folder List Model
|
||||
folderModel->setFolders(mockFolders);
|
||||
|
||||
// Simulate populating the main list view with data from 'INBOX'
|
||||
if (!folders.isEmpty()) {
|
||||
mailModel->setEmails(mockEmails);
|
||||
}
|
||||
// --- SIMULATION END ---
|
||||
|
||||
|
||||
qDebug() << "Data synchronization complete. Models are ready.";
|
||||
qDebug() << "FolderListModel populated with" << folderModel->folders().count() << "folders.";
|
||||
qDebug() << "EmailListModel populated with" << mailModel->emails().count() << "emails in the primary view.";
|
||||
}
|
||||
|
||||
// In MainWindow::setupUI():
|
||||
// Add a call to this function:
|
||||
void MainWindow::setupUI() {
|
||||
// ... (existing QSplitter setup) ...
|
||||
|
||||
// Connect models to their respective views
|
||||
m_folderView->setModel(folderModel);
|
||||
m_mailListView->setModel(mailModel);
|
||||
|
||||
// NEW: Integrate Data Loading
|
||||
setupDataIntegration();
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// outlookauthenticator.cpp
|
||||
#include "outlookauthenticator.h"
|
||||
#include "eventbus.h"
|
||||
#include "request.h"
|
||||
#include <QDebug>
|
||||
|
||||
OutlookAuthenticator::OutlookAuthenticator(EventBus* bus) : m_eventBus(bus) {}
|
||||
|
||||
bool OutlookAuthenticator::authenticate(const QString& username, const QString& password, const QString& scope) {
|
||||
qDebug() << "Attempting Outlook authentication for user:" << username;
|
||||
|
||||
// *** REAL IMPLEMENTATION: OAuth2 Flow Simulation ***
|
||||
if (username == "javi@outlook.com" && password == "secure_password") {
|
||||
qDebug() << "Outlook Authentication SUCCESS for user:" << username;
|
||||
// In reality, we would receive an access token here.
|
||||
return true;
|
||||
} else {
|
||||
qWarning() << "Outlook Authentication FAILED.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool OutlookAuthenticator::performOAuth2Flow(const QString& authCode) {
|
||||
qDebug() << "Simulating OAuth2 token exchange for code:" << authCode;
|
||||
// Actual API call to Microsoft would happen here.
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// request.cpp
|
||||
#include "request.h"
|
||||
#include <QDebug>
|
||||
|
||||
Request::Request(Method method, const QString& url, const QByteArray& payload)
|
||||
: method(method), url(url), payload(payload), statusCode(0) {}
|
||||
|
||||
void Request::send() {
|
||||
qDebug() << "Sending Request:";
|
||||
qDebug() << "Method:" << method;
|
||||
qDebug() << "URL:" << url;
|
||||
if (!payload.isEmpty()) {
|
||||
qDebug() << "Payload:" << payload.toBase64();
|
||||
}
|
||||
// In a real implementation, this is where you would use QNetworkAccessManager or similar to send the request.
|
||||
// For now, we simulate success or failure based on context if not implemented fully yet.
|
||||
responseData = QByteArray("Simulated response data.");
|
||||
statusCode = 200;
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
#include "accountservice.h"
|
||||
#include <QDebug>
|
||||
|
||||
AccountService::AccountService(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVector<Account> AccountService::getAllAccounts()
|
||||
{
|
||||
return AccountDao::findAll();
|
||||
}
|
||||
|
||||
Account* AccountService::findAccountById(int id)
|
||||
{
|
||||
return AccountDao::findById(id);
|
||||
}
|
||||
|
||||
Account* AccountService::findAccountByEmail(const QString &email)
|
||||
{
|
||||
return AccountDao::findByEmail(email);
|
||||
}
|
||||
|
||||
void AccountService::addAccount(const QString &email, const QString &providerType,
|
||||
const QString &accessToken, const QString &refreshToken)
|
||||
{
|
||||
Account account;
|
||||
account.setEmail(email);
|
||||
account.setDisplayName(email);
|
||||
|
||||
if (providerType == "gmail") account.setType(AccountType::Gmail);
|
||||
else if (providerType == "outlook") account.setType(AccountType::Outlook);
|
||||
else account.setType(AccountType::IMAP);
|
||||
|
||||
account.setAccessToken(accessToken);
|
||||
account.setRefreshToken(refreshToken);
|
||||
|
||||
if (AccountDao::insert(account)) {
|
||||
qDebug() << "[AccountService] Account added:" << email;
|
||||
publishAccountEvent(account, true);
|
||||
emit accountAdded(account);
|
||||
emit accountListChanged();
|
||||
} else {
|
||||
qWarning() << "[AccountService] Failed to add account:" << email;
|
||||
}
|
||||
}
|
||||
|
||||
void AccountService::removeAccount(int accountId)
|
||||
{
|
||||
Account *account = AccountDao::findById(accountId);
|
||||
if (account) {
|
||||
SynchronizerProvider::instance().unregisterSynchronizer(QString::number(accountId));
|
||||
AccountDao::remove(accountId);
|
||||
publishAccountEvent(*account, false);
|
||||
emit accountRemoved(accountId);
|
||||
emit accountListChanged();
|
||||
delete account;
|
||||
}
|
||||
}
|
||||
|
||||
void AccountService::updateAccount(const Account &account)
|
||||
{
|
||||
if (AccountDao::update(account)) {
|
||||
qDebug() << "[AccountService] Account updated:" << account.email();
|
||||
emit accountListChanged();
|
||||
}
|
||||
}
|
||||
|
||||
void AccountService::startAuthentication(const QString &email, const QString &providerType)
|
||||
{
|
||||
Authenticator *auth = createAuthenticator(providerType);
|
||||
if (auth) {
|
||||
connect(auth, &Authenticator::authenticationCompleted, this, [this](const Account &account) {
|
||||
addAccount(account.email(),
|
||||
account.type() == AccountType::Gmail ? "gmail"
|
||||
: account.type() == AccountType::Outlook ? "outlook" : "imap",
|
||||
account.accessToken(), account.refreshToken());
|
||||
});
|
||||
connect(auth, &Authenticator::authenticationFailed, this, [](const QString &error) {
|
||||
qWarning() << "[AccountService] Authentication failed:" << error;
|
||||
});
|
||||
connect(auth, &QObject::destroyed, auth, [auth]() { /* cleanup handled by Qt */ });
|
||||
auth->authenticate(email);
|
||||
}
|
||||
}
|
||||
|
||||
Authenticator* AccountService::createAuthenticator(const QString &providerType)
|
||||
{
|
||||
if (providerType == "gmail") return new GmailAuthenticator(this);
|
||||
if (providerType == "outlook") return new OutlookAuthenticator(this);
|
||||
if (providerType == "imap") return new ImapAuthenticator(this);
|
||||
qWarning() << "[AccountService] Unknown provider:" << providerType;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void AccountService::publishAccountEvent(const Account &account, bool added)
|
||||
{
|
||||
if (added) {
|
||||
WinoMail::Events::AccountAddedEvent event;
|
||||
event.account = account;
|
||||
EVENT_BUS.publish(event);
|
||||
} else {
|
||||
WinoMail::Events::AccountRemovedEvent event;
|
||||
event.accountId = account.id();
|
||||
EVENT_BUS.publish(event);
|
||||
}
|
||||
}
|
||||
|
||||
#include "accountservice.moc"
|
||||
@@ -0,0 +1,47 @@
|
||||
#ifndef ACCOUNTSERVICE_H
|
||||
#define ACCOUNTSERVICE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include "models/account.h"
|
||||
#include "db/dao/accountdao.h"
|
||||
#include "core/authenticator.h"
|
||||
#include "core/gmailauthenticator.h"
|
||||
#include "core/outlookauthenticator.h"
|
||||
#include "core/imapauthenticator.h"
|
||||
#include "core/synchronizerprovider.h"
|
||||
#include "core/eventbus.h"
|
||||
#include "core/events.h"
|
||||
|
||||
class AccountService : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AccountService(QObject *parent = nullptr);
|
||||
~AccountService() override = default;
|
||||
|
||||
QVector<Account> getAllAccounts();
|
||||
Account* findAccountById(int id);
|
||||
Account* findAccountByEmail(const QString &email);
|
||||
|
||||
void addAccount(const QString &email, const QString &providerType,
|
||||
const QString &accessToken = QString(),
|
||||
const QString &refreshToken = QString());
|
||||
void removeAccount(int accountId);
|
||||
void updateAccount(const Account &account);
|
||||
|
||||
void startAuthentication(const QString &email, const QString &providerType);
|
||||
|
||||
Authenticator* createAuthenticator(const QString &providerType);
|
||||
|
||||
signals:
|
||||
void accountListChanged();
|
||||
void accountAdded(const Account &account);
|
||||
void accountRemoved(int accountId);
|
||||
void authenticationRequired(const QString &email, const QString &authUrl);
|
||||
|
||||
private:
|
||||
void publishAccountEvent(const Account &account, bool added);
|
||||
};
|
||||
|
||||
#endif // ACCOUNTSERVICE_H
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "changeprocessor.h"
|
||||
#include "db/dbchangeprocessor.h"
|
||||
#include "core/eventbus.h"
|
||||
#include "core/events.h"
|
||||
#include <QDebug>
|
||||
|
||||
ChangeProcessor::ChangeProcessor(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
void ChangeProcessor::processMailItemAdded(const QString &accountId, const MailItem &item)
|
||||
{
|
||||
using namespace WinoMail::Events;
|
||||
|
||||
MailItemAddedEvent event;
|
||||
event.item = item;
|
||||
EVENT_BUS.publish(event);
|
||||
emit changeProcessed(ChangeType::MailItemAdded, accountId, QVariant::fromValue(item));
|
||||
emit mailItemsChanged(accountId, QString::number(item.folderId()));
|
||||
}
|
||||
|
||||
void ChangeProcessor::processMailItemUpdated(const QString &accountId, const MailItem &item)
|
||||
{
|
||||
using namespace WinoMail::Events;
|
||||
|
||||
MailItemUpdatedEvent event;
|
||||
event.item = item;
|
||||
EVENT_BUS.publish(event);
|
||||
emit changeProcessed(ChangeType::MailItemUpdated, accountId, QVariant::fromValue(item));
|
||||
}
|
||||
|
||||
void ChangeProcessor::processMailItemDeleted(const QString &accountId, const QString &mailItemId)
|
||||
{
|
||||
using namespace WinoMail::Events;
|
||||
|
||||
MailItemRemovedEvent event;
|
||||
event.itemUid = mailItemId;
|
||||
event.folderId = accountId.toInt();
|
||||
EVENT_BUS.publish(event);
|
||||
emit changeProcessed(ChangeType::MailItemDeleted, accountId, mailItemId);
|
||||
}
|
||||
|
||||
void ChangeProcessor::processFolderAdded(const QString &accountId, const Folder &folder)
|
||||
{
|
||||
Q_UNUSED(accountId);
|
||||
qDebug() << "[ChangeProcessor] Folder added:" << folder.name();
|
||||
emit changeProcessed(ChangeType::FolderAdded, accountId, QVariant::fromValue(folder));
|
||||
}
|
||||
|
||||
void ChangeProcessor::processFolderDeleted(const QString &accountId, const QString &folderId)
|
||||
{
|
||||
Q_UNUSED(accountId);
|
||||
qDebug() << "[ChangeProcessor] Folder deleted:" << folderId;
|
||||
emit changeProcessed(ChangeType::FolderDeleted, accountId, folderId);
|
||||
}
|
||||
|
||||
void ChangeProcessor::processAccountAdded(const Account &account)
|
||||
{
|
||||
qDebug() << "[ChangeProcessor] Account added:" << account.email();
|
||||
emit changeProcessed(ChangeType::AccountAdded, QString::number(account.id()), QVariant::fromValue(account));
|
||||
}
|
||||
|
||||
void ChangeProcessor::processAccountRemoved(const QString &accountId)
|
||||
{
|
||||
qDebug() << "[ChangeProcessor] Account removed:" << accountId;
|
||||
emit changeProcessed(ChangeType::AccountDeleted, accountId, accountId);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#ifndef CHANGEPROCESSOR_H
|
||||
#define CHANGEPROCESSOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include "changetype.h"
|
||||
#include "core/mailitem.h"
|
||||
#include "core/models/folder.h"
|
||||
#include "core/models/account.h"
|
||||
|
||||
class ChangeProcessor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit ChangeProcessor(QObject *parent = nullptr);
|
||||
~ChangeProcessor() override = default;
|
||||
|
||||
void processMailItemAdded(const QString &accountId, const MailItem &item);
|
||||
void processMailItemUpdated(const QString &accountId, const MailItem &item);
|
||||
void processMailItemDeleted(const QString &accountId, const QString &mailItemId);
|
||||
void processFolderAdded(const QString &accountId, const Folder &folder);
|
||||
void processFolderDeleted(const QString &accountId, const QString &folderId);
|
||||
void processAccountAdded(const Account &account);
|
||||
void processAccountRemoved(const QString &accountId);
|
||||
|
||||
signals:
|
||||
void changeProcessed(ChangeType type, const QString &accountId, const QVariant &data);
|
||||
void mailItemsChanged(const QString &accountId, const QString &folderId);
|
||||
};
|
||||
|
||||
#endif // CHANGEPROCESSOR_H
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef CHANGETYPE_H
|
||||
#define CHANGETYPE_H
|
||||
|
||||
#include <QString>
|
||||
|
||||
enum class ChangeType {
|
||||
MailItemAdded,
|
||||
MailItemUpdated,
|
||||
MailItemDeleted,
|
||||
MailItemFlagsChanged,
|
||||
FolderAdded,
|
||||
FolderUpdated,
|
||||
FolderDeleted,
|
||||
AccountAdded,
|
||||
AccountUpdated,
|
||||
AccountDeleted,
|
||||
Unknown
|
||||
};
|
||||
|
||||
inline QString changeTypeToString(ChangeType type) {
|
||||
switch (type) {
|
||||
case ChangeType::MailItemAdded: return "MailItemAdded";
|
||||
case ChangeType::MailItemUpdated: return "MailItemUpdated";
|
||||
case ChangeType::MailItemDeleted: return "MailItemDeleted";
|
||||
case ChangeType::MailItemFlagsChanged: return "MailItemFlagsChanged";
|
||||
case ChangeType::FolderAdded: return "FolderAdded";
|
||||
case ChangeType::FolderUpdated: return "FolderUpdated";
|
||||
case ChangeType::FolderDeleted: return "FolderDeleted";
|
||||
case ChangeType::AccountAdded: return "AccountAdded";
|
||||
case ChangeType::AccountUpdated: return "AccountUpdated";
|
||||
case ChangeType::AccountDeleted: return "AccountDeleted";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CHANGETYPE_H
|
||||
@@ -0,0 +1,62 @@
|
||||
#include "concreterequests.h"
|
||||
|
||||
// Gmail
|
||||
GmailListMessagesRequest::GmailListMessagesRequest(const QString &accountId, const QString &folderId,
|
||||
const QString &pageToken)
|
||||
: Request()
|
||||
{
|
||||
setAccountId(accountId);
|
||||
setType(RequestType::GmailApi);
|
||||
setMethod(Request::Method::GET);
|
||||
|
||||
// Build Gmail API URL
|
||||
// folderId for Gmail is typically "INBOX", "SENT", etc.
|
||||
QString path = QString("https://gmail.googleapis.com/gmail/v1/users/me/messages");
|
||||
QString query = QString("q=in:%1").arg(folderId.toLower());
|
||||
if (!pageToken.isEmpty()) {
|
||||
query += "&pageToken=" + pageToken;
|
||||
}
|
||||
QUrl url(path);
|
||||
url.setQuery(query);
|
||||
setUrl(url);
|
||||
}
|
||||
|
||||
GmailGetMessageRequest::GmailGetMessageRequest(const QString &accountId, const QString &messageId)
|
||||
: Request()
|
||||
{
|
||||
setAccountId(accountId);
|
||||
setType(RequestType::GmailApi);
|
||||
setMethod(Request::Method::GET);
|
||||
|
||||
QUrl url(QString("https://gmail.googleapis.com/gmail/v1/users/me/messages/%1").arg(messageId));
|
||||
setUrl(url);
|
||||
}
|
||||
|
||||
// Graph API
|
||||
GraphListMessagesRequest::GraphListMessagesRequest(const QString &accountId, const QString &folderId,
|
||||
const QString &deltaLink)
|
||||
: Request()
|
||||
{
|
||||
setAccountId(accountId);
|
||||
setType(RequestType::GraphApi);
|
||||
setMethod(Request::Method::GET);
|
||||
|
||||
// Use deltaLink if available (for incremental sync)
|
||||
if (!deltaLink.isEmpty()) {
|
||||
setUrl(QUrl(deltaLink));
|
||||
} else {
|
||||
QUrl url(QString("https://graph.microsoft.com/v1.0/me/mailFolders/%1/messages").arg(folderId));
|
||||
setUrl(url);
|
||||
}
|
||||
}
|
||||
|
||||
GraphGetMessageRequest::GraphGetMessageRequest(const QString &accountId, const QString &messageId)
|
||||
: Request()
|
||||
{
|
||||
setAccountId(accountId);
|
||||
setType(RequestType::GraphApi);
|
||||
setMethod(Request::Method::GET);
|
||||
|
||||
QUrl url(QString("https://graph.microsoft.com/v1.0/me/messages/%1").arg(messageId));
|
||||
setUrl(url);
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef CONCRETEREQUESTS_H
|
||||
#define CONCRETEREQUESTS_H
|
||||
|
||||
#include "request.h"
|
||||
|
||||
// Gmail API Requests
|
||||
class GmailListMessagesRequest : public Request
|
||||
{
|
||||
public:
|
||||
explicit GmailListMessagesRequest(const QString &accountId, const QString &folderId,
|
||||
const QString &pageToken = QString());
|
||||
};
|
||||
|
||||
class GmailGetMessageRequest : public Request
|
||||
{
|
||||
public:
|
||||
explicit GmailGetMessageRequest(const QString &accountId, const QString &messageId);
|
||||
};
|
||||
|
||||
// Outlook / Graph API Requests
|
||||
class GraphListMessagesRequest : public Request
|
||||
{
|
||||
public:
|
||||
explicit GraphListMessagesRequest(const QString &accountId, const QString &folderId,
|
||||
const QString &deltaLink = QString());
|
||||
};
|
||||
|
||||
class GraphGetMessageRequest : public Request
|
||||
{
|
||||
public:
|
||||
explicit GraphGetMessageRequest(const QString &accountId, const QString &messageId);
|
||||
};
|
||||
|
||||
#endif // CONCRETEREQUESTS_H
|
||||
@@ -0,0 +1,33 @@
|
||||
#include "folderservice.h"
|
||||
#include "core/synchronizerprovider.h"
|
||||
#include <QDebug>
|
||||
|
||||
FolderService::FolderService(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVector<Folder> FolderService::getFoldersForAccount(const QString &accountId)
|
||||
{
|
||||
return FolderDao::findByAccountId(accountId.toInt());
|
||||
}
|
||||
|
||||
void FolderService::refreshFolders(const QString &accountId)
|
||||
{
|
||||
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
|
||||
if (sync) {
|
||||
QVector<Folder> folders = sync->getFolders();
|
||||
for (const auto &f : folders) {
|
||||
FolderDao::insert(f);
|
||||
}
|
||||
}
|
||||
emit foldersChanged(accountId);
|
||||
}
|
||||
|
||||
Folder FolderService::getFolderById(const QString &folderId)
|
||||
{
|
||||
std::optional<Folder> f = FolderDao::findById(folderId.toInt());
|
||||
return f.value_or(Folder());
|
||||
}
|
||||
|
||||
#include "folderservice.moc"
|
||||
@@ -0,0 +1,24 @@
|
||||
#ifndef FOLDERSERVICE_H
|
||||
#define FOLDERSERVICE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include "models/folder.h"
|
||||
#include "db/dao/folderdao.h"
|
||||
|
||||
class FolderService : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FolderService(QObject *parent = nullptr);
|
||||
~FolderService() override = default;
|
||||
|
||||
QVector<Folder> getFoldersForAccount(const QString &accountId);
|
||||
void refreshFolders(const QString &accountId);
|
||||
Folder getFolderById(const QString &folderId);
|
||||
|
||||
signals:
|
||||
void foldersChanged(const QString &accountId);
|
||||
};
|
||||
|
||||
#endif // FOLDERSERVICE_H
|
||||
@@ -358,11 +358,32 @@ void GmailSynchronizer::setHistoryId(const QString& folderId, qint64 historyId)
|
||||
|
||||
MailItem GmailSynchronizer::parseMailItemFromGmail(const QJsonObject& mailJson) const
|
||||
{
|
||||
// Esta función convertiría un objeto JSON de Gmail API
|
||||
// a nuestro objeto MailItem
|
||||
MailItem item;
|
||||
|
||||
// Por ahora, devolvemos un objeto vacío como stub
|
||||
return MailItem();
|
||||
// ID de Gmail es la clave primaria
|
||||
item.setId(mailJson["id"].toString().toLongLong());
|
||||
|
||||
// El snippet sirve como cuerpo preliminar
|
||||
item.setBodyHtml(mailJson["snippet"].toString());
|
||||
|
||||
// Parsear headers
|
||||
QJsonArray headers = mailJson["payload"].toObject()["headers"].toArray();
|
||||
|
||||
QString from = extractHeaderValue(headers, "From").value(0);
|
||||
QString subject = extractHeaderValue(headers, "Subject").value(0);
|
||||
QString dateStr = extractHeaderValue(headers, "Date").value(0);
|
||||
|
||||
item.setSender(from.isEmpty() ? "Unknown" : from);
|
||||
item.setSubject(subject.isEmpty() ? "(No Subject)" : subject);
|
||||
|
||||
// Parseo básico de fecha (RFC 2822)
|
||||
QDateTime date = QDateTime::fromString(dateStr, Qt::ISODate);
|
||||
if (!date.isValid()) {
|
||||
date = QDateTime::currentDateTime();
|
||||
}
|
||||
item.setDate(date);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
Folder GmailSynchronizer::parseFolderFromGmail(const QJsonObject& labelJson) const
|
||||
@@ -386,4 +407,4 @@ QStringList GmailSynchronizer::extractHeaderValue(const QJsonArray& headers, con
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,4 +145,4 @@ QString ImapSynchronizer::generateEventId() const
|
||||
// Simple implementation using timestamp and random component
|
||||
return QString::number(QDateTime::currentMSecsSinceEpoch()) + "_" +
|
||||
QString::number(std::rand());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
// src/services/imapsynchronizer.cpp
|
||||
#include "imapsynchronizer.h"
|
||||
#include <QDebug>
|
||||
#include <Q例外>
|
||||
|
||||
// NOTE: Actual implementation requires an IMAP3 library (e.g., libimap or Qt's built-in network classes).
|
||||
// For this demonstration, we mock the connection and data fetching based on the requirement.
|
||||
|
||||
ImapSynchronizer::ImapSynchronizer(QObject *parent) : QObject(parent) {}
|
||||
|
||||
bool ImapSynchronizer::connectImap(const QString &host, int port, const QString &user, const QString &password) {
|
||||
qDebug() << "[IMAP] Attempting connection to" << host << ":" << port;
|
||||
// In a real implementation, this would involve setting up the IMAP3 client connection.
|
||||
if (host.isEmpty() || port == 0) {
|
||||
qWarning() << "[IMAP Error] Host or Port is invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mock successful connection for demonstration purposes.
|
||||
qDebug() << "[IMAP Success] Connected to server" << host;
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> ImapSynchronizer::fetchFromImap3(const QString &folderId, int offset) {
|
||||
if (!connectImap("imap.example.com", 993, "user", "password")) { // Mock credentials
|
||||
qWarning() << "[IMAP Error] Failed to connect.";
|
||||
return QVector<MailItem>();
|
||||
}
|
||||
|
||||
qDebug() << "[IMAP] Fetching items for folder:" << folderId << " starting at offset:" << offset;
|
||||
|
||||
// Mock data retrieval. In a real scenario, this reads from the IMAP stream.
|
||||
QVector<MailItem> mails;
|
||||
if (folderId == "inbox") {
|
||||
mails.append(MailItem{ "mail_id_123", "Subject 1", "Body 1", true });
|
||||
mails.append(MailItem{ "mail_id_456", "Subject 2", "Body 2", false });
|
||||
} else {
|
||||
// Simulate no mail found for other folders
|
||||
}
|
||||
|
||||
return mails;
|
||||
}
|
||||
|
||||
bool ImapSynchronizer::sendMail(const MailItem &mail, const QString &accountId) {
|
||||
qDebug() << "[IMAP] Attempting to send mail:" << mail.subject() << "to account:" << accountId;
|
||||
// In a real scenario, this would construct the IMAP command sequence to send the message.
|
||||
return true; // Mock success
|
||||
}
|
||||
|
||||
void ImapSynchronizer::fetchMails(const QString &accountId, const QString &folderId) {
|
||||
QVector<MailItem> items = fetchFromImap3(folderId, 0);
|
||||
if (!items.isEmpty()) {
|
||||
qDebug() << "[IMAP] Successfully fetched" << items.size() << " mail items.";
|
||||
} else {
|
||||
qDebug() << "[IMAP] No mail items found in folder:" << folderId;
|
||||
}
|
||||
}
|
||||
|
||||
void ImapSynchronizer::moveMail(const QString &mailItemId, const QString &targetFolderId) {
|
||||
qDebug() << "[IMAP] Moving mail:" << mailItemId << "to" << targetFolderId;
|
||||
// Real implementation would involve IMAP MOVE command.
|
||||
}
|
||||
|
||||
void ImapSynchronizer::deleteMail(const QString &mailItemId) {
|
||||
qDebug() << "[IMAP] Deleting mail:" << mailItemId;
|
||||
// Real implementation would involve IMAP DELE command.
|
||||
}
|
||||
|
||||
void ImapSynchronizer::markAsRead(const QString &mailItemId, bool read) {
|
||||
qDebug() << "[IMAP] Marking mail" << mailItemId << "as read:" << (read ? "True" : "False");
|
||||
// Real implementation would involve IMAP MARK_READ command.
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// src/services/imapsynchronizer.h
|
||||
#ifndef IMAP_SYNCHRONIZER_H
|
||||
#define IMAP_SYNCHRONIZER_H
|
||||
|
||||
#include "synchronizerprovider.h"
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class ImapSynchronizer : public SynchronizerProvider
|
||||
{
|
||||
public:
|
||||
ImapSynchronizer(QObject *parent = nullptr);
|
||||
~ImapSynchronizer() override = default;
|
||||
|
||||
bool isImapSupported() const { return true; } // Indicate that this implementation supports IMAP
|
||||
|
||||
// Implement the required methods from SynchronizerProvider
|
||||
QVector<MailItem> fetchMailItems(const QString &folderId, int offset) override;
|
||||
bool sendMail(const MailItem &mail, const QString &accountId) override;
|
||||
void fetchMails(const QString &accountId, const QString &folderId) override;
|
||||
void moveMail(const QString &mailItemId, const QString &targetFolderId) override;
|
||||
void deleteMail(const QString &mailItemId) override;
|
||||
void markAsRead(const QString &mailItemId, bool read) override;
|
||||
|
||||
private:
|
||||
// Private methods for actual IMAP3 logic (mocked here)
|
||||
bool connectImap(const QString &host, int port, const QString &user, const QString &password);
|
||||
QVector<MailItem> fetchFromImap3(const QString &folderId, int offset);
|
||||
bool sendViaImap3(const MailItem &mail, const QString &accountId);
|
||||
void moveViaImap3(const QString &mailItemId, const QString &targetFolderId);
|
||||
void deleteViaImap3(const QString &mailItemId);
|
||||
void markReadViaImap3(const QString &mailItemId, bool read);
|
||||
};
|
||||
|
||||
#endif // IMAP_SYNCHRONIZER_H
|
||||
@@ -0,0 +1,86 @@
|
||||
#include "mailservice.h"
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
#include <QSslSocket>
|
||||
#include <QUrl>
|
||||
#include "db/dao/accountdao.h"
|
||||
|
||||
static void sendViaSmtp(const MailItem &mail, const Account &account);
|
||||
|
||||
MailService::MailService(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_composer(new EmailComposerBridge(this))
|
||||
{
|
||||
}
|
||||
|
||||
QVector<MailItem> MailService::getMails(const QString &folderId)
|
||||
{
|
||||
return MailItemDao::findByFolderId(folderId.toInt());
|
||||
}
|
||||
|
||||
void MailService::sendMail(const MailItem &mail, const QString &accountId)
|
||||
{
|
||||
qDebug() << "[MailService] Sending mail:" << mail.subject() << "from account:" << accountId;
|
||||
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
|
||||
if (!sync) {
|
||||
emit mailSendFailed(mail.messageId(), "Account not synchronized");
|
||||
return;
|
||||
}
|
||||
Account *account = AccountDao::findById(accountId.toInt());
|
||||
if (account && account->type() == AccountType::IMAP) {
|
||||
sendViaSmtp(mail, *account);
|
||||
delete account;
|
||||
emit mailSent(mail.messageId());
|
||||
return;
|
||||
}
|
||||
if (account) delete account;
|
||||
MailItem copy = mail;
|
||||
copy.setFolderId(5);
|
||||
MailItemDao::insert(copy);
|
||||
emit mailSent(mail.messageId());
|
||||
}
|
||||
|
||||
void MailService::fetchMails(const QString &accountId, const QString &folderId)
|
||||
{
|
||||
Synchronizer *sync = SynchronizerProvider::instance().getSynchronizer(accountId);
|
||||
if (!sync) { qWarning() << "[MailService] No synchronizer"; return; }
|
||||
QVector<MailItem> items = sync->fetchMailItems(folderId);
|
||||
emit mailFetched(accountId, folderId, items);
|
||||
}
|
||||
|
||||
void MailService::moveMail(const QString &mailItemId, const QString &targetFolderId)
|
||||
{
|
||||
qDebug() << "[MailService] Moving mail:" << mailItemId;
|
||||
std::optional<MailItem> item = MailItemDao::findById(mailItemId.toLongLong());
|
||||
if (item.has_value()) {
|
||||
MailItem m = item.value();
|
||||
m.setFolderId(targetFolderId.toInt());
|
||||
MailItemDao::update(m);
|
||||
}
|
||||
emit mailMoved(mailItemId);
|
||||
}
|
||||
|
||||
void MailService::deleteMail(const QString &mailItemId)
|
||||
{
|
||||
MailItemDao::remove(mailItemId.toLongLong());
|
||||
emit mailDeleted(mailItemId);
|
||||
}
|
||||
|
||||
void MailService::markAsRead(const QString &mailItemId, bool read)
|
||||
{
|
||||
std::optional<MailItem> item = MailItemDao::findById(mailItemId.toLongLong());
|
||||
if (item.has_value()) {
|
||||
MailItem m = item.value();
|
||||
m.setRead(read);
|
||||
MailItemDao::update(m);
|
||||
}
|
||||
emit mailReadStateChanged(mailItemId, read);
|
||||
}
|
||||
|
||||
static void sendViaSmtp(const MailItem &mail, const Account &account)
|
||||
{
|
||||
Q_UNUSED(mail); Q_UNUSED(account);
|
||||
qDebug() << "[MailService] SMTP not implemented (requires GMime)";
|
||||
}
|
||||
|
||||
#include "mailservice.moc"
|
||||
@@ -0,0 +1,41 @@
|
||||
#ifndef MAILSERVICE_H
|
||||
#define MAILSERVICE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include <QSslSocket>
|
||||
#include <QString>
|
||||
#include "core/mailitem.h"
|
||||
#include "core/emailcomposerbridge.h"
|
||||
#include "core/synchronizerprovider.h"
|
||||
#include "models/account.h"
|
||||
#include "db/dao/mailitemdao.h"
|
||||
#include "db/dao/folderdao.h"
|
||||
|
||||
class MailService : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MailService(QObject *parent = nullptr);
|
||||
~MailService() override = default;
|
||||
|
||||
QVector<MailItem> getMails(const QString &folderId);
|
||||
void sendMail(const MailItem &mail, const QString &accountId);
|
||||
void fetchMails(const QString &accountId, const QString &folderId);
|
||||
void moveMail(const QString &mailItemId, const QString &targetFolderId);
|
||||
void deleteMail(const QString &mailItemId);
|
||||
void markAsRead(const QString &mailItemId, bool read);
|
||||
|
||||
signals:
|
||||
void mailSent(const QString &mailItemId);
|
||||
void mailSendFailed(const QString &mailItemId, const QString &error);
|
||||
void mailFetched(const QString &accountId, const QString &folderId, const QVector<MailItem> &items);
|
||||
void mailMoved(const QString &mailItemId);
|
||||
void mailDeleted(const QString &mailItemId);
|
||||
void mailReadStateChanged(const QString &mailItemId, bool read);
|
||||
|
||||
private:
|
||||
EmailComposerBridge *m_composer;
|
||||
};
|
||||
|
||||
#endif // MAILSERVICE_H
|
||||
@@ -0,0 +1,116 @@
|
||||
#include "mimestorage.h"
|
||||
#include <QStandardPaths>
|
||||
#include <QDebug>
|
||||
#include <QUuid>
|
||||
|
||||
MimeStorageService::MimeStorageService(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QString MimeStorageService::storagePath() const
|
||||
{
|
||||
QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/mails";
|
||||
return ensureDirectory(path);
|
||||
}
|
||||
|
||||
QString MimeStorageService::ensureDirectory(const QString &path) const
|
||||
{
|
||||
QDir dir(path);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
return dir.absolutePath();
|
||||
}
|
||||
|
||||
QString MimeStorageService::saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail)
|
||||
{
|
||||
QString dir = storagePath() + "/" + accountId + "/" + folderId;
|
||||
ensureDirectory(dir);
|
||||
|
||||
QString fileName = mail.messageId();
|
||||
if (fileName.isEmpty()) {
|
||||
fileName = QUuid::createUuid().toString(QUuid::WithoutBraces);
|
||||
}
|
||||
fileName.replace('/', '_').replace('\\', '_');
|
||||
QString filePath = dir + "/" + fileName + ".eml";
|
||||
|
||||
// Write a basic .eml structure
|
||||
QFile file(filePath);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
QTextStream stream(&file);
|
||||
stream << "From: " << mail.sender() << "\r\n";
|
||||
stream << "To: " << mail.recipient() << "\r\n";
|
||||
stream << "Subject: " << mail.subject() << "\r\n";
|
||||
stream << "Date: " << mail.date().toString(Qt::RFC2822Date) << "\r\n";
|
||||
stream << "MIME-Version: 1.0\r\n";
|
||||
stream << "Content-Type: text/html; charset=UTF-8\r\n";
|
||||
stream << "\r\n";
|
||||
stream << mail.bodyHtml() << "\r\n";
|
||||
file.close();
|
||||
qDebug() << "[MimeStorage] Saved .eml:" << filePath;
|
||||
return fileName;
|
||||
}
|
||||
qWarning() << "[MimeStorage] Failed to save .eml:" << filePath;
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString MimeStorageService::getEmlFilePath(const QString &fileId) const
|
||||
{
|
||||
// Search in all account/folder directories (simplified)
|
||||
QDir baseDir(storagePath());
|
||||
QStringList filters;
|
||||
filters << fileId + ".eml";
|
||||
|
||||
QFileInfoList files = baseDir.entryInfoList(filters, QDir::Files, QDir::Name);
|
||||
for (const auto &info : files) {
|
||||
QStringList parts = info.absoluteFilePath().split('/');
|
||||
if (parts.size() >= 2) {
|
||||
return info.absoluteFilePath();
|
||||
}
|
||||
}
|
||||
return QString();
|
||||
}
|
||||
|
||||
QByteArray MimeStorageService::readEmlFile(const QString &fileId) const
|
||||
{
|
||||
QString path = getEmlFilePath(fileId);
|
||||
if (path.isEmpty()) return {};
|
||||
|
||||
QFile file(path);
|
||||
if (file.open(QIODevice::ReadOnly)) {
|
||||
return file.readAll();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool MimeStorageService::deleteEmlFile(const QString &fileId)
|
||||
{
|
||||
QString path = getEmlFilePath(fileId);
|
||||
if (path.isEmpty()) return false;
|
||||
return QFile::remove(path);
|
||||
}
|
||||
|
||||
QStringList MimeStorageService::listAttachments(const QString &mailItemId) const
|
||||
{
|
||||
Q_UNUSED(mailItemId);
|
||||
// TODO: Parse .eml to get attachments when GMime is available
|
||||
return {};
|
||||
}
|
||||
|
||||
bool MimeStorageService::saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data)
|
||||
{
|
||||
Q_UNUSED(mailItemId);
|
||||
QString dir = storagePath() + "/attachments";
|
||||
ensureDirectory(dir);
|
||||
|
||||
QFile file(dir + "/" + fileName);
|
||||
if (file.open(QIODevice::WriteOnly)) {
|
||||
file.write(data);
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#include "mimestorage.moc"
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef MIMESTORAGESERVICE_H
|
||||
#define MIMESTORAGESERVICE_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include "core/mailitem.h"
|
||||
|
||||
class MimeStorageService : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit MimeStorageService(QObject *parent = nullptr);
|
||||
~MimeStorageService() override = default;
|
||||
|
||||
QString saveEmlFile(const QString &accountId, const QString &folderId, const MailItem &mail);
|
||||
QString getEmlFilePath(const QString &fileId) const;
|
||||
QByteArray readEmlFile(const QString &fileId) const;
|
||||
bool deleteEmlFile(const QString &fileId);
|
||||
|
||||
QStringList listAttachments(const QString &mailItemId) const;
|
||||
bool saveAttachment(const QString &mailItemId, const QString &fileName, const QByteArray &data);
|
||||
|
||||
private:
|
||||
QString storagePath() const;
|
||||
QString ensureDirectory(const QString &path) const;
|
||||
};
|
||||
|
||||
#endif // MIMESTORAGESERVICE_H
|
||||
@@ -423,4 +423,4 @@ Folder OutlookSynchronizer::parseFolderFromGraph(const QJsonObject& folderJson)
|
||||
|
||||
// Por ahora, devolvemos un objeto vacío como stub
|
||||
return Folder();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// src/services/pop/popsynchronizer.cpp
|
||||
#include "popsynchronizer.h"
|
||||
#include "../imaprequest.h"
|
||||
#include "../core/mailitem.h"
|
||||
#include "../core/models/account.h"
|
||||
#include "../core/models/folder.h"
|
||||
#include "../db/dao/mailitemdao.h"
|
||||
#include <QDebug>
|
||||
#include <QProcess>
|
||||
|
||||
PopSynchronizer::PopSynchronizer(QObject *parent) : Synchronizer(parent) {}
|
||||
|
||||
bool PopSynchronizer::initialize(const Account &account) {
|
||||
qDebug() << "[POP Synchronizer] Initializing for account:" << account.id();
|
||||
// In a real implementation, this would involve establishing the POP3 connection and authentication.
|
||||
m_isConnected = true; // Simulate successful connection for now
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PopSynchronizer::syncFolder(const Folder &folder) {
|
||||
qDebug() << "[POP Synchronizer] Syncing folder:" << folder.id();
|
||||
// In a full implementation, this would handle checking the folder state on the server.
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> PopSynchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid) {
|
||||
if (!m_isConnected) {
|
||||
qWarning() << "[POP Synchronizer] Not connected.";
|
||||
return {};
|
||||
}
|
||||
|
||||
qDebug() << "[POP Synchronizer] Fetching mails for folder:" << folderId << "since UID:" << sinceUid;
|
||||
|
||||
// *** Placeholder Implementation ***
|
||||
// In a real scenario, this calls PopRequest::createRequest(type, target) to fetch emails.
|
||||
QVector<MailItem> fetchedItems;
|
||||
|
||||
// Simulate fetching some data based on the folder ID
|
||||
if (folderId == 1) {
|
||||
fetchedItems.append(MailItem("12345", "Folder A", 100));
|
||||
fetchedItems.append(MailItem("12346", "Folder B", 101));
|
||||
} else {
|
||||
fetchedItems.append(MailItem("99999", "Folder C", 102));
|
||||
}
|
||||
|
||||
return fetchedItems;
|
||||
}
|
||||
|
||||
bool PopSynchronizer::appendMailItem(const QString &folderId, const MailItem &item) {
|
||||
qDebug() << "[POP Synchronizer] Appending mail to folder:" << folderId << "ID:" << item.messageId();
|
||||
// In a full implementation, this would use the IMAP request to move or copy the message.
|
||||
MailItemDao::insert(item);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PopSynchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) {
|
||||
qDebug() << "[POP Synchronizer] Updating flags for item:" << itemUid;
|
||||
// In a full implementation, this would use IMAP commands (e.g., UIDVALIDATE, SETUIDLATER, etc.) to update the server state.
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PopSynchronizer::deleteMailItem(const QString &folderId, const QString &itemUid) {
|
||||
qDebug() << "[POP Synchronizer] Deleting mail item:" << itemUid;
|
||||
// In a full implementation, this would execute the IMAP DELETE command.
|
||||
MailItemDao::remove(itemUid.toLongLong());
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// src/services/pop/popsynchronizer.h
|
||||
#ifndef POPSYNCHRONIZER_H
|
||||
#define POPSYNCHRONIZER_H
|
||||
|
||||
#include "synchronizer.h"
|
||||
#include "../imaprequest.h" // Include necessary request structures
|
||||
#include "../core/models/account.h"
|
||||
#include "../core/models/folder.h"
|
||||
#include "../core/mailitem.h"
|
||||
|
||||
class PopSynchronizer : public Synchronizer {
|
||||
public:
|
||||
explicit PopSynchronizer(QObject *parent = nullptr);
|
||||
|
||||
// Implement the core synchronization methods
|
||||
bool initialize(const Account &account) override;
|
||||
bool syncFolder(const Folder &folder) override;
|
||||
QVector<MailItem> fetchMailItems(const QString &folderId, qint64 sinceUid = 0) override;
|
||||
bool appendMailItem(const QString &folderId, const MailItem &item) override;
|
||||
bool updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) override;
|
||||
bool deleteMailItem(const QString &folderId, const QString &itemUid) override;
|
||||
|
||||
private:
|
||||
// Internal state related to POP synchronization (e.g., connection status)
|
||||
bool m_isConnected = false;
|
||||
};
|
||||
|
||||
#endif // POPSYNCHRONIZER_H
|
||||
@@ -0,0 +1,21 @@
|
||||
// poprequest.cpp
|
||||
#include "poprequest.h"
|
||||
#include "request.h"
|
||||
#include <QUrl>
|
||||
#include <QDebug>
|
||||
|
||||
PopRequest::PopRequest(Request* req) : Concreterequests() {}
|
||||
|
||||
Request* PopRequest::createRequest(const QString& type, const QString& target) {
|
||||
qDebug() << "PopRequest is handling request for target: " << target;
|
||||
|
||||
// Construct a request object for POP3 operations.
|
||||
Request* req = new Request(Request::Method::GET, QUrl(target));
|
||||
|
||||
// Add POP3 specific context
|
||||
req->setHeader("POP3_SERVER", "pop.example.com"); // Placeholder
|
||||
req->setHeader("PROTOCOL", "POP3");
|
||||
|
||||
qDebug() << "POP3 request object created successfully.";
|
||||
return req;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// poprequest.h
|
||||
#ifndef POPREQUEST_H
|
||||
#define POPREQUEST_H
|
||||
|
||||
#include "../../include/concreterequests.h"
|
||||
#include "../../include/request.h"
|
||||
|
||||
class PopRequest : public Concreterequests {
|
||||
public:
|
||||
PopRequest(Request* req);
|
||||
Request* createRequest(const QString& type, const QString& target) override;
|
||||
};
|
||||
|
||||
#endif // POPREQUEST_H
|
||||
@@ -0,0 +1,74 @@
|
||||
// src/services/popsynchronizer.cpp
|
||||
#include "popsynchronizer.h"
|
||||
#include <QDebug>
|
||||
#include <Q例外>
|
||||
#include <QFile>
|
||||
|
||||
// NOTE: Actual implementation requires a POP3 library (e.g., libpop3 or Qt's built-in network classes).
|
||||
// For this demonstration, we mock the connection and data fetching based on the requirement.
|
||||
|
||||
PopSynchronizer::PopSynchronizer(QObject *parent) : QObject(parent) {}
|
||||
|
||||
bool PopSynchronizer::connectPop3(const QString &host, int port, const QString &user, const QString &password) {
|
||||
qDebug() << "[POP3] Attempting connection to" << host << ":" << port;
|
||||
// In a real implementation, this would involve setting up the POP3 client connection.
|
||||
if (host.isEmpty() || port == 0) {
|
||||
qWarning() << "[POP3 Error] Host or Port is invalid.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mock successful connection for demonstration purposes.
|
||||
qDebug() << "[POP3 Success] Connected to server " << host;
|
||||
return true;
|
||||
}
|
||||
|
||||
QVector<MailItem> PopSynchronizer::fetchFromPop3(const QString &folderId, int offset) {
|
||||
if (!connectPop3("pop3.example.com", 110, "user", "password")) { // Mock credentials
|
||||
qWarning() << "[POP3 Error] Failed to connect.";
|
||||
return QVector<MailItem>();
|
||||
}
|
||||
|
||||
qDebug() << "[POP3] Fetching items for folder:" << folderId << " starting at offset:" << offset;
|
||||
|
||||
// Mock data retrieval. In a real scenario, this reads from the POP3 stream.
|
||||
QVector<MailItem> mails;
|
||||
if (folderId == "inbox") {
|
||||
mails.append(MailItem{ "mail_id_123", "Subject 1", "Body 1", true });
|
||||
mails.append(MailItem{ "mail_id_456", "Subject 2", "Body 2", false });
|
||||
} else {
|
||||
// Simulate no mail found for other folders
|
||||
}
|
||||
|
||||
return mails;
|
||||
}
|
||||
|
||||
bool PopSynchronizer::sendMail(const MailItem &mail, const QString &accountId) {
|
||||
qDebug() << "[POP3] Attempting to send mail:" << mail.subject() << "to account:" << accountId;
|
||||
// In a real scenario, this would construct the POP3 command sequence to send the message.
|
||||
return true; // Mock success
|
||||
}
|
||||
|
||||
void PopSynchronizer::fetchMails(const QString &accountId, const QString &folderId) {
|
||||
QVector<MailItem> items = fetchFromPop3(folderId, 0);
|
||||
if (!items.isEmpty()) {
|
||||
qDebug() << "[POP3] Successfully fetched" << items.size() << " mail items.";
|
||||
// The caller (MailService) will handle emitting the result.
|
||||
} else {
|
||||
qDebug() << "[POP3] No mail items found in folder:" << folderId;
|
||||
}
|
||||
}
|
||||
|
||||
void PopSynchronizer::moveMail(const QString &mailItemId, const QString &targetFolderId) {
|
||||
qDebug() << "[POP3] Moving mail:" << mailItemId << "to" << targetFolderId;
|
||||
// Real implementation would involve POP3 MOVE command.
|
||||
}
|
||||
|
||||
void PopSynchronizer::deleteMail(const QString &mailItemId) {
|
||||
qDebug() << "[POP3] Deleting mail:" << mailItemId;
|
||||
// Real implementation would involve POP3 DELE command.
|
||||
}
|
||||
|
||||
void PopSynchronizer::markAsRead(const QString &mailItemId, bool read) {
|
||||
qDebug() << "[POP3] Marking mail" << mailItemId << "as read:" << (read ? "True" : "False");
|
||||
// Real implementation would involve POP3 MARK_READ command.
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// src/services/popsynchronizer.h
|
||||
#ifndef POP_SYNCHRONIZER_H
|
||||
#define POP_SYNCHRONIZER_H
|
||||
|
||||
#include "synchronizerprovider.h"
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class PopSynchronizer : public SynchronizerProvider
|
||||
{
|
||||
public:
|
||||
PopSynchronizer(QObject *parent = nullptr);
|
||||
~PopSynchronizer() override = default;
|
||||
|
||||
bool isPop3Supported() const { return true; } // Indicate that this implementation supports POP3
|
||||
|
||||
// Implement the required methods from SynchronizerProvider
|
||||
QVector<MailItem> fetchMailItems(const QString &folderId, int offset) override;
|
||||
bool sendMail(const MailItem &mail, const QString &accountId) override;
|
||||
void fetchMails(const QString &accountId, const QString &folderId) override;
|
||||
void moveMail(const QString &mailItemId, const QString &targetFolderId) override;
|
||||
void deleteMail(const QString &mailItemId) override;
|
||||
void markAsRead(const QString &mailItemId, bool read) override;
|
||||
|
||||
private:
|
||||
// Private methods for actual POP3 logic
|
||||
bool connectPop3(const QString &host, int port, const QString &user, const QString &password);
|
||||
QVector<MailItem> fetchFromPop3(const QString &folderId, int offset);
|
||||
bool sendViaPop3(const MailItem &mail, const QString &accountId);
|
||||
void moveViaPop3(const QString &mailItemId, const QString &targetFolderId);
|
||||
void deleteViaPop3(const QString &mailItemId);
|
||||
void markReadViaPop3(const QString &mailItemId, bool read);
|
||||
};
|
||||
|
||||
#endif // POP_SYNCHRONIZER_H
|
||||
@@ -0,0 +1,83 @@
|
||||
#include "request.h"
|
||||
|
||||
Request::Request(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
Request::Request(Method method, const QUrl &url, const QByteArray &body)
|
||||
: QObject(nullptr)
|
||||
{
|
||||
setMethod(method);
|
||||
setUrl(url);
|
||||
setBody(body);
|
||||
}
|
||||
|
||||
void Request::setMethod(Request::Method method)
|
||||
{
|
||||
m_method = method;
|
||||
}
|
||||
|
||||
Request::Method Request::method() const
|
||||
{
|
||||
return m_method;
|
||||
}
|
||||
|
||||
void Request::setUrl(const QUrl &url)
|
||||
{
|
||||
m_url = url;
|
||||
}
|
||||
|
||||
QUrl Request::url() const
|
||||
{
|
||||
return m_url;
|
||||
}
|
||||
|
||||
void Request::setHeader(const QString &key, const QString &value)
|
||||
{
|
||||
m_headers.insert(key, value);
|
||||
}
|
||||
|
||||
QMap<QString, QString> Request::headers() const
|
||||
{
|
||||
return m_headers;
|
||||
}
|
||||
|
||||
void Request::setBody(const QByteArray &body)
|
||||
{
|
||||
m_body = body;
|
||||
}
|
||||
|
||||
QByteArray Request::body() const
|
||||
{
|
||||
return m_body;
|
||||
}
|
||||
|
||||
void Request::setType(RequestType type)
|
||||
{
|
||||
m_requestType = type;
|
||||
}
|
||||
|
||||
RequestType Request::type() const
|
||||
{
|
||||
return m_requestType;
|
||||
}
|
||||
|
||||
void Request::setAccountId(const QString &accountId)
|
||||
{
|
||||
m_accountId = accountId;
|
||||
}
|
||||
|
||||
QString Request::accountId() const
|
||||
{
|
||||
return m_accountId;
|
||||
}
|
||||
|
||||
QNetworkRequest Request::toNetworkRequest() const
|
||||
{
|
||||
QNetworkRequest netRequest(m_url);
|
||||
for (auto it = m_headers.constBegin(); it != m_headers.constEnd(); ++it) {
|
||||
netRequest.setRawHeader(it.key().toUtf8(), it.value().toUtf8());
|
||||
}
|
||||
return netRequest;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
#ifndef REQUEST_H
|
||||
#define REQUEST_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QUrl>
|
||||
#include <QMap>
|
||||
#include <QByteArray>
|
||||
#include <QNetworkRequest>
|
||||
|
||||
enum class RequestType {
|
||||
GmailApi,
|
||||
GraphApi,
|
||||
Imap,
|
||||
Custom
|
||||
};
|
||||
|
||||
class Request : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
enum class Method {
|
||||
GET,
|
||||
POST,
|
||||
PUT,
|
||||
DELETE,
|
||||
PATCH
|
||||
};
|
||||
|
||||
explicit Request(QObject *parent = nullptr);
|
||||
Request(Method method, const QUrl &url, const QByteArray &body = QByteArray());
|
||||
~Request() override = default;
|
||||
|
||||
void setMethod(Method method);
|
||||
Method method() const;
|
||||
|
||||
void setUrl(const QUrl &url);
|
||||
QUrl url() const;
|
||||
|
||||
void setHeader(const QString &key, const QString &value);
|
||||
QMap<QString, QString> headers() const;
|
||||
|
||||
void setBody(const QByteArray &body);
|
||||
QByteArray body() const;
|
||||
|
||||
void setType(RequestType type);
|
||||
RequestType type() const;
|
||||
|
||||
void setAccountId(const QString &accountId);
|
||||
QString accountId() const;
|
||||
|
||||
QNetworkRequest toNetworkRequest() const;
|
||||
|
||||
private:
|
||||
Method m_method = Method::GET;
|
||||
QUrl m_url;
|
||||
QMap<QString, QString> m_headers;
|
||||
QByteArray m_body;
|
||||
RequestType m_requestType = RequestType::Custom;
|
||||
QString m_accountId;
|
||||
};
|
||||
|
||||
#endif // REQUEST_H
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "requestprocessor.h"
|
||||
#include <QDebug>
|
||||
#include <QNetworkReply>
|
||||
|
||||
RequestProcessor::RequestProcessor(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_networkManager(new QNetworkAccessManager(this))
|
||||
{
|
||||
}
|
||||
|
||||
void RequestProcessor::execute(Request *request)
|
||||
{
|
||||
if (!request) {
|
||||
qWarning() << "[RequestProcessor] Null request";
|
||||
return;
|
||||
}
|
||||
|
||||
QNetworkRequest netRequest = request->toNetworkRequest();
|
||||
QNetworkReply *reply = nullptr;
|
||||
|
||||
switch (request->method()) {
|
||||
case Request::Method::GET:
|
||||
reply = m_networkManager->get(netRequest);
|
||||
break;
|
||||
case Request::Method::POST:
|
||||
reply = m_networkManager->post(netRequest, request->body());
|
||||
break;
|
||||
case Request::Method::PUT:
|
||||
reply = m_networkManager->put(netRequest, request->body());
|
||||
break;
|
||||
case Request::Method::DELETE:
|
||||
reply = m_networkManager->deleteResource(netRequest);
|
||||
break;
|
||||
case Request::Method::PATCH:
|
||||
reply = m_networkManager->sendCustomRequest(netRequest, "PATCH", request->body());
|
||||
break;
|
||||
}
|
||||
|
||||
if (reply) {
|
||||
m_pendingRequests.insert(reply, request);
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
|
||||
onReplyFinished(reply);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void RequestProcessor::cancelAll()
|
||||
{
|
||||
for (auto it = m_pendingRequests.begin(); it != m_pendingRequests.end(); ++it) {
|
||||
it.key()->abort();
|
||||
it.key()->deleteLater();
|
||||
}
|
||||
m_pendingRequests.clear();
|
||||
}
|
||||
|
||||
void RequestProcessor::onReplyFinished(QNetworkReply *reply)
|
||||
{
|
||||
if (!reply) return;
|
||||
|
||||
Request *request = m_pendingRequests.value(reply, nullptr);
|
||||
if (!request) {
|
||||
reply->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
QString accountId = request->accountId();
|
||||
int httpCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
|
||||
if (reply->error() == QNetworkReply::NoError) {
|
||||
QByteArray data = reply->readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(data);
|
||||
emit requestFinished(accountId, doc);
|
||||
} else {
|
||||
QString errorMsg = reply->errorString();
|
||||
qWarning() << "[RequestProcessor] Error:" << httpCode << errorMsg;
|
||||
emit requestFailed(accountId, httpCode, errorMsg);
|
||||
}
|
||||
|
||||
m_pendingRequests.remove(reply);
|
||||
reply->deleteLater();
|
||||
request->deleteLater();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#ifndef REQUESTPROCESSOR_H
|
||||
#define REQUESTPROCESSOR_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QNetworkAccessManager>
|
||||
#include <QNetworkReply>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include "request.h"
|
||||
|
||||
class RequestProcessor : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RequestProcessor(QObject *parent = nullptr);
|
||||
~RequestProcessor() override = default;
|
||||
|
||||
void execute(Request *request);
|
||||
void cancelAll();
|
||||
|
||||
signals:
|
||||
void requestFinished(const QString &accountId, const QJsonDocument &response);
|
||||
void requestFailed(const QString &accountId, int httpCode, const QString &errorMessage);
|
||||
|
||||
private slots:
|
||||
void onReplyFinished(QNetworkReply *reply);
|
||||
|
||||
private:
|
||||
QNetworkAccessManager *m_networkManager;
|
||||
QMap<QNetworkReply*, Request*> m_pendingRequests;
|
||||
};
|
||||
|
||||
#endif // REQUESTPROCESSOR_H
|
||||
@@ -1,6 +1,36 @@
|
||||
#include "synchronizer.h"
|
||||
|
||||
Synchronizer::Synchronizer(QObject* parent)
|
||||
Synchronizer::Synchronizer(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
}
|
||||
|
||||
QVector<MailItem> Synchronizer::fetchMailItems(const QString &folderId, qint64 sinceUid)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(sinceUid);
|
||||
return {};
|
||||
}
|
||||
|
||||
bool Synchronizer::appendMailItem(const QString &folderId, const MailItem &item)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(item);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Synchronizer::updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
Q_UNUSED(read);
|
||||
Q_UNUSED(flagged);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Synchronizer::deleteMailItem(const QString &folderId, const QString &itemUid)
|
||||
{
|
||||
Q_UNUSED(folderId);
|
||||
Q_UNUSED(itemUid);
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#ifndef SYNCHRONIZER_H
|
||||
#define SYNCHRONIZER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QVector>
|
||||
#include "core/models/account.h"
|
||||
#include "core/models/folder.h"
|
||||
#include "core/mailitem.h"
|
||||
|
||||
class Synchronizer : public QObject
|
||||
{
|
||||
public:
|
||||
explicit Synchronizer(QObject *parent = nullptr);
|
||||
~Synchronizer() override = default;
|
||||
|
||||
virtual bool initialize(const Account &account) = 0;
|
||||
virtual bool syncFolder(const Folder &folder) = 0;
|
||||
virtual QVector<Folder> getFolders() const = 0;
|
||||
virtual QVector<MailItem> fetchMailItems(const QString &folderId, qint64 sinceUid = 0) = 0;
|
||||
virtual bool appendMailItem(const QString &folderId, const MailItem &item) = 0;
|
||||
virtual bool updateMailItemFlags(const QString &folderId, const QString &itemUid, bool read, bool flagged) = 0;
|
||||
virtual bool deleteMailItem(const QString &folderId, const QString &itemUid) = 0;
|
||||
|
||||
protected:
|
||||
Account m_account;
|
||||
};
|
||||
|
||||
#endif // SYNCHRONIZER_H
|
||||
@@ -0,0 +1,15 @@
|
||||
// src/services/synchronizerprovider.cpp
|
||||
#include "synchronizerprovider.h"
|
||||
#include "imapsynchronizer.h" // Assuming IMAP implementation exists or will be created
|
||||
#include "popsynchronizer.h" // Already implemented
|
||||
|
||||
SynchronizerProvider* SynchronizerProviderInstance::instance() {
|
||||
if (m_provider == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return m_provider;
|
||||
}
|
||||
|
||||
void SynchronizerProviderInstance::setProvider(SynchronizerProvider* provider) {
|
||||
m_provider = provider;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// src/services/synchronizerprovider.h
|
||||
#ifndef SYNCHRONIZER_PROVIDER_H
|
||||
#define SYNCHRONIZER_PROVIDER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QObject>
|
||||
#include "mailservice.h" // Include MailService definitions if needed for context
|
||||
|
||||
class SynchronizerProvider {
|
||||
public:
|
||||
virtual ~SynchronizerProvider() = default;
|
||||
virtual bool isPop3Supported() const = 0;
|
||||
virtual QVector<MailItem> fetchMailItems(const QString &folderId, int offset) = 0;
|
||||
virtual bool sendMail(const MailItem &mail, const QString &accountId) = 0;
|
||||
virtual void fetchMails(const QString &accountId, const QString &folderId) = 0;
|
||||
virtual void moveMail(const QString &mailItemId, const QString &targetFolderId) = 0;
|
||||
virtual void deleteMail(const QString &mailItemId) = 0;
|
||||
virtual void markAsRead(const QString &mailItemId, bool read) = 0;
|
||||
};
|
||||
|
||||
// Singleton pattern for easy access
|
||||
class SynchronizerProviderInstance {
|
||||
public:
|
||||
static SynchronizerProvider* instance();
|
||||
void setProvider(SynchronizerProvider* provider);
|
||||
|
||||
private:
|
||||
SynchronizerProviderInstance() = default;
|
||||
SynchronizerProvider* m_provider = nullptr;
|
||||
};
|
||||
|
||||
#endif // SYNCHRONIZER_PROVIDER_H
|
||||
@@ -0,0 +1,19 @@
|
||||
// smtprequest.cpp
|
||||
#include "smtprequest.h"
|
||||
#include "request.h"
|
||||
#include <QDebug>
|
||||
|
||||
SmtpRequest::SmtpRequest(Request* req) : Concreterequests(req) {}
|
||||
|
||||
Request* SmtpRequest::createRequest(const QString& type, const QString& target) {
|
||||
qDebug() << "SmtpRequest is handling request for target:" << target;
|
||||
|
||||
// In a real scenario, this would construct an SMTP-specific Request object.
|
||||
Request* req = new Request(Request::POST, target, QByteArray()); // Typically POST for sending mail
|
||||
|
||||
// Add specific context or headers if necessary for SMTP connection setup (to be implemented in Phase 2)
|
||||
req->headers["SMTP_SERVER"] = "smtp.example.com"; // Placeholder
|
||||
|
||||
qDebug() << "SMTP request object created successfully.";
|
||||
return req;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// /mnt/c/Users/javie/wino-mail-dtkqt/src/synchronizerprovider.cpp
|
||||
#include "synchronizerprovider.h"
|
||||
#include <QDebug>
|
||||
|
||||
SynchronizerProvider::SynchronizerProvider(QObject *parent) : QObject(parent) {}
|
||||
|
||||
void SynchronizerProvider::requestSync()
|
||||
{
|
||||
qDebug() << "SynchronizerProvider: Initiating data synchronization request...";
|
||||
|
||||
// Simulate the time taken for a network operation
|
||||
// In a real app, this is where you'd handle threading/asynchronous calls.
|
||||
|
||||
// Simulated Data Fetching (This should be replaced by actual IMAP/SMTP calls)
|
||||
QList<QString> folders = fetchFoldersFromBackend();
|
||||
QList<EmailItem> emails = QList<EmailItem>();
|
||||
|
||||
if (!folders.isEmpty()) {
|
||||
// Simulate fetching emails for the first folder if available, or all of them
|
||||
qDebug() << "Synchronizing data for" << folders.count() << "folders.";
|
||||
for (const QString& folder : folders) {
|
||||
// Simulate fetching emails from each folder
|
||||
QList<EmailItem> folderEmails = fetchEmailsFromBackend(folder);
|
||||
emails.append(folderEmails);
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "Data synchronization complete. Total emails fetched:" << emails.count();
|
||||
|
||||
// Emit the result signal
|
||||
emit dataSynchronized(folders, emails);
|
||||
}
|
||||
|
||||
QList<QString> SynchronizerProvider::fetchFoldersFromBackend() const
|
||||
{
|
||||
// Simulated API call result
|
||||
return {"INBOX", "Sent", "Drafts", "Archive"};
|
||||
}
|
||||
|
||||
QList<EmailItem> SynchronizerProvider::fetchEmailsFromBackend(const QString& folder) const
|
||||
{
|
||||
// Simulated API call result based on folder logic
|
||||
if (folder == "INBOX") {
|
||||
return {
|
||||
{1, "Test Email 1", "test@example.com"},
|
||||
{2, "Important Update", "other@example.com"}
|
||||
};
|
||||
} else if (folder == "Sent") {
|
||||
return {
|
||||
{3, "Sent Mail A", "me@example.com"}
|
||||
};
|
||||
}
|
||||
return QList<EmailItem>();
|
||||
}
|
||||
@@ -123,4 +123,5 @@ void SyncScheduler::setLastSyncTimestamp(qint64 timestamp)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue("lastSyncTimestamp", timestamp);
|
||||
}
|
||||
}
|
||||
#include "syncscheduler.moc"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "ui/calendarview.h"
|
||||
|
||||
CalendarView::CalendarView(QWidget *parent) : QWidget(parent) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void CalendarView::setupUI() {
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setAlignment(Qt::AlignCenter);
|
||||
|
||||
QLabel *icon = new QLabel("📅");
|
||||
icon->setAlignment(Qt::AlignCenter);
|
||||
QFont iconFont = icon->font();
|
||||
iconFont.setPointSize(48);
|
||||
icon->setFont(iconFont);
|
||||
|
||||
QLabel *title = new QLabel("Calendar");
|
||||
title->setAlignment(Qt::AlignCenter);
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
title->setStyleSheet("color: #333;");
|
||||
|
||||
QLabel *subtitle = new QLabel("Coming soon — integrated calendar with email");
|
||||
subtitle->setAlignment(Qt::AlignCenter);
|
||||
subtitle->setStyleSheet("color: #888; font-size: 13px;");
|
||||
|
||||
layout->addWidget(icon);
|
||||
layout->addWidget(title);
|
||||
layout->addWidget(subtitle);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class CalendarView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit CalendarView(QWidget *parent = nullptr);
|
||||
~CalendarView() override = default;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
};
|
||||
@@ -0,0 +1,486 @@
|
||||
#include "ui/composeview.h"
|
||||
#include <QFrame>
|
||||
#include <QFileDialog>
|
||||
#include <QInputDialog>
|
||||
#include <QTextList>
|
||||
#include <QTextTable>
|
||||
#include <QTextCursor>
|
||||
#include <QTextBlockFormat>
|
||||
#include <QTextCharFormat>
|
||||
|
||||
// ===================== RichTextEditor =====================
|
||||
|
||||
RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) {
|
||||
setAcceptRichText(true);
|
||||
setPlaceholderText("Write your message here...");
|
||||
}
|
||||
|
||||
void RichTextEditor::setupToolbar(QVBoxLayout *layout) {
|
||||
m_toolbar = new QToolBar("Formatting");
|
||||
m_toolbar->setIconSize(QSize(16, 16));
|
||||
m_toolbar->setStyleSheet(
|
||||
"QToolBar { background: #f5f5f7; border: 1px solid #d1d1d6; border-radius: 4px; spacing: 2px; padding: 2px; }"
|
||||
"QToolButton { padding: 4px 6px; border-radius: 3px; }"
|
||||
"QToolButton:hover { background: #e0e0e0; }"
|
||||
"QToolButton:checked { background: #bbdefb; }"
|
||||
);
|
||||
|
||||
// Font family combo
|
||||
m_fontCombo = new QFontComboBox();
|
||||
m_fontCombo->setFixedWidth(150);
|
||||
connect(m_fontCombo, &QFontComboBox::currentFontChanged, this, &RichTextEditor::onFontChanged);
|
||||
m_toolbar->addWidget(m_fontCombo);
|
||||
|
||||
// Font size spin
|
||||
m_fontSizeSpin = new QSpinBox();
|
||||
m_fontSizeSpin->setRange(8, 72);
|
||||
m_fontSizeSpin->setValue(14);
|
||||
m_fontSizeSpin->setFixedWidth(50);
|
||||
connect(m_fontSizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), this, &RichTextEditor::onFontSizeChanged);
|
||||
m_toolbar->addWidget(m_fontSizeSpin);
|
||||
|
||||
m_toolbar->addSeparator();
|
||||
|
||||
// Bold / Italic / Underline
|
||||
QAction *boldAct = m_toolbar->addAction("B");
|
||||
boldAct->setCheckable(true);
|
||||
QFont boldFont = boldAct->font(); boldFont.setBold(true); boldAct->setFont(boldFont);
|
||||
connect(boldAct, &QAction::triggered, this, &RichTextEditor::onBold);
|
||||
|
||||
QAction *italicAct = m_toolbar->addAction("I");
|
||||
italicAct->setCheckable(true);
|
||||
QFont italicFont = italicAct->font(); italicFont.setItalic(true); italicAct->setFont(italicFont);
|
||||
connect(italicAct, &QAction::triggered, this, &RichTextEditor::onItalic);
|
||||
|
||||
QAction *underlineAct = m_toolbar->addAction("U");
|
||||
underlineAct->setCheckable(true);
|
||||
QFont uFont = underlineAct->font(); uFont.setUnderline(true); underlineAct->setFont(uFont);
|
||||
connect(underlineAct, &QAction::triggered, this, &RichTextEditor::onUnderline);
|
||||
|
||||
m_toolbar->addSeparator();
|
||||
|
||||
// Alignment
|
||||
QAction *alignLeft = m_toolbar->addAction("\xe2\x87\x94L");
|
||||
connect(alignLeft, &QAction::triggered, this, &RichTextEditor::onAlignLeft);
|
||||
|
||||
QAction *alignCenter = m_toolbar->addAction("\xe2\x86\x94C");
|
||||
connect(alignCenter, &QAction::triggered, this, &RichTextEditor::onAlignCenter);
|
||||
|
||||
QAction *alignRight = m_toolbar->addAction("\xe2\x87\x94R");
|
||||
connect(alignRight, &QAction::triggered, this, &RichTextEditor::onAlignRight);
|
||||
|
||||
QAction *alignJustify = m_toolbar->addAction("\xe2\x87\x94J");
|
||||
connect(alignJustify, &QAction::triggered, this, &RichTextEditor::onAlignJustify);
|
||||
|
||||
m_toolbar->addSeparator();
|
||||
|
||||
// Lists
|
||||
QAction *bulletAct = m_toolbar->addAction("\xe2\x80\xa2 List");
|
||||
connect(bulletAct, &QAction::triggered, this, &RichTextEditor::onBulletList);
|
||||
|
||||
QAction *numAct = m_toolbar->addAction("1. List");
|
||||
connect(numAct, &QAction::triggered, this, &RichTextEditor::onNumberedList);
|
||||
|
||||
m_toolbar->addSeparator();
|
||||
|
||||
// Indent / Outdent
|
||||
QAction *indentAct = m_toolbar->addAction("\xe2\x86\x92 Indent");
|
||||
connect(indentAct, &QAction::triggered, this, &RichTextEditor::onIndent);
|
||||
|
||||
QAction *outdentAct = m_toolbar->addAction("\xe2\x86\x90 Outdent");
|
||||
connect(outdentAct, &QAction::triggered, this, &RichTextEditor::onOutdent);
|
||||
|
||||
m_toolbar->addSeparator();
|
||||
|
||||
// Insert image
|
||||
QAction *imgAct = m_toolbar->addAction("\xf0\x9f\x96\xbc Image");
|
||||
connect(imgAct, &QAction::triggered, this, &RichTextEditor::onInsertImage);
|
||||
|
||||
// Insert table
|
||||
QAction *tableAct = m_toolbar->addAction("\xe2\x96\xa4 Table");
|
||||
connect(tableAct, &QAction::triggered, this, &RichTextEditor::onInsertTable);
|
||||
|
||||
layout->addWidget(m_toolbar);
|
||||
}
|
||||
|
||||
void RichTextEditor::onBold() {
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontWeight(textCursor().charFormat().fontWeight() == QFont::Bold ? QFont::Normal : QFont::Bold);
|
||||
mergeCurrentCharFormat(fmt);
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void RichTextEditor::onItalic() {
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontItalic(!textCursor().charFormat().fontItalic());
|
||||
mergeCurrentCharFormat(fmt);
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void RichTextEditor::onUnderline() {
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontUnderline(!textCursor().charFormat().fontUnderline());
|
||||
mergeCurrentCharFormat(fmt);
|
||||
setFocus();
|
||||
}
|
||||
|
||||
void RichTextEditor::onBulletList() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextList *list = cursor.currentList();
|
||||
if (list && list->format().style() == QTextListFormat::ListDisc) {
|
||||
// Remove list
|
||||
QTextBlockFormat bfmt;
|
||||
bfmt.setIndent(0);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
list->remove(cursor.block());
|
||||
} else {
|
||||
QTextListFormat listFormat;
|
||||
listFormat.setStyle(QTextListFormat::ListDisc);
|
||||
cursor.createList(listFormat);
|
||||
}
|
||||
}
|
||||
|
||||
void RichTextEditor::onNumberedList() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextList *list = cursor.currentList();
|
||||
if (list && list->format().style() == QTextListFormat::ListDecimal) {
|
||||
QTextBlockFormat bfmt;
|
||||
bfmt.setIndent(0);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
list->remove(cursor.block());
|
||||
} else {
|
||||
QTextListFormat listFormat;
|
||||
listFormat.setStyle(QTextListFormat::ListDecimal);
|
||||
cursor.createList(listFormat);
|
||||
}
|
||||
}
|
||||
|
||||
void RichTextEditor::onIndent() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextBlockFormat bfmt = cursor.blockFormat();
|
||||
bfmt.setIndent(bfmt.indent() + 1);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onOutdent() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextBlockFormat bfmt = cursor.blockFormat();
|
||||
int indent = bfmt.indent();
|
||||
if (indent > 0) {
|
||||
bfmt.setIndent(indent - 1);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
}
|
||||
}
|
||||
|
||||
void RichTextEditor::onAlignLeft() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextBlockFormat bfmt = cursor.blockFormat();
|
||||
bfmt.setAlignment(Qt::AlignLeft);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onAlignCenter() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextBlockFormat bfmt = cursor.blockFormat();
|
||||
bfmt.setAlignment(Qt::AlignCenter);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onAlignRight() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextBlockFormat bfmt = cursor.blockFormat();
|
||||
bfmt.setAlignment(Qt::AlignRight);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onAlignJustify() {
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextBlockFormat bfmt = cursor.blockFormat();
|
||||
bfmt.setAlignment(Qt::AlignJustify);
|
||||
cursor.setBlockFormat(bfmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onFontChanged(const QFont &font) {
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontFamilies({font.family()});
|
||||
mergeCurrentCharFormat(fmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onFontSizeChanged(int size) {
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontPointSize(size);
|
||||
mergeCurrentCharFormat(fmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onInsertImage() {
|
||||
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(), "Images (*.png *.jpg *.jpeg *.gif *.bmp)");
|
||||
if (filePath.isEmpty()) return;
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextImageFormat imgFmt;
|
||||
imgFmt.setName(filePath);
|
||||
// Scale down if too large
|
||||
QPixmap pm(filePath);
|
||||
if (pm.width() > 600) {
|
||||
imgFmt.setWidth(600);
|
||||
imgFmt.setHeight(pm.height() * 600 / pm.width());
|
||||
}
|
||||
cursor.insertImage(imgFmt);
|
||||
}
|
||||
|
||||
void RichTextEditor::onInsertTable() {
|
||||
bool ok;
|
||||
int rows = QInputDialog::getInt(this, "Table Rows", "Rows:", 3, 1, 50, 1, &ok);
|
||||
if (!ok) return;
|
||||
int cols = QInputDialog::getInt(this, "Table Columns", "Columns:", 3, 1, 20, 1, &ok);
|
||||
if (!ok) return;
|
||||
|
||||
QTextCursor cursor = textCursor();
|
||||
QTextTableFormat tableFmt;
|
||||
tableFmt.setBorder(1);
|
||||
tableFmt.setBorderStyle(QTextFrameFormat::BorderStyle_Solid);
|
||||
tableFmt.setCellPadding(4);
|
||||
tableFmt.setCellSpacing(0);
|
||||
tableFmt.setWidth(QTextLength(QTextLength::PercentageLength, 100));
|
||||
cursor.insertTable(rows, cols, tableFmt);
|
||||
}
|
||||
|
||||
// ===================== ComposeView =====================
|
||||
|
||||
ComposeView::ComposeView(QWidget *parent) : QWidget(parent) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void ComposeView::setupUI() {
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(20, 15, 20, 15);
|
||||
mainLayout->setSpacing(8);
|
||||
|
||||
this->setStyleSheet(
|
||||
"QLineEdit, QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
|
||||
"QLineEdit:focus, QTextEdit:focus { border-color: #1976D2; }"
|
||||
);
|
||||
|
||||
// Header row: Subject + Detach button
|
||||
QHBoxLayout *headerLayout = new QHBoxLayout();
|
||||
m_subjectField = new QLineEdit();
|
||||
m_subjectField->setPlaceholderText("Subject");
|
||||
m_subjectField->setFixedHeight(40);
|
||||
QFont subjectFont = m_subjectField->font();
|
||||
subjectFont.setPointSize(14);
|
||||
subjectFont.setBold(true);
|
||||
m_subjectField->setFont(subjectFont);
|
||||
headerLayout->addWidget(m_subjectField, 1);
|
||||
|
||||
m_detachButton = new QPushButton("\xe2\x87\xa5 Detach");
|
||||
m_detachButton->setToolTip("Open compose window in a separate window");
|
||||
m_detachButton->setStyleSheet(
|
||||
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 6px 12px; color: #555; font-size: 12px; }"
|
||||
"QPushButton:hover { background: #f0f0f0; }"
|
||||
);
|
||||
connect(m_detachButton, &QPushButton::clicked, this, &ComposeView::onDetachClicked);
|
||||
headerLayout->addWidget(m_detachButton);
|
||||
|
||||
mainLayout->addLayout(headerLayout);
|
||||
|
||||
// Separator line
|
||||
QFrame *line1 = new QFrame();
|
||||
line1->setFrameShape(QFrame::HLine);
|
||||
line1->setStyleSheet("color: #e0e0e0;");
|
||||
mainLayout->addWidget(line1);
|
||||
|
||||
// To: field + Cc/Bcc toggle buttons
|
||||
QHBoxLayout *toLayout = new QHBoxLayout();
|
||||
QLabel *toLabel = new QLabel("To:");
|
||||
toLabel->setFixedWidth(40);
|
||||
toLabel->setStyleSheet("font-weight: bold; color: #555;");
|
||||
m_toField = new QLineEdit();
|
||||
m_toField->setPlaceholderText("Recipients (comma separated)");
|
||||
toLayout->addWidget(toLabel);
|
||||
toLayout->addWidget(m_toField, 1);
|
||||
|
||||
m_ccButton = new QPushButton("Cc");
|
||||
m_ccButton->setFixedWidth(32);
|
||||
m_ccButton->setStyleSheet(
|
||||
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
|
||||
"QPushButton:hover { background: #d0d0d0; }"
|
||||
);
|
||||
connect(m_ccButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
|
||||
toLayout->addWidget(m_ccButton);
|
||||
|
||||
m_bccButton = new QPushButton("Bcc");
|
||||
m_bccButton->setFixedWidth(36);
|
||||
m_bccButton->setStyleSheet(
|
||||
"QPushButton { background: #e0e0e0; border: 1px solid #ccc; border-radius: 3px; font-size: 11px; padding: 2px; }"
|
||||
"QPushButton:hover { background: #d0d0d0; }"
|
||||
);
|
||||
connect(m_bccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
|
||||
toLayout->addWidget(m_bccButton);
|
||||
|
||||
mainLayout->addLayout(toLayout);
|
||||
|
||||
// Cc: row (hidden by default)
|
||||
m_ccRow = new QWidget();
|
||||
QHBoxLayout *ccLayout = new QHBoxLayout(m_ccRow);
|
||||
ccLayout->setContentsMargins(0, 0, 0, 0);
|
||||
QLabel *ccLabel = new QLabel("Cc:");
|
||||
ccLabel->setFixedWidth(40);
|
||||
ccLabel->setStyleSheet("color: #555;");
|
||||
m_ccField = new QLineEdit();
|
||||
m_ccField->setPlaceholderText("Carbon copy");
|
||||
ccLayout->addWidget(ccLabel);
|
||||
ccLayout->addWidget(m_ccField, 1);
|
||||
|
||||
m_hideCcButton = new QPushButton("x");
|
||||
m_hideCcButton->setFixedSize(20, 20);
|
||||
m_hideCcButton->setToolTip("Hide Cc");
|
||||
m_hideCcButton->setStyleSheet(
|
||||
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
|
||||
"QPushButton:hover { color: #333; }"
|
||||
);
|
||||
connect(m_hideCcButton, &QPushButton::clicked, this, &ComposeView::onCcToggle);
|
||||
ccLayout->addWidget(m_hideCcButton);
|
||||
|
||||
m_ccRow->setVisible(false);
|
||||
mainLayout->addWidget(m_ccRow);
|
||||
|
||||
// Bcc: row (hidden by default)
|
||||
m_bccRow = new QWidget();
|
||||
QHBoxLayout *bccLayout = new QHBoxLayout(m_bccRow);
|
||||
bccLayout->setContentsMargins(0, 0, 0, 0);
|
||||
QLabel *bccLabel = new QLabel("Bcc:");
|
||||
bccLabel->setFixedWidth(40);
|
||||
bccLabel->setStyleSheet("color: #555;");
|
||||
m_bccField = new QLineEdit();
|
||||
m_bccField->setPlaceholderText("Blind carbon copy");
|
||||
bccLayout->addWidget(bccLabel);
|
||||
bccLayout->addWidget(m_bccField, 1);
|
||||
|
||||
m_hideBccButton = new QPushButton("x");
|
||||
m_hideBccButton->setFixedSize(20, 20);
|
||||
m_hideBccButton->setToolTip("Hide Bcc");
|
||||
m_hideBccButton->setStyleSheet(
|
||||
"QPushButton { background: transparent; border: none; color: #999; font-weight: bold; }"
|
||||
"QPushButton:hover { color: #333; }"
|
||||
);
|
||||
connect(m_hideBccButton, &QPushButton::clicked, this, &ComposeView::onBccToggle);
|
||||
bccLayout->addWidget(m_hideBccButton);
|
||||
|
||||
m_bccRow->setVisible(false);
|
||||
mainLayout->addWidget(m_bccRow);
|
||||
|
||||
// Separator
|
||||
QFrame *line2 = new QFrame();
|
||||
line2->setFrameShape(QFrame::HLine);
|
||||
line2->setStyleSheet("color: #e0e0e0;");
|
||||
mainLayout->addWidget(line2);
|
||||
|
||||
// Rich text editor with toolbar
|
||||
m_bodyEditor = new RichTextEditor();
|
||||
m_bodyEditor->setupToolbar(mainLayout);
|
||||
mainLayout->addWidget(m_bodyEditor, 1);
|
||||
|
||||
// Schedule panel
|
||||
m_schedulePanel = new QWidget();
|
||||
QHBoxLayout *scheduleLayout = new QHBoxLayout(m_schedulePanel);
|
||||
scheduleLayout->setContentsMargins(0, 0, 0, 0);
|
||||
QLabel *scheduleLabel = new QLabel("Send at:");
|
||||
scheduleLabel->setStyleSheet("color: #555; font-weight: bold;");
|
||||
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600));
|
||||
m_schedulePicker->setCalendarPopup(true);
|
||||
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
|
||||
scheduleLayout->addWidget(scheduleLabel);
|
||||
scheduleLayout->addWidget(m_schedulePicker);
|
||||
scheduleLayout->addStretch();
|
||||
|
||||
m_scheduleSendButton = new QPushButton("Schedule Send");
|
||||
m_scheduleSendButton->setStyleSheet(
|
||||
"QPushButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 6px 16px; font-weight: bold; }"
|
||||
"QPushButton:hover { background-color: #1565C0; }"
|
||||
);
|
||||
connect(m_scheduleSendButton, &QPushButton::clicked, this, &ComposeView::onScheduleClicked);
|
||||
scheduleLayout->addWidget(m_scheduleSendButton);
|
||||
|
||||
m_schedulePanel->setVisible(false);
|
||||
mainLayout->addWidget(m_schedulePanel);
|
||||
|
||||
// Action buttons row
|
||||
QHBoxLayout *actionsLayout = new QHBoxLayout();
|
||||
actionsLayout->addStretch();
|
||||
|
||||
m_discardButton = new QPushButton("Discard");
|
||||
m_discardButton->setStyleSheet(
|
||||
"QPushButton { background: transparent; border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px 20px; color: #555; }"
|
||||
"QPushButton:hover { background: #f5f5f5; }"
|
||||
);
|
||||
actionsLayout->addWidget(m_discardButton);
|
||||
connect(m_discardButton, &QPushButton::clicked, this, &ComposeView::discardRequested);
|
||||
|
||||
// Split button for Send / Schedule
|
||||
m_sendMenu = new QMenu(this);
|
||||
m_sendNowAction = m_sendMenu->addAction("Send Now");
|
||||
connect(m_sendNowAction, &QAction::triggered, this, &ComposeView::onSendClicked);
|
||||
m_scheduleAction = m_sendMenu->addAction("Schedule for later...");
|
||||
connect(m_scheduleAction, &QAction::triggered, [this]() {
|
||||
m_schedulePanel->setVisible(true);
|
||||
});
|
||||
|
||||
m_sendSplit = new QToolButton();
|
||||
m_sendSplit->setText("Send");
|
||||
m_sendSplit->setToolButtonStyle(Qt::ToolButtonTextOnly);
|
||||
m_sendSplit->setPopupMode(QToolButton::MenuButtonPopup);
|
||||
m_sendSplit->setMenu(m_sendMenu);
|
||||
m_sendSplit->setStyleSheet(
|
||||
"QToolButton { background-color: #1976D2; color: white; border: none; border-radius: 4px; padding: 8px 24px; font-weight: bold; }"
|
||||
"QToolButton:hover { background-color: #1565C0; }"
|
||||
"QToolButton::menu-button { border-left: 1px solid rgba(255,255,255,0.3); padding-left: 8px; padding-right: 8px; }"
|
||||
"QToolButton::menu-button:hover { background-color: #1565C0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
|
||||
);
|
||||
connect(m_sendSplit, &QToolButton::clicked, this, &ComposeView::onSendClicked);
|
||||
actionsLayout->addWidget(m_sendSplit);
|
||||
|
||||
mainLayout->addLayout(actionsLayout);
|
||||
}
|
||||
|
||||
void ComposeView::onCcToggle() {
|
||||
m_ccVisible = !m_ccVisible;
|
||||
m_ccRow->setVisible(m_ccVisible);
|
||||
m_ccButton->setVisible(!m_ccVisible);
|
||||
if (m_ccVisible) m_ccField->setFocus();
|
||||
}
|
||||
|
||||
void ComposeView::onBccToggle() {
|
||||
m_bccVisible = !m_bccVisible;
|
||||
m_bccRow->setVisible(m_bccVisible);
|
||||
m_bccButton->setVisible(!m_bccVisible);
|
||||
if (m_bccVisible) m_bccField->setFocus();
|
||||
}
|
||||
|
||||
void ComposeView::onSendClicked() {
|
||||
emit sendRequested(
|
||||
m_toField->text(),
|
||||
m_ccVisible ? m_ccField->text() : QString(),
|
||||
m_bccVisible ? m_bccField->text() : QString(),
|
||||
m_subjectField->text(),
|
||||
m_bodyEditor->toHtml(),
|
||||
QDateTime() // null = send now
|
||||
);
|
||||
}
|
||||
|
||||
void ComposeView::onScheduleClicked() {
|
||||
emit sendRequested(
|
||||
m_toField->text(),
|
||||
m_ccVisible ? m_ccField->text() : QString(),
|
||||
m_bccVisible ? m_bccField->text() : QString(),
|
||||
m_subjectField->text(),
|
||||
m_bodyEditor->toHtml(),
|
||||
m_schedulePicker->dateTime()
|
||||
);
|
||||
}
|
||||
|
||||
void ComposeView::onDetachClicked() {
|
||||
emit detachRequested(this);
|
||||
}
|
||||
|
||||
void ComposeView::setTo(const QString &to) { m_toField->setText(to); }
|
||||
void ComposeView::setSubject(const QString &subject) { m_subjectField->setText(subject); }
|
||||
void ComposeView::setBody(const QString &body) { m_bodyEditor->setHtml(body); }
|
||||
@@ -0,0 +1,103 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QDateTime>
|
||||
#include <QDateTimeEdit>
|
||||
#include <QMenu>
|
||||
#include <QToolBar>
|
||||
#include <QToolButton>
|
||||
#include <QFontComboBox>
|
||||
#include <QSpinBox>
|
||||
#include <QAction>
|
||||
|
||||
class RichTextEditor : public QTextEdit {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit RichTextEditor(QWidget *parent = nullptr);
|
||||
void setupToolbar(QVBoxLayout *layout);
|
||||
|
||||
private slots:
|
||||
void onBold();
|
||||
void onItalic();
|
||||
void onUnderline();
|
||||
void onBulletList();
|
||||
void onNumberedList();
|
||||
void onIndent();
|
||||
void onOutdent();
|
||||
void onAlignLeft();
|
||||
void onAlignCenter();
|
||||
void onAlignRight();
|
||||
void onAlignJustify();
|
||||
void onFontChanged(const QFont &font);
|
||||
void onFontSizeChanged(int size);
|
||||
void onInsertImage();
|
||||
void onInsertTable();
|
||||
|
||||
private:
|
||||
QToolBar *m_toolbar;
|
||||
QFontComboBox *m_fontCombo;
|
||||
QSpinBox *m_fontSizeSpin;
|
||||
};
|
||||
|
||||
class ComposeView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ComposeView(QWidget *parent = nullptr);
|
||||
~ComposeView() override = default;
|
||||
|
||||
void setTo(const QString &to);
|
||||
void setSubject(const QString &subject);
|
||||
void setBody(const QString &body);
|
||||
|
||||
signals:
|
||||
void sendRequested(const QString &to, const QString &cc, const QString &bcc,
|
||||
const QString &subject, const QString &body,
|
||||
const QDateTime &scheduleTime);
|
||||
void discardRequested();
|
||||
void detachRequested(QWidget *composeView);
|
||||
|
||||
private slots:
|
||||
void onSendClicked();
|
||||
void onScheduleClicked();
|
||||
void onCcToggle();
|
||||
void onBccToggle();
|
||||
void onDetachClicked();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLineEdit *m_toField;
|
||||
QWidget *m_ccRow;
|
||||
QLineEdit *m_ccField;
|
||||
QWidget *m_bccRow;
|
||||
QLineEdit *m_bccField;
|
||||
QLineEdit *m_subjectField;
|
||||
RichTextEditor *m_bodyEditor;
|
||||
|
||||
QToolButton *m_sendSplit;
|
||||
QMenu *m_sendMenu;
|
||||
QAction *m_sendNowAction;
|
||||
QAction *m_scheduleAction;
|
||||
|
||||
QPushButton *m_discardButton;
|
||||
QPushButton *m_detachButton;
|
||||
|
||||
QPushButton *m_ccButton;
|
||||
QPushButton *m_bccButton;
|
||||
QPushButton *m_hideCcButton;
|
||||
QPushButton *m_hideBccButton;
|
||||
|
||||
QWidget *m_schedulePanel;
|
||||
QDateTimeEdit *m_schedulePicker;
|
||||
QPushButton *m_scheduleSendButton;
|
||||
|
||||
bool m_ccVisible = false;
|
||||
bool m_bccVisible = false;
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
#include "ui/contactsview.h"
|
||||
|
||||
ContactsView::ContactsView(QWidget *parent) : QWidget(parent) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void ContactsView::setupUI() {
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setAlignment(Qt::AlignCenter);
|
||||
|
||||
QLabel *icon = new QLabel("👥");
|
||||
icon->setAlignment(Qt::AlignCenter);
|
||||
QFont iconFont = icon->font();
|
||||
iconFont.setPointSize(48);
|
||||
icon->setFont(iconFont);
|
||||
|
||||
QLabel *title = new QLabel("Contacts");
|
||||
title->setAlignment(Qt::AlignCenter);
|
||||
QFont titleFont = title->font();
|
||||
titleFont.setPointSize(20);
|
||||
titleFont.setBold(true);
|
||||
title->setFont(titleFont);
|
||||
title->setStyleSheet("color: #333;");
|
||||
|
||||
QLabel *subtitle = new QLabel("Coming soon — manage your contacts here");
|
||||
subtitle->setAlignment(Qt::AlignCenter);
|
||||
subtitle->setStyleSheet("color: #888; font-size: 13px;");
|
||||
|
||||
layout->addWidget(icon);
|
||||
layout->addWidget(title);
|
||||
layout->addWidget(subtitle);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
class ContactsView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ContactsView(QWidget *parent = nullptr);
|
||||
~ContactsView() override = default;
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
};
|
||||
@@ -0,0 +1,103 @@
|
||||
#include "ui/maillistview.h"
|
||||
#include <QDateTime>
|
||||
|
||||
MailListView::MailListView(QWidget *parent) : QWidget(parent) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void MailListView::setupUI() {
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
layout->setSpacing(0);
|
||||
|
||||
// Header bar
|
||||
QWidget *headerBar = new QWidget();
|
||||
headerBar->setFixedHeight(48);
|
||||
headerBar->setStyleSheet("background-color: #1976D2;");
|
||||
QHBoxLayout *headerLayout = new QHBoxLayout(headerBar);
|
||||
headerLayout->setContentsMargins(16, 0, 10, 0);
|
||||
|
||||
QLabel *title = new QLabel("Wino Mail");
|
||||
title->setStyleSheet("color: white; font-size: 18px; font-weight: bold;");
|
||||
headerLayout->addWidget(title);
|
||||
headerLayout->addStretch();
|
||||
|
||||
m_composeButton = new QPushButton("✎");
|
||||
m_composeButton->setFixedSize(36, 36);
|
||||
m_composeButton->setStyleSheet(
|
||||
"QPushButton { background-color: #e0e0e0; border-radius: 4px; font-size: 18px; color: #333; }"
|
||||
"QPushButton:hover { background-color: #d0d0d0; }"
|
||||
);
|
||||
headerLayout->addWidget(m_composeButton);
|
||||
connect(m_composeButton, &QPushButton::clicked, this, &MailListView::composeRequested);
|
||||
|
||||
layout->addWidget(headerBar);
|
||||
|
||||
// Table
|
||||
m_tableView = new QTableView();
|
||||
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_tableView->setShowGrid(false);
|
||||
m_tableView->setAlternatingRowColors(true);
|
||||
m_tableView->verticalHeader()->hide();
|
||||
m_tableView->horizontalHeader()->setStretchLastSection(true);
|
||||
m_tableView->horizontalHeader()->setSectionsClickable(true);
|
||||
m_tableView->setSortingEnabled(true);
|
||||
m_tableView->setFrameShape(QFrame::NoFrame);
|
||||
m_tableView->setStyleSheet(
|
||||
"QTableView { background-color: #ffffff; alternate-background-color: #f9f9fb; border: none; }"
|
||||
"QTableView::item { padding: 8px; border-bottom: 1px solid #e8e8ed; }"
|
||||
"QTableView::item:selected { background-color: #e3f2fd; color: #1a1a2e; }"
|
||||
"QHeaderView::section { background-color: #f5f5f7; padding: 8px; border: none; border-bottom: 1px solid #d1d1d6; font-weight: 600; color: #555; }"
|
||||
);
|
||||
|
||||
m_proxyModel = new QSortFilterProxyModel(this);
|
||||
m_proxyModel->setSortRole(EmailListModel::DateRole);
|
||||
m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_proxyModel->setDynamicSortFilter(true);
|
||||
|
||||
m_tableView->setModel(m_proxyModel);
|
||||
|
||||
connect(m_tableView, &QTableView::clicked, this, &MailListView::onRowSelected);
|
||||
|
||||
layout->addWidget(m_tableView);
|
||||
}
|
||||
|
||||
void MailListView::setModel(EmailListModel *model) {
|
||||
m_proxyModel->setSourceModel(model);
|
||||
|
||||
m_tableView->setColumnHidden(EmailListModel::IdRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::RecipientRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::ReadRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::FlaggedRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::AttachmentsRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::FileIdRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::SizeRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::MessageIdRole - Qt::UserRole - 1, true);
|
||||
m_tableView->setColumnHidden(EmailListModel::SenderInitialRole - Qt::UserRole - 1, true);
|
||||
|
||||
m_tableView->horizontalHeader()->setSectionResizeMode(EmailListModel::SenderRole - Qt::UserRole - 1, QHeaderView::Stretch);
|
||||
m_tableView->horizontalHeader()->setSectionResizeMode(EmailListModel::SubjectRole - Qt::UserRole - 1, QHeaderView::Stretch);
|
||||
|
||||
// Default sort by date descending
|
||||
m_tableView->sortByColumn(EmailListModel::DateRole - Qt::UserRole - 1, Qt::DescendingOrder);
|
||||
|
||||
// Set column titles manually
|
||||
QHeaderView *header = m_tableView->horizontalHeader();
|
||||
for (int i = 0; i < m_proxyModel->columnCount(); ++i) {
|
||||
int role = Qt::UserRole + 1 + i;
|
||||
switch (role) {
|
||||
case EmailListModel::SenderRole: header->setSectionHidden(i, false); break;
|
||||
case EmailListModel::SubjectRole: header->setSectionHidden(i, false); break;
|
||||
case EmailListModel::DateRole: header->setSectionHidden(i, false); break;
|
||||
default: header->setSectionHidden(i, true); break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MailListView::onRowSelected(const QModelIndex &index) {
|
||||
if (!index.isValid()) return;
|
||||
QModelIndex sourceIndex = m_proxyModel->mapToSource(index);
|
||||
int mailId = sourceIndex.data(EmailListModel::IdRole).toInt();
|
||||
emit emailSelected(mailId);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTableView>
|
||||
#include <QHeaderView>
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QVBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
|
||||
#include "ui/models/EmailListModel.h"
|
||||
|
||||
class MailListView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MailListView(QWidget *parent = nullptr);
|
||||
~MailListView() override = default;
|
||||
|
||||
void setModel(EmailListModel *model);
|
||||
|
||||
signals:
|
||||
void emailSelected(int mailId);
|
||||
void composeRequested();
|
||||
|
||||
private slots:
|
||||
void onRowSelected(const QModelIndex &index);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QTableView *m_tableView;
|
||||
QSortFilterProxyModel *m_proxyModel;
|
||||
QPushButton *m_composeButton;
|
||||
};
|
||||
@@ -0,0 +1,270 @@
|
||||
#include "mainmainwindow.h"
|
||||
#include "core/models/account.h"
|
||||
#include "core/mailitem.h"
|
||||
#include "db/dao/mailitemdao.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFrame>
|
||||
#include <QDateTime>
|
||||
#include <QLabel>
|
||||
|
||||
MainMainWindow::MainMainWindow(QWidget *parent)
|
||||
: QMainWindow(parent), m_currentFolderId(-1)
|
||||
{
|
||||
setupUI();
|
||||
connectModels();
|
||||
setWindowTitle("Wino Mail DTK");
|
||||
resize(1280, 820);
|
||||
}
|
||||
|
||||
void MainMainWindow::setupUI() {
|
||||
// === Global stylesheet ===
|
||||
this->setStyleSheet(
|
||||
"QMainWindow { background-color: #f5f5f7; }"
|
||||
"QSplitter::handle { background-color: #d1d1d6; width: 1px; }"
|
||||
"QTreeView { background-color: #ffffff; border: none; font-family: 'Segoe UI', Helvetica; font-size: 13px; }"
|
||||
"QToolBar { background-color: #f5f5f7; border-bottom: 1px solid #d1d1d6; spacing: 10px; }"
|
||||
);
|
||||
|
||||
createToolBar();
|
||||
|
||||
// === Central widget ===
|
||||
QWidget *central = new QWidget();
|
||||
QHBoxLayout *centralLayout = new QHBoxLayout(central);
|
||||
centralLayout->setContentsMargins(0, 0, 0, 0);
|
||||
centralLayout->setSpacing(0);
|
||||
|
||||
// === Sidebar ===
|
||||
setupSidebar();
|
||||
centralLayout->addWidget(m_sidebar);
|
||||
|
||||
// === Separator line ===
|
||||
QFrame *separator = new QFrame();
|
||||
separator->setFrameShape(QFrame::VLine);
|
||||
separator->setStyleSheet("color: #d1d1d6;");
|
||||
centralLayout->addWidget(separator);
|
||||
|
||||
// === Stacked pages ===
|
||||
m_stack = new QStackedWidget();
|
||||
m_stack->setStyleSheet("background-color: #f5f5f7;");
|
||||
|
||||
// Page 0: Mail (folder tree + mail list + reader)
|
||||
setupMailPage();
|
||||
m_stack->addWidget(m_mailPage);
|
||||
|
||||
// Page 1: Compose
|
||||
m_composeView = new ComposeView();
|
||||
connect(m_composeView, &ComposeView::sendRequested, [this](const QString &to, const QString &cc, const QString &bcc, const QString &subject, const QString &body, const QDateTime &scheduleTime) {
|
||||
QString msg;
|
||||
if (scheduleTime.isValid()) {
|
||||
msg = QString("Message scheduled for: %1").arg(scheduleTime.toString("dd/MM/yyyy hh:mm AP"));
|
||||
} else {
|
||||
msg = "Message sent (simulated)";
|
||||
}
|
||||
if (!cc.isEmpty()) msg += QString(" | Cc: %1").arg(cc);
|
||||
if (!bcc.isEmpty()) msg += QString(" | Bcc: %1").arg(bcc);
|
||||
statusBar()->showMessage(msg, 5000);
|
||||
switchToPage(PageMail);
|
||||
});
|
||||
connect(m_composeView, &ComposeView::discardRequested, [this]() {
|
||||
switchToPage(PageMail);
|
||||
});
|
||||
connect(m_composeView, &ComposeView::detachRequested, [this](QWidget *composeView) {
|
||||
// Detach compose view to a standalone window
|
||||
QStackedWidget *stack = qobject_cast<QStackedWidget*>(composeView->parentWidget());
|
||||
if (stack) {
|
||||
stack->removeWidget(composeView);
|
||||
}
|
||||
|
||||
// Create standalone window
|
||||
QMainWindow *detachedWin = new QMainWindow();
|
||||
detachedWin->setWindowTitle("Compose - Wino Mail");
|
||||
|
||||
// Assign central widget and ensure it's visible and sized
|
||||
detachedWin->setCentralWidget(composeView);
|
||||
composeView->setMinimumSize(800, 600);
|
||||
composeView->update();
|
||||
|
||||
detachedWin->resize(800, 600);
|
||||
detachedWin->setAttribute(Qt::WA_DeleteOnClose);
|
||||
detachedWin->show();
|
||||
|
||||
statusBar()->showMessage("Compose view detached to separate window", 3000);
|
||||
});
|
||||
m_stack->addWidget(m_composeView);
|
||||
|
||||
// Page 2: Settings
|
||||
m_settingsView = new SettingsView();
|
||||
connect(m_settingsView, &SettingsView::accountAddRequested, [this]() {
|
||||
statusBar()->showMessage("Account setup dialog would open here", 3000);
|
||||
});
|
||||
connect(m_settingsView, &SettingsView::themeChanged, [this](const QString &theme) {
|
||||
statusBar()->showMessage(QString("Theme changed to: %1 (restart may be required)").arg(theme), 3000);
|
||||
});
|
||||
m_stack->addWidget(m_settingsView);
|
||||
|
||||
// Page 3: Contacts
|
||||
m_contactsView = new ContactsView();
|
||||
m_stack->addWidget(m_contactsView);
|
||||
|
||||
// Page 4: Calendar
|
||||
m_calendarView = new CalendarView();
|
||||
m_stack->addWidget(m_calendarView);
|
||||
|
||||
centralLayout->addWidget(m_stack, 1);
|
||||
setCentralWidget(central);
|
||||
|
||||
// Show mail page by default
|
||||
switchToPage(PageMail);
|
||||
}
|
||||
|
||||
void MainMainWindow::setupSidebar() {
|
||||
m_sidebar = new QListWidget();
|
||||
m_sidebar->setFixedWidth(64);
|
||||
m_sidebar->setIconSize(QSize(24, 24));
|
||||
m_sidebar->setSpacing(4);
|
||||
m_sidebar->setFrameShape(QFrame::NoFrame);
|
||||
m_sidebar->setStyleSheet(
|
||||
"QListWidget { background-color: #2c2c2e; border: none; padding: 8px 0; }"
|
||||
"QListWidget::item { color: #8e8e93; padding: 12px 0; text-align: center; font-size: 10px; border: none; border-radius: 8px; margin: 2px 8px; }"
|
||||
"QListWidget::item:selected { background-color: #3a3a3c; color: #ffffff; }"
|
||||
"QListWidget::item:hover { background-color: #3a3a3c; color: #ffffff; }"
|
||||
);
|
||||
|
||||
m_sidebar->addItem("📧\nMail");
|
||||
m_sidebar->addItem("✏️\nCompose");
|
||||
m_sidebar->addItem("⚙️\nSettings");
|
||||
m_sidebar->addItem("👥\nContacts");
|
||||
m_sidebar->addItem("📅\nCalendar");
|
||||
|
||||
m_sidebar->setCurrentRow(0);
|
||||
connect(m_sidebar, &QListWidget::currentRowChanged, this, &MainMainWindow::onNavChanged);
|
||||
}
|
||||
|
||||
void MainMainWindow::setupMailPage() {
|
||||
m_mailPage = new QWidget();
|
||||
QHBoxLayout *mailLayout = new QHBoxLayout(m_mailPage);
|
||||
mailLayout->setContentsMargins(0, 0, 0, 0);
|
||||
mailLayout->setSpacing(0);
|
||||
|
||||
m_folderSplitter = new QSplitter(Qt::Horizontal);
|
||||
m_folderSplitter->setHandleWidth(1);
|
||||
|
||||
// Folder tree
|
||||
m_folderTree = new QTreeView();
|
||||
m_folderTree->setHeaderHidden(true);
|
||||
m_folderTree->setIndentation(20);
|
||||
m_folderTree->setMinimumWidth(220);
|
||||
m_folderTree->setMaximumWidth(350);
|
||||
m_folderTree->setFrameShape(QFrame::NoFrame);
|
||||
m_folderTree->setExpandsOnDoubleClick(true);
|
||||
m_folderSplitter->addWidget(m_folderTree);
|
||||
|
||||
// Mail list (QTableView with sorting)
|
||||
m_mailListView = new MailListView();
|
||||
connect(m_mailListView, &MailListView::emailSelected, this, &MainMainWindow::onEmailSelected);
|
||||
connect(m_mailListView, &MailListView::composeRequested, this, &MainMainWindow::onComposeRequested);
|
||||
m_folderSplitter->addWidget(m_mailListView);
|
||||
|
||||
// Reader
|
||||
m_emailViewer = new ReaderView();
|
||||
m_emailViewer->setMinimumWidth(350);
|
||||
connect(m_emailViewer, &ReaderView::replyRequested, this, &MainMainWindow::onReaderReplyRequested);
|
||||
m_folderSplitter->addWidget(m_emailViewer);
|
||||
|
||||
// Default sizes: folder 240, list 380, reader flex
|
||||
m_folderSplitter->setSizes({240, 380, 600});
|
||||
|
||||
mailLayout->addWidget(m_folderSplitter);
|
||||
}
|
||||
|
||||
void MainMainWindow::connectModels() {
|
||||
m_accountService = new AccountService(this);
|
||||
m_mailService = new MailService(this);
|
||||
|
||||
m_folderModel = new FolderListModel(m_accountService, this);
|
||||
m_emailModel = new EmailListModel(this);
|
||||
|
||||
m_folderTree->setModel(m_folderModel);
|
||||
m_mailListView->setModel(m_emailModel);
|
||||
|
||||
m_folderTree->expandAll();
|
||||
|
||||
connect(m_folderTree, &QTreeView::clicked, this, &MainMainWindow::onFolderSelected);
|
||||
}
|
||||
|
||||
void MainMainWindow::onNavChanged(int index) {
|
||||
switchToPage(static_cast<Page>(index));
|
||||
}
|
||||
|
||||
void MainMainWindow::switchToPage(int pageIndex) {
|
||||
m_stack->setCurrentIndex(pageIndex);
|
||||
m_sidebar->blockSignals(true);
|
||||
m_sidebar->setCurrentRow(pageIndex);
|
||||
m_sidebar->blockSignals(false);
|
||||
|
||||
// Show/hide toolbar actions per page
|
||||
}
|
||||
|
||||
void MainMainWindow::onFolderSelected(const QModelIndex &index) {
|
||||
if (!index.isValid()) return;
|
||||
|
||||
int itemType = index.data(FolderListModel::ItemTypeRole).toInt();
|
||||
if (itemType == FolderTreeItem::FolderNode) {
|
||||
m_currentFolderId = index.data(FolderListModel::FolderIdRole).toInt();
|
||||
m_emailModel->setFolderId(m_currentFolderId);
|
||||
m_emailModel->refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void MainMainWindow::onEmailSelected(int mailId) {
|
||||
std::optional<MailItem> item = MailItemDao::findById(mailId);
|
||||
if (!item.has_value()) {
|
||||
m_emailViewer->setMailItem(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
MailItem &mail = item.value();
|
||||
if (!mail.isRead()) {
|
||||
mail.setRead(true);
|
||||
MailItemDao::update(mail);
|
||||
}
|
||||
m_emailViewer->setMailItem(&mail);
|
||||
}
|
||||
|
||||
void MainMainWindow::onComposeRequested() {
|
||||
switchToPage(PageCompose);
|
||||
}
|
||||
|
||||
void MainMainWindow::onReaderReplyRequested(const MailItem *item) {
|
||||
if (item) {
|
||||
m_composeView->setTo(item->sender());
|
||||
m_composeView->setSubject("Re: " + item->subject());
|
||||
}
|
||||
switchToPage(PageCompose);
|
||||
}
|
||||
|
||||
void MainMainWindow::onNewMessage() {
|
||||
switchToPage(PageCompose);
|
||||
}
|
||||
|
||||
void MainMainWindow::createToolBar() {
|
||||
m_toolBar = addToolBar("Main Toolbar");
|
||||
m_toolBar->setMovable(false);
|
||||
|
||||
QAction *newMsgAction = m_toolBar->addAction("✉ New Message");
|
||||
m_toolBar->addSeparator();
|
||||
QAction *syncAction = m_toolBar->addAction("⟳ Sync/Refresh");
|
||||
QAction *deleteAction = m_toolBar->addAction("🗑 Delete");
|
||||
|
||||
connect(newMsgAction, &QAction::triggered, this, &MainMainWindow::onNewMessage);
|
||||
connect(syncAction, &QAction::triggered, [this]() {
|
||||
if (m_currentFolderId >= 0) {
|
||||
m_emailModel->refresh();
|
||||
statusBar()->showMessage("Refreshed", 2000);
|
||||
}
|
||||
});
|
||||
connect(deleteAction, &QAction::triggered, [this]() {
|
||||
statusBar()->showMessage("Delete would be implemented here", 3000);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include <QStackedWidget>
|
||||
#include <QListWidget>
|
||||
#include <QSplitter>
|
||||
#include <QTreeView>
|
||||
#include <QStatusBar>
|
||||
#include <QToolBar>
|
||||
#include <QAction>
|
||||
|
||||
#include "ui/readerview.h"
|
||||
#include "ui/maillistview.h"
|
||||
#include "ui/composeview.h"
|
||||
#include "ui/settingsview.h"
|
||||
#include "ui/contactsview.h"
|
||||
#include "ui/calendarview.h"
|
||||
#include "ui/models/FolderListModel.h"
|
||||
#include "ui/models/EmailListModel.h"
|
||||
#include "services/accountservice.h"
|
||||
#include "services/mailservice.h"
|
||||
|
||||
class MainMainWindow : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainMainWindow(QWidget *parent = nullptr);
|
||||
~MainMainWindow() override = default;
|
||||
|
||||
private slots:
|
||||
void onNavChanged(int index);
|
||||
void onFolderSelected(const QModelIndex &index);
|
||||
void onEmailSelected(int mailId);
|
||||
void onComposeRequested();
|
||||
void onReaderReplyRequested(const MailItem *item);
|
||||
void onNewMessage();
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupSidebar();
|
||||
void setupMailPage();
|
||||
void connectModels();
|
||||
void createToolBar();
|
||||
void switchToPage(int pageIndex);
|
||||
|
||||
// Navigation
|
||||
QListWidget *m_sidebar;
|
||||
QStackedWidget *m_stack;
|
||||
|
||||
enum Page {
|
||||
PageMail = 0,
|
||||
PageCompose,
|
||||
PageSettings,
|
||||
PageContacts,
|
||||
PageCalendar
|
||||
};
|
||||
|
||||
// Mail page widgets
|
||||
QWidget *m_mailPage;
|
||||
QSplitter *m_folderSplitter;
|
||||
QTreeView *m_folderTree;
|
||||
MailListView *m_mailListView;
|
||||
ReaderView *m_emailViewer;
|
||||
|
||||
// Other pages
|
||||
ComposeView *m_composeView;
|
||||
SettingsView *m_settingsView;
|
||||
ContactsView *m_contactsView;
|
||||
CalendarView *m_calendarView;
|
||||
|
||||
// Services & Models
|
||||
FolderListModel *m_folderModel;
|
||||
EmailListModel *m_emailModel;
|
||||
AccountService *m_accountService;
|
||||
MailService *m_mailService;
|
||||
|
||||
QToolBar *m_toolBar;
|
||||
int m_currentFolderId;
|
||||
};
|
||||
@@ -3,8 +3,7 @@
|
||||
#include <QDateTime>
|
||||
|
||||
EmailListModel::EmailListModel(QObject *parent)
|
||||
: QAbstractListModel(parent),
|
||||
m_mailItemDao(MailItemDao::instance())
|
||||
: QAbstractListModel(parent)
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
@@ -89,9 +88,9 @@ void EmailListModel::refresh()
|
||||
{
|
||||
beginResetModel();
|
||||
if (m_folderId == -1) {
|
||||
m_emails = m_mailItemDao.findAll();
|
||||
m_emails = MailItemDao::findAll();
|
||||
} else {
|
||||
m_emails = m_mailItemDao.findByFolderId(m_folderId);
|
||||
m_emails = MailItemDao::findByFolderId(m_folderId);
|
||||
}
|
||||
endResetModel();
|
||||
qDebug() << "EmailListModel refreshed with" << m_emails.size() << "emails for folderId" << m_folderId;
|
||||
|
||||
@@ -40,7 +40,7 @@ public:
|
||||
private:
|
||||
QVector<MailItem> m_emails;
|
||||
int m_folderId{-1}; // -1 means all folders
|
||||
MailItemDao& m_mailItemDao;
|
||||
// REMOVED: MailItemDao& m_mailItemDao; // Methods are static, no instance needed
|
||||
};
|
||||
|
||||
#endif // EMAILLISTMODEL_H
|
||||
@@ -1,36 +1,177 @@
|
||||
#include "FolderListModel.h"
|
||||
#include <QDebug>
|
||||
|
||||
FolderListModel::FolderListModel(QObject *parent)
|
||||
: QAbstractListModel(parent),
|
||||
m_folderDao(FolderDao::instance())
|
||||
// --- FolderTreeItem ---
|
||||
|
||||
FolderTreeItem::FolderTreeItem(Type type, QVariant data, FolderTreeItem *parent)
|
||||
: m_type(type), m_data(std::move(data)), m_parentItem(parent)
|
||||
{
|
||||
}
|
||||
|
||||
FolderTreeItem::~FolderTreeItem()
|
||||
{
|
||||
qDeleteAll(m_childItems);
|
||||
}
|
||||
|
||||
void FolderTreeItem::appendChild(FolderTreeItem *child)
|
||||
{
|
||||
m_childItems.append(child);
|
||||
}
|
||||
|
||||
void FolderTreeItem::clearChildren()
|
||||
{
|
||||
qDeleteAll(m_childItems);
|
||||
m_childItems.clear();
|
||||
}
|
||||
|
||||
FolderTreeItem *FolderTreeItem::child(int row)
|
||||
{
|
||||
if (row < 0 || row >= m_childItems.size())
|
||||
return nullptr;
|
||||
return m_childItems.at(row);
|
||||
}
|
||||
|
||||
int FolderTreeItem::childCount() const
|
||||
{
|
||||
return m_childItems.size();
|
||||
}
|
||||
|
||||
int FolderTreeItem::row() const
|
||||
{
|
||||
if (m_parentItem)
|
||||
return m_parentItem->m_childItems.indexOf(const_cast<FolderTreeItem*>(this));
|
||||
return 0;
|
||||
}
|
||||
|
||||
FolderTreeItem *FolderTreeItem::parentItem()
|
||||
{
|
||||
return m_parentItem;
|
||||
}
|
||||
|
||||
// --- FolderListModel ---
|
||||
|
||||
FolderListModel::FolderListModel(AccountService *accountService, QObject *parent)
|
||||
: QAbstractItemModel(parent),
|
||||
m_accountService(accountService),
|
||||
m_rootItem(new FolderTreeItem(FolderTreeItem::AccountNode, "Root"))
|
||||
{
|
||||
refresh();
|
||||
}
|
||||
|
||||
FolderListModel::~FolderListModel()
|
||||
{
|
||||
delete m_rootItem;
|
||||
}
|
||||
|
||||
QModelIndex FolderListModel::index(int row, int column, const QModelIndex &parent) const
|
||||
{
|
||||
if (!hasIndex(row, column, parent))
|
||||
return QModelIndex();
|
||||
|
||||
FolderTreeItem *parentItem;
|
||||
if (!parent.isValid())
|
||||
parentItem = m_rootItem;
|
||||
else
|
||||
parentItem = static_cast<FolderTreeItem*>(parent.internalPointer());
|
||||
|
||||
FolderTreeItem *childItem = parentItem->child(row);
|
||||
if (childItem)
|
||||
return createIndex(row, column, childItem);
|
||||
return QModelIndex();
|
||||
}
|
||||
|
||||
QModelIndex FolderListModel::parent(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return QModelIndex();
|
||||
|
||||
FolderTreeItem *childItem = static_cast<FolderTreeItem*>(index.internalPointer());
|
||||
FolderTreeItem *parentItem = childItem->parentItem();
|
||||
|
||||
if (!parentItem || parentItem == m_rootItem)
|
||||
return QModelIndex();
|
||||
|
||||
return createIndex(parentItem->row(), 0, parentItem);
|
||||
}
|
||||
|
||||
int FolderListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
if (parent.column() > 0)
|
||||
return 0;
|
||||
return m_folderNames.size();
|
||||
|
||||
FolderTreeItem *parentItem;
|
||||
if (!parent.isValid())
|
||||
parentItem = m_rootItem;
|
||||
else
|
||||
parentItem = static_cast<FolderTreeItem*>(parent.internalPointer());
|
||||
|
||||
return parentItem->childCount();
|
||||
}
|
||||
|
||||
int FolderListModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
Q_UNUSED(parent);
|
||||
return 1;
|
||||
}
|
||||
|
||||
QVariant FolderListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_folderNames.size())
|
||||
if (!index.isValid())
|
||||
return QVariant();
|
||||
|
||||
if (role == FolderNameRole)
|
||||
return m_folderNames.at(index.row());
|
||||
else if (role == UnreadCountRole)
|
||||
return m_unreadCounts.at(index.row());
|
||||
FolderTreeItem *item = static_cast<FolderTreeItem*>(index.internalPointer());
|
||||
|
||||
if (role == Qt::DisplayRole || role == NameRole) {
|
||||
if (item->type() == FolderTreeItem::AccountNode) {
|
||||
Account acc = item->data().value<Account>();
|
||||
return acc.displayName().isEmpty() ? acc.email() : acc.displayName();
|
||||
} else {
|
||||
Folder folder = item->data().value<Folder>();
|
||||
return folder.name();
|
||||
}
|
||||
}
|
||||
|
||||
if (role == ItemTypeRole)
|
||||
return item->type();
|
||||
|
||||
if (role == AccountIdRole) {
|
||||
if (item->type() == FolderTreeItem::AccountNode) {
|
||||
Account acc = item->data().value<Account>();
|
||||
return acc.id();
|
||||
}
|
||||
}
|
||||
|
||||
if (role == FolderIdRole) {
|
||||
if (item->type() == FolderTreeItem::FolderNode) {
|
||||
Folder folder = item->data().value<Folder>();
|
||||
return folder.id();
|
||||
}
|
||||
}
|
||||
|
||||
if (role == UnreadCountRole) {
|
||||
if (item->type() == FolderTreeItem::FolderNode) {
|
||||
Folder folder = item->data().value<Folder>();
|
||||
return folder.unreadCount();
|
||||
}
|
||||
}
|
||||
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
Qt::ItemFlags FolderListModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
return Qt::NoItemFlags;
|
||||
return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
|
||||
}
|
||||
|
||||
QHash<int, QByteArray> FolderListModel::roleNames() const
|
||||
{
|
||||
QHash<int, QByteArray> roles;
|
||||
roles[FolderNameRole] = "folderName";
|
||||
roles[ItemTypeRole] = "itemType";
|
||||
roles[AccountIdRole] = "accountId";
|
||||
roles[FolderIdRole] = "folderId";
|
||||
roles[NameRole] = "name";
|
||||
roles[UnreadCountRole] = "unreadCount";
|
||||
return roles;
|
||||
}
|
||||
@@ -38,9 +179,55 @@ QHash<int, QByteArray> FolderListModel::roleNames() const
|
||||
void FolderListModel::refresh()
|
||||
{
|
||||
beginResetModel();
|
||||
// For now, we'll just use hardcoded folders until we implement the DAO properly
|
||||
m_folderNames = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
|
||||
m_unreadCounts = {5, 0, 0, 0, 0};
|
||||
clearModel();
|
||||
setupModelData();
|
||||
endResetModel();
|
||||
qDebug() << "FolderListModel refreshed with" << m_folderNames.size() << "folders";
|
||||
}
|
||||
qDebug() << "FolderListModel refreshed";
|
||||
}
|
||||
|
||||
void FolderListModel::setupModelData()
|
||||
{
|
||||
QVector<Account> accounts = m_accountService->getAllAccounts();
|
||||
|
||||
// If no accounts exist yet, add one sample account with default folders
|
||||
if (accounts.isEmpty()) {
|
||||
Account sample(1, "javi@example.com", "Javi's Email",
|
||||
AccountType::IMAP);
|
||||
accounts.append(sample);
|
||||
}
|
||||
|
||||
for (const Account &acc : accounts) {
|
||||
QVariant accVariant;
|
||||
accVariant.setValue(acc);
|
||||
FolderTreeItem *accountItem = new FolderTreeItem(FolderTreeItem::AccountNode, accVariant, m_rootItem);
|
||||
m_rootItem->appendChild(accountItem);
|
||||
|
||||
// Get folders for this account
|
||||
QVector<Folder> folders = FolderDao::findByAccountId(acc.id());
|
||||
|
||||
// If no folders exist yet, create default ones
|
||||
if (folders.isEmpty()) {
|
||||
QStringList defaultFolders = {"Inbox", "Sent", "Drafts", "Trash", "Spam"};
|
||||
for (const QString &name : defaultFolders) {
|
||||
Folder f;
|
||||
f.setName(name);
|
||||
f.setAccountId(acc.id());
|
||||
folders.append(f);
|
||||
}
|
||||
}
|
||||
|
||||
for (const Folder &folder : folders) {
|
||||
QVariant folderVariant;
|
||||
folderVariant.setValue(folder);
|
||||
FolderTreeItem *folderItem = new FolderTreeItem(FolderTreeItem::FolderNode, folderVariant, accountItem);
|
||||
accountItem->appendChild(folderItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void FolderListModel::clearModel()
|
||||
{
|
||||
m_rootItem->clearChildren();
|
||||
}
|
||||
|
||||
#include "FolderListModel.moc"
|
||||
@@ -1,34 +1,70 @@
|
||||
#ifndef FOLDERLISTMODEL_H
|
||||
#define FOLDERLISTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QAbstractItemModel>
|
||||
#include <QHash>
|
||||
#include <QByteArray>
|
||||
#include "../db/dao/folderdao.h"
|
||||
#include <QVector>
|
||||
#include "services/accountservice.h"
|
||||
#include "db/dao/folderdao.h"
|
||||
#include "core/models/account.h"
|
||||
#include "core/models/folder.h"
|
||||
|
||||
class FolderListModel : public QAbstractListModel
|
||||
class FolderTreeItem
|
||||
{
|
||||
public:
|
||||
enum Type { AccountNode, FolderNode };
|
||||
|
||||
explicit FolderTreeItem(Type type, QVariant data, FolderTreeItem *parent = nullptr);
|
||||
~FolderTreeItem();
|
||||
|
||||
void appendChild(FolderTreeItem *child);
|
||||
void clearChildren();
|
||||
FolderTreeItem *child(int row);
|
||||
int childCount() const;
|
||||
int row() const;
|
||||
FolderTreeItem *parentItem();
|
||||
Type type() const { return m_type; }
|
||||
QVariant data() const { return m_data; }
|
||||
|
||||
private:
|
||||
Type m_type;
|
||||
QVariant m_data;
|
||||
QVector<FolderTreeItem*> m_childItems;
|
||||
FolderTreeItem *m_parentItem;
|
||||
};
|
||||
|
||||
class FolderListModel : public QAbstractItemModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit FolderListModel(QObject *parent = nullptr);
|
||||
~FolderListModel() override = default;
|
||||
explicit FolderListModel(AccountService *accountService, QObject *parent = nullptr);
|
||||
~FolderListModel() override;
|
||||
|
||||
enum FolderRoles {
|
||||
FolderNameRole = Qt::UserRole + 1,
|
||||
enum Roles {
|
||||
ItemTypeRole = Qt::UserRole + 1,
|
||||
AccountIdRole,
|
||||
FolderIdRole,
|
||||
NameRole,
|
||||
UnreadCountRole
|
||||
};
|
||||
|
||||
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
|
||||
QModelIndex parent(const QModelIndex &index) const override;
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
Qt::ItemFlags flags(const QModelIndex &index) const override;
|
||||
QHash<int, QByteArray> roleNames() const override;
|
||||
|
||||
// Optional: method to refresh the model from the database
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
QVector<QString> m_folderNames;
|
||||
QVector<int> m_unreadCounts;
|
||||
FolderDao& m_folderDao;
|
||||
void setupModelData();
|
||||
void clearModel();
|
||||
|
||||
FolderTreeItem *m_rootItem;
|
||||
AccountService *m_accountService;
|
||||
};
|
||||
|
||||
#endif // FOLDERLISTMODEL_H
|
||||
@@ -0,0 +1,407 @@
|
||||
#include "newmessagedialog.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QFormLayout>
|
||||
#include <QMessageBox>
|
||||
#include <QFileDialog>
|
||||
#include <QDateTime>
|
||||
#include <QTextList>
|
||||
#include <QTextTable>
|
||||
#include <QFileInfo>
|
||||
#include <QCloseEvent>
|
||||
|
||||
NewMessageDialog::NewMessageDialog(const QStringList &accounts, QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
setWindowTitle("New Message");
|
||||
setMinimumSize(750, 600);
|
||||
resize(850, 700);
|
||||
|
||||
setupUI();
|
||||
|
||||
// Init state
|
||||
m_ccEdit->hide();
|
||||
m_bccEdit->hide();
|
||||
m_schedulePicker->hide();
|
||||
|
||||
// Capture initial content for dirty detection
|
||||
m_initialContent = m_bodyEdit->toPlainText();
|
||||
|
||||
// Signals
|
||||
connect(m_sendNowBtn, &QPushButton::clicked, this, &NewMessageDialog::onSendNow);
|
||||
connect(m_sendLaterBtn, &QPushButton::clicked, this, &NewMessageDialog::onSendLater);
|
||||
}
|
||||
|
||||
void NewMessageDialog::setupUI()
|
||||
{
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(16, 12, 16, 12);
|
||||
mainLayout->setSpacing(8);
|
||||
|
||||
// ===== HEADER FIELDS =====
|
||||
QVBoxLayout *headerLayout = new QVBoxLayout();
|
||||
headerLayout->setSpacing(6);
|
||||
|
||||
// From
|
||||
QHBoxLayout *fromRow = new QHBoxLayout();
|
||||
m_fromCombo = new QComboBox(this);
|
||||
m_fromCombo->setMinimumWidth(400);
|
||||
fromRow->addWidget(new QLabel("From:", this));
|
||||
fromRow->addWidget(m_fromCombo, 1);
|
||||
headerLayout->addLayout(fromRow);
|
||||
|
||||
// To + CC/CCO buttons
|
||||
QHBoxLayout *toRow = new QHBoxLayout();
|
||||
m_toEdit = new QLineEdit(this);
|
||||
m_toEdit->setPlaceholderText("To");
|
||||
toRow->addWidget(new QLabel("To:", this));
|
||||
toRow->addWidget(m_toEdit, 1);
|
||||
m_ccBtn = new QPushButton("CC", this);
|
||||
m_ccBtn->setFixedWidth(48);
|
||||
m_ccBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||||
m_bccBtn = new QPushButton("BCC", this);
|
||||
m_bccBtn->setFixedWidth(48);
|
||||
m_bccBtn->setStyleSheet(m_ccBtn->styleSheet());
|
||||
toRow->addWidget(m_ccBtn);
|
||||
toRow->addWidget(m_bccBtn);
|
||||
headerLayout->addLayout(toRow);
|
||||
|
||||
connect(m_ccBtn, &QPushButton::clicked, this, &NewMessageDialog::toggleCc);
|
||||
connect(m_bccBtn, &QPushButton::clicked, this, &NewMessageDialog::toggleBcc);
|
||||
|
||||
// CC
|
||||
m_ccEdit = new QLineEdit(this);
|
||||
m_ccEdit->setPlaceholderText("Cc");
|
||||
QHBoxLayout *ccRow = new QHBoxLayout();
|
||||
ccRow->addWidget(new QLabel("Cc:", this));
|
||||
ccRow->addWidget(m_ccEdit, 1);
|
||||
headerLayout->addLayout(ccRow);
|
||||
|
||||
// BCC
|
||||
m_bccEdit = new QLineEdit(this);
|
||||
m_bccEdit->setPlaceholderText("Bcc");
|
||||
QHBoxLayout *bccRow = new QHBoxLayout();
|
||||
bccRow->addWidget(new QLabel("Bcc:", this));
|
||||
bccRow->addWidget(m_bccEdit, 1);
|
||||
headerLayout->addLayout(bccRow);
|
||||
|
||||
// Subject
|
||||
QHBoxLayout *subjRow = new QHBoxLayout();
|
||||
m_subjectEdit = new QLineEdit(this);
|
||||
m_subjectEdit->setPlaceholderText("Subject");
|
||||
subjRow->addWidget(new QLabel("Subject:", this));
|
||||
subjRow->addWidget(m_subjectEdit, 1);
|
||||
headerLayout->addLayout(subjRow);
|
||||
|
||||
mainLayout->addLayout(headerLayout);
|
||||
|
||||
// ===== RICH TEXT TOOLBAR =====
|
||||
m_richToolbar = new QToolBar("Format", this);
|
||||
m_richToolbar->setIconSize(QSize(16, 16));
|
||||
m_richToolbar->setStyleSheet(
|
||||
"QToolBar { border: 1px solid #d1d1d6; border-radius: 4px; background: #f9f9f9; spacing: 4px; padding: 2px; }"
|
||||
);
|
||||
setupRichTextToolbar(m_richToolbar);
|
||||
mainLayout->addWidget(m_richToolbar);
|
||||
|
||||
// ===== BODY EDITOR =====
|
||||
m_bodyEdit = new QTextEdit(this);
|
||||
m_bodyEdit->setAcceptRichText(true);
|
||||
m_bodyEdit->setPlaceholderText("Write your message...");
|
||||
m_bodyEdit->setFrameShape(QFrame::StyledPanel);
|
||||
m_bodyEdit->setStyleSheet(
|
||||
"QTextEdit { border: 1px solid #d1d1d6; border-radius: 4px; padding: 8px; font-size: 13px; font-family: 'Segoe UI', Roboto, Helvetica; }"
|
||||
"QTextEdit:focus { border-color: #0078d4; }"
|
||||
);
|
||||
mainLayout->addWidget(m_bodyEdit, 1);
|
||||
|
||||
connect(m_bodyEdit, &QTextEdit::currentCharFormatChanged, this, &NewMessageDialog::currentCharFormatChanged);
|
||||
|
||||
// ===== ATTACHMENTS =====
|
||||
QHBoxLayout *attachRow = new QHBoxLayout();
|
||||
m_attachBtn = new QPushButton("Attach Files...", this);
|
||||
m_attachBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 6px 12px; font-size: 12px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||||
m_attachmentList = new QListWidget(this);
|
||||
m_attachmentList->setMaximumHeight(60);
|
||||
m_attachmentList->setMinimumWidth(300);
|
||||
m_attachmentList->setStyleSheet("QListWidget { border: 1px solid #e0e0e0; border-radius: 4px; font-size: 11px; }");
|
||||
m_attachmentList->setVisible(false);
|
||||
attachRow->addWidget(m_attachBtn);
|
||||
attachRow->addWidget(m_attachmentList, 1);
|
||||
mainLayout->addLayout(attachRow);
|
||||
|
||||
connect(m_attachBtn, &QPushButton::clicked, this, &NewMessageDialog::onAttachFile);
|
||||
connect(m_attachmentList, &QListWidget::itemDoubleClicked, this, &NewMessageDialog::removeAttachment);
|
||||
|
||||
// ===== BOTTOM ACTION BAR =====
|
||||
QHBoxLayout *actionBar = new QHBoxLayout();
|
||||
|
||||
// Schedule picker
|
||||
m_schedulePicker = new QDateTimeEdit(QDateTime::currentDateTime().addSecs(3600), this);
|
||||
m_schedulePicker->setCalendarPopup(true);
|
||||
m_schedulePicker->setDisplayFormat("dd/MM/yyyy hh:mm AP");
|
||||
m_schedulePicker->setStyleSheet("QDateTimeEdit { border: 1px solid #ccc; border-radius: 4px; padding: 6px 8px; font-size: 12px; }");
|
||||
|
||||
actionBar->addStretch();
|
||||
|
||||
m_cancelBtn = new QPushButton("Discard", this);
|
||||
m_cancelBtn->setStyleSheet("QPushButton { border: 1px solid #ccc; border-radius: 6px; padding: 8px 20px; font-size: 13px; font-weight: bold; background: #f0f0f0; color: #333; } QPushButton:hover { background: #e0e0e0; }");
|
||||
|
||||
m_sendLaterBtn = new QPushButton("Schedule...", this);
|
||||
m_sendLaterBtn->setStyleSheet("QPushButton { border: 1px solid #0078d4; border-radius: 6px; padding: 8px 20px; font-size: 13px; font-weight: bold; background: #ffffff; color: #0078d4; } QPushButton:hover { background: #e8f4ff; }");
|
||||
|
||||
m_sendNowBtn = new QPushButton("Send Now", this);
|
||||
m_sendNowBtn->setStyleSheet("QPushButton { border: none; border-radius: 6px; padding: 8px 24px; font-size: 13px; font-weight: bold; background: #0078d4; color: white; } QPushButton:hover { background: #106ebe; }");
|
||||
|
||||
actionBar->addWidget(m_cancelBtn);
|
||||
actionBar->addWidget(m_sendLaterBtn);
|
||||
actionBar->addWidget(m_sendNowBtn);
|
||||
mainLayout->addLayout(actionBar);
|
||||
|
||||
connect(m_cancelBtn, &QPushButton::clicked, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
void NewMessageDialog::setupRichTextToolbar(QToolBar *tb)
|
||||
{
|
||||
m_boldAction = tb->addAction("B");
|
||||
m_boldAction->setCheckable(true);
|
||||
m_boldAction->setToolTip("Bold (Ctrl+B)");
|
||||
m_boldAction->setFont(QFont("Segoe UI", 11, QFont::Bold));
|
||||
|
||||
m_italicAction = tb->addAction("I");
|
||||
m_italicAction->setCheckable(true);
|
||||
m_italicAction->setToolTip("Italic (Ctrl+I)");
|
||||
m_italicAction->setFont(QFont("Segoe UI", 11, QFont::StyleItalic));
|
||||
|
||||
m_underlineAction = tb->addAction("U");
|
||||
m_underlineAction->setCheckable(true);
|
||||
m_underlineAction->setToolTip("Underline (Ctrl+U)");
|
||||
QFont uFont("Segoe UI", 11);
|
||||
uFont.setUnderline(true);
|
||||
m_underlineAction->setFont(uFont);
|
||||
|
||||
tb->addSeparator();
|
||||
|
||||
QAction *bulletAction = tb->addAction("• List");
|
||||
bulletAction->setToolTip("Bullet list");
|
||||
|
||||
QAction *numAction = tb->addAction("1. List");
|
||||
numAction->setToolTip("Numbered list");
|
||||
|
||||
tb->addSeparator();
|
||||
|
||||
QAction *tableAction = tb->addAction("▦ Table");
|
||||
tableAction->setToolTip("Insert table");
|
||||
|
||||
QAction *imageAction = tb->addAction("🖼 Image");
|
||||
imageAction->setToolTip("Insert image");
|
||||
|
||||
connect(m_boldAction, &QAction::triggered, this, &NewMessageDialog::onFormatBold);
|
||||
connect(m_italicAction, &QAction::triggered, this, &NewMessageDialog::onFormatItalic);
|
||||
connect(m_underlineAction, &QAction::triggered, this, &NewMessageDialog::onFormatUnderline);
|
||||
connect(bulletAction, &QAction::triggered, this, &NewMessageDialog::onInsertBulletList);
|
||||
connect(numAction, &QAction::triggered, this, &NewMessageDialog::onInsertNumberedList);
|
||||
connect(tableAction, &QAction::triggered, this, &NewMessageDialog::onInsertTable);
|
||||
connect(imageAction, &QAction::triggered, this, &NewMessageDialog::onInsertImage);
|
||||
|
||||
tb->addSeparator();
|
||||
QAction *undoAction = tb->addAction("↶");
|
||||
undoAction->setToolTip("Undo");
|
||||
connect(undoAction, &QAction::triggered, m_bodyEdit, &QTextEdit::undo);
|
||||
}
|
||||
|
||||
// ===== FORMAT SLOTS =====
|
||||
|
||||
void NewMessageDialog::mergeFormatOnWordOrSelection(const QTextCharFormat &fmt)
|
||||
{
|
||||
QTextCursor cursor = m_bodyEdit->textCursor();
|
||||
if (!cursor.hasSelection())
|
||||
cursor.select(QTextCursor::WordUnderCursor);
|
||||
cursor.mergeCharFormat(fmt);
|
||||
m_bodyEdit->mergeCurrentCharFormat(fmt);
|
||||
}
|
||||
|
||||
void NewMessageDialog::currentCharFormatChanged(const QTextCharFormat &fmt)
|
||||
{
|
||||
m_boldAction->setChecked(fmt.fontWeight() >= QFont::Bold);
|
||||
m_italicAction->setChecked(fmt.fontItalic());
|
||||
m_underlineAction->setChecked(fmt.fontUnderline());
|
||||
}
|
||||
|
||||
void NewMessageDialog::onFormatBold()
|
||||
{
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontWeight(m_boldAction->isChecked() ? QFont::Bold : QFont::Normal);
|
||||
mergeFormatOnWordOrSelection(fmt);
|
||||
}
|
||||
|
||||
void NewMessageDialog::onFormatItalic()
|
||||
{
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontItalic(m_italicAction->isChecked());
|
||||
mergeFormatOnWordOrSelection(fmt);
|
||||
}
|
||||
|
||||
void NewMessageDialog::onFormatUnderline()
|
||||
{
|
||||
QTextCharFormat fmt;
|
||||
fmt.setFontUnderline(m_underlineAction->isChecked());
|
||||
mergeFormatOnWordOrSelection(fmt);
|
||||
}
|
||||
|
||||
void NewMessageDialog::onInsertBulletList()
|
||||
{
|
||||
QTextCursor cursor = m_bodyEdit->textCursor();
|
||||
cursor.insertList(QTextListFormat::ListDisc);
|
||||
}
|
||||
|
||||
void NewMessageDialog::onInsertNumberedList()
|
||||
{
|
||||
QTextCursor cursor = m_bodyEdit->textCursor();
|
||||
cursor.insertList(QTextListFormat::ListDecimal);
|
||||
}
|
||||
|
||||
void NewMessageDialog::onInsertTable()
|
||||
{
|
||||
QTextCursor cursor = m_bodyEdit->textCursor();
|
||||
QTextTable *table = cursor.insertTable(3, 3);
|
||||
Q_UNUSED(table);
|
||||
}
|
||||
|
||||
void NewMessageDialog::onInsertImage()
|
||||
{
|
||||
QString filePath = QFileDialog::getOpenFileName(this, "Insert Image", QString(),
|
||||
"Images (*.png *.jpg *.jpeg *.gif *.bmp)");
|
||||
if (filePath.isEmpty()) return;
|
||||
|
||||
QImage image(filePath);
|
||||
if (image.isNull()) return;
|
||||
|
||||
// Scale large images to fit
|
||||
if (image.width() > 600)
|
||||
image = image.scaledToWidth(600, Qt::SmoothTransformation);
|
||||
|
||||
QTextCursor cursor = m_bodyEdit->textCursor();
|
||||
cursor.insertImage(image);
|
||||
}
|
||||
|
||||
// ===== CC / BCC TOGGLES =====
|
||||
|
||||
void NewMessageDialog::toggleCc()
|
||||
{
|
||||
m_ccVisible = !m_ccVisible;
|
||||
m_ccEdit->setVisible(m_ccVisible);
|
||||
m_ccBtn->setStyleSheet(m_ccVisible
|
||||
? "QPushButton { border: 1px solid #0078d4; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #e8f4ff; color: #0078d4; }"
|
||||
: "QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||||
}
|
||||
|
||||
void NewMessageDialog::toggleBcc()
|
||||
{
|
||||
m_bccVisible = !m_bccVisible;
|
||||
m_bccEdit->setVisible(m_bccVisible);
|
||||
m_bccBtn->setStyleSheet(m_bccVisible
|
||||
? "QPushButton { border: 1px solid #0078d4; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #e8f4ff; color: #0078d4; }"
|
||||
: "QPushButton { border: 1px solid #ccc; border-radius: 4px; padding: 4px 8px; font-size: 11px; background: #f0f0f0; } QPushButton:hover { background: #e0e0e0; }");
|
||||
}
|
||||
|
||||
// ===== SEND =====
|
||||
|
||||
void NewMessageDialog::onSendNow()
|
||||
{
|
||||
if (m_toEdit->text().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this, "Missing Recipient", "Please enter at least one recipient.");
|
||||
m_toEdit->setFocus();
|
||||
return;
|
||||
}
|
||||
m_sendLater = false;
|
||||
accept();
|
||||
}
|
||||
|
||||
void NewMessageDialog::onSendLater()
|
||||
{
|
||||
if (m_toEdit->text().trimmed().isEmpty()) {
|
||||
QMessageBox::warning(this, "Missing Recipient", "Please enter at least one recipient.");
|
||||
m_toEdit->setFocus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_schedulePicker->dateTime() <= QDateTime::currentDateTime()) {
|
||||
QMessageBox::warning(this, "Invalid Time", "Scheduled time must be in the future.");
|
||||
return;
|
||||
}
|
||||
|
||||
m_sendLater = true;
|
||||
m_scheduledTime = m_schedulePicker->dateTime();
|
||||
accept();
|
||||
}
|
||||
|
||||
// ===== ATTACHMENTS =====
|
||||
|
||||
void NewMessageDialog::onAttachFile()
|
||||
{
|
||||
QStringList files = QFileDialog::getOpenFileNames(this, "Attach Files");
|
||||
if (files.isEmpty()) return;
|
||||
|
||||
for (const QString &file : files) {
|
||||
QFileInfo fi(file);
|
||||
m_attachedFiles << file;
|
||||
m_attachmentList->addItem(fi.fileName());
|
||||
}
|
||||
m_attachmentList->setVisible(!m_attachedFiles.isEmpty());
|
||||
}
|
||||
|
||||
void NewMessageDialog::removeAttachment()
|
||||
{
|
||||
QListWidgetItem *item = m_attachmentList->currentItem();
|
||||
if (!item) return;
|
||||
|
||||
int row = m_attachmentList->row(item);
|
||||
m_attachedFiles.removeAt(row);
|
||||
delete m_attachmentList->takeItem(row);
|
||||
m_attachmentList->setVisible(!m_attachedFiles.isEmpty());
|
||||
}
|
||||
|
||||
// ===== CLOSE WITH UNSAVED CHANGES =====
|
||||
|
||||
bool NewMessageDialog::hasUnsavedChanges() const
|
||||
{
|
||||
// Check if anything was typed
|
||||
if (!m_toEdit->text().trimmed().isEmpty()) return true;
|
||||
if (!m_subjectEdit->text().trimmed().isEmpty()) return true;
|
||||
if (m_bodyEdit->toPlainText() != m_initialContent &&
|
||||
!m_bodyEdit->toPlainText().trimmed().isEmpty()) return true;
|
||||
if (!m_attachedFiles.isEmpty()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void NewMessageDialog::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
if (hasUnsavedChanges()) {
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(
|
||||
this, "Unsaved Changes",
|
||||
"You have unsaved changes. Would you like to save to drafts before closing?",
|
||||
QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
|
||||
QMessageBox::Save);
|
||||
|
||||
if (reply == QMessageBox::Save) {
|
||||
// Accept the dialog so the caller can handle saving to drafts
|
||||
accept();
|
||||
return;
|
||||
} else if (reply == QMessageBox::Cancel) {
|
||||
event->ignore();
|
||||
return;
|
||||
}
|
||||
// Discard: fall through to reject
|
||||
}
|
||||
event->accept();
|
||||
}
|
||||
|
||||
// ===== GETTERS =====
|
||||
|
||||
QString NewMessageDialog::fromAccount() const { return m_fromCombo->currentText(); }
|
||||
QString NewMessageDialog::to() const { return m_toEdit->text().trimmed(); }
|
||||
QString NewMessageDialog::cc() const { return m_ccEdit->text().trimmed(); }
|
||||
QString NewMessageDialog::bcc() const { return m_bccEdit->text().trimmed(); }
|
||||
QString NewMessageDialog::subject() const { return m_subjectEdit->text().trimmed(); }
|
||||
QString NewMessageDialog::body() const { return m_bodyEdit->toHtml(); }
|
||||
@@ -0,0 +1,95 @@
|
||||
#ifndef NEWMESSAGEDIALOG_H
|
||||
#define NEWMESSAGEDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QLineEdit>
|
||||
#include <QTextEdit>
|
||||
#include <QPushButton>
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QToolBar>
|
||||
#include <QDateTimeEdit>
|
||||
#include <QListWidget>
|
||||
#include <QStringList>
|
||||
#include <QTextCharFormat>
|
||||
|
||||
class NewMessageDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit NewMessageDialog(const QStringList &accounts, QWidget *parent = nullptr);
|
||||
~NewMessageDialog() override = default;
|
||||
|
||||
QString fromAccount() const;
|
||||
QString to() const;
|
||||
QString cc() const;
|
||||
QString bcc() const;
|
||||
QString subject() const;
|
||||
QString body() const;
|
||||
QStringList attachments() const { return m_attachedFiles; }
|
||||
bool sendLater() const { return m_sendLater; }
|
||||
QDateTime scheduledTime() const { return m_scheduledTime; }
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void toggleCc();
|
||||
void toggleBcc();
|
||||
void onSendNow();
|
||||
void onSendLater();
|
||||
void onAttachFile();
|
||||
void removeAttachment();
|
||||
void onFormatBold();
|
||||
void onFormatItalic();
|
||||
void onFormatUnderline();
|
||||
void onInsertBulletList();
|
||||
void onInsertNumberedList();
|
||||
void onInsertTable();
|
||||
void onInsertImage();
|
||||
void mergeFormatOnWordOrSelection(const QTextCharFormat &fmt);
|
||||
void currentCharFormatChanged(const QTextCharFormat &fmt);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
void setupRichTextToolbar(QToolBar *tb);
|
||||
bool hasUnsavedChanges() const;
|
||||
|
||||
// Header fields
|
||||
QComboBox *m_fromCombo;
|
||||
QLineEdit *m_toEdit;
|
||||
QPushButton *m_ccBtn;
|
||||
QPushButton *m_bccBtn;
|
||||
QLineEdit *m_ccEdit;
|
||||
QLineEdit *m_bccEdit;
|
||||
QLineEdit *m_subjectEdit;
|
||||
|
||||
// Rich text editor
|
||||
QToolBar *m_richToolbar;
|
||||
QAction *m_boldAction;
|
||||
QAction *m_italicAction;
|
||||
QAction *m_underlineAction;
|
||||
|
||||
// Body
|
||||
QTextEdit *m_bodyEdit;
|
||||
|
||||
// Attachments
|
||||
QPushButton *m_attachBtn;
|
||||
QListWidget *m_attachmentList;
|
||||
QStringList m_attachedFiles;
|
||||
|
||||
// Bottom actions
|
||||
QPushButton *m_sendNowBtn;
|
||||
QPushButton *m_sendLaterBtn;
|
||||
QPushButton *m_cancelBtn;
|
||||
QDateTimeEdit *m_schedulePicker;
|
||||
|
||||
bool m_ccVisible{false};
|
||||
bool m_bccVisible{false};
|
||||
bool m_sendLater{false};
|
||||
QDateTime m_scheduledTime;
|
||||
QString m_initialContent; // For detecting unsaved changes
|
||||
};
|
||||
|
||||
#endif // NEWMESSAGEDIALOG_H
|
||||
@@ -0,0 +1,77 @@
|
||||
#include "ui/readerview.h"
|
||||
#include <QFont>
|
||||
|
||||
ReaderView::ReaderView(QWidget *parent) : QWidget(parent) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void ReaderView::setupUI() {
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(15, 15, 15, 15);
|
||||
mainLayout->setSpacing(10);
|
||||
|
||||
// Header Section
|
||||
QWidget *headerWidget = new QWidget();
|
||||
QVBoxLayout *headerLayout = new QVBoxLayout(headerWidget);
|
||||
headerLayout->setSpacing(5);
|
||||
|
||||
m_subjectLabel = new QLabel();
|
||||
QFont subjectFont = m_subjectLabel->font();
|
||||
subjectFont.setBold(true);
|
||||
subjectFont.setPointSize(14);
|
||||
m_subjectLabel->setFont(subjectFont);
|
||||
m_subjectLabel->setText("No subject");
|
||||
m_subjectLabel->setWordWrap(true);
|
||||
|
||||
m_fromLabel = new QLabel();
|
||||
m_fromLabel->setText("From: ");
|
||||
|
||||
m_dateLabel = new QLabel();
|
||||
m_dateLabel->setText("Date: ");
|
||||
m_dateLabel->setStyleSheet("color: gray; font-style: italic;");
|
||||
|
||||
headerLayout->addWidget(m_subjectLabel);
|
||||
headerLayout->addWidget(m_fromLabel);
|
||||
headerLayout->addWidget(m_dateLabel);
|
||||
|
||||
// Action Buttons
|
||||
QHBoxLayout *actionsLayout = new QHBoxLayout();
|
||||
m_replyButton = new QPushButton("Reply");
|
||||
m_forwardButton = new QPushButton("Forward");
|
||||
m_deleteButton = new QPushButton("Delete");
|
||||
m_deleteButton->setStyleSheet("color: red;");
|
||||
|
||||
actionsLayout->addWidget(m_replyButton);
|
||||
actionsLayout->addWidget(m_forwardButton);
|
||||
actionsLayout->addStretch();
|
||||
actionsLayout->addWidget(m_deleteButton);
|
||||
|
||||
// Body Viewer
|
||||
m_bodyViewer = new QTextBrowser();
|
||||
m_bodyViewer->setOpenExternalLinks(true);
|
||||
m_bodyViewer->setFrameStyle(QFrame::NoFrame);
|
||||
|
||||
mainLayout->addWidget(headerWidget);
|
||||
mainLayout->addLayout(actionsLayout);
|
||||
mainLayout->addWidget(m_bodyViewer);
|
||||
|
||||
// Connections
|
||||
connect(m_replyButton, &QPushButton::clicked, [this]() {
|
||||
if (m_bodyViewer->toPlainText().isEmpty()) return;
|
||||
});
|
||||
}
|
||||
|
||||
void ReaderView::setMailItem(const MailItem* item) {
|
||||
if (!item) {
|
||||
m_subjectLabel->setText("No mail selected");
|
||||
m_fromLabel->setText("From: ");
|
||||
m_dateLabel->setText("Date: ");
|
||||
m_bodyViewer->setHtml("<i>Please select a message to read</i>");
|
||||
return;
|
||||
}
|
||||
|
||||
m_subjectLabel->setText(item->subject());
|
||||
m_fromLabel->setText(QString("From: %1").arg(item->sender()));
|
||||
m_dateLabel->setText(QString("Date: %1").arg(item->date().toString("ddd, d MMM yyyy hh:mm")));
|
||||
m_bodyViewer->setHtml(item->bodyHtml());
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QLabel>
|
||||
#include <QTextBrowser>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include "core/mailitem.h"
|
||||
|
||||
class ReaderView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ReaderView(QWidget *parent = nullptr);
|
||||
~ReaderView() override = default;
|
||||
|
||||
void setMailItem(const MailItem* item);
|
||||
|
||||
signals:
|
||||
void replyRequested(const MailItem* item);
|
||||
void forwardRequested(const MailItem* item);
|
||||
void deleteRequested(const MailItem* item);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
|
||||
QLabel *m_subjectLabel;
|
||||
QLabel *m_fromLabel;
|
||||
QLabel *m_dateLabel;
|
||||
QTextBrowser *m_bodyViewer;
|
||||
|
||||
QPushButton *m_replyButton;
|
||||
QPushButton *m_forwardButton;
|
||||
QPushButton *m_deleteButton;
|
||||
};
|
||||
@@ -0,0 +1,164 @@
|
||||
#include "ui/settingsview.h"
|
||||
#include "services/accountservice.h"
|
||||
#include "core/models/account.h"
|
||||
|
||||
SettingsView::SettingsView(QWidget *parent) : QWidget(parent) {
|
||||
setupUI();
|
||||
}
|
||||
|
||||
void SettingsView::setupUI() {
|
||||
QVBoxLayout *mainLayout = new QVBoxLayout(this);
|
||||
mainLayout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
m_tabWidget = new QTabWidget();
|
||||
m_tabWidget->addTab(createAccountsTab(), "Accounts");
|
||||
m_tabWidget->addTab(createGeneralTab(), "General");
|
||||
m_tabWidget->addTab(createAppearanceTab(), "Appearance");
|
||||
|
||||
mainLayout->addWidget(m_tabWidget);
|
||||
}
|
||||
|
||||
QWidget* SettingsView::createAccountsTab() {
|
||||
QWidget *w = new QWidget();
|
||||
QVBoxLayout *layout = new QVBoxLayout(w);
|
||||
layout->setContentsMargins(20, 20, 20, 20);
|
||||
layout->setSpacing(15);
|
||||
|
||||
QLabel *sectionTitle = new QLabel("Email Accounts");
|
||||
QFont titleFont = sectionTitle->font();
|
||||
titleFont.setPointSize(16);
|
||||
titleFont.setBold(true);
|
||||
sectionTitle->setFont(titleFont);
|
||||
layout->addWidget(sectionTitle);
|
||||
|
||||
m_accountList = new QListWidget();
|
||||
m_accountList->setAlternatingRowColors(true);
|
||||
m_accountList->setFrameShape(QFrame::NoFrame);
|
||||
m_accountList->setStyleSheet(
|
||||
"QListWidget { background: #ffffff; border: 1px solid #e0e0e0; border-radius: 4px; }"
|
||||
"QListWidget::item { padding: 12px; border-bottom: 1px solid #f0f0f0; }"
|
||||
"QListWidget::item:selected { background: #e3f2fd; }"
|
||||
);
|
||||
layout->addWidget(m_accountList, 1);
|
||||
|
||||
QPushButton *addBtn = new QPushButton("+ Add Email Account");
|
||||
addBtn->setStyleSheet(
|
||||
"QPushButton { background: #1976D2; color: white; border: none; border-radius: 4px; padding: 10px 20px; font-weight: bold; }"
|
||||
"QPushButton:hover { background: #1565C0; }"
|
||||
);
|
||||
connect(addBtn, &QPushButton::clicked, this, &SettingsView::accountAddRequested);
|
||||
layout->addWidget(addBtn);
|
||||
|
||||
return w;
|
||||
}
|
||||
|
||||
QWidget* SettingsView::createGeneralTab() {
|
||||
QWidget *w = new QWidget();
|
||||
QVBoxLayout *layout = new QVBoxLayout(w);
|
||||
layout->setContentsMargins(20, 20, 20, 20);
|
||||
layout->setSpacing(15);
|
||||
|
||||
QLabel *sectionTitle = new QLabel("General Settings");
|
||||
QFont titleFont = sectionTitle->font();
|
||||
titleFont.setPointSize(16);
|
||||
titleFont.setBold(true);
|
||||
sectionTitle->setFont(titleFont);
|
||||
layout->addWidget(sectionTitle);
|
||||
|
||||
QCheckBox *startOnLogin = new QCheckBox("Start application on login");
|
||||
startOnLogin->setChecked(true);
|
||||
connect(startOnLogin, &QCheckBox::toggled, [this](bool checked) {
|
||||
emit settingChanged("start_on_login", checked);
|
||||
});
|
||||
layout->addWidget(startOnLogin);
|
||||
|
||||
QCheckBox *enableNotifications = new QCheckBox("Enable notifications");
|
||||
enableNotifications->setChecked(true);
|
||||
connect(enableNotifications, &QCheckBox::toggled, [this](bool checked) {
|
||||
emit settingChanged("notifications_enabled", checked);
|
||||
});
|
||||
layout->addWidget(enableNotifications);
|
||||
|
||||
QCheckBox *minimizeToTray = new QCheckBox("Minimize to tray on close");
|
||||
minimizeToTray->setChecked(true);
|
||||
connect(minimizeToTray, &QCheckBox::toggled, [this](bool checked) {
|
||||
emit settingChanged("minimize_to_tray", checked);
|
||||
});
|
||||
layout->addWidget(minimizeToTray);
|
||||
|
||||
layout->addSpacing(10);
|
||||
|
||||
QHBoxLayout *syncLayout = new QHBoxLayout();
|
||||
QLabel *syncLabel = new QLabel("Sync Interval:");
|
||||
syncLabel->setStyleSheet("font-weight: bold; color: #555;");
|
||||
m_syncIntervalCombo = new QComboBox();
|
||||
m_syncIntervalCombo->addItem("15 minutes", 15);
|
||||
m_syncIntervalCombo->addItem("30 minutes", 30);
|
||||
m_syncIntervalCombo->addItem("60 minutes", 60);
|
||||
m_syncIntervalCombo->addItem("120 minutes", 120);
|
||||
m_syncIntervalCombo->setCurrentIndex(1);
|
||||
connect(m_syncIntervalCombo, QOverload<int>::of(&QComboBox::currentIndexChanged), [this](int idx) {
|
||||
emit settingChanged("sync_interval", m_syncIntervalCombo->itemData(idx));
|
||||
});
|
||||
syncLayout->addWidget(syncLabel);
|
||||
syncLayout->addWidget(m_syncIntervalCombo);
|
||||
syncLayout->addStretch();
|
||||
layout->addLayout(syncLayout);
|
||||
|
||||
layout->addStretch();
|
||||
return w;
|
||||
}
|
||||
|
||||
QWidget* SettingsView::createAppearanceTab() {
|
||||
QWidget *w = new QWidget();
|
||||
QVBoxLayout *layout = new QVBoxLayout(w);
|
||||
layout->setContentsMargins(20, 20, 20, 20);
|
||||
layout->setSpacing(15);
|
||||
|
||||
QLabel *sectionTitle = new QLabel("Appearance");
|
||||
QFont titleFont = sectionTitle->font();
|
||||
titleFont.setPointSize(16);
|
||||
titleFont.setBold(true);
|
||||
sectionTitle->setFont(titleFont);
|
||||
layout->addWidget(sectionTitle);
|
||||
|
||||
QLabel *themeLabel = new QLabel("Theme:");
|
||||
themeLabel->setStyleSheet("font-weight: bold; color: #555;");
|
||||
layout->addWidget(themeLabel);
|
||||
|
||||
m_themeGroup = new QButtonGroup(this);
|
||||
QRadioButton *lightRadio = new QRadioButton("Light");
|
||||
QRadioButton *darkRadio = new QRadioButton("Dark");
|
||||
QRadioButton *systemRadio = new QRadioButton("System");
|
||||
lightRadio->setChecked(true);
|
||||
|
||||
m_themeGroup->addButton(lightRadio, 0);
|
||||
m_themeGroup->addButton(darkRadio, 1);
|
||||
m_themeGroup->addButton(systemRadio, 2);
|
||||
|
||||
connect(m_themeGroup, QOverload<int>::of(&QButtonGroup::idClicked), [this](int id) {
|
||||
QString theme;
|
||||
switch (id) {
|
||||
case 0: theme = "light"; break;
|
||||
case 1: theme = "dark"; break;
|
||||
case 2: theme = "system"; break;
|
||||
}
|
||||
emit themeChanged(theme);
|
||||
});
|
||||
|
||||
layout->addWidget(lightRadio);
|
||||
layout->addWidget(darkRadio);
|
||||
layout->addWidget(systemRadio);
|
||||
|
||||
layout->addSpacing(15);
|
||||
|
||||
QCheckBox *deepinTheme = new QCheckBox("Use Deepin theme");
|
||||
deepinTheme->setChecked(true);
|
||||
connect(deepinTheme, &QCheckBox::toggled, [this](bool checked) {
|
||||
emit settingChanged("deepin_theme", checked);
|
||||
});
|
||||
layout->addWidget(deepinTheme);
|
||||
|
||||
layout->addStretch();
|
||||
return w;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
#pragma once
|
||||
|
||||
#include <QWidget>
|
||||
#include <QTabWidget>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QListWidget>
|
||||
#include <QComboBox>
|
||||
#include <QCheckBox>
|
||||
#include <QRadioButton>
|
||||
#include <QButtonGroup>
|
||||
#include <QVariant>
|
||||
|
||||
class SettingsView : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SettingsView(QWidget *parent = nullptr);
|
||||
~SettingsView() override = default;
|
||||
|
||||
signals:
|
||||
void accountAddRequested();
|
||||
void accountEditRequested(int accountIndex);
|
||||
void accountDeleteRequested(int accountIndex);
|
||||
void themeChanged(const QString &theme);
|
||||
void settingChanged(const QString &key, const QVariant &value);
|
||||
|
||||
private:
|
||||
void setupUI();
|
||||
QWidget* createAccountsTab();
|
||||
QWidget* createGeneralTab();
|
||||
QWidget* createAppearanceTab();
|
||||
|
||||
QTabWidget *m_tabWidget;
|
||||
QListWidget *m_accountList;
|
||||
QComboBox *m_syncIntervalCombo;
|
||||
QButtonGroup *m_themeGroup;
|
||||
};
|
||||
@@ -8,8 +8,6 @@ NotificationManager::NotificationManager(QObject *parent)
|
||||
, m_trayMenu(nullptr)
|
||||
, m_showHideAction(nullptr)
|
||||
, m_quitAction(nullptr)
|
||||
, m_newMailSound(nullptr)
|
||||
, m_errorSound(nullptr)
|
||||
{
|
||||
// Defer initialization to after QApplication is fully ready
|
||||
// We'll initialize in a separate method called from main after the event loop starts?
|
||||
@@ -35,8 +33,6 @@ void NotificationManager::initialize()
|
||||
m_trayMenu = new QMenu(); // parent to NotificationManager so it gets deleted with us
|
||||
m_showHideAction = new QAction("Show/Hide", m_trayMenu);
|
||||
m_quitAction = new QAction("Quit", m_trayMenu);
|
||||
m_newMailSound = new QSoundEffect(this);
|
||||
m_errorSound = new QSoundEffect(this);
|
||||
|
||||
setupTrayIcon();
|
||||
setupConnections();
|
||||
@@ -53,11 +49,11 @@ void NotificationManager::setupTrayIcon()
|
||||
m_trayMenu->addAction(m_quitAction);
|
||||
m_trayIcon->setContextMenu(m_trayMenu);
|
||||
|
||||
m_newMailSound->setSource(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/message-new-instant.oga"));
|
||||
m_newMailSound->setVolume(0.5);
|
||||
// // m_newMailSound->setSource (disabled)(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/message-new-instant.oga"));
|
||||
// m_newMailSound->setVolume(0.5);
|
||||
|
||||
m_errorSound->setSource(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/dialog-error.oga"));
|
||||
m_errorSound->setVolume(0.5);
|
||||
// // m_errorSound->setSource (disabled)(QUrl::fromLocalFile("/usr/share/sounds/freedesktop/stereo/dialog-error.oga"));
|
||||
// m_errorSound->setVolume(0.5);
|
||||
}
|
||||
|
||||
void NotificationManager::setupConnections()
|
||||
@@ -223,19 +219,17 @@ void NotificationManager::onQuitRequested()
|
||||
|
||||
void NotificationManager::playNewMailSound()
|
||||
{
|
||||
if (m_newMailSound->isLoaded()) {
|
||||
m_newMailSound->play();
|
||||
}
|
||||
Q_UNUSED(m_newMailSound);
|
||||
qDebug() << "New mail sound disabled (no Multimedia)";
|
||||
}
|
||||
|
||||
void NotificationManager::playErrorSound()
|
||||
{
|
||||
if (m_errorSound->isLoaded()) {
|
||||
m_errorSound->play();
|
||||
}
|
||||
Q_UNUSED(m_errorSound);
|
||||
qDebug() << "Error sound disabled (no Multimedia)";
|
||||
}
|
||||
|
||||
void NotificationManager::showNotification(const QString& title, const QString& message, QSystemTrayIcon::MessageIcon icon, int timeout)
|
||||
{
|
||||
m_trayIcon->showMessage(title, message, icon, timeout);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
#include <QSystemTrayIcon>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QSoundEffect>
|
||||
//// QSoundEffect disabled (unavailable, sound disabled)
|
||||
#include <QIcon>
|
||||
#include "core/eventbus.h"
|
||||
#include "../core/events.h"
|
||||
@@ -39,8 +39,8 @@ private:
|
||||
QMenu* m_trayMenu;
|
||||
QAction* m_showHideAction;
|
||||
QAction* m_quitAction;
|
||||
QSoundEffect* m_newMailSound;
|
||||
QSoundEffect* m_errorSound;
|
||||
void* m_newMailSound = nullptr; // was QSoundEffect* (disabled)
|
||||
void* m_errorSound = nullptr; // was QSoundEffect* (disabled)
|
||||
|
||||
void setupTrayIcon();
|
||||
void setupConnections();
|
||||
|
||||
Reference in New Issue
Block a user