feat: add image display components
- ImageCard: image preview with comparison slider - ImageGrid: responsive grid layout for cards - ImagePreview: live preview with pipeline processing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
113
src/lib/components/ImagePreview.svelte
Normal file
113
src/lib/components/ImagePreview.svelte
Normal file
@@ -0,0 +1,113 @@
|
||||
<script lang="ts">
|
||||
import { Loader2 } from 'lucide-svelte';
|
||||
import type { ImageEntry, Device, PipelineConfig } from '$lib/types';
|
||||
import { processImageWithPipeline } from '$lib/processing/pipeline';
|
||||
|
||||
interface Props {
|
||||
image: ImageEntry;
|
||||
device: Device;
|
||||
config: PipelineConfig;
|
||||
onPreviewUpdate: (dataUrl: string) => void;
|
||||
onProcessingChange: (processing: boolean) => void;
|
||||
}
|
||||
|
||||
let { image, device, config, onPreviewUpdate, onProcessingChange }: Props = $props();
|
||||
|
||||
let previewDataUrl = $state<string | null>(null);
|
||||
let isProcessing = $state(false);
|
||||
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
// Track config changes and trigger debounced processing
|
||||
$effect(() => {
|
||||
// Access config to create dependency
|
||||
const _ = JSON.stringify(config);
|
||||
|
||||
// Clear existing timer
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
|
||||
// Debounce processing
|
||||
debounceTimer = setTimeout(() => {
|
||||
processPreview();
|
||||
}, 150);
|
||||
|
||||
return () => {
|
||||
if (debounceTimer) {
|
||||
clearTimeout(debounceTimer);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
async function processPreview(): Promise<void> {
|
||||
if (!image.file) return;
|
||||
|
||||
isProcessing = true;
|
||||
onProcessingChange(true);
|
||||
|
||||
try {
|
||||
const { dataUrl } = await processImageWithPipeline(image.file, device, config);
|
||||
previewDataUrl = dataUrl;
|
||||
onPreviewUpdate(dataUrl);
|
||||
} catch (err) {
|
||||
console.error('Preview processing failed:', err);
|
||||
} finally {
|
||||
isProcessing = false;
|
||||
onProcessingChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize with current processed image
|
||||
$effect(() => {
|
||||
if (image.processedDataUrl && !previewDataUrl) {
|
||||
previewDataUrl = image.processedDataUrl;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<!-- Preview container -->
|
||||
<div class="relative w-full max-w-md aspect-[3/4] bg-grey-100 rounded-lg overflow-hidden shadow-inner">
|
||||
{#if previewDataUrl}
|
||||
<img
|
||||
src={previewDataUrl}
|
||||
alt="Preview"
|
||||
class="absolute inset-0 w-full h-full object-contain"
|
||||
/>
|
||||
{:else if image.originalDataUrl}
|
||||
<img
|
||||
src={image.originalDataUrl}
|
||||
alt="Original"
|
||||
class="absolute inset-0 w-full h-full object-contain opacity-50"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<!-- Processing overlay -->
|
||||
{#if isProcessing}
|
||||
<div class="absolute inset-0 flex items-center justify-center bg-white/70">
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<Loader2 class="w-8 h-8 text-accent animate-spin" />
|
||||
<span class="text-sm text-grey-500">Processing...</span>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Image info -->
|
||||
<div class="text-center">
|
||||
<p class="text-sm font-medium text-ink">{image.filename}</p>
|
||||
{#if image.originalDimensions}
|
||||
<p class="text-xs text-grey-400 font-mono mt-1">
|
||||
{image.originalDimensions.width} × {image.originalDimensions.height}
|
||||
→
|
||||
{device.width} × {device.height}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Original vs Preview toggle (future enhancement) -->
|
||||
<!-- <div class="flex items-center gap-2">
|
||||
<button class="text-sm text-grey-500 hover:text-ink">Original</button>
|
||||
<button class="text-sm text-accent font-medium">Preview</button>
|
||||
</div> -->
|
||||
</div>
|
||||
Reference in New Issue
Block a user