Files
puttaalu a7783671f2 Add bulk multi-photo upload to the desktop app
Mirror the Android bulk feature in the Qt uploader: choose several photos
at once, each a card with its own caption, date, and upload status, and
post the batch.

- Per-photo cards (PhotoItemWidget) in a scrollable list; retry re-posts
  only the failures.
- One upload per photo with a per-item UUID slug suffix so a batch posted
  in the same second cannot collide.
- Image prep runs off the UI thread via QtConcurrent so the window stays
  responsive.
- A "Simultaneous uploads" setting (1–8, default 3) caps how many photos
  are prepared/uploaded at once; the rest queue and fill freed slots.
2026-09-26 10:17:18 +02:00

127 lines
3.4 KiB
C++

#include "settings.h"
#include <QEventLoop>
#include <QUrl>
#include <algorithm>
#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());
}
int Settings::concurrency() const
{
const int v = m_settings.value(QStringLiteral("concurrency"), kDefaultConcurrency).toInt();
return std::clamp(v, 1, kMaxConcurrency);
}
void Settings::setConcurrency(int value)
{
m_settings.setValue(QStringLiteral("concurrency"), std::clamp(value, 1, kMaxConcurrency));
}
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;
}