Refactor image conversion logic in convert-image-to-webp.ts to handle HEIC files and improve compatibility with Safari. Update ReceiptForm and +page components to streamline image upload process, removing camera functionality and enhancing user feedback during image processing. Adjust styles for better accessibility and clarity in image handling.
CI / build (push) Has been skipped
CI / deploy (push) Successful in 1m50s

This commit is contained in:
eewing
2026-02-19 15:07:32 -06:00
parent 132e376b9f
commit 8b977b5e86
3 changed files with 96 additions and 237 deletions
+4 -98
View File
@@ -125,11 +125,6 @@
let imageError = $state(false);
let imageConverting = $state(false);
let showCamera = $state(false);
let cameraError = $state<string | null>(null);
let videoEl = $state<HTMLVideoElement | undefined>(undefined);
let stream: MediaStream | null = null;
/** On small screens image is collapsed by default; on lg+ expanded. */
let imageOpen = $state(false);
let imageOpenInitialized = $state(false);
@@ -140,57 +135,6 @@
imageOpen = mq.matches;
});
async function startCamera() {
cameraError = null;
showCamera = true;
try {
stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: "environment", width: { ideal: 1280 }, height: { ideal: 720 } },
});
} catch (e) {
cameraError = e instanceof Error ? e.message : "Could not access camera.";
}
}
$effect(() => {
if (!showCamera || !stream || !videoEl) return;
videoEl.srcObject = stream;
videoEl.play().catch(() => {});
return () => {
if (videoEl?.srcObject) videoEl.srcObject = null;
};
});
function stopCamera() {
showCamera = false;
cameraError = null;
if (stream) {
stream.getTracks().forEach((t) => t.stop());
stream = null;
}
if (videoEl?.srcObject) videoEl.srcObject = null;
}
function capturePhoto() {
if (!videoEl || videoEl.readyState < 2) return;
const canvas = document.createElement("canvas");
canvas.width = videoEl.videoWidth;
canvas.height = videoEl.videoHeight;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.drawImage(videoEl, 0, 0);
canvas.toBlob(
(blob) => {
if (!blob) return;
selectedImageFile = new File([blob], "receipt.webp", { type: "image/webp", lastModified: Date.now() });
imageFileName = "receipt.webp";
stopCamera();
},
"image/webp",
0.85
);
}
async function onImageChange() {
const input = imageInputEl;
const file = input?.files?.[0] ?? null;
@@ -199,11 +143,6 @@
imageFileName = null;
return;
}
if (file.type === "image/webp") {
selectedImageFile = file;
imageFileName = file.name;
return;
}
imageConverting = true;
selectedImageFile = null;
imageFileName = null;
@@ -233,37 +172,6 @@
</svelte:head>
<main class="flex w-full max-w-full flex-col gap-6 px-4 py-6 sm:px-6 sm:py-8">
{#if showCamera}
<div
class="bg-background/95 fixed inset-0 z-[100] flex flex-col items-center justify-center gap-4 p-4 backdrop-blur-sm"
role="dialog"
aria-label="Take receipt photo"
>
<div class="flex w-full max-w-lg flex-col gap-3">
{#if cameraError}
<p class="text-destructive text-sm">{cameraError}</p>
{:else}
<video
bind:this={videoEl}
class="border-input aspect-video w-full rounded-lg border bg-black"
muted
playsinline
></video>
{/if}
<div class="flex gap-2">
<Button type="button" variant="outline" class="flex-1" onclick={stopCamera}>
Cancel
</Button>
{#if !cameraError}
<Button type="button" class="flex-1" onclick={capturePhoto}>
Capture
</Button>
{/if}
</div>
</div>
</div>
{/if}
<div class="flex items-center gap-3">
<a href="/receipts" class="text-muted-foreground hover:text-foreground shrink-0 text-sm" title="Back to receipts">Back</a>
<h1 class="text-xl font-semibold sm:text-2xl">Receipt</h1>
@@ -334,17 +242,18 @@
class="flex flex-col gap-4"
>
<input type="hidden" name="dataJson" value={buildDataJson()} />
<div class="relative flex flex-wrap items-center gap-2">
<div class="relative flex items-center gap-2">
<input
id="receipt-image-input"
bind:this={imageInputEl}
type="file"
name="Image"
accept="image/*"
capture="environment"
onchange={onImageChange}
class="absolute opacity-0 w-0 h-0 overflow-hidden pointer-events-none"
class="absolute opacity-0 w-0 h-0 overflow-hidden"
tabindex="-1"
aria-label="Choose image from gallery"
aria-label={receipt.Image ? "Replace image" : "Add image"}
/>
{#if imageConverting}
<div
@@ -356,9 +265,6 @@
<span>Processing image…</span>
</div>
{:else}
<Button type="button" variant="outline" size="sm" onclick={startCamera}>
Take photo
</Button>
<label
for="receipt-image-input"
class="border-input bg-background hover:bg-accent hover:text-accent-foreground inline-flex h-8 cursor-pointer items-center justify-center rounded-md border px-3 text-sm font-medium shadow-xs transition-colors"