From d396b5ec2b0d9b10b901cb04418565a8c2016413 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Thu, 26 Jun 2025 19:37:54 -0400 Subject: [PATCH 01/13] Add share target --- app/[locale]/calibrate/page.tsx | 28 ++++++++++++++++++ app/manifest.js | 13 +++++++++ app/sw.ts | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 92 insertions(+) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index cb18ef20..3ccf0163 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -385,6 +385,34 @@ export default function Page() { // EFFECTS + useEffect(() => { + if ("serviceWorker" in navigator && window.serwist !== undefined) { + window.serwist.register(); + + // Client-side logic to handle shared files: listens for messages from the Service Worker + const handleServiceWorkerMessage = (event) => { + if (event.data && event.data.type === "shared-file") { + console.log( + "Client: Received shared file from service worker:", + event.data, + ); + const fileData = event.data; + const blob = new Blob([fileData.data], { type: fileData.fileType }); + const url = URL.createObjectURL(blob); + setFile(new File([blob], fileData.name, { type: fileData.fileType })); + + return () => { + URL.revokeObjectURL(url); + }; + } + }; + navigator.serviceWorker.addEventListener( + "message", + handleServiceWorkerMessage, + ); + } + }); + // Allow the user to open the file from their file browser, e.g., "Open With" useEffect(() => { requestWakeLock(); diff --git a/app/manifest.js b/app/manifest.js index cac5e991..71a043e0 100644 --- a/app/manifest.js +++ b/app/manifest.js @@ -44,5 +44,18 @@ export default function manifest() { }, }, ], + share_target: { + action: "/shared-target", + method: "POST", + enctype: "multipart/form-data", + params: { + files: [ + { + name: "shared_file", + accept: ["application/pdf", "image/svg+xml"], + }, + ], + }, + }, }; } diff --git a/app/sw.ts b/app/sw.ts index 5cfac5da..7ad32dc6 100644 --- a/app/sw.ts +++ b/app/sw.ts @@ -1,6 +1,8 @@ import { defaultCache } from "@serwist/next/browser"; import type { PrecacheEntry } from "@serwist/precaching"; import { installSerwist } from "@serwist/sw"; +import { NavigationRoute, registerRoute } from "@serwist/routing"; +import { NetworkOnly } from "@serwist/strategies"; declare const self: ServiceWorkerGlobalScope & { // Change this attribute's name to your `injectionPoint`. @@ -9,6 +11,55 @@ declare const self: ServiceWorkerGlobalScope & { __SW_MANIFEST: (PrecacheEntry | string)[] | undefined; }; +registerRoute( + new NavigationRoute( + async ({ url, request, event }) => { + console.log( + `My Service Worker: Handling navigation request for ${url.pathname}`, + ); + if (request.method === "POST" && url.pathname === "/share-target") { + try { + const formData = await request.formData(); + const sharedFile = formData.get("shared_file"); + + if (sharedFile instanceof File) { + const fileBuffer = await sharedFile.arrayBuffer(); + const allClients = await self.clients.matchAll({ + includeUncontrolled: true, + type: "window", + }); + + for (const client of allClients) { + client.postMessage({ + type: "shared-file", + name: sharedFile.name, + size: sharedFile.size, + fileType: sharedFile.type, + data: fileBuffer, + }); + } + return Response.redirect("/calibrate", 303); + } else { + console.error( + "Service Worker: No file received or file is not an instance of File.", + ); + return new Response("No file received for shared-file.", { + status: 400, + }); + } + } catch (error) { + console.error("Service Worker: Error handling shared file:", error); + return new Response("Error processing shared file.", { status: 500 }); + } + } + return new NetworkOnly().handle({ url, request, event }); + }, + { + allowlist: [new RegExp("/share-target")], + }, + ), +); + installSerwist({ precacheEntries: self.__SW_MANIFEST, skipWaiting: true, From af49e94f23e5b5a55fd6784956b94d3f6c9b4369 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Thu, 26 Jun 2025 19:41:15 -0400 Subject: [PATCH 02/13] Fix build error --- app/[locale]/calibrate/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index 3ccf0163..f60205cd 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -390,7 +390,7 @@ export default function Page() { window.serwist.register(); // Client-side logic to handle shared files: listens for messages from the Service Worker - const handleServiceWorkerMessage = (event) => { + const handleServiceWorkerMessage = (event: any) => { if (event.data && event.data.type === "shared-file") { console.log( "Client: Received shared file from service worker:", From b641ebff648dc9b3d00a55bd21399fd566750672 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Thu, 26 Jun 2025 20:43:47 -0400 Subject: [PATCH 03/13] Fix typo --- app/manifest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/manifest.js b/app/manifest.js index 71a043e0..895a4b4b 100644 --- a/app/manifest.js +++ b/app/manifest.js @@ -45,7 +45,7 @@ export default function manifest() { }, ], share_target: { - action: "/shared-target", + action: "/share-target", method: "POST", enctype: "multipart/form-data", params: { From 2cbedd860cf16ff6d17904ee9213e016f087a482 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Fri, 27 Jun 2025 08:15:59 -0400 Subject: [PATCH 04/13] Add sharing --- app/manifest.js | 2 +- app/sw.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/manifest.js b/app/manifest.js index 895a4b4b..71a043e0 100644 --- a/app/manifest.js +++ b/app/manifest.js @@ -45,7 +45,7 @@ export default function manifest() { }, ], share_target: { - action: "/share-target", + action: "/shared-target", method: "POST", enctype: "multipart/form-data", params: { diff --git a/app/sw.ts b/app/sw.ts index 7ad32dc6..fa58ba24 100644 --- a/app/sw.ts +++ b/app/sw.ts @@ -17,7 +17,7 @@ registerRoute( console.log( `My Service Worker: Handling navigation request for ${url.pathname}`, ); - if (request.method === "POST" && url.pathname === "/share-target") { + if (request.method === "POST" && url.pathname === "/shared-target") { try { const formData = await request.formData(); const sharedFile = formData.get("shared_file"); @@ -55,7 +55,7 @@ registerRoute( return new NetworkOnly().handle({ url, request, event }); }, { - allowlist: [new RegExp("/share-target")], + allowlist: [new RegExp("/shared-target")], }, ), ); From fa7cad384118452c2c134593dc9d4c0ead7d1342 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Fri, 27 Jun 2025 08:39:23 -0400 Subject: [PATCH 05/13] Set POST --- app/sw.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/sw.ts b/app/sw.ts index fa58ba24..abb41e73 100644 --- a/app/sw.ts +++ b/app/sw.ts @@ -58,6 +58,8 @@ registerRoute( allowlist: [new RegExp("/shared-target")], }, ), + undefined, // This is the default handler for navigation requests. + "POST", ); installSerwist({ From e2a05dc5d5e677460b179fac568c2eb72bc86fc1 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Fri, 27 Jun 2025 09:01:43 -0400 Subject: [PATCH 06/13] Use callback so POST param is not ignored --- app/sw.ts | 77 ++++++++++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 41 deletions(-) diff --git a/app/sw.ts b/app/sw.ts index abb41e73..c121084e 100644 --- a/app/sw.ts +++ b/app/sw.ts @@ -12,53 +12,48 @@ declare const self: ServiceWorkerGlobalScope & { }; registerRoute( - new NavigationRoute( - async ({ url, request, event }) => { - console.log( - `My Service Worker: Handling navigation request for ${url.pathname}`, - ); - if (request.method === "POST" && url.pathname === "/shared-target") { - try { - const formData = await request.formData(); - const sharedFile = formData.get("shared_file"); + /shared-target/, + async ({ url, request, event }) => { + console.log( + `My Service Worker: Handling navigation request for ${url.pathname}`, + ); + if (request.method === "POST" && url.pathname === "/shared-target") { + try { + const formData = await request.formData(); + const sharedFile = formData.get("shared_file"); - if (sharedFile instanceof File) { - const fileBuffer = await sharedFile.arrayBuffer(); - const allClients = await self.clients.matchAll({ - includeUncontrolled: true, - type: "window", - }); + if (sharedFile instanceof File) { + const fileBuffer = await sharedFile.arrayBuffer(); + const allClients = await self.clients.matchAll({ + includeUncontrolled: true, + type: "window", + }); - for (const client of allClients) { - client.postMessage({ - type: "shared-file", - name: sharedFile.name, - size: sharedFile.size, - fileType: sharedFile.type, - data: fileBuffer, - }); - } - return Response.redirect("/calibrate", 303); - } else { - console.error( - "Service Worker: No file received or file is not an instance of File.", - ); - return new Response("No file received for shared-file.", { - status: 400, + for (const client of allClients) { + client.postMessage({ + type: "shared-file", + name: sharedFile.name, + size: sharedFile.size, + fileType: sharedFile.type, + data: fileBuffer, }); } - } catch (error) { - console.error("Service Worker: Error handling shared file:", error); - return new Response("Error processing shared file.", { status: 500 }); + return Response.redirect("/calibrate", 303); + } else { + console.error( + "Service Worker: No file received or file is not an instance of File.", + ); + return new Response("No file received for shared-file.", { + status: 400, + }); } + } catch (error) { + console.error("Service Worker: Error handling shared file:", error); + return new Response("Error processing shared file.", { status: 500 }); } - return new NetworkOnly().handle({ url, request, event }); - }, - { - allowlist: [new RegExp("/shared-target")], - }, - ), - undefined, // This is the default handler for navigation requests. + } + return new NetworkOnly().handle({ url, request, event }); + }, "POST", ); From fb72f5bedc8c2f1f15e41b8053aeb2d2d072edcb Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Fri, 27 Jun 2025 09:14:22 -0400 Subject: [PATCH 07/13] Add logging --- app/[locale]/calibrate/page.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index f60205cd..f6ddf0fe 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -386,11 +386,16 @@ export default function Page() { // EFFECTS useEffect(() => { + console.log("Client: Registering service worker..."); if ("serviceWorker" in navigator && window.serwist !== undefined) { window.serwist.register(); - + console.log("Client: Service Worker registered successfully."); // Client-side logic to handle shared files: listens for messages from the Service Worker const handleServiceWorkerMessage = (event: any) => { + console.log( + "Client: Received message from service worker:", + event.data, + ); if (event.data && event.data.type === "shared-file") { console.log( "Client: Received shared file from service worker:", From 9b13261571423e898bba6975c08f56879cd3eab2 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Fri, 27 Jun 2025 09:20:06 -0400 Subject: [PATCH 08/13] Add [] to useEffect --- app/[locale]/calibrate/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index f6ddf0fe..19fa34b4 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -416,7 +416,7 @@ export default function Page() { handleServiceWorkerMessage, ); } - }); + }, []); // Allow the user to open the file from their file browser, e.g., "Open With" useEffect(() => { From f71f09d2f6b41e0056ccf0553d7ae7ef9d195994 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Sat, 28 Jun 2025 09:35:02 -0400 Subject: [PATCH 09/13] Cache shared file and add open url parameter. --- app/[locale]/calibrate/page.tsx | 53 ++++++++++++++-------------- app/manifest.js | 2 +- app/sw.ts | 61 ++++++++++++++++++++++++--------- next.config.mjs | 3 +- 4 files changed, 76 insertions(+), 43 deletions(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index 19fa34b4..65625beb 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -384,37 +384,40 @@ export default function Page() { } // EFFECTS + useEffect(() => { + console.log("checking for open file in URL parameters"); + const params = new URL(location.href).searchParams; + const openFile = params.get("open"); + const name = params.get("name") ?? ""; + if (openFile !== null) { + console.log("Client: openFile found in URL parameters."); + fetch(openFile) + .then((response) => response.blob()) + .then((blob) => { + const file = new File([blob], name, { + type: blob.type, + }); + setFile(file); + console.log("Client: Shared file loaded successfully."); + // Check for shared file URL in query parameters on initial load + if (window.history.replaceState) { + const url = new URL(window.location.href); + url.searchParams.delete("open"); + url.searchParams.delete("name"); + window.history.replaceState({ path: url.href }, "", url.href); + } + }) + .catch((error) => { + console.error("Client: Error loading shared file:", error); + }); + } + }, []); useEffect(() => { console.log("Client: Registering service worker..."); if ("serviceWorker" in navigator && window.serwist !== undefined) { window.serwist.register(); console.log("Client: Service Worker registered successfully."); - // Client-side logic to handle shared files: listens for messages from the Service Worker - const handleServiceWorkerMessage = (event: any) => { - console.log( - "Client: Received message from service worker:", - event.data, - ); - if (event.data && event.data.type === "shared-file") { - console.log( - "Client: Received shared file from service worker:", - event.data, - ); - const fileData = event.data; - const blob = new Blob([fileData.data], { type: fileData.fileType }); - const url = URL.createObjectURL(blob); - setFile(new File([blob], fileData.name, { type: fileData.fileType })); - - return () => { - URL.revokeObjectURL(url); - }; - } - }; - navigator.serviceWorker.addEventListener( - "message", - handleServiceWorkerMessage, - ); } }, []); diff --git a/app/manifest.js b/app/manifest.js index 71a043e0..d8830131 100644 --- a/app/manifest.js +++ b/app/manifest.js @@ -23,7 +23,7 @@ export default function manifest() { orientation: "landscape", lang: "en-US", dir: "ltr", - scope: "https://patternprojector.com", + scope: "/", scope_extensions: [{ origin: "*.patternprojector.com" }], prefer_related_applications: false, launch_handler: { diff --git a/app/sw.ts b/app/sw.ts index c121084e..3824bca9 100644 --- a/app/sw.ts +++ b/app/sw.ts @@ -1,7 +1,7 @@ import { defaultCache } from "@serwist/next/browser"; import type { PrecacheEntry } from "@serwist/precaching"; import { installSerwist } from "@serwist/sw"; -import { NavigationRoute, registerRoute } from "@serwist/routing"; +import { registerRoute } from "@serwist/routing"; import { NetworkOnly } from "@serwist/strategies"; declare const self: ServiceWorkerGlobalScope & { @@ -24,21 +24,29 @@ registerRoute( if (sharedFile instanceof File) { const fileBuffer = await sharedFile.arrayBuffer(); - const allClients = await self.clients.matchAll({ - includeUncontrolled: true, - type: "window", - }); - - for (const client of allClients) { - client.postMessage({ - type: "shared-file", - name: sharedFile.name, - size: sharedFile.size, - fileType: sharedFile.type, - data: fileBuffer, - }); - } - return Response.redirect("/calibrate", 303); + // use the cache fo all requests + const cache = await caches.open("shared-file-cache"); + console.log( + `Service Worker: Caching shared file ${sharedFile.name} with size ${sharedFile.size} bytes`, + ); + // Store the file in the cache with a request to the shared-file endpoint + // This allows us to retrieve it later using the same URL + // The request URL is `/shared-file/` and the response is the file content + // with appropriate headers + await cache.put( + new Request(`/shared-file/`), + new Response(fileBuffer, { + headers: { + "Content-Type": sharedFile.type, + "Content-Length": sharedFile.size.toString(), + "Content-Disposition": `attachment; filename="${sharedFile.name}"`, + }, + }), + ); + const openFileUrl = new URL("/calibrate", self.location.origin); + openFileUrl.searchParams.set("name", sharedFile.name); + openFileUrl.searchParams.set("open", "/shared-file/"); + return Response.redirect(openFileUrl, 303); } else { console.error( "Service Worker: No file received or file is not an instance of File.", @@ -57,6 +65,27 @@ registerRoute( "POST", ); +registerRoute( + ({ url }) => url.pathname.startsWith("/shared-file/"), + async ({ url, request }) => { + console.log( + `My Service Worker: Handling request for shared file ${url.pathname}`, + ); + const cache = await caches.open("shared-file-cache"); + const cachedResponse = await cache.match(request); + if (cachedResponse) { + console.log( + `My Service Worker: Found cached response for ${url.pathname}`, + ); + return cachedResponse; + } else { + console.log(`My Service Worker: No cached response for ${url.pathname}`); + return new Response("Shared file not found in cache.", { status: 404 }); + } + }, + "GET", +); + installSerwist({ precacheEntries: self.__SW_MANIFEST, skipWaiting: true, diff --git a/next.config.mjs b/next.config.mjs index bc54aa18..4af9c029 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -6,7 +6,8 @@ const withSerwist = withSerwistInit({ swDest: "public/sw.js", cacheOnFrontEndNav: true, reloadOnOnline: true, - disable: process.env.NODE_ENV === "development", + //disable: process.env.NODE_ENV === "development", + register: false, }); const withNextIntl = createNextIntlPlugin(); From 44fe254af94851e2e69cf06a8653bdaa35055b6a Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Sat, 28 Jun 2025 19:48:03 -0400 Subject: [PATCH 10/13] Auto register service worker --- app/[locale]/calibrate/page.tsx | 8 -------- next.config.mjs | 3 +-- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index 65625beb..ebd4c91b 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -413,14 +413,6 @@ export default function Page() { } }, []); - useEffect(() => { - console.log("Client: Registering service worker..."); - if ("serviceWorker" in navigator && window.serwist !== undefined) { - window.serwist.register(); - console.log("Client: Service Worker registered successfully."); - } - }, []); - // Allow the user to open the file from their file browser, e.g., "Open With" useEffect(() => { requestWakeLock(); diff --git a/next.config.mjs b/next.config.mjs index 4af9c029..bc54aa18 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -6,8 +6,7 @@ const withSerwist = withSerwistInit({ swDest: "public/sw.js", cacheOnFrontEndNav: true, reloadOnOnline: true, - //disable: process.env.NODE_ENV === "development", - register: false, + disable: process.env.NODE_ENV === "development", }); const withNextIntl = createNextIntlPlugin(); From d3b5cd9445544f93f4e3b96f9d1a1a4d8a2efab4 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Thu, 3 Jul 2025 20:33:38 -0400 Subject: [PATCH 11/13] support drag and drop files --- app/[locale]/calibrate/page.tsx | 139 +++++++++++++++++++++----------- 1 file changed, 93 insertions(+), 46 deletions(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index ebd4c91b..4901797d 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -102,6 +102,7 @@ export default function Page() { const width = Number(widthInput) > 0 ? Number(widthInput) : 1; const height = Number(heightInput) > 0 ? Number(heightInput) : 1; const [isCalibrating, setIsCalibrating] = useState(true); + const [isFileDragging, setIsFileDragging] = useState(false); const [fileLoadStatus, setFileLoadStatus] = useState( LoadStatusEnum.DEFAULT, ); @@ -279,55 +280,52 @@ export default function Page() { updateLocalSettings({ width: w }); } - // Set new file; reset file based state; and if available, load file based state from localStorage - function handleFileChange(e: ChangeEvent): void { - const { files } = e.target; + function openPatternFile(file: File): boolean { + if (!isValidFile(file)) { + return false; + } + setFile(file); + setFileLoadStatus(LoadStatusEnum.LOADING); + setRestoreTransforms(null); + setZoomedOut(false); + setMagnifying(false); + setMeasuring(false); + setPageCount(0); + setLayers({}); + dispatchPatternScaleAction({ type: "set", scale: "1.00" }); + const lineThicknessString = localStorage.getItem( + `lineThickness:${file.name}`, + ); + if (lineThicknessString !== null) { + setLineThickness(Number(lineThicknessString)); + } else { + setLineThickness(0); + } - if (files && files[0] && isValidFile(files[0])) { - setFile(files[0]); - setFileLoadStatus(LoadStatusEnum.LOADING); - setRestoreTransforms(null); - setZoomedOut(false); - setMagnifying(false); - setMeasuring(false); - setPageCount(0); - setLayers({}); - dispatchPatternScaleAction({ type: "set", scale: "1.00" }); - const lineThicknessString = localStorage.getItem( - `lineThickness:${files[0].name}`, - ); - if (lineThicknessString !== null) { - setLineThickness(Number(lineThicknessString)); - } else { - setLineThickness(0); + const key = `stitchSettings:${file.name ?? "default"}`; + const stitchSettingsString = localStorage.getItem(key); + if (stitchSettingsString !== null) { + const stitchSettings = JSON.parse(stitchSettingsString); + if (!stitchSettings.lineCount) { + // Old naming + stitchSettings.lineCount = stitchSettings.columnCount; } - - const key = `stitchSettings:${files[0].name ?? "default"}`; - const stitchSettingsString = localStorage.getItem(key); - if (stitchSettingsString !== null) { - const stitchSettings = JSON.parse(stitchSettingsString); - if (!stitchSettings.lineCount) { - // Old naming - stitchSettings.lineCount = stitchSettings.columnCount; - } - if (!stitchSettings.lineDirection) { - // For people who saved stitch settings before Line Direction was an option - stitchSettings.lineDirection = LineDirection.Column; - } - dispatchStitchSettings({ type: "set", stitchSettings }); - } else { - dispatchStitchSettings({ - type: "set", - stitchSettings: { - ...defaultStitchSettings, - key, - }, - }); + if (!stitchSettings.lineDirection) { + // For people who saved stitch settings before Line Direction was an option + stitchSettings.lineDirection = LineDirection.Column; } - - calibrationCallback(); + dispatchStitchSettings({ type: "set", stitchSettings }); + } else { + dispatchStitchSettings({ + type: "set", + stitchSettings: { + ...defaultStitchSettings, + key, + }, + }); } + calibrationCallback(); // If the user calibrated in full screen, try to go back into full screen after opening the file: some browsers pop users out of full screen when selecting a file const expectedContext = localStorage.getItem("calibrationContext"); if (expectedContext !== null) { @@ -338,8 +336,53 @@ export default function Page() { } } catch (e) {} } + return true; } + // Set new file; reset file based state; and if available, load file based state from localStorage + function handleFileChange(e: ChangeEvent): void { + const { files } = e.target; + if (files && files[0]) { + openPatternFile(files[0]); + } + } + + const handleDragEnter = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsFileDragging(true); + }, []); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsFileDragging(false); + }, []); + + const handleDragOver = useCallback((e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + // Set effect to copy (optional, but good practice for clarity) + e.dataTransfer.dropEffect = "copy"; + setIsFileDragging(true); // Keep it highlighted during dragover + }, []); + + const handleDrop = useCallback( + (e: React.DragEvent) => { + e.preventDefault(); + e.stopPropagation(); + setIsFileDragging(false); // Reset dragging state + + const files = e.dataTransfer.files; + + if (files && files.length > 0) { + const file = files[0]; + openPatternFile(file); + } + }, + [openPatternFile], + ); + function handlePointerDown(e: React.PointerEvent) { resetIdle(); @@ -397,7 +440,7 @@ export default function Page() { const file = new File([blob], name, { type: blob.type, }); - setFile(file); + openPatternFile(file); console.log("Client: Shared file loaded successfully."); // Check for shared file URL in query parameters on initial load if (window.history.replaceState) { @@ -422,7 +465,7 @@ export default function Page() { for (const handle of launchParams.files) { if (handle.kind == "file") { const file = await (handle as FileSystemFileHandle).getFile(); - setFile(file); + openPatternFile(file); return; } } @@ -549,6 +592,10 @@ export default function Page() {
From dc08b75a77b3d12030be404e357ed3e51a3f7928 Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Fri, 4 Jul 2025 06:09:39 -0400 Subject: [PATCH 12/13] Show calibration warning below menus --- app/[locale]/calibrate/page.tsx | 45 ++++++++++++++++----------------- 1 file changed, 22 insertions(+), 23 deletions(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index 4901797d..c49f98db 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -604,28 +604,6 @@ export default function Page() { handle={fullScreenHandle} className="bg-white dark:bg-black transition-all duration-500 w-screen h-screen" > - {showCalibrationAlert ? ( -
- -

{t("calibrationAlert")}

- -

{t("calibrationAlertContinue")}

-
- ) : null} {g("error")} @@ -758,7 +736,28 @@ export default function Page() { patternScale={String(patternScaleFactor)} /> - + {showCalibrationAlert ? ( +
+ +

{t("calibrationAlert")}

+ +

{t("calibrationAlertContinue")}

+
+ ) : null} From 9028dcfc55a95754579b15963c3675089158e72f Mon Sep 17 00:00:00 2001 From: courtneypattison Date: Fri, 4 Jul 2025 06:39:46 -0400 Subject: [PATCH 13/13] drag through warning. --- app/[locale]/calibrate/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/[locale]/calibrate/page.tsx b/app/[locale]/calibrate/page.tsx index c49f98db..57440c2e 100644 --- a/app/[locale]/calibrate/page.tsx +++ b/app/[locale]/calibrate/page.tsx @@ -737,11 +737,11 @@ export default function Page() { /> {showCalibrationAlert ? ( -
+

{t("calibrationAlert")}