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.
This commit is contained in:
1 parent
bb8c7109fa
commit
504b96ed31
12 files changed
+677
-2
No files matched your search
+241
-1
@@ -1,17 +1,25 @@
|
||||
#include "mainwindow.h"
|
||||
|
||||
#include "imageprep.h"
|
||||
#include "managepostwidget.h"
|
||||
#include "photoitemwidget.h"
|
||||
#include "tagcompleter.h"
|
||||
#include "uploadclient.h"
|
||||
|
||||
#include <QDate>
|
||||
#include <QDateEdit>
|
||||
#include <QDateTime>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QFormLayout>
|
||||
#include <QFuture>
|
||||
#include <QFutureWatcher>
|
||||
#include <QHBoxLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QSpinBox>
|
||||
@@ -29,6 +37,7 @@ MainWindow::MainWindow(QWidget *parent)
|
||||
m_stack = new QStackedWidget(this);
|
||||
m_stack->addWidget(buildSettingsPage()); // index 0
|
||||
m_stack->addWidget(buildPostPage()); // index 1
|
||||
m_stack->addWidget(buildManagePage()); // index 2
|
||||
setCentralWidget(m_stack);
|
||||
|
||||
if (m_settings.isConfigured())
|
||||
@@ -112,9 +121,51 @@ QWidget *MainWindow::buildPostPage()
|
||||
m_lastPost->setWordWrap(true);
|
||||
outer->addWidget(m_lastPost);
|
||||
|
||||
auto *bottomRow = new QHBoxLayout;
|
||||
auto *manageButton = new QPushButton(QStringLiteral("Manage"));
|
||||
connect(manageButton, &QPushButton::clicked, this, &MainWindow::showManage);
|
||||
bottomRow->addWidget(manageButton);
|
||||
|
||||
auto *settingsButton = new QPushButton(QStringLiteral("Settings"));
|
||||
connect(settingsButton, &QPushButton::clicked, this, &MainWindow::showSettings);
|
||||
outer->addWidget(settingsButton);
|
||||
bottomRow->addWidget(settingsButton);
|
||||
outer->addLayout(bottomRow);
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
QWidget *MainWindow::buildManagePage()
|
||||
{
|
||||
auto *page = new QWidget;
|
||||
auto *outer = new QVBoxLayout(page);
|
||||
outer->setContentsMargins(24, 24, 24, 24);
|
||||
|
||||
auto *header = new QHBoxLayout;
|
||||
auto *title = new QLabel(QStringLiteral("<b>Manage posts</b>"));
|
||||
header->addWidget(title);
|
||||
header->addStretch();
|
||||
m_manageRefresh = new QPushButton(QStringLiteral("Refresh"));
|
||||
connect(m_manageRefresh, &QPushButton::clicked, this, &MainWindow::loadManage);
|
||||
header->addWidget(m_manageRefresh);
|
||||
auto *back = new QPushButton(QStringLiteral("Back"));
|
||||
connect(back, &QPushButton::clicked, this, &MainWindow::showPost);
|
||||
header->addWidget(back);
|
||||
outer->addLayout(header);
|
||||
|
||||
m_manageStatus = new QLabel;
|
||||
m_manageStatus->setWordWrap(true);
|
||||
outer->addWidget(m_manageStatus);
|
||||
|
||||
auto *scroll = new QScrollArea;
|
||||
scroll->setWidgetResizable(true);
|
||||
auto *container = new QWidget;
|
||||
m_manageLayout = new QVBoxLayout(container);
|
||||
m_manageLayout->setAlignment(Qt::AlignTop);
|
||||
m_manageEmptyHint = new QLabel(QStringLiteral("No posts."));
|
||||
m_manageEmptyHint->setAlignment(Qt::AlignCenter);
|
||||
m_manageLayout->addWidget(m_manageEmptyHint);
|
||||
scroll->setWidget(container);
|
||||
outer->addWidget(scroll, 1);
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -139,9 +190,21 @@ void MainWindow::showPost()
|
||||
} else {
|
||||
m_lastPost->clear();
|
||||
}
|
||||
refreshTags();
|
||||
m_stack->setCurrentIndex(1);
|
||||
}
|
||||
|
||||
void MainWindow::showManage()
|
||||
{
|
||||
if (!m_settings.isConfigured()) {
|
||||
showSettings();
|
||||
return;
|
||||
}
|
||||
m_stack->setCurrentIndex(2);
|
||||
refreshTags();
|
||||
loadManage();
|
||||
}
|
||||
|
||||
void MainWindow::saveAndTest()
|
||||
{
|
||||
// Concurrency is not a credential; persist it regardless of URL validity.
|
||||
@@ -162,6 +225,13 @@ void MainWindow::saveAndTest()
|
||||
m_settings.writePassword(m_password->text());
|
||||
m_baseUrl->setText(m_settings.baseUrl());
|
||||
|
||||
// The manage client caches the credentials it was built with; drop it so
|
||||
// the next manage action rebuilds with the ones just saved.
|
||||
if (m_manageClient) {
|
||||
m_manageClient->deleteLater();
|
||||
m_manageClient = nullptr;
|
||||
}
|
||||
|
||||
m_saveButton->setEnabled(false);
|
||||
m_settingsStatus->setText(QStringLiteral("Testing…"));
|
||||
|
||||
@@ -192,6 +262,7 @@ void MainWindow::addItem(const QString &path)
|
||||
{
|
||||
m_emptyHint->hide();
|
||||
auto *item = new PhotoItemWidget(path);
|
||||
item->setTagSuggestions(m_tagSuggestions);
|
||||
connect(item, &PhotoItemWidget::removeRequested, this, &MainWindow::removeItem);
|
||||
m_items.append(item);
|
||||
m_itemsLayout->addWidget(item);
|
||||
@@ -359,3 +430,172 @@ void MainWindow::refreshPosting()
|
||||
m_postSummary->setText(summary);
|
||||
}
|
||||
}
|
||||
|
||||
// --- Manage page -----------------------------------------------------------
|
||||
|
||||
UploadClient *MainWindow::manageClient()
|
||||
{
|
||||
if (!m_manageClient) {
|
||||
m_manageClient = new UploadClient(m_settings.baseUrl(), m_settings.username(),
|
||||
m_settings.readPassword(), this);
|
||||
|
||||
// Thumbnails arrive one per request; route each to its row by slug.
|
||||
connect(m_manageClient, &UploadClient::thumbnailReady, this,
|
||||
[this](const QString &slug, const QImage &image) {
|
||||
for (ManagePostWidget *w : std::as_const(m_manageItems))
|
||||
if (w->slug() == slug)
|
||||
w->setThumbnail(image);
|
||||
});
|
||||
|
||||
// The tag list feeds both the post cards and the edit dialog.
|
||||
connect(m_manageClient, &UploadClient::tagsFetched, this,
|
||||
[this](const QStringList &tags) {
|
||||
if (tags.isEmpty())
|
||||
return; // best-effort; keep whatever we had
|
||||
m_tagSuggestions = tags;
|
||||
applyTagSuggestions();
|
||||
});
|
||||
}
|
||||
return m_manageClient;
|
||||
}
|
||||
|
||||
void MainWindow::refreshTags()
|
||||
{
|
||||
if (m_settings.isConfigured())
|
||||
manageClient()->fetchTags();
|
||||
}
|
||||
|
||||
void MainWindow::applyTagSuggestions()
|
||||
{
|
||||
for (PhotoItemWidget *item : std::as_const(m_items))
|
||||
item->setTagSuggestions(m_tagSuggestions);
|
||||
}
|
||||
|
||||
void MainWindow::clearManageItems()
|
||||
{
|
||||
for (ManagePostWidget *item : std::as_const(m_manageItems))
|
||||
item->deleteLater();
|
||||
m_manageItems.clear();
|
||||
m_manageEmptyHint->show();
|
||||
}
|
||||
|
||||
void MainWindow::loadManage()
|
||||
{
|
||||
if (!m_settings.isConfigured()) {
|
||||
m_manageStatus->setText(QStringLiteral("Set the server URL and credentials first"));
|
||||
return;
|
||||
}
|
||||
m_manageStatus->setText(QStringLiteral("Loading…"));
|
||||
m_manageRefresh->setEnabled(false);
|
||||
|
||||
connect(
|
||||
manageClient(), &UploadClient::photosListed, this,
|
||||
[this](const QList<RemotePhoto> &photos, const QString &error) {
|
||||
m_manageRefresh->setEnabled(true);
|
||||
clearManageItems();
|
||||
if (!error.isEmpty()) {
|
||||
m_manageStatus->setText(error);
|
||||
return;
|
||||
}
|
||||
m_manageStatus->clear();
|
||||
if (photos.isEmpty())
|
||||
return;
|
||||
m_manageEmptyHint->hide();
|
||||
for (const RemotePhoto &p : photos) {
|
||||
auto *item = new ManagePostWidget(p.slug, p.caption, p.date, p.tags,
|
||||
m_settings.baseUrl() + p.url);
|
||||
connect(item, &ManagePostWidget::editRequested, this, &MainWindow::editItem);
|
||||
connect(item, &ManagePostWidget::deleteRequested, this, &MainWindow::deleteItem);
|
||||
m_manageItems.append(item);
|
||||
m_manageLayout->addWidget(item);
|
||||
manageClient()->fetchThumbnail(p.slug);
|
||||
}
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
manageClient()->listPhotos();
|
||||
}
|
||||
|
||||
void MainWindow::editItem(ManagePostWidget *item)
|
||||
{
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle(QStringLiteral("Edit %1").arg(item->slug()));
|
||||
auto *layout = new QVBoxLayout(&dialog);
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
auto *caption = new QLineEdit(item->caption());
|
||||
auto *date = new QDateEdit(QDate::fromString(item->date(), QStringLiteral("yyyy-MM-dd")));
|
||||
date->setCalendarPopup(true);
|
||||
date->setDisplayFormat(QStringLiteral("yyyy-MM-dd"));
|
||||
if (!date->date().isValid())
|
||||
date->setDate(QDate::currentDate());
|
||||
auto *tags = new QLineEdit(item->tags().join(QStringLiteral(", ")));
|
||||
auto *completer = new TagCompleter(&dialog);
|
||||
completer->setSuggestions(m_tagSuggestions);
|
||||
tags->setCompleter(completer);
|
||||
form->addRow(QStringLiteral("Caption"), caption);
|
||||
form->addRow(QStringLiteral("Date"), date);
|
||||
form->addRow(QStringLiteral("Tags"), tags);
|
||||
layout->addLayout(form);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel);
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
layout->addWidget(buttons);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted)
|
||||
return;
|
||||
|
||||
const QString newCaption = caption->text();
|
||||
const QString newDate = date->date().toString(QStringLiteral("yyyy-MM-dd"));
|
||||
const QString newTags = tags->text();
|
||||
|
||||
item->setBusy(true);
|
||||
connect(
|
||||
manageClient(), &UploadClient::editFinished, this,
|
||||
[this, item, newCaption, newDate, newTags](const QString &slug, const QString &error) {
|
||||
item->setBusy(false);
|
||||
if (!error.isEmpty()) {
|
||||
m_manageStatus->setText(QStringLiteral("Edit failed: %1").arg(error));
|
||||
return;
|
||||
}
|
||||
QStringList tagList;
|
||||
const auto parts = newTags.split(QLatin1Char(','), Qt::SkipEmptyParts);
|
||||
for (const QString &t : parts) {
|
||||
const QString trimmed = t.trimmed();
|
||||
if (!trimmed.isEmpty())
|
||||
tagList << trimmed;
|
||||
}
|
||||
item->applyEdit(newCaption, newDate, tagList);
|
||||
m_manageStatus->setText(QStringLiteral("Saved %1").arg(slug));
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
manageClient()->editPost(item->slug(), newCaption, newDate, newTags);
|
||||
}
|
||||
|
||||
void MainWindow::deleteItem(ManagePostWidget *item)
|
||||
{
|
||||
const QString name = item->caption().isEmpty() ? item->slug() : item->caption();
|
||||
const auto choice = QMessageBox::question(
|
||||
this, QStringLiteral("Move to trash?"),
|
||||
QStringLiteral("\"%1\" will be moved to the server's trash.").arg(name));
|
||||
if (choice != QMessageBox::Yes)
|
||||
return;
|
||||
|
||||
item->setBusy(true);
|
||||
connect(
|
||||
manageClient(), &UploadClient::deleteFinished, this,
|
||||
[this, item](const QString &, const QString &error) {
|
||||
if (!error.isEmpty()) {
|
||||
item->setBusy(false);
|
||||
m_manageStatus->setText(QStringLiteral("Delete failed: %1").arg(error));
|
||||
return;
|
||||
}
|
||||
m_manageItems.removeOne(item);
|
||||
item->deleteLater();
|
||||
m_manageStatus->setText(QStringLiteral("Moved to trash"));
|
||||
if (m_manageItems.isEmpty())
|
||||
m_manageEmptyHint->show();
|
||||
},
|
||||
Qt::SingleShotConnection);
|
||||
manageClient()->deletePost(item->slug());
|
||||
}
|
||||
Reference in new issue
Block a user