diff --git a/android/app/src/main/java/ws/inflo/photogallery/MainActivity.kt b/android/app/src/main/java/ws/inflo/photogallery/MainActivity.kt index fdfac9e..be34219 100644 --- a/android/app/src/main/java/ws/inflo/photogallery/MainActivity.kt +++ b/android/app/src/main/java/ws/inflo/photogallery/MainActivity.kt @@ -81,6 +81,7 @@ class MainActivity : ComponentActivity() { ) }, onCaptionChange = viewModel::setCaption, + onTagsChange = viewModel::setTags, onDateChange = viewModel::setDate, onRemove = viewModel::removeItem, onSubmit = viewModel::submit, diff --git a/android/app/src/main/java/ws/inflo/photogallery/net/UploadClient.kt b/android/app/src/main/java/ws/inflo/photogallery/net/UploadClient.kt index 7b23cc5..2bf28d1 100644 --- a/android/app/src/main/java/ws/inflo/photogallery/net/UploadClient.kt +++ b/android/app/src/main/java/ws/inflo/photogallery/net/UploadClient.kt @@ -60,6 +60,7 @@ class UploadClient( fileName: String, caption: String, date: String, + tags: String, ): UploadOutcome { val mediaType = if (fileName.endsWith(".png", ignoreCase = true)) { "image/png".toMediaType() @@ -72,6 +73,7 @@ class UploadClient( .addFormDataPart("image", fileName, file.asRequestBody(mediaType)) .addFormDataPart("caption", caption) .addFormDataPart("date", date) + .addFormDataPart("tags", tags) .build() val request = Request.Builder() diff --git a/android/app/src/main/java/ws/inflo/photogallery/ui/PostScreen.kt b/android/app/src/main/java/ws/inflo/photogallery/ui/PostScreen.kt index 60df797..64dc993 100644 --- a/android/app/src/main/java/ws/inflo/photogallery/ui/PostScreen.kt +++ b/android/app/src/main/java/ws/inflo/photogallery/ui/PostScreen.kt @@ -51,6 +51,7 @@ fun PostScreen( state: PostUiState, onPickImages: () -> Unit, onCaptionChange: (id: String, value: String) -> Unit, + onTagsChange: (id: String, value: String) -> Unit, onDateChange: (id: String, value: LocalDate) -> Unit, onRemove: (id: String) -> Unit, onSubmit: () -> Unit, @@ -107,6 +108,7 @@ fun PostScreen( ItemCard( item = item, onCaptionChange = { onCaptionChange(item.id, it) }, + onTagsChange = { onTagsChange(item.id, it) }, onEditDate = { dateEditFor = item }, onRemove = { onRemove(item.id) }, onOpenUrl = onOpenUrl, @@ -185,6 +187,7 @@ fun PostScreen( private fun ItemCard( item: PickedItem, onCaptionChange: (String) -> Unit, + onTagsChange: (String) -> Unit, onEditDate: () -> Unit, onRemove: () -> Unit, onOpenUrl: (String) -> Unit, @@ -220,6 +223,16 @@ private fun ItemCard( modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(6.dp)) + OutlinedTextField( + value = item.tags, + onValueChange = onTagsChange, + label = { Text("Tags") }, + placeholder = { Text("comma-separated") }, + singleLine = true, + enabled = item.status !is ItemStatus.Uploading, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(6.dp)) Row(verticalAlignment = Alignment.CenterVertically) { OutlinedButton( onClick = onEditDate, diff --git a/android/app/src/main/java/ws/inflo/photogallery/ui/UploadViewModel.kt b/android/app/src/main/java/ws/inflo/photogallery/ui/UploadViewModel.kt index 2bf4d37..182a57c 100644 --- a/android/app/src/main/java/ws/inflo/photogallery/ui/UploadViewModel.kt +++ b/android/app/src/main/java/ws/inflo/photogallery/ui/UploadViewModel.kt @@ -41,6 +41,7 @@ data class PickedItem( val source: Uri, val preview: Bitmap? = null, val caption: String = "", + val tags: String = "", // raw comma-separated; the server normalizes val date: LocalDate = LocalDate.now(), val status: ItemStatus = ItemStatus.Pending, ) @@ -114,6 +115,10 @@ class UploadViewModel(app: Application) : AndroidViewModel(app) { updateItem(id) { it.copy(caption = value) } } + fun setTags(id: String, value: String) { + updateItem(id) { it.copy(tags = value) } + } + fun setDate(id: String, value: LocalDate) { updateItem(id) { it.copy(date = value) } } @@ -159,6 +164,7 @@ class UploadViewModel(app: Application) : AndroidViewModel(app) { uri = Uri.fromFile(staged), caption = current.caption, date = current.date, + tags = current.tags, uniqueSuffix = item.id.take(6), ) observe(item.id, workId) diff --git a/android/app/src/main/java/ws/inflo/photogallery/work/UploadQueue.kt b/android/app/src/main/java/ws/inflo/photogallery/work/UploadQueue.kt index a35fae3..dbde648 100644 --- a/android/app/src/main/java/ws/inflo/photogallery/work/UploadQueue.kt +++ b/android/app/src/main/java/ws/inflo/photogallery/work/UploadQueue.kt @@ -34,6 +34,7 @@ object UploadQueue { uri: Uri, caption: String, date: LocalDate, + tags: String, uniqueSuffix: String, ): UUID { val slug = LocalDateTime.now().format(SLUG_FORMAT) + "-" + uniqueSuffix @@ -45,6 +46,7 @@ object UploadQueue { .putString(UploadWorker.KEY_SLUG, slug) .putString(UploadWorker.KEY_CAPTION, caption) .putString(UploadWorker.KEY_DATE, date.format(ISO_DATE)) + .putString(UploadWorker.KEY_TAGS, tags) .build(), ) .setConstraints( diff --git a/android/app/src/main/java/ws/inflo/photogallery/work/UploadWorker.kt b/android/app/src/main/java/ws/inflo/photogallery/work/UploadWorker.kt index 70837c3..47e08be 100644 --- a/android/app/src/main/java/ws/inflo/photogallery/work/UploadWorker.kt +++ b/android/app/src/main/java/ws/inflo/photogallery/work/UploadWorker.kt @@ -38,6 +38,7 @@ class UploadWorker( val slug = inputData.getString(KEY_SLUG) ?: return@withContext Result.failure() val caption = inputData.getString(KEY_CAPTION).orEmpty() val date = inputData.getString(KEY_DATE).orEmpty() + val tags = inputData.getString(KEY_TAGS).orEmpty() val settings = SettingsStore(applicationContext) if (!settings.isConfigured) { @@ -66,6 +67,7 @@ class UploadWorker( fileName = prepared.file.name, caption = caption, date = date, + tags = tags, ) // Keep the staged and prepared copies only while a retry might still @@ -147,6 +149,7 @@ class UploadWorker( const val KEY_SLUG = "slug" const val KEY_CAPTION = "caption" const val KEY_DATE = "date" + const val KEY_TAGS = "tags" const val KEY_RESULT_SLUG = "result_slug" const val KEY_RESULT_URL = "result_url" diff --git a/desktop/mainwindow.cpp b/desktop/mainwindow.cpp index 0cfb829..7fbd453 100644 --- a/desktop/mainwindow.cpp +++ b/desktop/mainwindow.cpp @@ -313,7 +313,7 @@ void MainWindow::startItem(PhotoItemWidget *item) refreshPosting(); }); client->upload(prepared.filePath, QFileInfo(prepared.filePath).fileName(), - caption, item->date()); + caption, item->date(), item->tags()); }); watcher->setFuture(QtConcurrent::run([source, cacheDir, slug]() { return ImagePrep::prepare(source, cacheDir, slug); diff --git a/desktop/photoitemwidget.cpp b/desktop/photoitemwidget.cpp index 1943d0c..ab7230d 100644 --- a/desktop/photoitemwidget.cpp +++ b/desktop/photoitemwidget.cpp @@ -47,6 +47,10 @@ PhotoItemWidget::PhotoItemWidget(QString sourcePath, QWidget *parent) m_date->setDisplayFormat(QStringLiteral("yyyy-MM-dd")); fields->addWidget(m_date); + m_tags = new QLineEdit; + m_tags->setPlaceholderText(QStringLiteral("tags (comma-separated)")); + fields->addWidget(m_tags); + m_statusLabel = new QLabel; m_statusLabel->setOpenExternalLinks(true); m_statusLabel->setWordWrap(true); @@ -70,6 +74,11 @@ QString PhotoItemWidget::date() const return m_date->date().toString(QStringLiteral("yyyy-MM-dd")); } +QString PhotoItemWidget::tags() const +{ + return m_tags->text(); +} + void PhotoItemWidget::setStatus(Status status, const QString &message, const QString &url) { m_status = status; @@ -79,6 +88,7 @@ void PhotoItemWidget::setStatus(Status status, const QString &message, const QSt const bool editable = status == Status::Pending || status == Status::Error; m_caption->setEnabled(editable); m_date->setEnabled(editable); + m_tags->setEnabled(editable); m_remove->setEnabled(status != Status::Preparing && status != Status::Uploading); switch (status) { diff --git a/desktop/photoitemwidget.h b/desktop/photoitemwidget.h index 21f7023..da6a007 100644 --- a/desktop/photoitemwidget.h +++ b/desktop/photoitemwidget.h @@ -21,6 +21,7 @@ public: QString sourcePath() const { return m_sourcePath; } QString caption() const; QString date() const; // yyyy-MM-dd + QString tags() const; // raw comma-separated; the server normalizes Status status() const { return m_status; } void setStatus(Status status, const QString &message = {}, const QString &url = {}); @@ -35,6 +36,7 @@ private: QLabel *m_thumb = nullptr; QLineEdit *m_caption = nullptr; QDateEdit *m_date = nullptr; + QLineEdit *m_tags = nullptr; QLabel *m_statusLabel = nullptr; QPushButton *m_remove = nullptr; }; diff --git a/desktop/uploadclient.cpp b/desktop/uploadclient.cpp index 9f2d515..118460b 100644 --- a/desktop/uploadclient.cpp +++ b/desktop/uploadclient.cpp @@ -26,7 +26,7 @@ QByteArray UploadClient::authHeader() const } void UploadClient::upload(const QString &filePath, const QString &fileName, - const QString &caption, const QString &date) + const QString &caption, const QString &date, const QString &tags) { auto *file = new QFile(filePath); if (!file->open(QIODevice::ReadOnly)) { @@ -57,6 +57,7 @@ void UploadClient::upload(const QString &filePath, const QString &fileName, }; multiPart->append(textPart(QStringLiteral("caption"), caption)); multiPart->append(textPart(QStringLiteral("date"), date)); + multiPart->append(textPart(QStringLiteral("tags"), tags)); QNetworkRequest request(QUrl(m_base + QStringLiteral("/upload"))); request.setRawHeader("Authorization", authHeader()); diff --git a/desktop/uploadclient.h b/desktop/uploadclient.h index 79ab7af..8bd3247 100644 --- a/desktop/uploadclient.h +++ b/desktop/uploadclient.h @@ -29,7 +29,7 @@ public: // 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 &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.