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