From fea6f59005e7178abffb65b26149f18267f84093 Mon Sep 17 00:00:00 2001 From: Zed Date: Sat, 21 Mar 2026 11:28:57 +0100 Subject: [PATCH] Fix mobile gallery and grid, add size preference Fixes #1379 --- public/js/infiniteScroll.js | 9 ++++++++- src/prefs_impl.nim | 4 ++++ src/sass/timeline.scss | 29 +++++++++-------------------- src/views/general.nim | 2 +- src/views/renderutils.nim | 7 +++++++ src/views/timeline.nim | 12 +++++++----- src/views/tweet.nim | 28 +++++++++++++++------------- 7 files changed, 51 insertions(+), 40 deletions(-) diff --git a/public/js/infiniteScroll.js b/public/js/infiniteScroll.js index 1ea35d1..f79912f 100644 --- a/public/js/infiniteScroll.js +++ b/public/js/infiniteScroll.js @@ -27,6 +27,13 @@ const GAP = 10; class Masonry { constructor(container) { this.container = container; + const colSizes = { + small: w => Math.max(130, w * 0.11), + medium: w => Math.max(190, Math.min(350, w * 0.22)), + large: w => Math.max(350, Math.min(480, w * 0.22)), + }; + const size = container.dataset.colSize || "medium"; + this._targetWidth = colSizes[size] || colSizes.medium; this.colHeights = []; this.colCounts = []; this.colCount = 0; @@ -90,8 +97,8 @@ class Masonry { } _rebuild() { - const n = Math.max(1, Math.floor(this.container.clientWidth / 350)); const w = this.container.clientWidth; + const n = Math.max(1, Math.floor(w / this._targetWidth(w))); if (n === this.colCount && w === this._lastWidth) return; const isFirst = this.colCount === 0; diff --git a/src/prefs_impl.nim b/src/prefs_impl.nim index 8ad2413..699ec4c 100644 --- a/src/prefs_impl.nim +++ b/src/prefs_impl.nim @@ -103,6 +103,10 @@ genPrefs: compactGallery(checkbox, false): "Compact media gallery (no profile info or text)" + gallerySize(select, "Medium"): + "Gallery column size" + options: @["Small", "Medium", "Large"] + mediaView(select, "Timeline"): "Default media view" options: @["Timeline", "Grid", "Gallery"] diff --git a/src/sass/timeline.scss b/src/sass/timeline.scss index 192073f..e167448 100644 --- a/src/sass/timeline.scss +++ b/src/sass/timeline.scss @@ -293,7 +293,15 @@ .gallery-masonry { margin: 10px 0; column-gap: 10px; - column-width: 350px; + column-width: unquote("clamp(190px, 22vw, 350px)"); + + &[data-col-size="small"] { + column-width: unquote("max(130px, 11vw)"); + } + + &[data-col-size="large"] { + column-width: unquote("clamp(350px, 22vw, 480px)"); + } &.masonry-active { column-width: unset; @@ -470,27 +478,8 @@ } } -@media (max-width: 900px) { - .timeline.media-grid-view { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - @media (max-width: 520px) { - .timeline.media-grid-view { - grid-template-columns: 1fr; - } - .timeline.media-gallery-view { padding: 8px 0; - - .gallery-masonry { - columns: 1; - column-gap: 0; - - &.masonry-active { - columns: unset; - } - } } } diff --git a/src/views/general.nim b/src/views/general.nim index 7bad908..d7af7e8 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; let opensearchUrl = getUrlPrefix(cfg) & "/opensearch" buildHtml(head): - link(rel="stylesheet", type="text/css", href="/css/style.css?v=31") + link(rel="stylesheet", type="text/css", href="/css/style.css?v=32") link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=5") if theme.len > 0: diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index 0045a5a..0bd9789 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -4,6 +4,7 @@ import karax/[karaxdsl, vdom, vstyles] import ".."/[types, utils] const smallWebp* = "?name=small&format=webp" +const mediumWebp* = "?name=medium&format=webp" proc getSmallPic*(url: string): string = result = url @@ -11,6 +12,12 @@ proc getSmallPic*(url: string): string = result &= smallWebp result = getPicUrl(result) +proc getMediumPic*(url: string): string = + result = url + if "?" notin url and not url.endsWith("placeholder.png"): + result &= mediumWebp + result = getPicUrl(result) + proc icon*(icon: string; text=""; title=""; class=""; href=""): VNode = var c = "icon-" & icon if class.len > 0: c = &"{c} {class}" diff --git a/src/views/timeline.nim b/src/views/timeline.nim index f4899c8..b911765 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -65,7 +65,7 @@ proc renderNoneFound(): VNode = h2(class="timeline-none"): text "No items found" -proc renderThread(thread: Tweets; prefs: Prefs; path: string): VNode = +proc renderThread(thread: Tweets; prefs: Prefs; path: string; bigThumb=false): VNode = buildHtml(tdiv(class="thread-line")): let sortedThread = thread.sortedByIt(it.id) for i, tweet in sortedThread: @@ -79,7 +79,7 @@ proc renderThread(thread: Tweets; prefs: Prefs; path: string): VNode = let show = i == thread.high and sortedThread[0].id != tweet.threadId let header = if tweet.pinned or tweet.retweet.isSome: "with-header " else: "" renderTweet(tweet, prefs, path, class=(header & "thread"), - index=i, last=(i == thread.high)) + index=i, last=(i == thread.high), bigThumb=bigThumb) proc renderUser(user: User; prefs: Prefs): VNode = buildHtml(tdiv(class="timeline-item", data-username=user.username)): @@ -146,10 +146,12 @@ proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string; let filtered = filterThreads(results.content, prefs) if results.query.view == "gallery": - tdiv(class=if prefs.compactGallery: "gallery-masonry compact" else: "gallery-masonry"): + let bigThumb = prefs.gallerySize == "Large" + let galClass = if prefs.compactGallery: "gallery-masonry compact" else: "gallery-masonry" + tdiv(class=galClass, `data-col-size`=prefs.gallerySize.toLowerAscii): for thread in filtered: - if thread.len == 1: renderTweet(thread[0], prefs, path) - else: renderThread(thread, prefs, path) + if thread.len == 1: renderTweet(thread[0], prefs, path, bigThumb=bigThumb) + else: renderThread(thread, prefs, path, bigThumb) else: for thread in filtered: if thread.len == 1: renderTweet(thread[0], prefs, path) diff --git a/src/views/tweet.nim b/src/views/tweet.nim index aae56e9..ba8e9f1 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -42,13 +42,15 @@ proc renderAltText(altText: string): VNode = buildHtml(p(class="alt-text")): text "ALT " & altText -proc renderPhotoAttachment(photo: Photo): VNode = +proc renderPhotoAttachment(photo: Photo; bigThumb=false): VNode = buildHtml(tdiv(class="attachment")): let named = "name=" in photo.url - small = if named: photo.url else: photo.url & smallWebp + thumb = if named: photo.url + elif bigThumb: photo.url & mediumWebp + else: photo.url & smallWebp a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"): - genImg(small, alt=photo.altText) + genImg(thumb, alt=photo.altText) if photo.altText.len > 0: renderAltText(photo.altText) @@ -76,11 +78,11 @@ proc renderVideoUnavailable(video: Video): VNode = else: p: text "This media is unavailable" -proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""): VNode = +proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""; bigThumb=false): VNode = let playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4 else: videoData.playbackType - thumb = getSmallPic(videoData.thumb) + thumb = if bigThumb: getMediumPic(videoData.thumb) else: getSmallPic(videoData.thumb) buildHtml(tdiv(class="attachment")): if not videoData.available: @@ -106,12 +108,12 @@ proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""): VNode = tdiv(class="overlay-duration"): text getDuration(videoData) verbatim "" -proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode = +proc renderVideo*(video: Video; prefs: Prefs; path: string; bigThumb=false): VNode = let hasCardContent = video.description.len > 0 or video.title.len > 0 buildHtml(tdiv(class="attachments card")): tdiv(class=("gallery-video" & (if hasCardContent: " card-container" else: ""))): - renderVideoAttachment(video, prefs, path) + renderVideoAttachment(video, prefs, path, bigThumb) if hasCardContent: tdiv(class="card-content"): h2(class="card-title"): text video.title @@ -138,14 +140,14 @@ proc renderGif(gif: Gif; prefs: Prefs): VNode = buildHtml(tdiv(class="attachments media-gif")): renderGifAttachment(gif, prefs) -proc renderMedia(media: seq[Media]; prefs: Prefs; path: string): VNode = +proc renderMedia(media: seq[Media]; prefs: Prefs; path: string; bigThumb=false): VNode = if media.len == 0: return nil if media.len == 1: let item = media[0] if item.kind == videoMedia: - return renderVideo(item.video, prefs, path) + return renderVideo(item.video, prefs, path, bigThumb) if item.kind == gifMedia: return renderGif(item.gif, prefs) @@ -162,9 +164,9 @@ proc renderMedia(media: seq[Media]; prefs: Prefs; path: string): VNode = for mediaItem in mediaGroup: case mediaItem.kind of photoMedia: - renderPhotoAttachment(mediaItem.photo) + renderPhotoAttachment(mediaItem.photo, bigThumb) of videoMedia: - renderVideoAttachment(mediaItem.video, prefs, path) + renderVideoAttachment(mediaItem.video, prefs, path, bigThumb) of gifMedia: renderGifAttachment(mediaItem.gif, prefs) @@ -336,7 +338,7 @@ proc renderLocation*(tweet: Tweet): string = return $node proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; - last=false; mainTweet=false; afterTweet=false): VNode = + last=false; mainTweet=false; afterTweet=false; bigThumb=false): VNode = var divClass = class if index == -1 or last: divClass = "thread-last " & class @@ -389,7 +391,7 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; renderCard(tweet.card.get(), prefs, path) if tweet.media.len > 0: - renderMedia(tweet.media, prefs, path) + renderMedia(tweet.media, prefs, path, bigThumb) if tweet.poll.isSome: renderPoll(tweet.poll.get())