feat: sidebar for media now shows thumbnails

This commit is contained in:
2026-04-25 21:08:39 +02:00
parent e02e5eb6f6
commit 8838b10403
6 changed files with 170 additions and 1 deletions

View File

@@ -1636,6 +1636,36 @@ button {
font-size: 20px;
}
.media-thumbnail.has-image {
position: relative;
}
.media-thumbnail-fallback {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
}
.media-thumbnail-image {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0;
transition: opacity 0.15s ease;
}
.media-thumbnail.is-loaded .media-thumbnail-image {
opacity: 1;
}
.media-thumbnail.is-loaded .media-thumbnail-fallback {
opacity: 0;
}
.media-item-info {
flex: 1;
min-width: 0;

View File

@@ -439,6 +439,7 @@ function renderSidebarMediaItem(item, view) {
const itemRoute = item.route || view.editor_route;
const tabId = tabIdForItem(item, itemRoute);
const active = tabRef && tabRef.type === itemRoute && tabRef.id === tabId;
const thumbnail = renderMediaThumbnail(item);
return `
<button
@@ -449,7 +450,7 @@ function renderSidebarMediaItem(item, view) {
type="button"
title="${escapeHtmlAttribute(item.title || "") }"
>
<span class="media-thumbnail">${escapeHtml(mediaThumbnailGlyph(item.mime_type))}</span>
${thumbnail}
<span class="media-item-info">
<span class="media-item-name">${escapeHtml(item.title || "")}</span>
<span class="media-item-size">${escapeHtml(item.meta || "")}</span>
@@ -458,6 +459,28 @@ function renderSidebarMediaItem(item, view) {
`;
}
function renderMediaThumbnail(item) {
const fallback = escapeHtml(mediaThumbnailGlyph(item.mime_type));
if (!String(item.mime_type || "").startsWith("image/")) {
return `<span class="media-thumbnail"><span class="media-thumbnail-fallback">${fallback}</span></span>`;
}
return `
<span class="media-thumbnail has-image" data-media-thumbnail-state="pending">
<span class="media-thumbnail-fallback">${fallback}</span>
<img
class="media-thumbnail-image"
data-media-thumbnail-image
src="${escapeHtmlAttribute(mediaThumbnailUrl(item.id))}"
alt=""
loading="lazy"
decoding="async"
>
</span>
`;
}
function renderSidebarEntityList(data, view) {
const items = Array.isArray(data.items) ? data.items : [];
@@ -1258,6 +1281,25 @@ function bindEvents() {
};
});
root.querySelectorAll("[data-media-thumbnail-image]").forEach((image) => {
const container = image.closest(".media-thumbnail");
image.onload = () => {
container?.classList.add("is-loaded");
container?.classList.remove("is-error");
};
image.onerror = () => {
container?.classList.add("is-error");
container?.classList.remove("is-loaded");
};
if (image.complete && image.naturalWidth > 0) {
container?.classList.add("is-loaded");
container?.classList.remove("is-error");
}
});
root.querySelectorAll("[data-open-tab]").forEach((button) => {
button.onclick = () => {
openTab(button.dataset.openRoute, button.dataset.openTab, button.dataset.openTitle, true);
@@ -2420,6 +2462,10 @@ function mediaThumbnailGlyph(mimeType) {
return "📄";
}
function mediaThumbnailUrl(mediaId) {
return `/api/media-thumbnail/${encodeURIComponent(mediaId)}`;
}
function buildDashboardTagCloudItems(items) {
if (!Array.isArray(items) || !items.length) {
return [];