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.
This commit is contained in:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user