Files
my-notes-viewer/frontend/src/widgets/MedialibImage.svelte
T
apairon e84b87ed16 feat: enhance accessibility with skip to main content button and improve navigation handling
🔧 fix: update navigation href resolution to include localized paths

🆕 feat: add new FeatureIcon component for feature boxes

🎨 style: improve styling for prose elements in richtext blocks

🛠️ refactor: streamline medialib image loading and caching logic

📦 chore: update mock data handling to support new medialib entries

🔄 chore: synchronize i18n initialization and locale management

📝 docs: update video tour descriptions to reflect recent changes
2026-05-12 13:55:32 +00:00

138 lines
4.7 KiB
Svelte

<script lang="ts">
import { apiBaseURL } from "../config"
import { currentLanguage, DEFAULT_LANGUAGE } from "../lib/i18n"
interface Props {
id: string
entry?: MedialibEntry | null
filter?: string | null
noPlaceholder?: boolean
caption?: string
showCaption?: boolean
minWidth?: number
widthMultiplier?: number
lazy?: boolean
style?: string
}
let {
id,
entry = null,
filter = null,
noPlaceholder = false,
caption = "",
showCaption = false,
minWidth = 0,
widthMultiplier = 1,
lazy = false,
style = "",
}: Props = $props()
let imgEl = $state<HTMLImageElement | null>(null)
let currentFilter = $state<string>("l-webp")
const effectiveId = $derived(entry?.id || entry?._id || id || "")
const fileSrc = $derived(resolveFileSrc(entry?.file?.src, entry?.id || entry?._id || effectiveId))
// Sync explicit filter prop reactively
$effect(() => {
if (filter) currentFilter = filter
})
function getAutoFilter(imgWidth: number): string {
const width = minWidth ? Math.max(imgWidth, minWidth) : imgWidth
const effectiveWidth = width * (widthMultiplier || 1)
if (effectiveWidth <= 90) return "xs-webp"
if (effectiveWidth <= 300) return "s-webp"
if (effectiveWidth <= 600) return "m-webp"
if (effectiveWidth <= 1200) return "l-webp"
if (effectiveWidth <= 2000) return "xl-webp"
return "xxl-webp"
}
function isRasterImage(entry: MedialibEntry | null): boolean {
if (entry?.file?.type?.match(/^image\/(png|jpe?g|webp)/)) return true
// Fallback: check file extension when MIME type is missing (e.g. public projection)
if (entry?.file?.src?.match(/\.(jpe?g|png|webp)$/i)) return true
return false
}
function resolveFileSrc(src: string | undefined, entryId: string | undefined): string | null {
if (!src) return null
if (/^(https?:)?\/\//.test(src) || src.startsWith("/")) return src
if (!entryId) return null
const normalizedApiBase = apiBaseURL.replace(/\/+$/, "")
return `${normalizedApiBase}/medialib/${entryId}/${src.replace(/^\/+/, "")}`
}
// ResizeObserver: only when no explicit filter and raster image
$effect(() => {
const el = imgEl
if (!el || filter || !entry || !isRasterImage(entry)) return
if (typeof ResizeObserver === "undefined") return
let maxObservedWidth = 0
const observer = new ResizeObserver(() => {
const newWidth = el.clientWidth
if (newWidth <= maxObservedWidth) return // only scale up
maxObservedWidth = newWidth
currentFilter = getAutoFilter(newWidth)
})
observer.observe(el)
return () => observer.disconnect()
})
function getSrc(src: string | null, entry: MedialibEntry | null): string {
if (!src) return "/assets/img/placeholder-image.svg"
if (!isRasterImage(entry)) return src
if (filter) return src + `?filter=${filter}`
return src + `?filter=${currentFilter}`
}
function resolveLocalizedText(value: string | LocalizedText | undefined, lang: string): string {
if (!value) return ""
if (typeof value === "string") return value
return value[lang] || value[DEFAULT_LANGUAGE] || Object.values(value).find((entry) => !!entry) || ""
}
</script>
{#if effectiveId}
{#if entry && fileSrc}
{#if showCaption && caption}
<figure>
<picture>
<img
bind:this={imgEl}
src={getSrc(fileSrc, entry)}
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
data-entry-id={id}
loading={lazy ? "lazy" : undefined}
{style}
/>
</picture>
<figcaption class="mt-2 text-sm text-gray-500 text-center italic">
{@html caption}
</figcaption>
</figure>
{:else}
<picture>
<img
bind:this={imgEl}
src={getSrc(fileSrc, entry)}
alt={resolveLocalizedText(entry.alt, $currentLanguage)}
data-entry-id={id}
loading={lazy ? "lazy" : undefined}
{style}
/>
</picture>
{/if}
{:else if !noPlaceholder}
<picture>
<img src="/assets/img/placeholder-image.svg" alt="not found" data-entry-id={effectiveId} />
</picture>
{/if}
{/if}