109 lines
2.7 KiB
Svelte
109 lines
2.7 KiB
Svelte
<script lang="ts">
|
|
import { commands } from "$lib/api/bindings";
|
|
import { auth } from "$lib/stores/auth";
|
|
import { get } from "svelte/store";
|
|
|
|
interface Props {
|
|
itemId: string;
|
|
imageType?: string;
|
|
tag?: string | null;
|
|
maxWidth?: number;
|
|
maxHeight?: number;
|
|
class?: string;
|
|
alt?: string;
|
|
/**
|
|
* Called once the bitmap is decoded, with its intrinsic pixel size. Lets a
|
|
* layout that sizes boxes from artwork (the mosaic) use the shape the image
|
|
* actually has rather than the one its item type suggests.
|
|
* TRACES: UR-075 | DR-174
|
|
*/
|
|
onNaturalSize?: (width: number, height: number) => void;
|
|
}
|
|
|
|
let {
|
|
itemId,
|
|
imageType = "Primary",
|
|
tag,
|
|
maxWidth,
|
|
maxHeight,
|
|
class: className = "",
|
|
alt = "",
|
|
onNaturalSize,
|
|
}: Props = $props();
|
|
|
|
let imageUrl = $state<string | null>(null);
|
|
let loading = $state(true);
|
|
let error = $state(false);
|
|
let lastLoadKey = "";
|
|
|
|
async function loadImage() {
|
|
if (!itemId) {
|
|
loading = false;
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loading = true;
|
|
error = false;
|
|
|
|
// Get repository handle from auth store
|
|
const authState = get(auth);
|
|
if (!authState.isAuthenticated) {
|
|
throw new Error("Not authenticated");
|
|
}
|
|
const repository = auth.getRepository();
|
|
const repositoryHandle = repository.getHandle();
|
|
|
|
// Call Rust to get image as base64 data URL
|
|
const dataUrl = await commands.imageGetUrl(repositoryHandle, {
|
|
itemId,
|
|
imageType,
|
|
maxWidth,
|
|
maxHeight,
|
|
tag,
|
|
});
|
|
|
|
// Use data URL directly
|
|
imageUrl = dataUrl;
|
|
error = false;
|
|
} catch (e) {
|
|
error = true;
|
|
imageUrl = null;
|
|
} finally {
|
|
loading = false;
|
|
}
|
|
}
|
|
|
|
// Only reload when the image identity changes (not on parent re-renders)
|
|
$effect(() => {
|
|
const loadKey = `${itemId}|${imageType}|${tag || ""}`;
|
|
if (loadKey !== lastLoadKey) {
|
|
lastLoadKey = loadKey;
|
|
imageUrl = null;
|
|
loadImage();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
{#if loading}
|
|
<div class="{className} bg-gray-700 animate-pulse" aria-busy="true" aria-label="Loading image"></div>
|
|
{:else if error || !imageUrl}
|
|
<div class="{className} bg-gray-800 flex items-center justify-center">
|
|
<svg class="w-8 h-8 text-gray-600" fill="currentColor" viewBox="0 0 24 24">
|
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
|
</svg>
|
|
</div>
|
|
{:else}
|
|
<img
|
|
src={imageUrl}
|
|
{alt}
|
|
class={className}
|
|
onload={(e) => {
|
|
const img = e.currentTarget as HTMLImageElement;
|
|
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
|
|
onNaturalSize?.(img.naturalWidth, img.naturalHeight);
|
|
}
|
|
}}
|
|
/>
|
|
{/if}
|