Support image alt text

Fixes #559
This commit is contained in:
Zed
2026-02-10 22:42:06 +01:00
parent 40b1ba4e4e
commit 1c06a67afd
9 changed files with 60 additions and 19 deletions

View File

@@ -139,7 +139,10 @@ proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) =
for m in jsMedia: for m in jsMedia:
case m.getTypeName: case m.getTypeName:
of "photo": of "photo":
result.photos.add m{"media_url_https"}.getImageStr result.photos.add Photo(
url: m{"media_url_https"}.getImageStr,
altText: m{"ext_alt_text"}.getStr
)
of "video": of "video":
result.video = some(parseVideo(m)) result.video = some(parseVideo(m))
with user, m{"additional_media_info", "source_user"}: with user, m{"additional_media_info", "source_user"}:
@@ -165,7 +168,10 @@ proc parseMediaEntities(js: JsonNode; result: var Tweet) =
with mediaInfo, mediaEntity{"media_results", "result", "media_info"}: with mediaInfo, mediaEntity{"media_results", "result", "media_info"}:
case mediaInfo.getTypeName case mediaInfo.getTypeName
of "ApiImage": of "ApiImage":
result.photos.add mediaInfo{"original_img_url"}.getImageStr result.photos.add Photo(
url: mediaInfo{"original_img_url"}.getImageStr,
altText: mediaInfo{"alt_text"}.getStr
)
of "ApiVideo": of "ApiVideo":
let status = mediaEntity{"media_results", "result", "media_availability_v2", "status"} let status = mediaEntity{"media_results", "result", "media_availability_v2", "status"}
result.video = some Video( result.video = some Video(
@@ -332,11 +338,13 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet =
let name = jsCard{"name"}.getStr let name = jsCard{"name"}.getStr
if "poll" in name: if "poll" in name:
if "image" in name: if "image" in name:
result.photos.add jsCard{"binding_values", "image_large"}.getImageVal result.photos.add Photo(
url: jsCard{"binding_values", "image_large"}.getImageVal
)
result.poll = some parsePoll(jsCard) result.poll = some parsePoll(jsCard)
elif name == "amplify": elif name == "amplify":
result.video = some(parsePromoVideo(jsCard{"binding_values"})) result.video = some parsePromoVideo(jsCard{"binding_values"})
else: else:
result.card = some parseCard(jsCard, js{"entities", "urls"}) result.card = some parseCard(jsCard, js{"entities", "urls"})

View File

@@ -333,7 +333,7 @@ proc expandNoteTweetEntities*(tweet: Tweet; js: JsonNode) =
proc extractGalleryPhoto*(t: Tweet): GalleryPhoto = proc extractGalleryPhoto*(t: Tweet): GalleryPhoto =
let url = let url =
if t.photos.len > 0: t.photos[0] if t.photos.len > 0: t.photos[0].url
elif t.video.isSome: get(t.video).thumb elif t.video.isSome: get(t.video).thumb
elif t.gif.isSome: get(t.gif).thumb elif t.gif.isSome: get(t.gif).thumb
elif t.card.isSome: get(t.card).image elif t.card.isSome: get(t.card).image

View File

@@ -44,7 +44,7 @@ proc createStatusRouter*(cfg: Config) =
desc = conv.tweet.text desc = conv.tweet.text
var var
images = conv.tweet.photos images = conv.tweet.photos.mapIt(it.url)
video = "" video = ""
if conv.tweet.video.isSome(): if conv.tweet.video.isSome():

View File

@@ -4,7 +4,6 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: nowrap; flex-wrap: nowrap;
align-items: center;
overflow: hidden; overflow: hidden;
flex-grow: 1; flex-grow: 1;
max-height: 379.5px; max-height: 379.5px;
@@ -13,7 +12,7 @@
.still-image { .still-image {
width: 100%; width: 100%;
display: flex; align-self: center;
} }
} }
@@ -58,7 +57,6 @@
.still-image { .still-image {
max-height: 379.5px; max-height: 379.5px;
max-width: 533px; max-width: 533px;
justify-content: center;
img { img {
object-fit: cover; object-fit: cover;
@@ -69,8 +67,37 @@
} }
} }
.alt-text {
margin: 0px;
padding: 11px 7px;
box-sizing: border-box;
position: absolute;
bottom: 10px;
left: 10px;
width: 2.98em;
max-height: 25px;
white-space: pre;
overflow: hidden;
border-radius: 10px;
color: var(--fg_color);
font-size: 12px;
font-weight: bold;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(12px);
}
.alt-text:hover {
padding: 7px;
width: Min(230px, calc(100% - 10px * 2));
max-height: calc(100% - 10px);
line-height: 1.2em;
white-space: pre-wrap;
transition-duration: 0.4s;
transition-property: max-height;
}
.image { .image {
display: inline-block; display: flex;
} }
// .single-image { // .single-image {

