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 ---