#include "mainwindow.h" #include "imageprep.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr int kPreviewSize = 360; } MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { setWindowTitle(QStringLiteral("Photo Gallery Uploader")); m_stack = new QStackedWidget(this); m_stack->addWidget(buildSettingsPage()); // index 0 m_stack->addWidget(buildPostPage()); // index 1 setCentralWidget(m_stack); // Land on settings until there is somewhere to post to. if (m_settings.isConfigured()) showPost(); else showSettings(); } QWidget *MainWindow::buildSettingsPage() { auto *page = new QWidget; auto *layout = new QVBoxLayout(page); layout->setContentsMargins(24, 24, 24, 24); auto *form = new QFormLayout; m_baseUrl = new QLineEdit; m_baseUrl->setPlaceholderText(QStringLiteral("https://photos.example.com")); m_username = new QLineEdit; m_password = new QLineEdit; m_password->setEchoMode(QLineEdit::Password); form->addRow(QStringLiteral("Base URL"), m_baseUrl); form->addRow(QStringLiteral("Username"), m_username); form->addRow(QStringLiteral("Password"), m_password); layout->addLayout(form); m_saveButton = new QPushButton(QStringLiteral("Save && test connection")); connect(m_saveButton, &QPushButton::clicked, this, &MainWindow::saveAndTest); layout->addWidget(m_saveButton); m_settingsStatus = new QLabel; m_settingsStatus->setWordWrap(true); layout->addWidget(m_settingsStatus); m_settingsBack = new QPushButton(QStringLiteral("Back")); connect(m_settingsBack, &QPushButton::clicked, this, &MainWindow::showPost); layout->addWidget(m_settingsBack); layout->addWidget(new QLabel(QStringLiteral( "The password is stored in your system keychain, never in plain text. " "https is required — Basic auth would otherwise send it in the clear."))); layout->addStretch(); return page; } QWidget *MainWindow::buildPostPage() { auto *page = new QWidget; auto *layout = new QVBoxLayout(page); layout->setContentsMargins(24, 24, 24, 24); m_preview = new QLabel(QStringLiteral("No photo selected")); m_preview->setAlignment(Qt::AlignCenter); m_preview->setFixedSize(kPreviewSize, kPreviewSize); m_preview->setFrameShape(QFrame::Box); layout->addWidget(m_preview, 0, Qt::AlignHCenter); m_chooseButton = new QPushButton(QStringLiteral("Choose a photo…")); connect(m_chooseButton, &QPushButton::clicked, this, &MainWindow::chooseImage); layout->addWidget(m_chooseButton); auto *form = new QFormLayout; m_caption = new QLineEdit; m_date = new QDateEdit(QDate::currentDate()); m_date->setCalendarPopup(true); m_date->setDisplayFormat(QStringLiteral("yyyy-MM-dd")); form->addRow(QStringLiteral("Caption"), m_caption); form->addRow(QStringLiteral("Date"), m_date); layout->addLayout(form); m_postButton = new QPushButton(QStringLiteral("Post")); m_postButton->setEnabled(false); connect(m_postButton, &QPushButton::clicked, this, &MainWindow::submit); layout->addWidget(m_postButton); m_postStatus = new QLabel; m_postStatus->setWordWrap(true); layout->addWidget(m_postStatus); m_lastPost = new QLabel; m_lastPost->setOpenExternalLinks(true); m_lastPost->setWordWrap(true); layout->addWidget(m_lastPost); auto *settingsButton = new QPushButton(QStringLiteral("Settings")); connect(settingsButton, &QPushButton::clicked, this, &MainWindow::showSettings); layout->addWidget(settingsButton); layout->addStretch(); return page; } void MainWindow::showSettings() { m_baseUrl->setText(m_settings.baseUrl()); m_username->setText(m_settings.username()); m_password->setText(m_settings.readPassword()); m_settingsStatus->clear(); m_settingsBack->setVisible(m_settings.isConfigured()); m_stack->setCurrentIndex(0); } void MainWindow::showPost() { const Settings::LastPost last = m_settings.lastPost(); if (last.valid()) { m_lastPost->setText(QStringLiteral("Last post: %2") .arg(last.url, last.caption.isEmpty() ? last.slug : last.caption)); } else { m_lastPost->clear(); } m_stack->setCurrentIndex(1); } void MainWindow::saveAndTest() { const QString urlError = Settings::validateBaseUrl(m_baseUrl->text()); if (!urlError.isEmpty()) { m_settingsStatus->setText(urlError); return; } if (m_username->text().trimmed().isEmpty() || m_password->text().isEmpty()) { m_settingsStatus->setText(QStringLiteral("Username and password are required")); return; } m_settings.setBaseUrl(m_baseUrl->text()); m_settings.setUsername(m_username->text()); m_settings.writePassword(m_password->text()); m_baseUrl->setText(m_settings.baseUrl()); // show normalised value m_saveButton->setEnabled(false); m_settingsStatus->setText(QStringLiteral("Testing…")); auto *client = new UploadClient(m_settings.baseUrl(), m_settings.username(), m_settings.readPassword(), this); connect(client, &UploadClient::testFinished, this, [this, client](const QString &error) { m_saveButton->setEnabled(true); m_settingsBack->setVisible(m_settings.isConfigured()); m_settingsStatus->setText(error.isEmpty() ? QStringLiteral("Saved — server reachable and credentials accepted") : error); client->deleteLater(); }); client->testConnection(); } void MainWindow::chooseImage() { const QString path = QFileDialog::getOpenFileName( this, QStringLiteral("Choose a photo"), QString(), QStringLiteral("Images (*.jpg *.jpeg *.png *.heic *.heif *.webp *.tif *.tiff *.bmp);;All files (*)")); if (path.isEmpty()) return; m_imagePath = path; QImageReader reader(path); reader.setAutoTransform(true); const QImage image = reader.read(); if (image.isNull()) { m_preview->setText(QStringLiteral("Could not preview this image")); } else { m_preview->setPixmap(QPixmap::fromImage(image).scaled( kPreviewSize, kPreviewSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)); } m_postStatus->clear(); m_postButton->setEnabled(true); } void MainWindow::submit() { if (m_imagePath.isEmpty()) return; if (!m_settings.isConfigured()) { m_postStatus->setText(QStringLiteral("Set the server URL and credentials first")); return; } m_postButton->setEnabled(false); m_postStatus->setText(QStringLiteral("Preparing…")); // Slug from wall-clock time: posts are made one at a time, so this cannot // collide, and re-sending the same filename overwrites the same post. The // real capture date still travels in the date field. const QString slug = QDateTime::currentDateTime().toString(QStringLiteral("yyyy-MM-dd-HHmmss")); const QString cacheDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); const ImagePrep::Prepared prepared = ImagePrep::prepare(m_imagePath, cacheDir, slug); if (!prepared.ok()) { m_postStatus->setText(prepared.error); m_postButton->setEnabled(true); return; } m_postStatus->setText(QStringLiteral("Uploading…")); const QString caption = m_caption->text(); const QString date = m_date->date().toString(QStringLiteral("yyyy-MM-dd")); const QString fileName = QFileInfo(prepared.filePath).fileName(); auto *client = new UploadClient(m_settings.baseUrl(), m_settings.username(), m_settings.readPassword(), this); connect(client, &UploadClient::uploadFinished, this, [this, client, prepared, caption](UploadClient::Result result, const QString &slug, const QString &url, const QString &message) { QFile::remove(prepared.filePath); // no retry queue in this MVP m_postButton->setEnabled(true); if (result == UploadClient::Result::Success) { Settings::LastPost last{slug, url, caption, QDateTime::currentDateTime()}; m_settings.setLastPost(last); QString msg = QStringLiteral("Posted. View").arg(url); if (!prepared.note.isEmpty()) msg += QStringLiteral("
%1").arg(prepared.note); m_postStatus->setText(msg); m_postStatus->setOpenExternalLinks(true); } else { m_postStatus->setText(message); } client->deleteLater(); }); client->upload(prepared.filePath, fileName, caption, date); }