feat: enriched media model with ALT badges (Release 3+4)

Change photo storage from bare URL strings to objects with url, alt,
width, height (AP) plus blurhash and focus (Mastodon API). Templates
handle both old string and new object format for backward compat.

Add ALT text badges on gallery images — click to expand the full
alt text in an overlay. Renders in both reader and explore views.

Also pass alt text through to lightbox and quote embed photos.

Bump version to 2.5.3.

Confab-Link: http://localhost:8080/sessions/e9d666ac-3c90-4298-9e92-9ac9d142bc06
This commit is contained in:
Ricardo
2026-03-03 13:46:58 +01:00
parent e34d9c124d
commit c243b70629
6 changed files with 83 additions and 12 deletions
+42
View File
@@ -2537,3 +2537,45 @@
margin: 0 0.05em; margin: 0 0.05em;
} }
/* Gallery items — positioned for ALT badge overlay */
.ap-card__gallery-item {
position: relative;
}
/* ALT text badges */
.ap-media__alt-badge {
position: absolute;
bottom: 0.5rem;
left: 0.5rem;
background: rgba(0, 0, 0, 0.7);
color: white;
font-size: 0.65rem;
font-weight: 700;
padding: 0.15rem 0.35rem;
border-radius: var(--border-radius-small);
border: none;
cursor: pointer;
text-transform: uppercase;
letter-spacing: 0.03em;
z-index: 1;
}
.ap-media__alt-badge:hover {
background: rgba(0, 0, 0, 0.9);
}
.ap-media__alt-text {
position: absolute;
bottom: 2.2rem;
left: 0.5rem;
right: 0.5rem;
background: rgba(0, 0, 0, 0.85);
color: white;
font-size: var(--font-size-s);
padding: 0.5rem;
border-radius: var(--border-radius-small);
max-height: 8rem;
overflow-y: auto;
z-index: 2;
}
+14 -2
View File
@@ -84,7 +84,14 @@ export function mapMastodonStatusToItem(status, instance) {
const url = att.url || att.remote_url || ""; const url = att.url || att.remote_url || "";
if (!url) continue; if (!url) continue;
if (att.type === "image" || att.type === "gifv") { if (att.type === "image" || att.type === "gifv") {
photo.push(url); photo.push({
url,
alt: att.description || "",
width: att.meta?.original?.width || null,
height: att.meta?.original?.height || null,
blurhash: att.blurhash || "",
focus: att.meta?.focus || null,
});
} else if (att.type === "video") { } else if (att.type === "video") {
video.push(url); video.push(url);
} else if (att.type === "audio") { } else if (att.type === "audio") {
@@ -144,7 +151,12 @@ export function mapMastodonStatusToItem(status, instance) {
for (const att of q.media_attachments || []) { for (const att of q.media_attachments || []) {
const attUrl = att.url || att.remote_url || ""; const attUrl = att.url || att.remote_url || "";
if (attUrl && (att.type === "image" || att.type === "gifv")) { if (attUrl && (att.type === "image" || att.type === "gifv")) {
qPhoto.push(attUrl); qPhoto.push({
url: attUrl,
alt: att.description || "",
width: att.meta?.original?.width || null,
height: att.meta?.original?.height || null,
});
} }
} }
+6 -1
View File
@@ -256,7 +256,12 @@ export async function extractObjectData(object, options = {}) {
const mediaType = att.mediaType?.toLowerCase() || ""; const mediaType = att.mediaType?.toLowerCase() || "";
if (mediaType.startsWith("image/")) { if (mediaType.startsWith("image/")) {
photo.push(mediaUrl); photo.push({
url: mediaUrl,
alt: att.name?.toString() || "",
width: att.width || null,
height: att.height || null,
});
} else if (mediaType.startsWith("video/")) { } else if (mediaType.startsWith("video/")) {
video.push(mediaUrl); video.push(mediaUrl);
} else if (mediaType.startsWith("audio/")) { } else if (mediaType.startsWith("audio/")) {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@rmdes/indiekit-endpoint-activitypub", "name": "@rmdes/indiekit-endpoint-activitypub",
"version": "2.5.2", "version": "2.5.3",
"description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.", "description": "ActivityPub federation endpoint for Indiekit via Fedify. Adds full fediverse support: actor, inbox, outbox, followers, following, syndication, and Mastodon migration.",
"keywords": [ "keywords": [
"indiekit", "indiekit",
+14 -3
View File
@@ -6,14 +6,23 @@
{% set extraCount = item.photo.length - 4 %} {% set extraCount = item.photo.length - 4 %}
{% set totalPhotos = item.photo.length %} {% set totalPhotos = item.photo.length %}
<div x-data="{ lightbox: false, idx: 0 }" class="ap-card__gallery ap-card__gallery--{{ displayCount }}"> <div x-data="{ lightbox: false, idx: 0 }" class="ap-card__gallery ap-card__gallery--{{ displayCount }}">
{% for photoUrl in item.photo %} {% for photo in item.photo %}
{# Support both old string format and new object format #}
{% set photoSrc = photo.url if photo.url else photo %}
{% set photoAlt = photo.alt if photo.alt else "" %}
{% if loop.index0 < 4 %} {% if loop.index0 < 4 %}
<div class="ap-card__gallery-item" x-data="{ showAlt: false }">
<button type="button" @click="idx = {{ loop.index0 }}; lightbox = true" class="ap-card__gallery-link{% if loop.index0 == 3 and extraCount > 0 %} ap-card__gallery-link--more{% endif %}"> <button type="button" @click="idx = {{ loop.index0 }}; lightbox = true" class="ap-card__gallery-link{% if loop.index0 == 3 and extraCount > 0 %} ap-card__gallery-link--more{% endif %}">
<img src="{{ photoUrl }}" alt="" loading="lazy"> <img src="{{ photoSrc }}" alt="{{ photoAlt }}" loading="lazy">
{% if loop.index0 == 3 and extraCount > 0 %} {% if loop.index0 == 3 and extraCount > 0 %}
<span class="ap-card__gallery-more">+{{ extraCount }}</span> <span class="ap-card__gallery-more">+{{ extraCount }}</span>
{% endif %} {% endif %}
</button> </button>
{% if photoAlt %}
<button type="button" class="ap-media__alt-badge" @click.stop="showAlt = !showAlt" :aria-expanded="showAlt">ALT</button>
<div class="ap-media__alt-text" x-show="showAlt" x-cloak @click.stop>{{ photoAlt }}</div>
{% endif %}
</div>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
@@ -24,7 +33,9 @@
{% if totalPhotos > 1 %} {% if totalPhotos > 1 %}
<button type="button" @click="idx = (idx - 1 + {{ totalPhotos }}) % {{ totalPhotos }}" class="ap-lightbox__prev" aria-label="Previous image">&lsaquo;</button> <button type="button" @click="idx = (idx - 1 + {{ totalPhotos }}) % {{ totalPhotos }}" class="ap-lightbox__prev" aria-label="Previous image">&lsaquo;</button>
{% endif %} {% endif %}
<img :src="[{% for p in item.photo %}'{{ p }}'{% if not loop.last %},{% endif %}{% endfor %}][idx]" class="ap-lightbox__img" alt=""> <img :src="[{% for photo in item.photo %}'{{ photo.url if photo.url else photo }}'{% if not loop.last %},{% endif %}{% endfor %}][idx]"
:alt="[{% for photo in item.photo %}'{{ (photo.alt if photo.alt else '') | replace(\"'\", \"\\'\") }}'{% if not loop.last %},{% endif %}{% endfor %}][idx]"
class="ap-lightbox__img">
{% if totalPhotos > 1 %} {% if totalPhotos > 1 %}
<button type="button" @click="idx = (idx + 1) % {{ totalPhotos }}" class="ap-lightbox__next" aria-label="Next image">&rsaquo;</button> <button type="button" @click="idx = (idx + 1) % {{ totalPhotos }}" class="ap-lightbox__next" aria-label="Next image">&rsaquo;</button>
<div class="ap-lightbox__counter" x-text="(idx + 1) + ' / ' + {{ totalPhotos }}"></div> <div class="ap-lightbox__counter" x-text="(idx + 1) + ' / ' + {{ totalPhotos }}"></div>
+2 -1
View File
@@ -25,8 +25,9 @@
<div class="ap-quote-embed__content">{{ item.quote.content.html | safe }}</div> <div class="ap-quote-embed__content">{{ item.quote.content.html | safe }}</div>
{% endif %} {% endif %}
{% if item.quote.photo and item.quote.photo.length > 0 %} {% if item.quote.photo and item.quote.photo.length > 0 %}
{% set qPhoto = item.quote.photo[0] %}
<div class="ap-quote-embed__media"> <div class="ap-quote-embed__media">
<img src="{{ item.quote.photo[0] }}" alt="" loading="lazy" class="ap-quote-embed__photo"> <img src="{{ qPhoto.url if qPhoto.url else qPhoto }}" alt="{{ qPhoto.alt if qPhoto.alt else '' }}" loading="lazy" class="ap-quote-embed__photo">
</div> </div>
{% endif %} {% endif %}
</a> </a>