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>
This commit is contained in:
188
hooks/use-auto-save.ts
Normal file
188
hooks/use-auto-save.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user