#pragma once #include #include #include #include #include class QNetworkAccessManager; // 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/" }; // 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, const QString &caption, const QString &date, const QString &tags); // Credential check via GET /upload (same Basic-auth gate the POST uses). // /health is not usable: nginx only proxies location /upload. void testConnection(); // 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/ (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) 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); // Manage signals. The error strings are empty on success. void photosListed(const QList &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); private: QByteArray authHeader() const; QString m_base; QString m_username; QString m_password; QNetworkAccessManager *m_net; };