From 44aed00b5e9c181a4862eb13ed6a43a66bd1acfa Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Fri, 9 Jan 2026 03:03:35 +0000
Subject: [PATCH] Add thumbnail upload support to UploadModal and R2Service
- Added file input and browse button for thumbnails in `upload-modal.html`.
- Updated `UploadModal.js` to handle thumbnail file selection and UI toggling.
- Updated `r2Service.js` to upload thumbnail file (if present) to R2 and update metadata.
- Ensured thumbnail keys are derived from video keys for consistent naming.
---
components/upload-modal.html | 13 +++++++++++-
js/services/r2Service.js | 28 +++++++++++++++++++++++++
js/ui/components/UploadModal.js | 36 +++++++++++++++++++++++++++++++++
3 files changed, 76 insertions(+), 1 deletion(-)
diff --git a/components/upload-modal.html b/components/upload-modal.html
index 70273174..4de81d6e 100644
--- a/components/upload-modal.html
+++ b/components/upload-modal.html
@@ -199,7 +199,18 @@
diff --git a/js/services/r2Service.js b/js/services/r2Service.js
index 65612bb8..55b40593 100644
--- a/js/services/r2Service.js
+++ b/js/services/r2Service.js
@@ -636,6 +636,7 @@ class R2Service {
async handleCloudflareUploadSubmit({
npub = "",
file = null,
+ thumbnailFile = null,
metadata = {},
settingsInput = null,
publishVideoNote,
@@ -748,6 +749,33 @@ class R2Service {
try {
const s3 = makeR2Client({ accountId, accessKeyId, secretAccessKey });
+ if (thumbnailFile) {
+ this.setCloudflareUploadStatus("Uploading thumbnail...", "info");
+ const thumbExt = thumbnailFile.name.split('.').pop() || 'jpg';
+ const thumbKey = key.replace(/\.[^/.]+$/, "") + `.thumb.${thumbExt}`;
+
+ try {
+ await multipartUpload({
+ s3,
+ bucket: bucketEntry.bucket,
+ key: thumbKey,
+ file: thumbnailFile,
+ contentType: thumbnailFile.type || "image/jpeg",
+ });
+ const thumbUrl = buildPublicUrl(bucketEntry.publicBaseUrl, thumbKey);
+ if (typeof metadata === "object") {
+ metadata.thumbnail = thumbUrl;
+ }
+ } catch (err) {
+ userLogger.warn("Thumbnail upload failed, continuing with video...", err);
+ }
+ }
+
+ this.setCloudflareUploadStatus(
+ statusMessage,
+ bucketResult?.usedManagedFallback ? "warning" : "info"
+ );
+
await multipartUpload({
s3,
bucket: bucketEntry.bucket,
diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js
index 5bb4e610..81a59654 100644
--- a/js/ui/components/UploadModal.js
+++ b/js/ui/components/UploadModal.js
@@ -146,6 +146,7 @@ export class UploadModal {
title: $("#input-title"),
description: $("#input-description"),
thumbnail: $("#input-thumbnail"),
+ thumbnailFile: $("#input-thumbnail-file"),
file: $("#input-file"),
url: $("#input-url"),
magnet: $("#input-magnet"),
@@ -180,6 +181,7 @@ export class UploadModal {
storageSettings: $("#btn-storage-settings"),
r2Advanced: $("#btn-r2-advanced"),
saveSettings: $("#btn-save-settings"),
+ browseThumbnail: $("#btn-thumbnail-file"),
};
// Status text
@@ -217,6 +219,7 @@ export class UploadModal {
this.setupDescriptionMirror();
this.setupMutuallyExclusiveCheckboxes(this.toggles.nsfw, this.toggles.kids);
this.setupNsfwToContentWarning();
+ this.setupThumbnailInput();
// Close
this.closeButton.addEventListener("click", () => this.close());
@@ -329,6 +332,27 @@ export class UploadModal {
});
}
+ setupThumbnailInput() {
+ const { thumbnailFile, thumbnail } = this.inputs;
+ const { browseThumbnail } = this.toggles;
+
+ if (!thumbnailFile || !browseThumbnail) return;
+
+ browseThumbnail.addEventListener("click", () => thumbnailFile.click());
+
+ thumbnailFile.addEventListener("change", () => {
+ const file = thumbnailFile.files?.[0];
+ if (file) {
+ thumbnail.value = ""; // Clear explicit URL
+ thumbnail.placeholder = `Selected: ${file.name}`;
+ thumbnail.disabled = true;
+ } else {
+ thumbnail.placeholder = "https://example.com/thumbnail.jpg";
+ thumbnail.disabled = false;
+ }
+ });
+ }
+
// --- R2 Integration ---
async loadR2Settings() {
@@ -481,6 +505,8 @@ export class UploadModal {
const file = this.inputs.file?.files?.[0];
if (!file) throw new Error("Please select a video file to upload.");
+ const thumbnailFile = this.inputs.thumbnailFile?.files?.[0];
+
if (!this.hasValidR2Settings()) throw new Error("Please configure R2 storage credentials.");
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
@@ -491,6 +517,7 @@ export class UploadModal {
await this.r2Service.uploadVideo({
npub,
file,
+ thumbnailFile,
metadata,
settingsInput: this.collectSettingsForm(),
publishVideoNote: this.publishVideoNote,
@@ -546,6 +573,15 @@ export class UploadModal {
this.toggles.nsfw.checked = false;
this.toggles.kids.checked = false;
this.updateProgress(null);
+
+ // Reset thumbnail UI
+ if (this.inputs.thumbnail) {
+ this.inputs.thumbnail.disabled = false;
+ this.inputs.thumbnail.placeholder = "https://example.com/thumbnail.jpg";
+ }
+ if (this.inputs.thumbnailFile) {
+ this.inputs.thumbnailFile.value = "";
+ }
}
// --- Modal Control ---