feat: load blocks from previous session

also remove annoying unsaved changes popup
This commit is contained in:
trafficlunar 2025-10-08 17:31:11 +01:00
parent 7a115e751d
commit 9b0cdfdc9b
5 changed files with 62 additions and 18 deletions

View file

@ -34,6 +34,7 @@
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"lucide-react": "^0.544.0", "lucide-react": "^0.544.0",
"nbtify": "^2.2.0", "nbtify": "^2.2.0",
"pako": "^2.1.0",
"pixi.js": "^8.13.2", "pixi.js": "^8.13.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
@ -46,6 +47,7 @@
"@eslint/js": "^9.36.0", "@eslint/js": "^9.36.0",
"@tailwindcss/postcss": "^4.1.14", "@tailwindcss/postcss": "^4.1.14",
"@types/node": "^24.6.1", "@types/node": "^24.6.1",
"@types/pako": "^2.0.4",
"@types/react": "^19.1.17", "@types/react": "^19.1.17",
"@types/react-dom": "^19.1.11", "@types/react-dom": "^19.1.11",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",

View file

@ -80,6 +80,9 @@ importers:
nbtify: nbtify:
specifier: ^2.2.0 specifier: ^2.2.0
version: 2.2.0 version: 2.2.0
pako:
specifier: ^2.1.0
version: 2.1.0
pixi.js: pixi.js:
specifier: ^8.13.2 specifier: ^8.13.2
version: 8.13.2 version: 8.13.2
@ -111,6 +114,9 @@ importers:
'@types/node': '@types/node':
specifier: ^24.6.1 specifier: ^24.6.1
version: 24.6.1 version: 24.6.1
'@types/pako':
specifier: ^2.0.4
version: 2.0.4
'@types/react': '@types/react':
specifier: ^19.1.17 specifier: ^19.1.17
version: 19.1.17 version: 19.1.17
@ -1270,6 +1276,9 @@ packages:
'@types/node@24.6.1': '@types/node@24.6.1':
resolution: {integrity: sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==} resolution: {integrity: sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==}
'@types/pako@2.0.4':
resolution: {integrity: sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==}
'@types/react-dom@19.1.11': '@types/react-dom@19.1.11':
resolution: {integrity: sha512-3BKc/yGdNTYQVVw4idqHtSOcFsgGuBbMveKCOgF8wQ5QtrYOc3jDIlzg3jef04zcXFIHLelyGlj0T+BJ8+KN+w==} resolution: {integrity: sha512-3BKc/yGdNTYQVVw4idqHtSOcFsgGuBbMveKCOgF8wQ5QtrYOc3jDIlzg3jef04zcXFIHLelyGlj0T+BJ8+KN+w==}
peerDependencies: peerDependencies:
@ -2090,6 +2099,9 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'} engines: {node: '>=10'}
pako@2.1.0:
resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
parent-module@1.0.1: parent-module@1.0.1:
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -3479,6 +3491,8 @@ snapshots:
dependencies: dependencies:
undici-types: 7.13.0 undici-types: 7.13.0
'@types/pako@2.0.4': {}
'@types/react-dom@19.1.11(@types/react@19.1.17)': '@types/react-dom@19.1.11(@types/react@19.1.17)':
dependencies: dependencies:
'@types/react': 19.1.17 '@types/react': 19.1.17
@ -4347,6 +4361,8 @@ snapshots:
dependencies: dependencies:
p-limit: 3.1.0 p-limit: 3.1.0
pako@2.1.0: {}
parent-module@1.0.1: parent-module@1.0.1:
dependencies: dependencies:
callsites: 3.1.0 callsites: 3.1.0

View file

@ -456,18 +456,6 @@ function Canvas() {
return () => resizeObserver.disconnect(); return () => resizeObserver.disconnect();
}, [loading, setStageSize]); }, [loading, setStageSize]);
// Window events handler
useEffect(() => {
const onBeforeUnload = (e: BeforeUnloadEvent) => {
e.preventDefault();
};
window.addEventListener("beforeunload", onBeforeUnload);
return () => {
window.removeEventListener("beforeunload", onBeforeUnload);
};
}, [onKeyDown, onKeyUp]);
useGesture( useGesture(
{ {
onPinch: ({ offset: [d] }) => { onPinch: ({ offset: [d] }) => {

View file

@ -17,6 +17,7 @@ import {
SquareDashedIcon, SquareDashedIcon,
Trash2Icon, Trash2Icon,
WandIcon, WandIcon,
SaveIcon,
} from "lucide-react"; } from "lucide-react";
import { HistoryContext } from "@/context/History"; import { HistoryContext } from "@/context/History";
@ -32,6 +33,7 @@ const iconMap = {
"Magic Wand": WandIcon, "Magic Wand": WandIcon,
"Move Selection": MoveIcon, "Move Selection": MoveIcon,
"New Canvas": PresentationIcon, "New Canvas": PresentationIcon,
"Load Previous Session": SaveIcon,
"Open Image": ImageIcon, "Open Image": ImageIcon,
"Open Schematic": FileIcon, "Open Schematic": FileIcon,
"Paint Bucket": PaintBucketIcon, "Paint Bucket": PaintBucketIcon,

View file

@ -1,4 +1,5 @@
import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react"; import React, { createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
import pako from "pako";
import { HistoryContext } from "./History"; import { HistoryContext } from "./History";
import welcomeBlocksData from "@/data/welcome.json"; import welcomeBlocksData from "@/data/welcome.json";
@ -89,14 +90,49 @@ export const CanvasProvider = ({ children }: Props) => {
}, [canvasSize, stageSize]); }, [canvasSize, stageSize]);
useEffect(() => { useEffect(() => {
addHistory( // Load blocks from previous session (if any)
"New Canvas", const localStorageBlocks = localStorage.getItem("blocks");
() => setBlocks(welcomeBlocksData), if (localStorageBlocks) {
() => setBlocks([]) try {
); // Convert stored string (Base64) back to bytes
// eslint-disable-next-line react-hooks/exhaustive-deps const compressedData = Uint8Array.from(atob(localStorageBlocks), (c) => c.charCodeAt(0));
const decompressed = pako.inflate(compressedData, { to: "string" });
const parsedBlocks = JSON.parse(decompressed);
setBlocks(parsedBlocks);
addHistory(
"Load Previous Session",
() => setBlocks(parsedBlocks),
() => setBlocks(welcomeBlocksData)
);
} catch (err) {
console.error("Failed to load blocks from localStorage:", err);
localStorage.removeItem("blocks");
}
} else {
// Add history entry for new canvas
addHistory(
"New Canvas",
() => setBlocks(welcomeBlocksData),
() => setBlocks([])
);
}
}, []); }, []);
// Set blocks to localStorage for session persistence
useEffect(() => {
// If blocks are the same as the starting welcome blocks, return
if (JSON.stringify(blocks) === JSON.stringify(welcomeBlocksData)) return;
const encoder = new TextEncoder();
const data = encoder.encode(JSON.stringify(blocks));
const compressed = pako.deflate(data);
// Store compressed data as Base64 string
const base64 = btoa(String.fromCharCode(...compressed));
localStorage.setItem("blocks", base64);
}, [blocks]);
return ( return (
<CanvasContext.Provider <CanvasContext.Provider
value={{ value={{