Files
PaintPlus/frontend/src/components/EyeCatalog.jsx
T
Claude 2813d930b0 Fix UX issues and add Eye Catalog UI
Fixes:
- Remove scale limit to allow image to fill canvas
- Fix Lasso tool by using Polygon instead of Polyline
- Remove duplicate Clear Selection button
- Fix Undo/Redo dependencies with useCallback

New Features:
- Eye Catalog UI for browsing and applying saved eyes
- Upload new eyes to catalog
- Apply eyes to selected areas with feathering
- Delete eyes from catalog
2026-01-25 05:17:10 +00:00

194 lines
5.4 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect } from 'react';
import { patchesApi } from '../utils/api';
import './EyeCatalog.css';
const EyeCatalog = ({
projectId,
selection,
feather,
onApply,
isProcessing,
}) => {
const [patches, setPatches] = useState([]);
const [selectedPatch, setSelectedPatch] = useState(null);
const [isUploading, setIsUploading] = useState(false);
const [showUpload, setShowUpload] = useState(false);
const [uploadName, setUploadName] = useState('');
const [uploadFile, setUploadFile] = useState(null);
const [uploadTags, setUploadTags] = useState('');
const [error, setError] = useState(null);
// Load patches on mount
useEffect(() => {
loadPatches();
}, []);
const loadPatches = async () => {
try {
const data = await patchesApi.list('eyes');
setPatches(data);
} catch (err) {
console.error('Failed to load patches:', err);
}
};
const handleUpload = async () => {
if (!uploadFile || !uploadName.trim()) {
setError('Please provide a name and select a file');
return;
}
try {
setIsUploading(true);
setError(null);
await patchesApi.create(uploadName.trim(), uploadFile, 'eyes', uploadTags);
await loadPatches();
setShowUpload(false);
setUploadName('');
setUploadFile(null);
setUploadTags('');
} catch (err) {
setError(`Upload failed: ${err.message}`);
} finally {
setIsUploading(false);
}
};
const handleApply = async () => {
if (!selectedPatch || !selection || !projectId) {
setError('Please select an eye and make a selection on the image');
return;
}
try {
setError(null);
await patchesApi.apply(projectId, selectedPatch.id, selection.bbox, feather);
onApply();
setSelectedPatch(null);
} catch (err) {
setError(`Failed to apply: ${err.message}`);
}
};
const handleDelete = async (patchId) => {
if (!window.confirm('Delete this eye from the catalog?')) return;
try {
await patchesApi.delete(patchId);
await loadPatches();
if (selectedPatch?.id === patchId) {
setSelectedPatch(null);
}
} catch (err) {
setError(`Delete failed: ${err.message}`);
}
};
return (
<div className="eye-catalog">
<div className="catalog-header">
<h3>Eye Catalog</h3>
<button
className="upload-toggle-btn"
onClick={() => setShowUpload(!showUpload)}
>
{showUpload ? 'Cancel' : '+ Add Eye'}
</button>
</div>
{error && (
<div className="catalog-error">
{error}
<button onClick={() => setError(null)}>×</button>
</div>
)}
{showUpload && (
<div className="upload-form">
<input
type="text"
placeholder="Eye name (e.g., 'Greek Serene Left')"
value={uploadName}
onChange={(e) => setUploadName(e.target.value)}
/>
<input
type="file"
accept="image/*"
onChange={(e) => setUploadFile(e.target.files[0])}
/>
<input
type="text"
placeholder="Tags (e.g., 'greek,serene,left')"
value={uploadTags}
onChange={(e) => setUploadTags(e.target.value)}
/>
<button
className="upload-btn"
onClick={handleUpload}
disabled={isUploading || !uploadFile || !uploadName.trim()}
>
{isUploading ? 'Uploading...' : 'Upload Eye'}
</button>
</div>
)}
<div className="patches-grid">
{patches.length === 0 ? (
<div className="no-patches">
No eyes in catalog yet. Click "+ Add Eye" to upload.
</div>
) : (
patches.map((patch) => (
<div
key={patch.id}
className={`patch-item ${selectedPatch?.id === patch.id ? 'selected' : ''}`}
onClick={() => setSelectedPatch(patch)}
>
<img
src={patchesApi.getImageUrl(patch.id, true)}
alt={patch.name}
onError={(e) => {
e.target.src = patchesApi.getImageUrl(patch.id, false);
}}
/>
<div className="patch-info">
<span className="patch-name">{patch.name}</span>
{patch.tags && <span className="patch-tags">{patch.tags}</span>}
</div>
<button
className="delete-patch-btn"
onClick={(e) => {
e.stopPropagation();
handleDelete(patch.id);
}}
>
×
</button>
</div>
))
)}
</div>
{selectedPatch && (
<div className="apply-section">
<div className="selected-preview">
<strong>Selected:</strong> {selectedPatch.name}
</div>
<button
className="apply-btn"
onClick={handleApply}
disabled={isProcessing || !selection}
>
{!selection ? 'Draw selection first' : 'Apply to Selection'}
</button>
<p className="apply-hint">
Draw a rectangle/ellipse where you want to place the eye
</p>
</div>
)}
</div>
);
};
export default EyeCatalog;