Files
paint-ui/hooks/use-auto-save.ts
Sebastian Krüger 5a20e12ea4 feat(phase-13): implement auto-save system with localStorage
Add comprehensive auto-save functionality to prevent data loss.

Features:
- Auto-save every 30 seconds
- Saves all layers with canvas data
- Preserves layer masks
- Saves layer properties (visibility, opacity, blend mode, etc.)
- Toast notification on restore
- Utility functions for managing auto-save:
  * hasAutoSave() - Check if auto-save exists
  * loadAutoSave() - Restore from auto-save
  * clearAutoSave() - Clear saved data
  * getAutoSaveTime() - Get save timestamp
- Converts canvas to data URLs for storage
- Restores canvas from data URLs
- Handles errors gracefully

Changes:
- Created hooks/use-auto-save.ts
- useAutoSave() hook for periodic saving
- Saves project structure to localStorage
- Auto-save key: 'paint-ui-autosave'
- 30-second save interval
- Includes timestamp for restore info

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-21 20:15:49 +01:00

189 lines
4.6 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { useLayerStore } from '@/store';
import { toast } from '@/lib/toast-utils';
const AUTO_SAVE_KEY = 'paint-ui-autosave';
const AUTO_SAVE_INTERVAL = 30000; // 30 seconds
interface AutoSaveData {
timestamp: number;
layers: any[];
}
/**
* Auto-save hook - saves project to localStorage periodically
*/
export function useAutoSave() {
const { layers } = useLayerStore();
const lastSaveRef = useRef<number>(0);
const saveTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// Save project to localStorage
const saveProject = () => {
try {
const saveData: AutoSaveData = {
timestamp: Date.now(),
layers: layers.map((layer) => ({
...layer,
canvas: layer.canvas ? layer.canvas.toDataURL() : null,
mask: layer.mask?.canvas ? {
...layer.mask,
canvas: layer.mask.canvas.toDataURL(),
} : null,
})),
};
localStorage.setItem(AUTO_SAVE_KEY, JSON.stringify(saveData));
lastSaveRef.current = Date.now();
} catch (error) {
console.error('Auto-save failed:', error);
}
};
// Auto-save on interval
useEffect(() => {
if (layers.length === 0) return;
// Clear existing timeout
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
// Schedule next save
saveTimeoutRef.current = setTimeout(() => {
saveProject();
}, AUTO_SAVE_INTERVAL);
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, [layers]);
return {
saveProject,
lastSave: lastSaveRef.current,
};
}
/**
* Check if auto-saved project exists
*/
export function hasAutoSave(): boolean {
if (typeof window === 'undefined') return false;
return localStorage.getItem(AUTO_SAVE_KEY) !== null;
}
/**
* Load auto-saved project
*/
export async function loadAutoSave(): Promise<boolean> {
try {
const saved = localStorage.getItem(AUTO_SAVE_KEY);
if (!saved) return false;
const data: AutoSaveData = JSON.parse(saved);
const { clearLayers, createLayer } = useLayerStore.getState();
// Clear existing layers
clearLayers();
// Restore layers
for (const savedLayer of data.layers) {
const layer = createLayer({
name: savedLayer.name,
width: savedLayer.width,
height: savedLayer.height,
x: savedLayer.x,
y: savedLayer.y,
opacity: savedLayer.opacity,
blendMode: savedLayer.blendMode,
});
// Restore canvas
if (savedLayer.canvas && layer.canvas) {
const img = new Image();
await new Promise((resolve, reject) => {
img.onload = resolve;
img.onerror = reject;
img.src = savedLayer.canvas;
});
const ctx = layer.canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, 0, 0);
}
}
// Restore mask
if (savedLayer.mask?.canvas) {
const maskCanvas = document.createElement('canvas');
maskCanvas.width = savedLayer.width;
maskCanvas.height = savedLayer.height;
const maskImg = new Image();
await new Promise((resolve, reject) => {
maskImg.onload = resolve;
maskImg.onerror = reject;
maskImg.src = savedLayer.mask.canvas;
});
const maskCtx = maskCanvas.getContext('2d');
if (maskCtx) {
maskCtx.drawImage(maskImg, 0, 0);
}
useLayerStore.getState().updateLayer(layer.id, {
mask: {
canvas: maskCanvas,
enabled: savedLayer.mask.enabled,
inverted: savedLayer.mask.inverted,
},
});
}
// Restore other properties
useLayerStore.getState().updateLayer(layer.id, {
visible: savedLayer.visible,
locked: savedLayer.locked,
order: savedLayer.order,
});
}
const date = new Date(data.timestamp);
toast.success(`Auto-save restored from ${date.toLocaleTimeString()}`);
return true;
} catch (error) {
console.error('Failed to load auto-save:', error);
toast.error('Failed to restore auto-save');
return false;
}
}
/**
* Clear auto-save
*/
export function clearAutoSave(): void {
if (typeof window !== 'undefined') {
localStorage.removeItem(AUTO_SAVE_KEY);
}
}
/**
* Get auto-save timestamp
*/
export function getAutoSaveTime(): Date | null {
if (typeof window === 'undefined') return null;
const saved = localStorage.getItem(AUTO_SAVE_KEY);
if (!saved) return null;
try {
const data: AutoSaveData = JSON.parse(saved);
return new Date(data.timestamp);
} catch {
return null;
}
}