View File

@@ -137,6 +137,10 @@ type
url*: string url*: string
thumb*: string thumb*: string
Photo* = object
url*: string
altText*: string
GalleryPhoto* = object GalleryPhoto* = object
url*: string url*: string
tweetId*: string tweetId*: string
@@ -217,7 +221,7 @@ type
poll*: Option[Poll] poll*: Option[Poll]
gif*: Option[Gif] gif*: Option[Gif]
video*: Option[Video] video*: Option[Video]
photos*: seq[string] photos*: seq[Photo]
Tweets* = seq[Tweet] Tweets* = seq[Tweet]

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=23") link(rel="stylesheet", type="text/css", href="/css/style.css?v=24")
link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4") link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=4")
if theme.len > 0: if theme.len > 0:

View File

@@ -97,9 +97,9 @@ proc genNumberInput*(pref, label, state, placeholder: string; class=""; autofocu
label(`for`=pref): text label label(`for`=pref): text label
input(name=pref, `type`="number", placeholder=p, value=state, autofocus=(autofocus and state.len == 0), min=min, step="1") input(name=pref, `type`="number", placeholder=p, value=state, autofocus=(autofocus and state.len == 0), min=min, step="1")
proc genImg*(url: string; class=""): VNode = proc genImg*(url: string; class=""; alt=""): VNode =
buildHtml(): buildHtml():
img(src=getPicUrl(url), class=class, alt="", loading="lazy") img(src=getPicUrl(url), class=class, alt=alt, loading="lazy")
proc getTabClass*(query: Query; tab: QueryKind): string = proc getTabClass*(query: Query; tab: QueryKind): string =
if query.kind == tab: "tab-item active" if query.kind == tab: "tab-item active"

View File

@@ -56,7 +56,7 @@ Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)}
<p>${text.replace("\n", "<br>\n")}</p> <p>${text.replace("\n", "<br>\n")}</p>
#if tweet.photos.len > 0: #if tweet.photos.len > 0:
# for photo in tweet.photos: # for photo in tweet.photos:
<img src="${urlPrefix}${getPicUrl(photo)}" style="max-width:250px;" /> <img src="${urlPrefix}${getPicUrl(photo.url)}" style="max-width:250px;" />
# end for # end for
#elif tweet.video.isSome: #elif tweet.video.isSome:
<a href="${urlPrefix}${tweet.getLink}"> <a href="${urlPrefix}${tweet.getLink}">

View File

@@ -50,10 +50,12 @@ proc renderAlbum(tweet: Tweet): VNode =
for photo in photos: for photo in photos:
tdiv(class="attachment image"): tdiv(class="attachment image"):
let let
named = "name=" in photo named = "name=" in photo.url
small = if named: photo else: photo & smallWebp small = if named: photo.url else: photo.url & smallWebp
a(href=getOrigPicUrl(photo), class="still-image", target="_blank"): a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"):
genImg(small) genImg(small, alt=photo.altText)
if photo.altText.len > 0:
p(class="alt-text"): text "ALT " & photo.altText
proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool = proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool =
case playbackType case playbackType