@yogiswara/honcho-editor-ui 2.6.14 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/editor/HAccordionPreset.js +5 -4
- package/dist/hooks/demo/HonchoEditorBulkDemo.js +217 -70
- package/dist/hooks/editor/type.d.ts +2 -2
- package/dist/hooks/editor/useHonchoEditorBulk.js +12 -13
- package/dist/lib/context/EditorProcessingService.d.ts +2 -0
- package/dist/lib/context/EditorProcessingService.js +60 -1
- package/dist/lib/hooks/useImageProcessor.d.ts +4 -1
- package/dist/lib/hooks/useImageProcessor.js +66 -10
- package/dist/utils/imageLoader.js +1 -6
- package/package.json +1 -1
|
@@ -40,15 +40,16 @@ export default function HAccordionPreset(props) {
|
|
|
40
40
|
justifyContent: 'flex-start',
|
|
41
41
|
...typography.bodyMedium
|
|
42
42
|
}, onClick: () => props.onSelectPreset(preset.id), children: preset.name })), props.selectedPreset === preset.id && (_jsx(Button, { sx: {
|
|
43
|
-
|
|
43
|
+
flex: 1,
|
|
44
|
+
minWidth: 0,
|
|
44
45
|
textWrap: 'nowrap',
|
|
45
46
|
overflow: 'hidden',
|
|
46
47
|
textOverflow: 'ellipsis',
|
|
48
|
+
textTransform: 'none',
|
|
47
49
|
display: 'block',
|
|
48
50
|
color: colors.surface,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
ml: "0px",
|
|
51
|
+
pl: 0,
|
|
52
|
+
ml: 0,
|
|
52
53
|
justifyContent: 'flex-start',
|
|
53
54
|
...typography.bodyMedium
|
|
54
55
|
}, onClick: () => props.onSelectPreset(preset.id), children: preset.name })), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, children: [props.selectedPreset === preset.id && (_jsx(CardMedia, { component: "img", image: "v1/svg/check-ratio-editor.svg", sx: { width: "20px", height: "20px" } })), _jsx(IconButton, { "aria-label": preset.name, onClick: (event) => props.onPresetMenuClick(event, preset.id), sx: { padding: "0px", margin: "0px" }, children: _jsx(CardMedia, { component: "img", image: "/v1/svg/dots-editor.svg", alt: "Options", sx: { padding: "0px", margin: "0px" } }) })] })] }, preset.id))), _jsx(Button, { variant: "text", sx: { color: colors.surface, border: "1px solid",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import React, { useState } from 'react';
|
|
3
|
-
import { Box, Container, Typography, Button, Grid, Card,
|
|
2
|
+
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
|
3
|
+
import { Box, Container, Typography, Button, Grid, Card, CardContent, Checkbox, Chip, Alert, CircularProgress, ButtonGroup, Paper, Divider, FormControl, InputLabel, Select, MenuItem, Stack } from '@mui/material';
|
|
4
4
|
import { useHonchoEditorBulk } from '../editor/useHonchoEditorBulk';
|
|
5
|
+
import { useImageProcessor } from "../../lib/hooks/useImageProcessor";
|
|
5
6
|
// Mock data for demonstration
|
|
6
7
|
const createMockGallery = (id, adjustments) => ({
|
|
7
8
|
id,
|
|
@@ -9,28 +10,28 @@ const createMockGallery = (id, adjustments) => ({
|
|
|
9
10
|
event_id: 'demo-event',
|
|
10
11
|
download: {
|
|
11
12
|
key: `${id}-download`,
|
|
12
|
-
path: `https://
|
|
13
|
+
path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
|
|
13
14
|
size: 1024000,
|
|
14
15
|
width: 800,
|
|
15
16
|
height: 600,
|
|
16
17
|
},
|
|
17
18
|
download_edited: {
|
|
18
19
|
key: `${id}-download-edited`,
|
|
19
|
-
path: `https://
|
|
20
|
+
path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
|
|
20
21
|
size: 1024000,
|
|
21
22
|
width: 800,
|
|
22
23
|
height: 600,
|
|
23
24
|
},
|
|
24
25
|
thumbnail: {
|
|
25
26
|
key: `${id}-thumb`,
|
|
26
|
-
path: `https://
|
|
27
|
+
path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
|
|
27
28
|
size: 50000,
|
|
28
29
|
width: 300,
|
|
29
30
|
height: 200,
|
|
30
31
|
},
|
|
31
32
|
thumbnail_edited: {
|
|
32
33
|
key: `${id}-thumb-edited`,
|
|
33
|
-
path: `https://
|
|
34
|
+
path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/thumbnail.jpeg`,
|
|
34
35
|
size: 50000,
|
|
35
36
|
width: 300,
|
|
36
37
|
height: 200,
|
|
@@ -64,18 +65,13 @@ const createMockGallery = (id, adjustments) => ({
|
|
|
64
65
|
// Mock Controller implementation
|
|
65
66
|
const createMockController = () => {
|
|
66
67
|
console.log('[Controller] 🏭 createMockController() called - Creating new mock controller instance for bulk editor');
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
createMockGallery(
|
|
73
|
-
|
|
74
|
-
createMockGallery('7', { tempScore: -8, tintScore: 4 }),
|
|
75
|
-
createMockGallery('8'),
|
|
76
|
-
createMockGallery('9'),
|
|
77
|
-
createMockGallery('10'),
|
|
78
|
-
];
|
|
68
|
+
// Generate mock images from 1 to 1277 using picsum.dev static URLs
|
|
69
|
+
const mockImages = [];
|
|
70
|
+
for (let i = 1; i <= 1000; i++) {
|
|
71
|
+
const id = i.toString();
|
|
72
|
+
// let adjustments: Partial<AdjustmentState> | undefined;
|
|
73
|
+
mockImages.push(createMockGallery(id, undefined));
|
|
74
|
+
}
|
|
79
75
|
return {
|
|
80
76
|
onGetImage: async (uid, imageId) => {
|
|
81
77
|
console.log(`[Controller] 📷 onGetImage called: uid=${uid}, imageId=${imageId}`);
|
|
@@ -91,7 +87,7 @@ const createMockController = () => {
|
|
|
91
87
|
getImageList: async (uid, eventId, page) => {
|
|
92
88
|
console.log(`[Controller] 📋 getImageList called: uid=${uid}, eventId=${eventId}, page=${page}`);
|
|
93
89
|
await new Promise(resolve => setTimeout(resolve, 800));
|
|
94
|
-
const pageSize =
|
|
90
|
+
const pageSize = 1000; // Increased page size for better UX with more images
|
|
95
91
|
const startIndex = (page - 1) * pageSize;
|
|
96
92
|
const endIndex = startIndex + pageSize;
|
|
97
93
|
const pageImages = mockImages.slice(startIndex, endIndex);
|
|
@@ -103,7 +99,7 @@ const createMockController = () => {
|
|
|
103
99
|
next_page: endIndex < mockImages.length ? page + 1 : 0,
|
|
104
100
|
sum_of_image: pageImages.length,
|
|
105
101
|
};
|
|
106
|
-
console.log(`[Controller] 📋 getImageList returning ${pageImages.length} images for page ${page}`, result);
|
|
102
|
+
console.log(`[Controller] 📋 getImageList returning ${pageImages.length} images for page ${page} (total available: ${mockImages.length})`, result);
|
|
107
103
|
return result;
|
|
108
104
|
},
|
|
109
105
|
syncConfig: async (uid) => {
|
|
@@ -173,6 +169,191 @@ const createMockController = () => {
|
|
|
173
169
|
};
|
|
174
170
|
};
|
|
175
171
|
const AdjustmentControls = ({ label, onDecreaseMax, onDecrease, onIncrease, onIncreaseMax, disabled = false }) => (_jsxs(Box, { mb: 2, children: [_jsx(Typography, { variant: "body2", gutterBottom: true, children: label }), _jsxs(ButtonGroup, { size: "small", variant: "outlined", children: [_jsx(Button, { onClick: onDecreaseMax, disabled: disabled, children: "--" }), _jsx(Button, { onClick: onDecrease, disabled: disabled, children: "-" }), _jsx(Button, { onClick: onIncrease, disabled: disabled, children: "+" }), _jsx(Button, { onClick: onIncreaseMax, disabled: disabled, children: "++" })] })] }));
|
|
172
|
+
const ImageCard = React.memo(({ image, onToggleSelection }) => {
|
|
173
|
+
const [isVisible, setIsVisible] = useState(false);
|
|
174
|
+
const [shouldLoadImage, setShouldLoadImage] = useState(false);
|
|
175
|
+
const [isInView, setIsInView] = useState(false);
|
|
176
|
+
const cardRef = useRef(null);
|
|
177
|
+
const abortControllerRef = useRef(null);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const visibilityObserver = new IntersectionObserver(([entry]) => {
|
|
180
|
+
if (entry.isIntersecting) {
|
|
181
|
+
setIsVisible(true);
|
|
182
|
+
setIsInView(true);
|
|
183
|
+
// Delay image loading slightly to ensure smooth scrolling
|
|
184
|
+
setTimeout(() => setShouldLoadImage(true), 100);
|
|
185
|
+
// Don't disconnect here as we want to track when it goes out of view
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
setIsInView(false);
|
|
189
|
+
// Cancel processing when going out of view
|
|
190
|
+
if (abortControllerRef.current) {
|
|
191
|
+
console.debug(`[ImageCard] Cancelling processing for image ${image.key} - out of view`);
|
|
192
|
+
abortControllerRef.current.abort();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, {
|
|
196
|
+
threshold: 0.1, // Trigger when 10% of the element is visible
|
|
197
|
+
rootMargin: '100px', // Start loading 100px before the element comes into view
|
|
198
|
+
});
|
|
199
|
+
if (cardRef.current) {
|
|
200
|
+
visibilityObserver.observe(cardRef.current);
|
|
201
|
+
}
|
|
202
|
+
return () => {
|
|
203
|
+
visibilityObserver.disconnect();
|
|
204
|
+
// Cancel any ongoing processing when component unmounts
|
|
205
|
+
if (abortControllerRef.current) {
|
|
206
|
+
abortControllerRef.current.abort();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
}, [image.key]);
|
|
210
|
+
// Track adjustments changes for debugging
|
|
211
|
+
useEffect(() => {
|
|
212
|
+
console.log("[ImageCard] Adjustments changed for image", image.key, ":", image.adjustments);
|
|
213
|
+
}, [image.adjustments, image.key]);
|
|
214
|
+
// Create a key that changes when adjustments change to force abort controller recreation
|
|
215
|
+
const adjustmentsKey = useMemo(() => {
|
|
216
|
+
return JSON.stringify(image.adjustments);
|
|
217
|
+
}, [image.adjustments]);
|
|
218
|
+
// Store the current abort signal in a ref to prevent it from changing on every render
|
|
219
|
+
const currentAbortSignalRef = useRef();
|
|
220
|
+
const isInitializedRef = useRef(false);
|
|
221
|
+
// Much more conservative abort controller management - only recreate when absolutely necessary
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (shouldLoadImage && isInView) {
|
|
224
|
+
// Only create abort controller if we don't have one OR if this is the first time
|
|
225
|
+
if (!abortControllerRef.current || !isInitializedRef.current) {
|
|
226
|
+
console.debug(`[ImageCard] Creating initial abort controller for image ${image.key}`);
|
|
227
|
+
if (abortControllerRef.current) {
|
|
228
|
+
abortControllerRef.current.abort();
|
|
229
|
+
}
|
|
230
|
+
abortControllerRef.current = new AbortController();
|
|
231
|
+
currentAbortSignalRef.current = abortControllerRef.current.signal;
|
|
232
|
+
isInitializedRef.current = true;
|
|
233
|
+
}
|
|
234
|
+
// For adjustment changes, don't recreate the controller - let useImageProcessor handle it
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
// Only clean up when not visible or not loading
|
|
238
|
+
if (abortControllerRef.current && isInitializedRef.current) {
|
|
239
|
+
console.debug(`[ImageCard] Cleaning up abort controller for image ${image.key} - not visible or not loading`);
|
|
240
|
+
abortControllerRef.current.abort();
|
|
241
|
+
abortControllerRef.current = null;
|
|
242
|
+
currentAbortSignalRef.current = undefined;
|
|
243
|
+
isInitializedRef.current = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return () => {
|
|
247
|
+
if (abortControllerRef.current) {
|
|
248
|
+
abortControllerRef.current.abort();
|
|
249
|
+
abortControllerRef.current = null;
|
|
250
|
+
currentAbortSignalRef.current = undefined;
|
|
251
|
+
isInitializedRef.current = false;
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}, [shouldLoadImage, isInView, image.key]); // Remove adjustmentsKey from dependencies
|
|
255
|
+
// Separate effect to handle adjustment changes without recreating abort controller
|
|
256
|
+
useEffect(() => {
|
|
257
|
+
if (shouldLoadImage && isInView && isInitializedRef.current) {
|
|
258
|
+
console.debug(`[ImageCard] Adjustments changed for image ${image.key} - letting useImageProcessor handle the change`);
|
|
259
|
+
}
|
|
260
|
+
}, [adjustmentsKey, shouldLoadImage, isInView, image.key]);
|
|
261
|
+
// Memoize the useImageProcessor call to prevent unnecessary hook calls
|
|
262
|
+
const imageProcessorParams = useMemo(() => ({
|
|
263
|
+
photoId: image.key,
|
|
264
|
+
photoSrc: image.src,
|
|
265
|
+
adjustments: image.adjustments,
|
|
266
|
+
enableEditor: shouldLoadImage && isInView, // Only enable when we should load the image AND it's in view
|
|
267
|
+
abortSignal: currentAbortSignalRef.current // Use stable abort signal
|
|
268
|
+
}), [image.key, image.src, image.adjustments, shouldLoadImage, isInView]);
|
|
269
|
+
// Only call useImageProcessor when the card is visible AND we should load the image AND it's in view
|
|
270
|
+
const { processedImageSrc, isProcessingComplete, cancelProcessing, isProcessing } = useImageProcessor(imageProcessorParams);
|
|
271
|
+
// Cancel processing when card goes out of view
|
|
272
|
+
useEffect(() => {
|
|
273
|
+
if (!isInView && isProcessing) {
|
|
274
|
+
console.debug(`[ImageCard] Cancelling processing for image ${image.key} - no longer in view`);
|
|
275
|
+
cancelProcessing();
|
|
276
|
+
}
|
|
277
|
+
}, [isInView, isProcessing, cancelProcessing, image.key]);
|
|
278
|
+
// Memoize the toggle selection handler to prevent unnecessary re-renders
|
|
279
|
+
const handleToggleSelection = useMemo(() => {
|
|
280
|
+
return () => onToggleSelection(image.key);
|
|
281
|
+
}, [onToggleSelection, image.key]);
|
|
282
|
+
const handleCheckboxClick = useMemo(() => {
|
|
283
|
+
return (e) => {
|
|
284
|
+
e.stopPropagation();
|
|
285
|
+
onToggleSelection(image.key);
|
|
286
|
+
};
|
|
287
|
+
}, [onToggleSelection, image.key]);
|
|
288
|
+
// Create placeholder content for non-visible cards
|
|
289
|
+
const renderPlaceholder = () => (_jsxs(Box, { sx: {
|
|
290
|
+
height: 200,
|
|
291
|
+
backgroundColor: 'grey.100',
|
|
292
|
+
display: 'flex',
|
|
293
|
+
alignItems: 'center',
|
|
294
|
+
justifyContent: 'center',
|
|
295
|
+
flexDirection: 'column'
|
|
296
|
+
}, children: [_jsx(CircularProgress, { size: 24, sx: { mb: 1 } }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: "Loading..." })] }));
|
|
297
|
+
// Create processing indicator
|
|
298
|
+
const renderProcessingOverlay = () => {
|
|
299
|
+
return isProcessing && shouldLoadImage && isInView ? (_jsxs(Box, { sx: {
|
|
300
|
+
position: 'absolute',
|
|
301
|
+
top: 8,
|
|
302
|
+
right: 8,
|
|
303
|
+
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
|
304
|
+
color: 'white',
|
|
305
|
+
borderRadius: 1,
|
|
306
|
+
px: 1,
|
|
307
|
+
py: 0.5,
|
|
308
|
+
display: 'flex',
|
|
309
|
+
alignItems: 'center',
|
|
310
|
+
gap: 0.5,
|
|
311
|
+
zIndex: 1,
|
|
312
|
+
maxWidth: '120px'
|
|
313
|
+
}, children: [_jsx(CircularProgress, { size: 12, color: "inherit" }), _jsx(Typography, { variant: "caption", children: "Processing" })] })) : null;
|
|
314
|
+
};
|
|
315
|
+
return (_jsxs(Card, { ref: cardRef, sx: {
|
|
316
|
+
border: image.isSelected ? 2 : 1,
|
|
317
|
+
borderColor: image.isSelected ? 'primary.main' : 'divider',
|
|
318
|
+
cursor: 'pointer',
|
|
319
|
+
transition: 'all 0.2s',
|
|
320
|
+
position: 'relative',
|
|
321
|
+
opacity: 1,
|
|
322
|
+
'&:hover': {
|
|
323
|
+
transform: 'translateY(-2px)',
|
|
324
|
+
boxShadow: 2,
|
|
325
|
+
}
|
|
326
|
+
}, onClick: handleToggleSelection, children: [shouldLoadImage ? (_jsxs(Box, { sx: { position: 'relative', width: '100%', height: '200px' }, children: [_jsx("img", { id: "image-card-media", src: processedImageSrc, alt: "", style: {
|
|
327
|
+
width: '100%',
|
|
328
|
+
height: '200px',
|
|
329
|
+
objectFit: 'cover',
|
|
330
|
+
opacity: 1,
|
|
331
|
+
display: 'block'
|
|
332
|
+
}, loading: "lazy" }), renderProcessingOverlay()] })) : (renderPlaceholder()), _jsx(CardContent, { sx: { pb: 1 }, children: _jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "space-between", children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Image ", image.key, !shouldLoadImage && (_jsx(Typography, { component: "span", variant: "caption", sx: { ml: 1 }, children: "(waiting...)" })), !isInView && shouldLoadImage && (_jsx(Typography, { component: "span", variant: "caption", sx: { ml: 1, color: 'warning.main' }, children: "(paused)" }))] }), _jsx(Checkbox, { checked: image.isSelected, size: "small", onClick: handleCheckboxClick })] }) })] }));
|
|
333
|
+
}, (prevProps, nextProps) => {
|
|
334
|
+
// Custom comparison function for React.memo - be more strict to prevent unnecessary re-renders
|
|
335
|
+
const prevAdj = prevProps.image.adjustments || {};
|
|
336
|
+
const nextAdj = nextProps.image.adjustments || {};
|
|
337
|
+
// Only re-render if key properties actually change
|
|
338
|
+
const shouldUpdate = (prevProps.image.key !== nextProps.image.key ||
|
|
339
|
+
prevProps.image.isSelected !== nextProps.image.isSelected ||
|
|
340
|
+
prevProps.image.src !== nextProps.image.src ||
|
|
341
|
+
// Compare individual adjustment properties to avoid JSON.stringify issues
|
|
342
|
+
prevAdj.temperature !== nextAdj.temperature ||
|
|
343
|
+
prevAdj.tint !== nextAdj.tint ||
|
|
344
|
+
prevAdj.exposure !== nextAdj.exposure ||
|
|
345
|
+
prevAdj.contrast !== nextAdj.contrast ||
|
|
346
|
+
prevAdj.clarity !== nextAdj.clarity ||
|
|
347
|
+
prevAdj.vibrance !== nextAdj.vibrance ||
|
|
348
|
+
prevAdj.saturation !== nextAdj.saturation ||
|
|
349
|
+
prevAdj.highlights !== nextAdj.highlights ||
|
|
350
|
+
prevAdj.shadows !== nextAdj.shadows ||
|
|
351
|
+
prevAdj.whites !== nextAdj.whites ||
|
|
352
|
+
prevAdj.blacks !== nextAdj.blacks ||
|
|
353
|
+
prevAdj.sharpness !== nextAdj.sharpness);
|
|
354
|
+
// Return false to re-render, true to skip re-render
|
|
355
|
+
return !shouldUpdate;
|
|
356
|
+
});
|
|
176
357
|
export const HonchoEditorBulkDemo = () => {
|
|
177
358
|
const [controller] = useState(() => createMockController());
|
|
178
359
|
const [showAdjustments, setShowAdjustments] = useState(false);
|
|
@@ -191,42 +372,23 @@ export const HonchoEditorBulkDemo = () => {
|
|
|
191
372
|
handleBulkClarityDecreaseMax, handleBulkClarityDecrease, handleBulkClarityIncrease, handleBulkClarityIncreaseMax,
|
|
192
373
|
// History actions
|
|
193
374
|
handleUndo, handleRedo, handleReset, historyInfo, } = useHonchoEditorBulk(controller, 'demo-event', 'demo-user');
|
|
194
|
-
// Debug logging for preset state - less frequent
|
|
195
|
-
React.useEffect(() => {
|
|
196
|
-
const interval = setInterval(() => {
|
|
197
|
-
console.log('[HonchoEditorBulkDemo] Preset state:', {
|
|
198
|
-
presets: presets.map(p => ({ id: p.id, name: p.name })),
|
|
199
|
-
selectedBulkPreset,
|
|
200
|
-
activePreset: activePreset ? { id: activePreset.id, name: activePreset.name } : null,
|
|
201
|
-
selectedCount: selectedIds.length
|
|
202
|
-
});
|
|
203
|
-
}, 5000); // Every 5 seconds instead of on every render
|
|
204
|
-
return () => clearInterval(interval);
|
|
205
|
-
}, [presets, selectedBulkPreset, activePreset, selectedIds.length]);
|
|
206
375
|
const selectedCount = selectedIds.length;
|
|
207
376
|
const totalCount = imageData.length;
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
if (image.clarityScore !== 0)
|
|
224
|
-
adjustments.push(`Cla: ${image.clarityScore > 0 ? '+' : ''}${image.clarityScore}`);
|
|
225
|
-
if (image.vibranceScore !== 0)
|
|
226
|
-
adjustments.push(`Vib: ${image.vibranceScore > 0 ? '+' : ''}${image.vibranceScore}`);
|
|
227
|
-
return adjustments;
|
|
228
|
-
};
|
|
229
|
-
return (_jsxs(Container, { maxWidth: "xl", sx: { py: 4 }, children: [_jsx(Typography, { variant: "h3", gutterBottom: true, align: "center", children: "Honcho Editor Bulk Demo" }), _jsx(Typography, { variant: "subtitle1", align: "center", color: "text.secondary", gutterBottom: true, children: "This demo shows the useHonchoEditorBulk hook in action with mock data" }), error && (_jsx(Alert, { severity: "error", sx: { mb: 3 }, children: error })), _jsx(Paper, { sx: { p: 3, mb: 3 }, children: _jsxs(Stack, { direction: "row", spacing: 2, alignItems: "center", flexWrap: "wrap", children: [_jsx(Button, { variant: "contained", onClick: handleRefresh, disabled: isLoading, children: isLoading ? _jsx(CircularProgress, { size: 20 }) : 'Refresh Images' }), _jsxs(Button, { variant: "outlined", onClick: () => setShowAdjustments(!showAdjustments), children: [showAdjustments ? 'Hide' : 'Show', " Adjustments"] }), _jsx(Button, { variant: "outlined", onClick: () => {
|
|
377
|
+
return (_jsxs(Container, { maxWidth: "xl", sx: { py: 4 }, children: [_jsx(Typography, { variant: "h3", gutterBottom: true, align: "center", children: "Honcho Editor Bulk Demo" }), _jsx(Typography, { variant: "subtitle1", align: "center", color: "text.secondary", gutterBottom: true, children: "This demo shows the useHonchoEditorBulk hook with 1,277 mock images from picsum.dev" }), error && (_jsx(Alert, { severity: "error", sx: { mb: 3 }, children: error })), _jsx(Paper, { sx: { p: 3, mb: 3 }, children: _jsxs(Stack, { direction: "row", spacing: 2, alignItems: "center", flexWrap: "wrap", children: [_jsx(Button, { variant: "contained", onClick: handleRefresh, disabled: isLoading, children: isLoading ? _jsx(CircularProgress, { size: 20 }) : 'Refresh Images' }), _jsxs(Button, { variant: "outlined", onClick: () => setShowAdjustments(!showAdjustments), children: [showAdjustments ? 'Hide' : 'Show', " Adjustments"] }), _jsx(Button, { variant: "outlined", onClick: () => {
|
|
378
|
+
// Select all images on current page
|
|
379
|
+
imageData.forEach(image => {
|
|
380
|
+
if (!image.isSelected) {
|
|
381
|
+
handleToggleImageSelection(image.key);
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
}, disabled: isLoading || imageData.length === 0, children: "Select All" }), _jsx(Button, { variant: "outlined", onClick: () => {
|
|
385
|
+
// Deselect all images
|
|
386
|
+
imageData.forEach(image => {
|
|
387
|
+
if (image.isSelected) {
|
|
388
|
+
handleToggleImageSelection(image.key);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}, disabled: isLoading || selectedCount === 0, children: "Clear Selection" }), _jsx(Button, { variant: "outlined", onClick: () => {
|
|
230
392
|
console.log('Debug Info:', {
|
|
231
393
|
selectedIds,
|
|
232
394
|
selectedCount,
|
|
@@ -236,22 +398,7 @@ export const HonchoEditorBulkDemo = () => {
|
|
|
236
398
|
activePreset: activePreset ? { id: activePreset.id, name: activePreset.name } : null,
|
|
237
399
|
imageData: imageData.map(img => ({ key: img.key, isSelected: img.isSelected }))
|
|
238
400
|
});
|
|
239
|
-
}, children: "Debug Selection" }), _jsx(Button, { variant: "outlined", onClick: handleBackCallbackBulk, children: "Back" }), _jsx(Chip, { label: `${selectedCount} of ${totalCount} selected`, color: selectedCount > 0 ? "primary" : "default" }), hasMore && (_jsx(Button, { variant: "outlined", onClick: handleLoadMore, disabled: isLoading, children: "Load More" })), _jsxs(FormControl, { size: "small", sx: { minWidth: 150 }, children: [_jsx(InputLabel, { children: "Bulk Preset" }), _jsxs(Select, { value: selectedBulkPreset, onChange: handleSelectBulkPreset, label: "Bulk Preset", displayEmpty: true, children: [_jsx(MenuItem, { value: "", children: _jsx("em", { children: "No Preset" }) }), presets.map((preset) => (_jsxs(MenuItem, { value: preset.id, children: [preset.name, activePreset?.id === preset.id && ' ✓'] }, preset.id)))] })] }), _jsxs(Typography, { variant: "body2", sx: { ml: 2, opacity: 0.7 }, children: ["Active: ", activePreset ? activePreset.name : 'None', " | Presets: ", presets.length] })] }) }), showAdjustments && (_jsxs(Paper, { sx: { p: 3, mb: 3 }, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { mb: 2 }, children: [_jsxs(Typography, { variant: "h6", children: ["Bulk Adjustments (", selectedCount, " images selected)"] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Button, { variant: "outlined", size: "small", onClick: handleUndo, disabled: !historyInfo.canUndo, sx: { minWidth: 80 }, children: "Undo" }), _jsx(Button, { variant: "outlined", size: "small", onClick: handleRedo, disabled: !historyInfo.canRedo, sx: { minWidth: 80 }, children: "Redo" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => handleReset(), disabled: selectedCount === 0, color: "warning", sx: { minWidth: 80 }, children: "Reset" })] })] }), selectedCount === 0 && (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: "Select one or more images below to enable bulk adjustments" })), _jsxs(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: ["History: ", historyInfo.currentIndex + 1, "/", historyInfo.totalStates, " states | Can Undo: ", historyInfo.canUndo ? 'Yes' : 'No', " | Can Redo: ", historyInfo.canRedo ? 'Yes' : 'No'] }), _jsxs(Grid, { container: true, spacing: 3, children: [_jsxs(Grid, { item: true, xs: 12, md: 3, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: "Color" }), _jsx(AdjustmentControls, { label: "Temperature", onDecreaseMax: handleBulkTempDecreaseMax, onDecrease: handleBulkTempDecrease, onIncrease: handleBulkTempIncrease, onIncreaseMax: handleBulkTempIncreaseMax, disabled: selectedCount === 0 }), _jsx(AdjustmentControls, { label: "Tint", onDecreaseMax: handleBulkTintDecreaseMax, onDecrease: handleBulkTintDecrease, onIncrease: handleBulkTintIncrease, onIncreaseMax: handleBulkTintIncreaseMax, disabled: selectedCount === 0 })] }), _jsxs(Grid, { item: true, xs: 12, md: 3, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: "Light" }), _jsx(AdjustmentControls, { label: "Exposure", onDecreaseMax: handleBulkExposureDecreaseMax, onDecrease: handleBulkExposureDecrease, onIncrease: handleBulkExposureIncrease, onIncreaseMax: handleBulkExposureIncreaseMax, disabled: selectedCount === 0 }), _jsx(AdjustmentControls, { label: "Contrast", onDecreaseMax: handleBulkContrastDecreaseMax, onDecrease: handleBulkContrastDecrease, onIncrease: handleBulkContrastIncrease, onIncreaseMax: handleBulkContrastIncreaseMax, disabled: selectedCount === 0 })] }), _jsxs(Grid, { item: true, xs: 12, md: 3, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: "Details" }), _jsx(AdjustmentControls, { label: "Clarity", onDecreaseMax: handleBulkClarityDecreaseMax, onDecrease: handleBulkClarityDecrease, onIncrease: handleBulkClarityIncrease, onIncreaseMax: handleBulkClarityIncreaseMax, disabled: selectedCount === 0 })] })] })] })), _jsx(Grid, { container: true, spacing: 2, children: imageData.map((image) => {
|
|
240
|
-
const adjustments = getAdjustmentSummary(image);
|
|
241
|
-
return (_jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, lg: 3, children: _jsxs(Card, { sx: {
|
|
242
|
-
border: image.isSelected ? 2 : 1,
|
|
243
|
-
borderColor: image.isSelected ? 'primary.main' : 'divider',
|
|
244
|
-
cursor: 'pointer',
|
|
245
|
-
transition: 'all 0.2s',
|
|
246
|
-
'&:hover': {
|
|
247
|
-
transform: 'translateY(-2px)',
|
|
248
|
-
boxShadow: 2,
|
|
249
|
-
}
|
|
250
|
-
}, onClick: () => handleToggleImageSelection(image.key), children: [_jsx(CardMedia, { component: "img", height: "200", image: image.src, alt: image.alt, sx: { objectFit: 'cover' } }), _jsxs(CardContent, { sx: { pb: 1 }, children: [_jsxs(Box, { display: "flex", alignItems: "center", justifyContent: "space-between", children: [_jsxs(Typography, { variant: "body2", color: "text.secondary", children: ["Image ", image.key] }), _jsx(Checkbox, { checked: image.isSelected, size: "small", onClick: (e) => {
|
|
251
|
-
e.stopPropagation();
|
|
252
|
-
handleToggleImageSelection(image.key);
|
|
253
|
-
} })] }), adjustments.length > 0 && (_jsx(Box, { mt: 1, children: _jsx(Stack, { direction: "row", spacing: 0.5, flexWrap: "wrap", children: adjustments.map((adj, index) => (_jsx(Chip, { label: adj, size: "small", variant: "outlined", sx: { fontSize: '0.7rem', height: 20 } }, index))) }) }))] })] }) }, image.key));
|
|
254
|
-
}) }), isLoading && imageData.length === 0 && (_jsx(Box, { display: "flex", justifyContent: "center", py: 4, children: _jsx(CircularProgress, {}) })), _jsxs(Box, { mt: 4, children: [_jsx(Divider, {}), _jsx(Typography, { variant: "body2", color: "text.secondary", align: "center", sx: { mt: 2 }, children: "Demo Notes: This uses mock data and simulated API calls. Select images and try the bulk adjustment controls above." })] })] }));
|
|
401
|
+
}, children: "Debug Selection" }), _jsx(Button, { variant: "outlined", onClick: handleBackCallbackBulk, children: "Back" }), _jsx(Chip, { label: `${selectedCount} of ${totalCount} selected`, color: selectedCount > 0 ? "primary" : "default" }), hasMore && (_jsx(Button, { variant: "outlined", onClick: handleLoadMore, disabled: isLoading, children: "Load More" })), _jsxs(FormControl, { size: "small", sx: { minWidth: 150 }, children: [_jsx(InputLabel, { children: "Bulk Preset" }), _jsxs(Select, { value: selectedBulkPreset, onChange: handleSelectBulkPreset, label: "Bulk Preset", displayEmpty: true, children: [_jsx(MenuItem, { value: "", children: _jsx("em", { children: "No Preset" }) }), presets.map((preset) => (_jsxs(MenuItem, { value: preset.id, children: [preset.name, activePreset?.id === preset.id && ' ✓'] }, preset.id)))] })] }), _jsxs(Typography, { variant: "body2", sx: { ml: 2, opacity: 0.7 }, children: ["Active: ", activePreset ? activePreset.name : 'None', " | Presets: ", presets.length] })] }) }), showAdjustments && (_jsxs(Paper, { sx: { p: 3, mb: 3 }, children: [_jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { mb: 2 }, children: [_jsxs(Typography, { variant: "h6", children: ["Bulk Adjustments (", selectedCount, " images selected)"] }), _jsxs(Stack, { direction: "row", spacing: 1, children: [_jsx(Button, { variant: "outlined", size: "small", onClick: handleUndo, disabled: !historyInfo.canUndo, sx: { minWidth: 80 }, children: "Undo" }), _jsx(Button, { variant: "outlined", size: "small", onClick: handleRedo, disabled: !historyInfo.canRedo, sx: { minWidth: 80 }, children: "Redo" }), _jsx(Button, { variant: "outlined", size: "small", onClick: () => handleReset(), disabled: selectedCount === 0, color: "warning", sx: { minWidth: 80 }, children: "Reset" })] })] }), selectedCount === 0 && (_jsx(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: "Select one or more images below to enable bulk adjustments" })), _jsxs(Typography, { variant: "body2", color: "text.secondary", sx: { mb: 2 }, children: ["History: ", historyInfo.currentIndex + 1, "/", historyInfo.totalStates, " states | Can Undo: ", historyInfo.canUndo ? 'Yes' : 'No', " | Can Redo: ", historyInfo.canRedo ? 'Yes' : 'No'] }), _jsxs(Grid, { container: true, spacing: 3, children: [_jsxs(Grid, { item: true, xs: 12, md: 3, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: "Color" }), _jsx(AdjustmentControls, { label: "Temperature", onDecreaseMax: handleBulkTempDecreaseMax, onDecrease: handleBulkTempDecrease, onIncrease: handleBulkTempIncrease, onIncreaseMax: handleBulkTempIncreaseMax, disabled: selectedCount === 0 }), _jsx(AdjustmentControls, { label: "Tint", onDecreaseMax: handleBulkTintDecreaseMax, onDecrease: handleBulkTintDecrease, onIncrease: handleBulkTintIncrease, onIncreaseMax: handleBulkTintIncreaseMax, disabled: selectedCount === 0 })] }), _jsxs(Grid, { item: true, xs: 12, md: 3, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: "Light" }), _jsx(AdjustmentControls, { label: "Exposure", onDecreaseMax: handleBulkExposureDecreaseMax, onDecrease: handleBulkExposureDecrease, onIncrease: handleBulkExposureIncrease, onIncreaseMax: handleBulkExposureIncreaseMax, disabled: selectedCount === 0 }), _jsx(AdjustmentControls, { label: "Contrast", onDecreaseMax: handleBulkContrastDecreaseMax, onDecrease: handleBulkContrastDecrease, onIncrease: handleBulkContrastIncrease, onIncreaseMax: handleBulkContrastIncreaseMax, disabled: selectedCount === 0 })] }), _jsxs(Grid, { item: true, xs: 12, md: 3, children: [_jsx(Typography, { variant: "subtitle2", gutterBottom: true, children: "Details" }), _jsx(AdjustmentControls, { label: "Clarity", onDecreaseMax: handleBulkClarityDecreaseMax, onDecrease: handleBulkClarityDecrease, onIncrease: handleBulkClarityIncrease, onIncreaseMax: handleBulkClarityIncreaseMax, disabled: selectedCount === 0 })] })] })] })), _jsx(Grid, { container: true, spacing: 2, children: imageData.map((image) => (_jsx(Grid, { item: true, xs: 12, sm: 6, md: 4, lg: 3, children: _jsx(ImageCard, { image: image, onToggleSelection: handleToggleImageSelection }) }, image.key))) }), isLoading && imageData.length === 0 && (_jsx(Box, { display: "flex", justifyContent: "center", py: 4, children: _jsx(CircularProgress, {}) })), _jsxs(Box, { mt: 4, children: [_jsx(Divider, {}), _jsx(Typography, { variant: "body2", color: "text.secondary", align: "center", sx: { mt: 2 }, children: "Demo Notes: This uses mock data and simulated API calls. Select images and try the bulk adjustment controls above." })] })] }));
|
|
255
402
|
};
|
|
256
403
|
// Add debugging function to global scope for manual testing
|
|
257
404
|
if (typeof window !== 'undefined') {
|
|
@@ -41,8 +41,8 @@ export interface Watermark {
|
|
|
41
41
|
}
|
|
42
42
|
export interface EditorConfig {
|
|
43
43
|
color_adjustment: ColorAdjustment;
|
|
44
|
-
transformation_adjustment
|
|
45
|
-
watermarks
|
|
44
|
+
transformation_adjustment?: TransformationAdjustment[];
|
|
45
|
+
watermarks?: Watermark[];
|
|
46
46
|
}
|
|
47
47
|
export interface Content {
|
|
48
48
|
key: string;
|
|
@@ -3,6 +3,7 @@ import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
|
|
|
3
3
|
import { useAdjustmentHistoryBatch } from '../useAdjustmentHistoryBatch';
|
|
4
4
|
import { usePaging } from "../usePaging";
|
|
5
5
|
import { usePreset } from "../usePreset";
|
|
6
|
+
import { mapAdjustmentStateToAdjustmentEditor } from "../../utils/adjustment";
|
|
6
7
|
// Helper function to map the API response to the format our UI component needs
|
|
7
8
|
const mapGalleryToPhotoData = (gallery, selectedIds) => {
|
|
8
9
|
return {
|
|
@@ -138,24 +139,22 @@ export function useHonchoEditorBulk(controller, eventID, firebaseUid) {
|
|
|
138
139
|
const error = info.error;
|
|
139
140
|
const hasMore = info.hasMore;
|
|
140
141
|
const imageData = useMemo(() => {
|
|
141
|
-
console.debug("imageData recalculating with:", {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
});
|
|
142
|
+
// console.debug("imageData recalculating with:", {
|
|
143
|
+
// imagesLength: images.length,
|
|
144
|
+
// selectedIds,
|
|
145
|
+
// currentBatch_allImages: currentBatch.allImages,
|
|
146
|
+
// currentBatch_allImages_keys: Object.keys(currentBatch.allImages)
|
|
147
|
+
// });
|
|
147
148
|
const res = images.map(item => {
|
|
148
149
|
const basePhoto = mapGalleryToPhotoData(item, selectedIds);
|
|
149
150
|
const batchAdjustment = currentBatch.allImages[item.id];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
151
|
+
if (batchAdjustment) {
|
|
152
|
+
const adjustmentsValues = mapAdjustmentStateToAdjustmentEditor(batchAdjustment);
|
|
153
|
+
basePhoto.adjustments = adjustmentsValues;
|
|
154
|
+
}
|
|
155
155
|
// Merge batch adjustments over backend adjustments
|
|
156
|
-
return batchAdjustment ? { ...basePhoto
|
|
156
|
+
return batchAdjustment ? { ...basePhoto } : basePhoto;
|
|
157
157
|
});
|
|
158
|
-
console.debug("imageData result:", res);
|
|
159
158
|
return res;
|
|
160
159
|
}, [images, selectedIds, currentBatch.allImages]);
|
|
161
160
|
// Store the latest batchActions.syncAdjustment in a ref to avoid dependency issues
|
|
@@ -5,6 +5,7 @@ export interface EditorTask {
|
|
|
5
5
|
frame: string | null;
|
|
6
6
|
adjustments?: Partial<AdjustmentValues>;
|
|
7
7
|
priority?: number;
|
|
8
|
+
abortSignal?: AbortSignal;
|
|
8
9
|
}
|
|
9
10
|
export interface EditorResponse {
|
|
10
11
|
id: string;
|
|
@@ -24,6 +25,7 @@ export declare class EditorProcessingService {
|
|
|
24
25
|
requestProcessing(task: EditorTask): Promise<EditorResponse>;
|
|
25
26
|
private scheduleProcessing;
|
|
26
27
|
private processQueue;
|
|
28
|
+
private removeFromQueue;
|
|
27
29
|
getQueueStatus(): {
|
|
28
30
|
queueLength: number;
|
|
29
31
|
isProcessing: boolean;
|
|
@@ -25,6 +25,25 @@ class PriorityQueue {
|
|
|
25
25
|
clear() {
|
|
26
26
|
return this.heap.splice(0);
|
|
27
27
|
}
|
|
28
|
+
// Remove item by ID (for cancellation)
|
|
29
|
+
removeById(id) {
|
|
30
|
+
const index = this.heap.findIndex(item => item.id === id);
|
|
31
|
+
if (index === -1)
|
|
32
|
+
return false;
|
|
33
|
+
// Replace with last element and heapify
|
|
34
|
+
const removed = this.heap[index];
|
|
35
|
+
if (index === this.heap.length - 1) {
|
|
36
|
+
this.heap.pop();
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
this.heap[index] = this.heap.pop();
|
|
40
|
+
// Re-heapify from this position
|
|
41
|
+
this.heapifyDown(index);
|
|
42
|
+
this.heapifyUp(index);
|
|
43
|
+
}
|
|
44
|
+
console.debug(`Removed task ${id} from queue`);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
28
47
|
heapifyUp(index) {
|
|
29
48
|
while (index > 0) {
|
|
30
49
|
const parentIndex = Math.floor((index - 1) / 2);
|
|
@@ -93,6 +112,12 @@ export class EditorProcessingService {
|
|
|
93
112
|
// Add task to processing queue
|
|
94
113
|
async requestProcessing(task) {
|
|
95
114
|
return new Promise((resolve, reject) => {
|
|
115
|
+
// Check if already aborted before adding to queue
|
|
116
|
+
if (task.abortSignal?.aborted) {
|
|
117
|
+
console.debug(`Task ${task.id} already aborted, not adding to queue`);
|
|
118
|
+
reject(new Error('Task aborted before processing'));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
96
121
|
// Validate that we have a processor
|
|
97
122
|
if (!this.processImage) {
|
|
98
123
|
console.warn('No processor available, rejecting task:', task.id);
|
|
@@ -105,7 +130,17 @@ export class EditorProcessingService {
|
|
|
105
130
|
resolve,
|
|
106
131
|
reject,
|
|
107
132
|
timestamp: Date.now(),
|
|
133
|
+
abortSignal: task.abortSignal,
|
|
108
134
|
};
|
|
135
|
+
// Set up abort listener to remove from queue if cancelled
|
|
136
|
+
if (task.abortSignal) {
|
|
137
|
+
const abortHandler = () => {
|
|
138
|
+
console.debug(`Task ${task.id} aborted, removing from queue if still pending`);
|
|
139
|
+
this.removeFromQueue(task.id);
|
|
140
|
+
reject(new Error('Task aborted'));
|
|
141
|
+
};
|
|
142
|
+
task.abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
143
|
+
}
|
|
109
144
|
// Add to priority queue - O(log n)
|
|
110
145
|
this.processingQueue.enqueue(queueItem);
|
|
111
146
|
console.debug(`Added task ${task.id} to queue. Queue length: ${this.processingQueue.length}`);
|
|
@@ -135,10 +170,26 @@ export class EditorProcessingService {
|
|
|
135
170
|
while (this.processingQueue.length > 0) {
|
|
136
171
|
// Get highest priority item - O(log n) vs O(n log n) sorting
|
|
137
172
|
const item = this.processingQueue.dequeue();
|
|
173
|
+
// Check if the item was aborted before processing
|
|
174
|
+
if (item.abortSignal?.aborted) {
|
|
175
|
+
console.debug(`Skipping aborted task ${item.id}`);
|
|
176
|
+
item.reject(new Error('Task aborted'));
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
138
179
|
console.debug(`Processing task ${item.id} (priority: ${item.priority || 0}, queue remaining: ${this.processingQueue.length})`);
|
|
139
180
|
try {
|
|
181
|
+
// Check abort signal again before actual processing
|
|
182
|
+
if (item.abortSignal?.aborted) {
|
|
183
|
+
throw new Error('Task aborted during processing');
|
|
184
|
+
}
|
|
140
185
|
const result = await this.processImage(item);
|
|
141
|
-
|
|
186
|
+
// Final check before resolving
|
|
187
|
+
if (!item.abortSignal?.aborted) {
|
|
188
|
+
item.resolve(result);
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
item.reject(new Error('Task aborted after processing'));
|
|
192
|
+
}
|
|
142
193
|
}
|
|
143
194
|
catch (error) {
|
|
144
195
|
console.error(`Failed to process task ${item.id}:`, error);
|
|
@@ -161,6 +212,14 @@ export class EditorProcessingService {
|
|
|
161
212
|
this.notifyStatusChange();
|
|
162
213
|
console.debug('Queue processing complete');
|
|
163
214
|
}
|
|
215
|
+
// Remove task from queue by ID
|
|
216
|
+
removeFromQueue(id) {
|
|
217
|
+
const removed = this.processingQueue.removeById(id);
|
|
218
|
+
if (removed) {
|
|
219
|
+
this.notifyStatusChange();
|
|
220
|
+
}
|
|
221
|
+
return removed;
|
|
222
|
+
}
|
|
164
223
|
// Get current queue status
|
|
165
224
|
getQueueStatus() {
|
|
166
225
|
return {
|
|
@@ -6,10 +6,13 @@ interface UseImageProcessorProps {
|
|
|
6
6
|
adjustments?: Partial<AdjustmentValues>;
|
|
7
7
|
frame?: string | null;
|
|
8
8
|
priority?: 'high' | 'low';
|
|
9
|
+
abortSignal?: AbortSignal;
|
|
9
10
|
}
|
|
10
11
|
interface UseImageProcessorReturn {
|
|
11
12
|
processedImageSrc: string;
|
|
12
13
|
isProcessingComplete: boolean;
|
|
14
|
+
cancelProcessing: () => void;
|
|
15
|
+
isProcessing: boolean;
|
|
13
16
|
}
|
|
14
|
-
export declare function useImageProcessor({ photoId, photoSrc, enableEditor, adjustments, frame, priority, }: UseImageProcessorProps): UseImageProcessorReturn;
|
|
17
|
+
export declare function useImageProcessor({ photoId, photoSrc, enableEditor, adjustments, frame, priority, abortSignal, }: UseImageProcessorProps): UseImageProcessorReturn;
|
|
15
18
|
export {};
|
|
@@ -1,57 +1,113 @@
|
|
|
1
|
-
import { useState, useEffect, useMemo } from 'react';
|
|
1
|
+
import { useState, useEffect, useMemo, useRef } from 'react';
|
|
2
2
|
import { useEditorHighPriority, useEditorLowPriority } from '../hooks/useEditor';
|
|
3
|
-
export function useImageProcessor({ photoId, photoSrc, enableEditor = true, adjustments, frame, priority = 'low', }) {
|
|
3
|
+
export function useImageProcessor({ photoId, photoSrc, enableEditor = true, adjustments, frame, priority = 'low', abortSignal, }) {
|
|
4
4
|
const { processImage, isEditorReady } = priority === 'high'
|
|
5
5
|
? useEditorHighPriority()
|
|
6
6
|
: useEditorLowPriority();
|
|
7
7
|
// State for processed image and processing status
|
|
8
8
|
const [processedImageSrc, setProcessedImageSrc] = useState(photoSrc);
|
|
9
9
|
const [isProcessingComplete, setIsProcessingComplete] = useState(false);
|
|
10
|
+
const [isProcessing, setIsProcessing] = useState(false);
|
|
11
|
+
// Internal abort controller for this hook
|
|
12
|
+
const abortControllerRef = useRef(null);
|
|
13
|
+
// Function to cancel current processing
|
|
14
|
+
const cancelProcessing = () => {
|
|
15
|
+
if (abortControllerRef.current) {
|
|
16
|
+
abortControllerRef.current.abort();
|
|
17
|
+
abortControllerRef.current = null;
|
|
18
|
+
}
|
|
19
|
+
setIsProcessing(false);
|
|
20
|
+
console.debug(`[useImageProcessor] Cancelled processing for image ${photoId}`);
|
|
21
|
+
};
|
|
10
22
|
// Memoize adjustments to prevent unnecessary effect triggers
|
|
11
23
|
const adjustmentsString = useMemo(() => adjustments ? JSON.stringify(adjustments) : '', [adjustments]);
|
|
12
24
|
const frameMemoized = useMemo(() => frame ? frame : null, [frame]);
|
|
13
25
|
// Process image when adjustments change
|
|
14
26
|
useEffect(() => {
|
|
27
|
+
// Cancel any previous processing
|
|
28
|
+
if (abortControllerRef.current) {
|
|
29
|
+
abortControllerRef.current.abort();
|
|
30
|
+
}
|
|
15
31
|
if (!enableEditor || !isEditorReady || !adjustments) {
|
|
16
32
|
console.debug("Skipping editor processing:", {
|
|
17
33
|
enableEditor,
|
|
18
34
|
isEditorReady,
|
|
19
35
|
hasAdjustments: !!adjustments,
|
|
20
|
-
hasFrame: frameMemoized
|
|
36
|
+
hasFrame: frameMemoized,
|
|
37
|
+
photoId
|
|
21
38
|
});
|
|
22
39
|
setProcessedImageSrc(photoSrc);
|
|
23
40
|
setIsProcessingComplete(true);
|
|
41
|
+
setIsProcessing(false);
|
|
24
42
|
return;
|
|
25
43
|
}
|
|
44
|
+
// Create new abort controller for this processing cycle
|
|
45
|
+
const controller = new AbortController();
|
|
46
|
+
abortControllerRef.current = controller;
|
|
26
47
|
// Reset processing state when starting new processing
|
|
27
48
|
setIsProcessingComplete(false);
|
|
28
|
-
|
|
49
|
+
setIsProcessing(true);
|
|
29
50
|
const processImageWithEditor = async () => {
|
|
30
51
|
try {
|
|
52
|
+
// Check if already aborted before starting
|
|
53
|
+
if (controller.signal.aborted || abortSignal?.aborted) {
|
|
54
|
+
console.debug(`[useImageProcessor] Aborted before processing started for image ${photoId}`);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
console.debug(`[useImageProcessor] Starting processing for image ${photoId}`);
|
|
31
58
|
const result = await processImage({
|
|
32
59
|
id: photoId,
|
|
33
60
|
path: photoSrc,
|
|
34
61
|
frame: frameMemoized,
|
|
35
|
-
adjustments: adjustments
|
|
62
|
+
adjustments: adjustments,
|
|
63
|
+
abortSignal: controller.signal // Pass the abort signal to the processing service
|
|
36
64
|
});
|
|
37
|
-
if
|
|
65
|
+
// Check if aborted after processing completes
|
|
66
|
+
if (!controller.signal.aborted && !abortSignal?.aborted) {
|
|
67
|
+
console.debug(`[useImageProcessor] Completed processing for image ${photoId}`);
|
|
38
68
|
setProcessedImageSrc(result.path);
|
|
39
69
|
setIsProcessingComplete(true);
|
|
70
|
+
setIsProcessing(false);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
console.debug(`[useImageProcessor] Aborted after processing completed for image ${photoId}`);
|
|
40
74
|
}
|
|
41
75
|
}
|
|
42
76
|
catch (error) {
|
|
43
|
-
if (
|
|
77
|
+
if (error.name === 'AbortError' || controller.signal.aborted || abortSignal?.aborted) {
|
|
78
|
+
console.debug(`[useImageProcessor] Processing cancelled for image ${photoId}`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
44
81
|
console.error({ error, photoKey: photoId, isEditorReady }, 'Failed to process image with editor');
|
|
45
82
|
}
|
|
83
|
+
setIsProcessing(false);
|
|
46
84
|
}
|
|
47
85
|
};
|
|
48
86
|
processImageWithEditor();
|
|
49
87
|
return () => {
|
|
50
|
-
|
|
88
|
+
// Cleanup: abort the controller when effect cleanup runs
|
|
89
|
+
controller.abort();
|
|
90
|
+
if (abortControllerRef.current === controller) {
|
|
91
|
+
abortControllerRef.current = null;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
}, [photoSrc, adjustmentsString, frameMemoized, enableEditor, isEditorReady, processImage, photoId, abortSignal]);
|
|
95
|
+
// Listen to external abort signal
|
|
96
|
+
useEffect(() => {
|
|
97
|
+
if (!abortSignal)
|
|
98
|
+
return;
|
|
99
|
+
const handleAbort = () => {
|
|
100
|
+
cancelProcessing();
|
|
101
|
+
};
|
|
102
|
+
abortSignal.addEventListener('abort', handleAbort);
|
|
103
|
+
return () => {
|
|
104
|
+
abortSignal.removeEventListener('abort', handleAbort);
|
|
51
105
|
};
|
|
52
|
-
}, [
|
|
106
|
+
}, [abortSignal]);
|
|
53
107
|
return {
|
|
54
108
|
processedImageSrc,
|
|
55
|
-
isProcessingComplete
|
|
109
|
+
isProcessingComplete,
|
|
110
|
+
cancelProcessing,
|
|
111
|
+
isProcessing
|
|
56
112
|
};
|
|
57
113
|
}
|
|
@@ -17,7 +17,6 @@ export async function loadImageAsBlob(url) {
|
|
|
17
17
|
return response.blob();
|
|
18
18
|
}
|
|
19
19
|
catch (error) {
|
|
20
|
-
console.warn(`Direct fetch failed for ${url}, trying proxy fallback:`, error);
|
|
21
20
|
// Fallback to proxy API if CORS or other fetch issues
|
|
22
21
|
try {
|
|
23
22
|
const proxyUrl = `/api/image?imageUrl=${encodeURIComponent(url)}`;
|
|
@@ -38,13 +37,9 @@ export async function loadImageAsBlob(url) {
|
|
|
38
37
|
*/
|
|
39
38
|
export async function loadImageAsFile(url) {
|
|
40
39
|
try {
|
|
41
|
-
console.debug(`Loading image from URL: ${url}`);
|
|
42
40
|
// Load image as blob with CORS handling
|
|
43
41
|
const imageBlob = await loadImageAsBlob(url);
|
|
44
|
-
|
|
45
|
-
const imageFile = new File([imageBlob], 'image', { type: imageBlob.type });
|
|
46
|
-
console.debug('Image loaded and converted to File successfully');
|
|
47
|
-
return imageFile;
|
|
42
|
+
return new File([imageBlob], 'image', { type: imageBlob.type });
|
|
48
43
|
}
|
|
49
44
|
catch (error) {
|
|
50
45
|
console.error(`Failed to load image from URL ${url}:`, error);
|