Files
photog/desktop/uploadclient.h
T
puttaalu 504b96ed31 Qt desktop: add manage (edit/delete) and tag autocomplete
Brings the Qt uploader to parity with the web /manage page and the Android
app. Adds a stacked Manage page (reached from the post page) that lists
posts via GET /api/photos with thumbnails from GET /api/thumb/<slug>, edits
caption/date/tags in a QDialog via POST /api/edit, and deletes to trash via
POST /api/delete with a QMessageBox confirm. As a non-browser client it sends
no Origin and so passes the server's same-origin CSRF check.

Tag autocomplete now backs every tags field (post cards and the edit dialog)
via a per-token TagCompleter (QCompleter subclass, MatchContains) fed from
the unguarded GET /api/tags.json.

New files: managepostwidget.{h,cpp}, tagcompleter.{h,cpp}; network methods
and a RemotePhoto struct added to uploadclient. Built and linked clean; not
yet runtime-tested on a real display.
2026-11-04 22:04:06 +01:00

82 lines
3.2 KiB
C++

#pragma once
#include <QImage>
#include <QList>
#include <QObject>
#include <QString>
#include <QStringList>
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/<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)
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<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);
private:
QByteArray authHeader() const;
QString m_base;
QString m_username;
QString m_password;
QNetworkAccessManager *m_net;
};