From 8042c65ce3e72d9b5cfa53f213da8c74b69beddd Mon Sep 17 00:00:00 2001 From: Cynthia Foxwell Date: Fri, 4 Apr 2025 17:11:40 -0600 Subject: [PATCH] Refactor images to use an Image type to supply alt text --- src/parser.nim | 11 ++++++++--- src/routes/activityspoof.nim | 6 +++--- src/routes/status.nim | 4 ++-- src/routes/timeline.nim | 2 +- src/sass/tweet/media.scss | 13 +++++++++++++ src/types.nim | 6 +++++- src/views/embed.nim | 2 +- src/views/general.nim | 11 +++++++---- src/views/mastoapi.nim | 8 ++++---- src/views/renderutils.nim | 4 ++-- src/views/tweet.nim | 13 ++++++++----- 11 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/parser.nim b/src/parser.nim index 360b20a..584ff7e 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -250,7 +250,9 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet = let name = jsCard{"name"}.getStr if "poll" in name: if "image" in name: - result.photos.add jsCard{"binding_values", "image_large"}.getImageVal + result.photos.add Image( + url: jsCard{"binding_values", "image_large"}.getImageVal + ) result.poll = some parsePoll(jsCard) elif name == "amplify": @@ -264,7 +266,10 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull()): Tweet = for m in jsMedia: case m{"type"}.getStr of "photo": - result.photos.add m{"media_url_https"}.getImageStr + result.photos.add Image( + url: m{"media_url_https"}.getImageStr, + description: m{"ext_alt_text"}.getStr, + ) of "video": result.video = some(parseVideo(m)) with user, m{"additional_media_info", "source_user"}: @@ -418,7 +423,7 @@ proc parsePhotoRail*(js: JsonNode): PhotoRail = for tweet in js: let t = parseTweet(tweet, js{"tweet_card"}) - url = if t.photos.len > 0: t.photos[0] + url = if t.photos.len > 0: t.photos[0].url elif t.video.isSome: get(t.video).thumb elif t.gif.isSome: get(t.gif).thumb elif t.card.isSome: get(t.card).image diff --git a/src/routes/activityspoof.nim b/src/routes/activityspoof.nim index 0203126..e8e06d9 100644 --- a/src/routes/activityspoof.nim +++ b/src/routes/activityspoof.nim @@ -45,8 +45,8 @@ proc createActivityPubRouter*(cfg: Config) = var media: seq[JsonNode] = @[] if tweet.photos.len > 0: - for url in tweet.photos: - let image = getUrlPrefix(cfg) & getPicUrl(url) + for imageObj in tweet.photos: + let image = getUrlPrefix(cfg) & getPicUrl(imageObj.url) var mediaObj = newJObject() mediaObj["id"] = %"150745989836308480" # idk if discord even parses this snowflake, but its my user id why not @@ -56,7 +56,7 @@ proc createActivityPubRouter*(cfg: Config) = mediaObj["remote_url"] = %image mediaObj["preview_remote_url"] = %image mediaObj["text_url"] = newJNull() - mediaObj["description"] = newJNull() # FIXME this requires refactoring images + mediaObj["description"] = %imageObj.description # FIXME but this probably isnt used by discord mediaObj["meta"] = newJObject() diff --git a/src/routes/status.nim b/src/routes/status.nim index aacd15f..23e09ff 100644 --- a/src/routes/status.nim +++ b/src/routes/status.nim @@ -123,7 +123,7 @@ proc createStatusRouter*(cfg: Config) = if tweet.video.isSome(): let videoObj = get(tweet.video) - images = @[videoObj.thumb] + images = @[Image(url:videoObj.thumb)] let vars = videoObj.variants.filterIt(it.contentType == mp4) # idk why this wont sort when it sorts everywhere else @@ -131,7 +131,7 @@ proc createStatusRouter*(cfg: Config) = video = vars[^1].url elif tweet.gif.isSome(): let gif = get(tweet.gif) - images = @[gif.thumb] + images = @[Image(url:gif.thumb)] video = getUrlPrefix(cfg) & getPicUrl(gif.url) #elif tweet.card.isSome(): # let card = tweet.card.get() diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index c951747..df0d4b2 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -96,7 +96,7 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs; let pHtml = renderProfile(profile, cfg, prefs, getPath()) result = renderMain(pHtml, request, cfg, prefs, pageTitle(u), pageDesc(u), - rss=rss, images = @[u.getUserPic("_400x400")], + rss=rss, images = @[Image(url: u.getUserPic("_400x400"))], banner=u.banner) template respTimeline*(timeline: typed) = diff --git a/src/sass/tweet/media.scss b/src/sass/tweet/media.scss index 91c9dab..658ee32 100644 --- a/src/sass/tweet/media.scss +++ b/src/sass/tweet/media.scss @@ -48,6 +48,19 @@ margin: 0; max-height: 530px; } + + .alt { + position: relative; + bottom: 15px; + left: 4px; + padding: 4px; + background: #101010; + color: white; + border-radius: 4px; + pointer-events: none; + font-size: 10px; + font-weight: 600; + } } .gallery-gif video { diff --git a/src/types.nim b/src/types.nim index feb06e6..fdc1eef 100644 --- a/src/types.nim +++ b/src/types.nim @@ -123,6 +123,10 @@ type playbackType*: VideoType variants*: seq[VideoVariant] + Image* = object + url*: string + description*: string + QueryKind* = enum posts, replies, media, users, tweets, userList, favorites @@ -228,7 +232,7 @@ type poll*: Option[Poll] gif*: Option[Gif] video*: Option[Video] - photos*: seq[string] + photos*: seq[Image] birdwatch*: Option[BirdwatchNote] Tweets* = seq[Tweet] diff --git a/src/views/embed.nim b/src/views/embed.nim index e82ca63..bfe0b1f 100644 --- a/src/views/embed.nim +++ b/src/views/embed.nim @@ -15,7 +15,7 @@ proc renderVideoEmbed*(tweet: Tweet; cfg: Config; req: Request): string = let vidUrl = vars.sortedByIt(it.bitrate)[^1].url let prefs = Prefs(hlsPlayback: true, mp4Playback: true) let node = buildHtml(html(lang="en")): - renderHead(prefs, cfg, req, video=vidUrl, images=(@[thumb])) + renderHead(prefs, cfg, req, video=vidUrl, images=(@[Image(url:thumb)])) body: tdiv(class="embed-video"): diff --git a/src/views/general.nim b/src/views/general.nim index 556beec..940e300 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -37,7 +37,7 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode = icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path)) proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; - video=""; images: seq[string] = @[]; banner=""; ogTitle=""; + video=""; images: seq[Image] = @[]; banner=""; ogTitle=""; rss=""; canonical=""; avatar=""; context=""; contextUrl=""; id=""; time: Option[DateTime] = none(DateTime)): VNode = var theme = prefs.theme.toTheme @@ -120,13 +120,16 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; link(rel="preload", type="image/png", href=bannerUrl, `as`="image") if images.len > 0: - for url in images: - let preloadUrl = if "400x400" in url: getPicUrl(url) + for imageObj in images: + let + url = imageObj.url + preloadUrl = if "400x400" in url: getPicUrl(url) else: getSmallPic(url) link(rel="preload", type="image/png", href=preloadUrl, `as`="image") let image = getUrlPrefix(cfg) & getPicUrl(url) meta(property="og:image", content=image) + meta(property="og:image:alt", content=imageObj.description) if video.len == 0: meta(property="twitter:image:src", content=image) meta(property="twitter:card", content="summary_large_image") @@ -172,7 +175,7 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs; titleText=""; desc=""; ogTitle=""; rss=""; video=""; - images: seq[string] = @[]; banner=""; avatar=""; context=""; + images: seq[Image] = @[]; banner=""; avatar=""; context=""; contextUrl=""; id=""; time: Option[DateTime] = none(DateTime) ): string = diff --git a/src/views/mastoapi.nim b/src/views/mastoapi.nim index e489f00..faec186 100644 --- a/src/views/mastoapi.nim +++ b/src/views/mastoapi.nim @@ -38,10 +38,10 @@ proc getActivityStream*(tweet: Tweet, cfg: Config, prefs: Prefs): JsonNode = var media: seq[JsonNode] = @[] if tweet.photos.len > 0: - for url in tweet.photos: + for imageObj in tweet.photos: let - image = getUrlPrefix(cfg) & getPicUrl(url) - splitUrl = url.split('.') + image = getUrlPrefix(cfg) & getPicUrl(imageObj.url) + splitUrl = imageObj.url.split('.') var filetype = splitUrl[^1] if filetype == "jpg": filetype = "jpeg" @@ -50,7 +50,7 @@ proc getActivityStream*(tweet: Tweet, cfg: Config, prefs: Prefs): JsonNode = mediaObj["type"] = %"Document" mediaObj["mediaType"] = %("image/" & filetype) mediaObj["url"] = %image - mediaObj["name"] = newJNull() # FIXME a11y + mediaObj["name"] = %imageObj.description media.add(mediaObj) diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index 86a6806..310d283 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -89,9 +89,9 @@ proc genDate*(pref, state: string): VNode = input(name=pref, `type`="date", value=state) icon "calendar" -proc genImg*(url: string; class=""): VNode = +proc genImg*(url: string; alt=""; class=""): VNode = buildHtml(): - img(src=getPicUrl(url), class=class, alt="") + img(src=getPicUrl(url), class=class, alt=alt) proc getTabClass*(query: Query; tab: QueryKind): string = if query.kind == tab: "tab-item active" diff --git a/src/views/tweet.nim b/src/views/tweet.nim index d986097..11bb4c0 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -49,12 +49,15 @@ proc renderAlbum(tweet: Tweet): VNode = let margin = if i > 0: ".25em" else: "" tdiv(class="gallery-row", style={marginTop: margin}): for photo in photos: - tdiv(class="attachment image"): + tdiv(class="attachment image", title=photo.description): let - named = "name=" in photo - small = if named: photo else: photo & smallWebp - a(href=getOrigPicUrl(photo), class="still-image", target="_blank"): - genImg(small) + url = photo.url + named = "name=" in url + small = if named: url else: url & smallWebp + a(href=getOrigPicUrl(url), class="still-image", target="_blank", data-caption=photo.description): + genImg(small, alt=photo.description) + if photo.description.len > 0: + span(class="alt"): text "ALT" proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool = case playbackType