feat: Native previews for video, audio, and PDF files

Use presigned R2 URLs with native HTML elements instead of generic
icons for all previewable file types on the download page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-23 11:16:18 -07:00
parent 18442a7564
commit e938f6d9a9
3 changed files with 28 additions and 29 deletions

View File

@ -57,15 +57,11 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
} }
} }
// Determine thumbnail type // Determine preview type and generate presigned URL for inline rendering
thumbType := "" // "image", "video", "audio", "pdf", or "" thumbType := "" // "image", "video", "audio", "pdf", or ""
thumbnailURL := "" previewURL := ""
if strings.HasPrefix(rec.ContentType, "image/") { if strings.HasPrefix(rec.ContentType, "image/") {
thumbType = "image" thumbType = "image"
// Generate inline presigned URL for the image thumbnail
if url, err := h.r2.PresignGet(r.Context(), rec.R2Key, rec.Filename, true); err == nil {
thumbnailURL = url
}
} else if strings.HasPrefix(rec.ContentType, "video/") { } else if strings.HasPrefix(rec.ContentType, "video/") {
thumbType = "video" thumbType = "video"
} else if strings.HasPrefix(rec.ContentType, "audio/") { } else if strings.HasPrefix(rec.ContentType, "audio/") {
@ -73,6 +69,11 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
} else if rec.ContentType == "application/pdf" { } else if rec.ContentType == "application/pdf" {
thumbType = "pdf" thumbType = "pdf"
} }
if thumbType != "" {
if url, err := h.r2.PresignGet(r.Context(), rec.R2Key, rec.Filename, true); err == nil {
previewURL = url
}
}
data := map[string]any{ data := map[string]any{
"ID": rec.ID, "ID": rec.ID,
@ -84,8 +85,8 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
"Previewable": isPreviewable(rec.ContentType), "Previewable": isPreviewable(rec.ContentType),
"ViewURL": "/f/" + id + "/view", "ViewURL": "/f/" + id + "/view",
"DownloadURL": "/f/" + id + "/dl", "DownloadURL": "/f/" + id + "/dl",
"ThumbType": thumbType, "ThumbType": thumbType,
"ThumbnailURL": thumbnailURL, "PreviewURL": previewURL,
} }
if rec.ExpiresAt != nil { if rec.ExpiresAt != nil {
data["ExpiresAt"] = rec.ExpiresAt.Format("Jan 2, 2006 at 3:04 PM") data["ExpiresAt"] = rec.ExpiresAt.Format("Jan 2, 2006 at 3:04 PM")

View File

@ -11,30 +11,13 @@
<div class="file-card"> <div class="file-card">
<div class="file-preview"> <div class="file-preview">
{{if eq .ThumbType "image"}} {{if eq .ThumbType "image"}}
<a href="{{.ViewURL}}"><img src="{{.ThumbnailURL}}" alt="{{.Filename}}" class="thumbnail"></a> <a href="{{.ViewURL}}"><img src="{{.PreviewURL}}" alt="{{.Filename}}" class="thumbnail"></a>
{{else if eq .ThumbType "video"}} {{else if eq .ThumbType "video"}}
<div class="thumb-icon"> <video src="{{.PreviewURL}}" class="thumbnail" preload="metadata" controls muted playsinline></video>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</div>
{{else if eq .ThumbType "audio"}} {{else if eq .ThumbType "audio"}}
<div class="thumb-icon"> <audio src="{{.PreviewURL}}" class="audio-preview" preload="metadata" controls></audio>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M9 18V5l12-2v13"/>
<circle cx="6" cy="18" r="3"/>
<circle cx="18" cy="16" r="3"/>
</svg>
</div>
{{else if eq .ThumbType "pdf"}} {{else if eq .ThumbType "pdf"}}
<div class="thumb-icon"> <iframe src="{{.PreviewURL}}" class="pdf-preview" title="{{.Filename}}"></iframe>
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
</svg>
</div>
{{else}} {{else}}
<div class="thumb-icon"> <div class="thumb-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"> <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">

View File

@ -277,6 +277,21 @@ footer a:hover { color: var(--accent); }
margin: 0 auto; margin: 0 auto;
} }
.audio-preview {
width: 100%;
border-radius: 8px;
background: var(--bg);
padding: 1rem;
}
.pdf-preview {
width: 100%;
height: 360px;
border: none;
border-radius: 8px;
background: var(--bg);
}
.thumb-icon { .thumb-icon {
display: flex; display: flex;
align-items: center; align-items: center;