Add support for multiple images from Twitter links

- Fetch all images from Twitter syndication API (photos array)
- Store images with unified 'image' media type (first = thumbnail by ID order)
- Display multi-image tweets in grid layout on detail page
- Add hover cycling through images on home grid (2s interval)
- Show image count indicator on multi-image items
- Extract shared downloadAndStoreImages() helper for create/refresh
- Add migration to convert existing thumbnail/gallery types to image
- Make images clickable to open in new tab on detail page
This commit is contained in:
soup 2026-01-17 13:28:20 -05:00
parent e917e67930
commit 007e167707
Signed by: soup
SSH key fingerprint: SHA256:GYxje8eQkJ6HZKzVWDdyOUF1TyDiprruGhE0Ym8qYDY
9 changed files with 625 additions and 403 deletions

View file

@ -31,6 +31,7 @@ type itemPageData struct {
ThumbnailID *int64
MediaID *int64
MediaIsVideo bool
GalleryIDs []int64 // Additional images for multi-image embeds
}
func (c itemPageContent) Render(sw *ssr.Writer) error {
@ -60,22 +61,27 @@ func (c itemPageContent) Render(sw *ssr.Writer) error {
<span class="play-button">&#9654;</span>
</div>
</div>
{{else if .Item.GalleryIDs}}
<div class="image-gallery">
{{if .Item.ThumbnailID}}<a href="/media/{{.Item.ThumbnailID}}" target="_blank"><img src="/media/{{.Item.ThumbnailID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Image{{end}}"></a>{{end}}
{{range .Item.GalleryIDs}}<a href="/media/{{.}}" target="_blank"><img src="/media/{{.}}" alt="Image"></a>{{end}}
</div>
{{else if .Item.ThumbnailID}}
<div class="image-container">
<img src="/media/{{.Item.ThumbnailID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Embed{{end}}">
<a href="/media/{{.Item.ThumbnailID}}" target="_blank"><img src="/media/{{.Item.ThumbnailID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Embed{{end}}"></a>
</div>
{{else if .Item.MediaID}}
<div class="image-container">
<img src="/media/{{.Item.MediaID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Embed{{end}}">
<a href="/media/{{.Item.MediaID}}" target="_blank"><img src="/media/{{.Item.MediaID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Embed{{end}}"></a>
</div>
{{end}}
{{else if .Item.MediaID}}
<div class="image-container">
<img src="/media/{{.Item.MediaID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Image{{end}}">
<a href="/media/{{.Item.MediaID}}" target="_blank"><img src="/media/{{.Item.MediaID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Image{{end}}"></a>
</div>
{{else if .Item.ThumbnailID}}
<div class="image-container">
<img src="/media/{{.Item.ThumbnailID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Image{{end}}">
<a href="/media/{{.Item.ThumbnailID}}" target="_blank"><img src="/media/{{.Item.ThumbnailID}}" alt="{{if .Item.Title}}{{.Item.Title}}{{else}}Image{{end}}"></a>
</div>
{{end}}
@ -173,18 +179,26 @@ func HandleItemPage(rc *RequestContext, w http.ResponseWriter, r *http.Request)
}
// Get media
// Media is ordered by ID, so first "image" is the thumbnail, rest are gallery
var thumbnailID, mediaID *int64
var mediaIsVideo bool
var galleryIDs []int64
mediaList, err := media.QFindByItemID(ctx, rc.DB, it.ID)
if err != nil {
return err
}
firstImage := true
for _, m := range mediaList {
if m.MediaType == "thumbnail" {
thumbnailID = &m.ID
} else if m.MediaType == "original" {
if m.MediaType == "original" {
mediaID = &m.ID
mediaIsVideo = strings.HasPrefix(m.ContentType, "video/")
} else if m.MediaType == "image" {
if firstImage {
thumbnailID = &m.ID
firstImage = false
} else {
galleryIDs = append(galleryIDs, m.ID)
}
}
}
@ -201,6 +215,7 @@ func HandleItemPage(rc *RequestContext, w http.ResponseWriter, r *http.Request)
ThumbnailID: thumbnailID,
MediaID: mediaID,
MediaIsVideo: mediaIsVideo,
GalleryIDs: galleryIDs,
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")