diff --git a/.svelte-kit/generated/server/internal.js b/.svelte-kit/generated/server/internal.js
index d1b56de8..d835717f 100644
--- a/.svelte-kit/generated/server/internal.js
+++ b/.svelte-kit/generated/server/internal.js
@@ -24,7 +24,7 @@ export const options = {
app: ({ head, body, assets, nonce, env }) => "\n\n
\n \n S\" />\n \n \n \n \n \n \n \n \n " + head + "\n \n \n " + body + "
\n \n\n",
error: ({ status, message }) => "\n\n\t\n\t\t\n\t\t" + message + "\n\n\t\t\n\t\n\t\n\t\t\n\t\t\t
" + status + "\n\t\t\t
\n\t\t\t\t
" + message + "
\n\t\t\t\n\t\t
\n\t\n\n"
},
- version_hash: "qnx61n"
+ version_hash: "co0587"
};
export async function get_hooks() {
diff --git a/src/lib/convert-image-to-webp.ts b/src/lib/convert-image-to-webp.ts
index fcf9480e..cdaf9623 100644
--- a/src/lib/convert-image-to-webp.ts
+++ b/src/lib/convert-image-to-webp.ts
@@ -11,6 +11,13 @@ function isHeicFile(file: File): boolean {
);
}
+/** True if the file is an image we can process (image/* or HEIC by type/extension). */
+export function isAcceptableImageFile(file: File | null): boolean {
+ if (!file) return false;
+ const type = (file.type ?? "").toLowerCase();
+ return type.startsWith("image/") || isHeicFile(file);
+}
+
function makeOutputFile(blob: Blob, baseName: string, ext: string): File {
return new File([blob], `${baseName}.${ext}`, {
type: blob.type,
@@ -18,78 +25,93 @@ function makeOutputFile(blob: Blob, baseName: string, ext: string): File {
});
}
-/**
- * Converts an image File to WebP (or JPEG on Safari where WebP isn't supported).
- * HEIC from iPhone is handled: if the image fails to load, we decode with heic2any first.
- */
-export function convertImageToWebP(file: File, quality = 0.85): Promise {
+function isSafari(): boolean {
+ if (typeof navigator === "undefined") return false;
+ const ua = navigator.userAgent;
+ return ua.includes("Safari") && !ua.includes("Chrome");
+}
+
+/** Load blob in Image, draw to canvas, encode as WebP or JPEG. Rejects if image fails to load or encode. */
+function tryCanvas(sourceBlob: Blob, baseName: string, quality: number): Promise {
return new Promise((resolve, reject) => {
- const baseName = file.name.replace(/\.[^.]+$/i, "") || "image";
-
- const tryCanvas = (sourceBlob: Blob) => {
- const url = URL.createObjectURL(sourceBlob);
- const img = new Image();
- img.onload = () => {
- URL.revokeObjectURL(url);
- const canvas = document.createElement("canvas");
- canvas.width = img.naturalWidth;
- canvas.height = img.naturalHeight;
- const ctx = canvas.getContext("2d");
- if (!ctx) {
- reject(new Error("Could not get canvas context"));
- return;
- }
- ctx.drawImage(img, 0, 0);
- // Safari doesn't support WebP in toBlob; try WebP first, fall back to JPEG
- canvas.toBlob(
- (blob) => {
- if (blob && blob.type === "image/webp") {
- resolve(makeOutputFile(blob, baseName, "webp"));
- return;
- }
- canvas.toBlob(
- (jpegBlob) => {
- if (!jpegBlob) {
- reject(new Error("Failed to encode image"));
- return;
- }
- resolve(makeOutputFile(jpegBlob, baseName, "jpg"));
- },
- "image/jpeg",
- quality
- );
- },
- "image/webp",
- quality
- );
- };
- img.onerror = () => {
- URL.revokeObjectURL(url);
- reject(new Error("Failed to load image"));
- };
- img.src = url;
- };
-
- const url = URL.createObjectURL(file);
+ const url = URL.createObjectURL(sourceBlob);
const img = new Image();
img.onload = () => {
URL.revokeObjectURL(url);
- tryCanvas(file);
- };
- img.onerror = async () => {
- URL.revokeObjectURL(url);
- // HEIC often fails to load in Image() on iOS; decode with heic2any then run canvas
- if (isHeicFile(file)) {
- try {
- const jpegBlob = await convertHeicBlobToJpeg(file);
- tryCanvas(jpegBlob);
- } catch (e) {
- reject(e instanceof Error ? e : new Error("Failed to convert HEIC"));
- }
- } else {
- reject(new Error("Failed to load image"));
+ const canvas = document.createElement("canvas");
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ const ctx = canvas.getContext("2d");
+ if (!ctx) {
+ reject(new Error("Could not get canvas context"));
+ return;
}
+ ctx.drawImage(img, 0, 0);
+ // Safari doesn't support WebP in toBlob; try WebP first, fall back to JPEG
+ canvas.toBlob(
+ (blob) => {
+ if (blob && blob.type === "image/webp") {
+ resolve(makeOutputFile(blob, baseName, "webp"));
+ return;
+ }
+ canvas.toBlob(
+ (jpegBlob) => {
+ if (!jpegBlob) {
+ reject(new Error("Failed to encode image"));
+ return;
+ }
+ resolve(makeOutputFile(jpegBlob, baseName, "jpg"));
+ },
+ "image/jpeg",
+ quality
+ );
+ },
+ "image/webp",
+ quality
+ );
+ };
+ img.onerror = () => {
+ URL.revokeObjectURL(url);
+ reject(new Error("Failed to load image"));
};
img.src = url;
});
}
+
+/**
+ * Converts an image File to WebP (or JPEG on Safari where WebP isn't supported).
+ * HEIC: Safari uses native Image() first, then heic2any fallback; other browsers use heic2any first, then Image() fallback.
+ */
+export async function convertImageToWebP(file: File, quality = 0.85): Promise {
+ const baseName = file.name.replace(/\.[^.]+$/i, "") || "image";
+
+ if (isHeicFile(file)) {
+ const safari = isSafari();
+ if (safari) {
+ // Safari can decode HEIC in Image(); heic2any is unreliable on Safari (e.g. transparent output)
+ try {
+ return await tryCanvas(file, baseName, quality);
+ } catch {
+ try {
+ const jpegBlob = await convertHeicBlobToJpeg(file);
+ return await tryCanvas(jpegBlob, baseName, quality);
+ } catch (e) {
+ throw e instanceof Error ? e : new Error("Failed to convert HEIC");
+ }
+ }
+ }
+ // Non-Safari: Image() usually can't decode HEIC; use heic2any first
+ try {
+ const jpegBlob = await convertHeicBlobToJpeg(file);
+ return await tryCanvas(jpegBlob, baseName, quality);
+ } catch {
+ try {
+ return await tryCanvas(file, baseName, quality);
+ } catch (e) {
+ throw e instanceof Error ? e : new Error("Failed to convert HEIC");
+ }
+ }
+ }
+
+ return await tryCanvas(file, baseName, quality);
+}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 5a745672..422a6805 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -17,27 +17,39 @@
Stackq
Go to
{/if}
diff --git a/src/routes/HomeWithDialog.svelte b/src/routes/HomeWithDialog.svelte
index cb1eeda1..16808db8 100644
--- a/src/routes/HomeWithDialog.svelte
+++ b/src/routes/HomeWithDialog.svelte
@@ -42,31 +42,41 @@
Stackq
-
-
-
-
+
+ Scan
+ Coming soon
+
+
+ New transaction
+ Coming soon
+
-
+
+ New Item
+ Coming soon
+
diff --git a/src/routes/receipts/ReceiptForm.svelte b/src/routes/receipts/ReceiptForm.svelte
index b19901a0..6f6e2ac0 100644
--- a/src/routes/receipts/ReceiptForm.svelte
+++ b/src/routes/receipts/ReceiptForm.svelte
@@ -1,7 +1,7 @@