2026-08-26 00:28:35 +02:00
|
|
|
#pragma once
|
|
|
|
|
|
2026-11-04 22:04:06 +01:00
|
|
|
#include <QImage>
|
|
|
|
|
#include <QList>
|
2026-08-26 00:28:35 +02:00
|
|
|
#include <QObject>
|
|
|
|
|
#include <QString>
|
2026-11-04 22:04:06 +01:00
|
|
|
#include <QStringList>
|
2026-08-26 00:28:35 +02:00
|
|
|
|
|
|
|
|
class QNetworkAccessManager;
|
|
|
|
|
|
2026-11-04 22:04:06 +01:00
|
|
|
// One published post, as returned by GET /api/photos.
|
|
|
|
|
struct RemotePhoto {
|
|
|
|
|
QString slug;
|
|
|
|
|
QString caption;
|
|
|
|
|
QString date; // yyyy-MM-dd
|
|
|
|
|
QStringList tags;
|
|
|
|
|
QString thumb; // site-relative, e.g. "/thumbs/foo.jpg"
|
|
|
|
|
QString url; // site-relative, e.g. "/photo/foo/"
|
|
|
|
|
};
|
|
|
|
|
|
2026-08-26 00:28:35 +02:00
|
|
|
// Talks to the gallery's POST /upload endpoint, which sits behind nginx HTTP
|
|
|
|
|
// Basic auth. The credentials go out preemptively in the Authorization header
|
|
|
|
|
// on every request (never via QNetworkAccessManager's authenticationRequired
|
|
|
|
|
// signal, which only fires on a 401 and would replay a multipart body already
|
|
|
|
|
// streamed off disk).
|
|
|
|
|
//
|
|
|
|
|
// Asynchronous: results arrive via signals so the UI stays responsive during an
|
|
|
|
|
// upload over a slow link.
|
|
|
|
|
class UploadClient : public QObject
|
|
|
|
|
{
|
|
|
|
|
Q_OBJECT
|
|
|
|
|
public:
|
|
|
|
|
enum class Result {
|
|
|
|
|
Success, // uploaded; slug/url populated
|
|
|
|
|
AuthFailed, // wrong credentials — a retry will not help
|
|
|
|
|
Rejected, // server said no for a reason a retry won't change
|
|
|
|
|
Transient, // network trouble or a 5xx — worth retrying
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
UploadClient(QString baseUrl, QString username, QString password, QObject *parent = nullptr);
|
|
|
|
|
|
|
|
|
|
// The filename matters: server.go derives the slug from it, so re-sending
|
|
|
|
|
// the same name overwrites the same post instead of duplicating it.
|
|
|
|
|
void upload(const QString &filePath, const QString &fileName,
|
2026-10-08 16:19:25 +02:00
|
|
|
const QString &caption, const QString &date, const QString &tags);
|
2026-08-26 00:28:35 +02:00
|
|
|
|
|
|
|
|
// Credential check via GET /upload (same Basic-auth gate the POST uses).
|
|
|
|
|
// /health is not usable: nginx only proxies location /upload.
|
|
|
|
|
void testConnection();
|
|
|
|
|
|
2026-11-04 22:04:06 +01:00
|
|
|
// Manage API, mirroring the web /manage and the Android app. The desktop
|
|
|
|
|
// app is a non-browser client: it sends no Origin/Sec-Fetch-Site header, so
|
|
|
|
|
// the state-changing posts pass the server's same-origin (CSRF) check.
|
|
|
|
|
void listPhotos(); // GET /api/photos
|
|
|
|
|
void fetchTags(); // GET /api/tags.json (unguarded)
|
|
|
|
|
void fetchThumbnail(const QString &slug); // GET /api/thumb/<slug> (unguarded)
|
|
|
|
|
void editPost(const QString &slug, const QString &caption,
|
|
|
|
|
const QString &date, const QString &tags); // POST /api/edit
|
|
|
|
|
void deletePost(const QString &slug); // POST /api/delete (moves to trash)
|
|
|
|
|
|
2026-08-26 00:28:35 +02:00
|
|
|
signals:
|
|
|
|
|
void uploadFinished(UploadClient::Result result, const QString &slug,
|
|
|
|
|
const QString &url, const QString &message);
|
|
|
|
|
// errorMessage is empty on success.
|
|
|
|
|
void testFinished(const QString &errorMessage);
|
|
|
|
|
|
2026-11-04 22:04:06 +01:00
|
|
|
// Manage signals. The error strings are empty on success.
|
|
|
|
|
void photosListed(const QList<RemotePhoto> &photos, const QString &error);
|
|
|
|
|
void tagsFetched(const QStringList &tags);
|
|
|
|
|
void thumbnailReady(const QString &slug, const QImage &image);
|
|
|
|
|
void editFinished(const QString &slug, const QString &error);
|
|
|
|
|
void deleteFinished(const QString &slug, const QString &error);
|
|
|
|
|
|
2026-08-26 00:28:35 +02:00
|
|
|
private:
|
|
|
|
|
QByteArray authHeader() const;
|
|
|
|
|
|
|
|
|
|
QString m_base;
|
|
|
|
|
QString m_username;
|
|
|
|
|
QString m_password;
|
|
|
|
|
QNetworkAccessManager *m_net;
|
|
|
|
|
};
|