189 lines
4.6 KiB
TypeScript
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;
|
||
|
|
}
|
||
|
|
}
|