Files
paint-ui/hooks/use-file-operations.ts
Sebastian Krüger b93ae377d0 feat(phase-6): implement comprehensive file operations system
This commit completes Phase 6 of the paint-ui implementation, adding full
file import/export capabilities with drag-drop and clipboard support.

**New Files:**
- lib/file-utils.ts: Core file operations (open, save, export, project format)
- hooks/use-file-operations.ts: React hook for file operations
- hooks/use-drag-drop.ts: Drag & drop state management
- hooks/use-clipboard.ts: Clipboard paste event handling
- components/editor/file-menu.tsx: File menu dropdown component
- components/modals/export-dialog.tsx: Export dialog with format/quality options
- components/modals/new-image-dialog.tsx: New image dialog with presets
- components/modals/index.ts: Modals barrel export

**Updated Files:**
- components/editor/editor-layout.tsx: Integrated FileMenu, drag-drop overlay, clipboard paste
- components/editor/index.ts: Added file-menu export

**Features:**
-  Create new images with dimension presets (Full HD, HD, 800x600, custom)
-  Open image files (PNG, JPG, WEBP) as new layers
-  Save/load .paint project files (JSON with base64 layer data)
-  Export as PNG/JPEG/WEBP with quality control
-  Drag & drop file upload with visual overlay
-  Clipboard paste support (Ctrl+V)
-  File type validation and error handling
-  DataTransfer API integration for unified file handling

**Project File Format (.paint):**
- JSON structure with version, dimensions, layer metadata
- Base64-encoded PNG data for each layer
- Preserves layer properties (opacity, blend mode, order, visibility)

Build verified: ✓ Compiled successfully in 1233ms

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

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

198 lines
5.0 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 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);
}
}
} catch (error) {
console.error('Failed to open image:', error);
alert('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);
}
}
}
} catch (error) {
console.error('Failed to open project:', error);
alert('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) => {
// Create temporary canvas with all layers
const tempCanvas = document.createElement('canvas');
tempCanvas.width = width;
tempCanvas.height = height;
const ctx = tempCanvas.getContext('2d');
if (!ctx) 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);
},
[layers, width, height]
);
/**
* Save project file
*/
const saveProject = useCallback(
async (filename: string) => {
await exportProject(layers, width, height, filename);
},
[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 {
alert('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,
};
}