Add Qt6 desktop uploader
A native Qt6/C++ desktop client for the gallery's POST /upload API, in desktop/ with its own flake (kept separate from the Go server module the same way android/ is). Single-photo parity with the Android app: - multipart upload with preemptive HTTP Basic auth and retry-aware outcome classification (uploadclient) - JPEG/PNG metadata stripped byte-for-byte to drop embedded GPS, EXIF orientation baked in, and a quality/scale valve for the 20M nginx cap; HEIC decodes via the qtimageformats plugin (imageprep) - base URL / username in QSettings, password in the system keychain via qtkeychain; https required and a trailing /upload stripped (settings) - settings and post screens in a QStackedWidget (mainwindow) Native Wayland or xcb is selected at runtime. Bulk upload, EXIF capture-date prefill, and moving image prep off the UI thread are left as follow-ups.
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
#include "settings.h"
|
||||
|
||||
#include <QEventLoop>
|
||||
#include <QUrl>
|
||||
|
||||
#include <qt6keychain/keychain.h>
|
||||
|
||||
namespace {
|
||||
const QLatin1String kService("photogallery-uploader");
|
||||
const QLatin1String kPasswordKey("password");
|
||||
} // namespace
|
||||
|
||||
Settings::Settings()
|
||||
: m_settings(QStringLiteral("inflo.ws"), QStringLiteral("photogallery-uploader"))
|
||||
{
|
||||
}
|
||||
|
||||
QString Settings::baseUrl() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("baseUrl")).toString();
|
||||
}
|
||||
|
||||
void Settings::setBaseUrl(const QString &value)
|
||||
{
|
||||
m_settings.setValue(QStringLiteral("baseUrl"), normalizeBaseUrl(value));
|
||||
}
|
||||
|
||||
QString Settings::username() const
|
||||
{
|
||||
return m_settings.value(QStringLiteral("username")).toString();
|
||||
}
|
||||
|
||||
void Settings::setUsername(const QString &value)
|
||||
{
|
||||
m_settings.setValue(QStringLiteral("username"), value.trimmed());
|
||||
}
|
||||
|
||||
QString Settings::readPassword() const
|
||||
{
|
||||
QKeychain::ReadPasswordJob job(kService);
|
||||
job.setAutoDelete(false);
|
||||
job.setKey(kPasswordKey);
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
|
||||
if (job.error() != QKeychain::NoError)
|
||||
return QString();
|
||||
return job.textData();
|
||||
}
|
||||
|
||||
void Settings::writePassword(const QString &value) const
|
||||
{
|
||||
QKeychain::WritePasswordJob job(kService);
|
||||
job.setAutoDelete(false);
|
||||
job.setKey(kPasswordKey);
|
||||
job.setTextData(value);
|
||||
|
||||
QEventLoop loop;
|
||||
QObject::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
|
||||
job.start();
|
||||
loop.exec();
|
||||
}
|
||||
|
||||
bool Settings::isConfigured() const
|
||||
{
|
||||
return !baseUrl().isEmpty() && !username().isEmpty() && !readPassword().isEmpty();
|
||||
}
|
||||
|
||||
Settings::LastPost Settings::lastPost() const
|
||||
{
|
||||
LastPost p;
|
||||
p.slug = m_settings.value(QStringLiteral("last/slug")).toString();
|
||||
p.url = m_settings.value(QStringLiteral("last/url")).toString();
|
||||
p.caption = m_settings.value(QStringLiteral("last/caption")).toString();
|
||||
p.postedAt = m_settings.value(QStringLiteral("last/postedAt")).toDateTime();
|
||||
return p;
|
||||
}
|
||||
|
||||
void Settings::setLastPost(const LastPost &value)
|
||||
{
|
||||
m_settings.setValue(QStringLiteral("last/slug"), value.slug);
|
||||
m_settings.setValue(QStringLiteral("last/url"), value.url);
|
||||
m_settings.setValue(QStringLiteral("last/caption"), value.caption);
|
||||
m_settings.setValue(QStringLiteral("last/postedAt"), value.postedAt);
|
||||
}
|
||||
|
||||
QString Settings::validateBaseUrl(const QString &raw)
|
||||
{
|
||||
const QString trimmed = raw.trimmed();
|
||||
if (trimmed.isEmpty())
|
||||
return QStringLiteral("Base URL is required");
|
||||
const QUrl url(trimmed);
|
||||
if (!url.isValid() || url.host().isEmpty())
|
||||
return QStringLiteral("Not a valid URL");
|
||||
if (url.scheme() != QStringLiteral("https"))
|
||||
return QStringLiteral("Must be https — plain http sends your password in the clear");
|
||||
return QString();
|
||||
}
|
||||
|
||||
QString Settings::normalizeBaseUrl(const QString &raw)
|
||||
{
|
||||
QString v = raw.trimmed();
|
||||
while (v.endsWith(QLatin1Char('/')))
|
||||
v.chop(1);
|
||||
if (v.endsWith(QStringLiteral("/upload"), Qt::CaseInsensitive))
|
||||
v.chop(QStringLiteral("/upload").size());
|
||||
while (v.endsWith(QLatin1Char('/')))
|
||||
v.chop(1);
|
||||
return v;
|
||||
}
|
||||
Reference in New Issue
Block a user