Files
paint-ui/hooks/use-file-operations.ts
Sebastian Krüger 3ad7dbf314 feat(ui): implement comprehensive toast notification system
Added a complete toast notification system with:
- Toast store using Zustand for state management
- Toast component with 4 types: success, error, warning, info
- Animated slide-in/slide-out transitions
- Auto-dismiss after configurable duration
- Close button on each toast
- Utility functions for easy access (toast.success(), toast.error(), etc.)

Integrated toast notifications into file operations:
- Success notifications for: open image, open project, export image, save project
- Error notifications for: failed operations
- Warning notifications for: unsupported file types

UI Features:
- Stacks toasts in top-right corner
- Color-coded by type with icons (CheckCircle, AlertCircle, AlertTriangle, Info)
- Accessible with ARIA attributes
- Smooth animations using custom CSS keyframes

This provides immediate user feedback for all major operations throughout
the application.

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

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

218 lines
5.7 KiB
TypeScript

import { useCallback } from 'react';
import { useCanvasStore, useLayerStore } from '@/store';
import { useHistoryStore } from '@/store/history-store';
import {
openImageFile,
exportCanvasAsImage,
exportProject,
loadProject,
createCanvasFromDataURL,
extractImageFromDataTransfer,
isImageFile,
isProjectFile,
} from '@/lib/file-utils';
import { toast } from '@/lib/toast-utils';
import type { Layer } from '@/types';
export function useFileOperations() {
const { width, height, setDimensions } = useCanvasStore();
const { layers, createLayer, clearLayers } = useLayerStore();
const { clearHistory } = useHistoryStore();
/**
* Create new image
*/
const createNewImage = useCallback(
(newWidth: number, newHeight: number, backgroundColor: string) => {
clearLayers();
clearHistory();
setDimensions(newWidth, newHeight);
createLayer({
name: 'Background',
width: newWidth,
height: newHeight,
fillColor: backgroundColor,
});
},
[clearLayers, clearHistory, setDimensions, createLayer]
);
/**
* Open image file
*/
const openImage = useCallback(
async (file: File) => {
try {
const img = await openImageFile(file);
// Create new image with loaded dimensions
clearLayers();
clearHistory();
setDimensions(img.width, img.height);
// Create layer with loaded image
const layer = createLayer({
name: file.name,
width: img.width,
height: img.height,
});
if (layer.canvas) {
const ctx = layer.canvas.getContext('2d');
if (ctx) {
ctx.drawImage(img, 0, 0);
}
}
toast.success(`Opened ${file.name}`);
} catch (error) {
console.error('Failed to open image:', error);
toast.error('Failed to open image file');
}
},
[clearLayers, clearHistory, setDimensions, createLayer]
);
/**
* Open project file
*/
const openProject = useCallback(
async (file: File) => {
try {
const projectData = await loadProject(file);
clearLayers();
clearHistory();
setDimensions(projectData.width, projectData.height);
// Recreate layers
for (const layerData of projectData.layers) {
const layer = createLayer({
name: layerData.name,
width: layerData.width,
height: layerData.height,
opacity: layerData.opacity,
blendMode: layerData.blendMode as any,
});
// Load image data
if (layerData.imageData && layer.canvas) {
const canvas = await createCanvasFromDataURL(
layerData.imageData,
layerData.width,
layerData.height
);
const ctx = layer.canvas.getContext('2d');
if (ctx) {
ctx.drawImage(canvas, 0, 0);
}
}
}
toast.success(`Opened project ${file.name}`);
} catch (error) {
console.error('Failed to open project:', error);
toast.error('Failed to open project file');
}
},
[clearLayers, clearHistory, setDimensions, createLayer]
);
/**
* Export current view as image
*/
const exportImage = useCallback(
async (format: 'png' | 'jpeg' | 'webp', quality: number, filename: string) => {
try {
// Create temporary canvas with all layers
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const ctx = tempCanvas.getContext('2d');
if (!ctx) {
toast.error('Failed to create export canvas');
return;
}
// Draw all visible layers
layers
.filter((layer) => layer.visible && layer.canvas)
.sort((a, b) => a.order - b.order)
.forEach((layer) => {
if (!layer.canvas) return;
ctx.globalAlpha = layer.opacity;
ctx.globalCompositeOperation = layer.blendMode as GlobalCompositeOperation;
ctx.drawImage(layer.canvas, layer.x, layer.y);
});
ctx.globalAlpha = 1;
ctx.globalCompositeOperation = 'source-over';
await exportCanvasAsImage(tempCanvas, format, quality, filename);
toast.success(`Exported ${filename}.${format}`);
} catch (error) {
console.error('Failed to export image:', error);
toast.error('Failed to export image');
}
},
[layers, width, height]
);
/**
* Save project file
*/
const saveProject = useCallback(
async (filename: string) => {
try {
await exportProject(layers, width, height, filename);
toast.success(`Saved project ${filename}.json`);
} catch (error) {
console.error('Failed to save project:', error);
toast.error('Failed to save project');
}
},
[layers, width, height]
);
/**
* Handle file drop or paste
*/
const handleFileInput = useCallback(
async (file: File) => {
if (isProjectFile(file)) {
await openProject(file);
} else if (isImageFile(file)) {
await openImage(file);
} else {
toast.warning('Unsupported file type');
}
},
[openProject, openImage]
);
/**
* Handle data transfer (drag & drop or paste)
*/
const handleDataTransfer = useCallback(
async (dataTransfer: DataTransfer) => {
const file = extractImageFromDataTransfer(dataTransfer);
if (file) {
await handleFileInput(file);
}
},
[handleFileInput]
);
return {
createNewImage,
openImage,
openProject,
exportImage,
saveProject,
handleFileInput,
handleDataTransfer,
};
}