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 ""
thumbnailURL := ""
previewURL := ""
if strings.HasPrefix(rec.ContentType, "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/") {
thumbType = "video"
} 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" {
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{
"ID": rec.ID,
@ -84,8 +85,8 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
"Previewable": isPreviewable(rec.ContentType),
"ViewURL": "/f/" + id + "/view",
"DownloadURL": "/f/" + id + "/dl",
"ThumbType": thumbType,
"ThumbnailURL": thumbnailURL,
"ThumbType": thumbType,
"PreviewURL": previewURL,
}
if rec.ExpiresAt != nil {
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-preview">
{{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"}}
<div class="thumb-icon">
<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>
<video src="{{.PreviewURL}}" class="thumbnail" preload="metadata" controls muted playsinline></video>
{{else if eq .ThumbType "audio"}}
<div class="thumb-icon">
<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>
<audio src="{{.PreviewURL}}" class="audio-preview" preload="metadata" controls></audio>
{{else if eq .ThumbType "pdf"}}
<div class="thumb-icon">
<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>
<iframe src="{{.PreviewURL}}" class="pdf-preview" title="{{.Filename}}"></iframe>
{{else}}
<div class="thumb-icon">
<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;
}
.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 {
display: flex;
align-items: center;