feat: Add file thumbnails and fix download page layout

- Images show actual thumbnail from R2 (presigned inline URL)
- Video/audio/PDF show type-specific icons
- Use flexbox gap for consistent spacing (no overlap)
- Move filename into card as h2, remove top-level h1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-23 10:47:44 -07:00
parent df182ca09f
commit 9ab1cb3349
3 changed files with 93 additions and 25 deletions

View File

@ -57,16 +57,35 @@ func (h *Handler) Download(w http.ResponseWriter, r *http.Request) {
}
}
// Determine thumbnail type
thumbType := "" // "image", "video", "audio", "pdf", or ""
thumbnailURL := ""
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/") {
thumbType = "audio"
} else if rec.ContentType == "application/pdf" {
thumbType = "pdf"
}
data := map[string]any{
"ID": rec.ID,
"Filename": rec.Filename,
"Size": rec.SizeBytes,
"ContentType": rec.ContentType,
"UploadedAt": rec.UploadedAt.Format("Jan 2, 2006 at 3:04 PM"),
"Downloads": rec.DownloadCount,
"Previewable": isPreviewable(rec.ContentType),
"ViewURL": "/f/" + id + "/view",
"DownloadURL": "/f/" + id + "/dl",
"ID": rec.ID,
"Filename": rec.Filename,
"Size": rec.SizeBytes,
"ContentType": rec.ContentType,
"UploadedAt": rec.UploadedAt.Format("Jan 2, 2006 at 3:04 PM"),
"Downloads": rec.DownloadCount,
"Previewable": isPreviewable(rec.ContentType),
"ViewURL": "/f/" + id + "/view",
"DownloadURL": "/f/" + id + "/dl",
"ThumbType": thumbType,
"ThumbnailURL": thumbnailURL,
}
if rec.ExpiresAt != nil {
data["ExpiresAt"] = rec.ExpiresAt.Format("Jan 2, 2006 at 3:04 PM")

View File

@ -8,23 +8,45 @@
</head>
<body>
<div class="container">
<h1>{{.Filename}}</h1>
<div class="file-card">
<div class="file-icon">
{{if .Previewable}}
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
<div class="file-preview">
{{if eq .ThumbType "image"}}
<a href="{{.ViewURL}}"><img src="{{.ThumbnailURL}}" 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>
{{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>
{{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>
{{else}}
<svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
<polyline points="13 2 13 9 20 9"/>
</svg>
<div class="thumb-icon">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
<polyline points="13 2 13 9 20 9"/>
</svg>
</div>
{{end}}
</div>
<h2 class="file-title">{{.Filename}}</h2>
<div class="file-meta">
<div class="meta-row">
<span class="meta-label">Size</span>

View File

@ -224,24 +224,51 @@ footer a:hover { color: var(--accent); }
/* Download page */
.file-card {
margin-top: 1.5rem;
margin-top: 0;
padding: 1.5rem;
background: var(--surface);
border-radius: 12px;
border: 1px solid var(--border);
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.file-icon {
.file-preview {
text-align: center;
}
.thumbnail {
max-width: 100%;
max-height: 280px;
border-radius: 8px;
object-fit: contain;
background: var(--bg);
display: block;
margin: 0 auto;
}
.thumb-icon {
display: flex;
align-items: center;
justify-content: center;
height: 120px;
color: var(--text-dim);
margin-bottom: 1.25rem;
background: var(--bg);
border-radius: 8px;
}
.file-title {
font-size: 1rem;
font-weight: 600;
word-break: break-all;
line-height: 1.4;
}
.file-meta {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-bottom: 1.25rem;
}
.meta-row {