1
0
mirror of https://github.com/zedeus/nitter.git synced 2026-04-03 20:32:10 -04:00

Fix mobile gallery and grid, add size preference

Fixes #1379
This commit is contained in:
Zed
2026-03-21 11:28:57 +01:00
parent b6ccea0c7a
commit fea6f59005
7 changed files with 51 additions and 40 deletions

View File

@@ -27,6 +27,13 @@ const GAP = 10;
class Masonry { class Masonry {
constructor(container) { constructor(container) {
this.container = 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.colHeights = [];
this.colCounts = []; this.colCounts = [];
this.colCount = 0; this.colCount = 0;
@@ -90,8 +97,8 @@ class Masonry {
} }
_rebuild() { _rebuild() {
const n = Math.max(1, Math.floor(this.container.clientWidth / 350));
const w = this.container.clientWidth; const w = this.container.clientWidth;
const n = Math.max(1, Math.floor(w / this._targetWidth(w)));
if (n === this.colCount && w === this._lastWidth) return; if (n === this.colCount && w === this._lastWidth) return;
const isFirst = this.colCount === 0; const isFirst = this.colCount === 0;

View File

@@ -103,6 +103,10 @@ genPrefs:
compactGallery(checkbox, false): compactGallery(checkbox, false):
"Compact media gallery (no profile info or text)" "Compact media gallery (no profile info or text)"
gallerySize(select, "Medium"):
"Gallery column size"
options: @["Small", "Medium", "Large"]
mediaView(select, "Timeline"): mediaView(select, "Timeline"):
"Default media view" "Default media view"
options: @["Timeline", "Grid", "Gallery"] options: @["Timeline", "Grid", "Gallery"]

View File

@@ -293,7 +293,15 @@
.gallery-masonry { .gallery-masonry {
margin: 10px 0; margin: 10px 0;
column-gap: 10px; 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 { &.masonry-active {
column-width: unset; 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) { @media (max-width: 520px) {
.timeline.media-grid-view {
grid-template-columns: 1fr;
}
.timeline.media-gallery-view { .timeline.media-gallery-view {
padding: 8px 0; padding: 8px 0;
.gallery-masonry {
columns: 1;
column-gap: 0;
&.masonry-active {
columns: unset;
}
}
} }
} }

View File

@@ -50,7 +50,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc="";
let opensearchUrl = getUrlPrefix(cfg) & "/opensearch" let opensearchUrl = getUrlPrefix(cfg) & "/opensearch"
buildHtml(head): 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") link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=5")
if theme.len > 0: if theme.len > 0:

View File

@@ -4,6 +4,7 @@ import karax/[karaxdsl, vdom, vstyles]
import ".."/[types, utils] import ".."/[types, utils]
const smallWebp* = "?name=small&format=webp" const smallWebp* = "?name=small&format=webp"
const mediumWebp* = "?name=medium&format=webp"
proc getSmallPic*(url: string): string = proc getSmallPic*(url: string): string =
result = url result = url
@@ -11,6 +12,12 @@ proc getSmallPic*(url: string): string =
result &= smallWebp result &= smallWebp
result = getPicUrl(result) 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 = proc icon*(icon: string; text=""; title=""; class=""; href=""): VNode =
var c = "icon-" & icon var c = "icon-" & icon
if class.len > 0: c = &"{c} {class}" if class.len > 0: c = &"{c} {class}"

View File

@@ -65,7 +65,7 @@ proc renderNoneFound(): VNode =
h2(class="timeline-none"): h2(class="timeline-none"):
text "No items found" 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")): buildHtml(tdiv(class="thread-line")):
let sortedThread = thread.sortedByIt(it.id) let sortedThread = thread.sortedByIt(it.id)
for i, tweet in sortedThread: 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 show = i == thread.high and sortedThread[0].id != tweet.threadId
let header = if tweet.pinned or tweet.retweet.isSome: "with-header " else: "" let header = if tweet.pinned or tweet.retweet.isSome: "with-header " else: ""
renderTweet(tweet, prefs, path, class=(header & "thread"), 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 = proc renderUser(user: User; prefs: Prefs): VNode =
buildHtml(tdiv(class="timeline-item", data-username=user.username)): 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) let filtered = filterThreads(results.content, prefs)
if results.query.view == "gallery": 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: for thread in filtered:
if thread.len == 1: renderTweet(thread[0], prefs, path) if thread.len == 1: renderTweet(thread[0], prefs, path, bigThumb=bigThumb)
else: renderThread(thread, prefs, path) else: renderThread(thread, prefs, path, bigThumb)
else: else:
for thread in filtered: for thread in filtered:
if thread.len == 1: renderTweet(thread[0], prefs, path) if thread.len == 1: renderTweet(thread[0], prefs, path)

View File

@@ -42,13 +42,15 @@ proc renderAltText(altText: string): VNode =
buildHtml(p(class="alt-text")): buildHtml(p(class="alt-text")):
text "ALT " & altText text "ALT " & altText
proc renderPhotoAttachment(photo: Photo): VNode = proc renderPhotoAttachment(photo: Photo; bigThumb=false): VNode =
buildHtml(tdiv(class="attachment")): buildHtml(tdiv(class="attachment")):
let let
named = "name=" in photo.url 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"): 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: if photo.altText.len > 0:
renderAltText(photo.altText) renderAltText(photo.altText)
@@ -76,11 +78,11 @@ proc renderVideoUnavailable(video: Video): VNode =
else: else:
p: text "This media is unavailable" 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 let
playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4 playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4
else: videoData.playbackType else: videoData.playbackType
thumb = getSmallPic(videoData.thumb) thumb = if bigThumb: getMediumPic(videoData.thumb) else: getSmallPic(videoData.thumb)
buildHtml(tdiv(class="attachment")): buildHtml(tdiv(class="attachment")):
if not videoData.available: if not videoData.available:
@@ -106,12 +108,12 @@ proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""): VNode =
tdiv(class="overlay-duration"): text getDuration(videoData) tdiv(class="overlay-duration"): text getDuration(videoData)
verbatim "</div>" verbatim "</div>"
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 let hasCardContent = video.description.len > 0 or video.title.len > 0
buildHtml(tdiv(class="attachments card")): buildHtml(tdiv(class="attachments card")):
tdiv(class=("gallery-video" & (if hasCardContent: " card-container" else: ""))): tdiv(class=("gallery-video" & (if hasCardContent: " card-container" else: ""))):
renderVideoAttachment(video, prefs, path) renderVideoAttachment(video, prefs, path, bigThumb)
if hasCardContent: if hasCardContent:
tdiv(class="card-content"): tdiv(class="card-content"):
h2(class="card-title"): text video.title h2(class="card-title"): text video.title
@@ -138,14 +140,14 @@ proc renderGif(gif: Gif; prefs: Prefs): VNode =
buildHtml(tdiv(class="attachments media-gif")): buildHtml(tdiv(class="attachments media-gif")):
renderGifAttachment(gif, prefs) 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: if media.len == 0:
return nil return nil
if media.len == 1: if media.len == 1:
let item = media[0] let item = media[0]
if item.kind == videoMedia: if item.kind == videoMedia:
return renderVideo(item.video, prefs, path) return renderVideo(item.video, prefs, path, bigThumb)
if item.kind == gifMedia: if item.kind == gifMedia:
return renderGif(item.gif, prefs) return renderGif(item.gif, prefs)
@@ -162,9 +164,9 @@ proc renderMedia(media: seq[Media]; prefs: Prefs; path: string): VNode =
for mediaItem in mediaGroup: for mediaItem in mediaGroup:
case mediaItem.kind case mediaItem.kind
of photoMedia: of photoMedia:
renderPhotoAttachment(mediaItem.photo) renderPhotoAttachment(mediaItem.photo, bigThumb)
of videoMedia: of videoMedia:
renderVideoAttachment(mediaItem.video, prefs, path) renderVideoAttachment(mediaItem.video, prefs, path, bigThumb)
of gifMedia: of gifMedia:
renderGifAttachment(mediaItem.gif, prefs) renderGifAttachment(mediaItem.gif, prefs)
@@ -336,7 +338,7 @@ proc renderLocation*(tweet: Tweet): string =
return $node return $node
proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; 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 var divClass = class
if index == -1 or last: if index == -1 or last:
divClass = "thread-last " & class 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) renderCard(tweet.card.get(), prefs, path)
if tweet.media.len > 0: if tweet.media.len > 0:
renderMedia(tweet.media, prefs, path) renderMedia(tweet.media, prefs, path, bigThumb)
if tweet.poll.isSome: if tweet.poll.isSome:
renderPoll(tweet.poll.get()) renderPoll(tweet.poll.get())