@yogiswara/honcho-editor-ui 2.6.15 → 2.7.1

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.
@@ -26,32 +26,20 @@ export default function HAccordionPreset(props) {
26
26
  }
27
27
  };
28
28
  const isPanelExpanded = (panelName) => props.expandedPanels.includes(panelName);
29
- return (_jsx(_Fragment, { children: _jsx(Box, { children: _jsxs(Accordion, { sx: accordionStyle, expanded: isPanelExpanded('preset'), onChange: props.onChange('preset'), disableGutters: true, children: [_jsx(AccordionSummary, { sx: { pr: 0 }, children: _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { width: '100%' }, children: [_jsx(Typography, { sx: { ...typography.titleMedium, color: colors.surface }, children: "Preset" }), _jsx(CardMedia, { component: "img", image: isPanelExpanded('preset') ? "/v1/svg/expanded-editor.svg" : "/v1/svg/expand-editor.svg", sx: { width: "11.67px", height: "5.83px" } })] }) }), _jsx(AccordionDetails, { sx: { pr: "4px" }, children: _jsxs(Stack, { direction: "column", gap: "8px", sx: { pt: '0px', pb: '0px', mx: '0px', width: '100%' }, children: [props.presets.map((preset) => (_jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { width: '100%' }, children: [props.selectedPreset != preset.id && (_jsx(Button, { sx: {
30
- width: '100px',
31
- textWrap: 'nowrap',
32
- overflow: 'hidden',
33
- textOverflow: 'ellipsis',
34
- display: 'block',
35
- textTransform: 'none',
29
+ return (_jsx(_Fragment, { children: _jsx(Box, { children: _jsxs(Accordion, { sx: accordionStyle, expanded: isPanelExpanded('preset'), onChange: props.onChange('preset'), disableGutters: true, children: [_jsx(AccordionSummary, { sx: { pr: 0 }, children: _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { width: '100%' }, children: [_jsx(Typography, { sx: { ...typography.titleMedium, color: colors.surface }, children: "Preset" }), _jsx(CardMedia, { component: "img", image: isPanelExpanded('preset') ? "/v1/svg/expanded-editor.svg" : "/v1/svg/expand-editor.svg", sx: { width: "11.67px", height: "5.83px" } })] }) }), _jsx(AccordionDetails, { sx: { pr: "4px" }, children: _jsxs(Stack, { direction: "column", gap: "8px", sx: { pt: '0px', pb: '0px', mx: '0px', width: '100%' }, children: [props.presets.map((preset) => (_jsxs(Stack, { direction: "row", alignItems: "center", sx: { width: "100%" }, children: [_jsx(Button, { sx: {
30
+ width: props.selectedPreset !== preset.id ? "100px" : "100px", // always fixed width
31
+ minWidth: 0,
32
+ textWrap: "nowrap",
33
+ overflow: "hidden",
34
+ textOverflow: "ellipsis",
35
+ textTransform: "none",
36
+ display: "block",
36
37
  color: colors.surface,
37
- pr: "120px",
38
- pl: "0px",
39
- ml: "0px",
40
- justifyContent: 'flex-start',
41
- ...typography.bodyMedium
42
- }, onClick: () => props.onSelectPreset(preset.id), children: preset.name })), props.selectedPreset === preset.id && (_jsx(Button, { sx: {
43
- width: '84px',
44
- textWrap: 'nowrap',
45
- overflow: 'hidden',
46
- textOverflow: 'ellipsis',
47
- display: 'block',
48
- color: colors.surface,
49
- pr: "82px",
50
- pl: "0px",
51
- ml: "0px",
52
- justifyContent: 'flex-start',
53
- ...typography.bodyMedium
54
- }, 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",
38
+ pl: 0,
39
+ ml: 0,
40
+ justifyContent: "flex-start",
41
+ ...typography.bodyMedium,
42
+ }, onClick: () => props.onSelectPreset(preset.id), children: preset.name }), props.selectedPreset === preset.id && (_jsx(CardMedia, { component: "img", image: "v1/svg/check-ratio-editor.svg", sx: { width: "20px", height: "20px", ml: 1 } })), _jsx(IconButton, { "aria-label": preset.name, onClick: (event) => props.onPresetMenuClick(event, preset.id), sx: { p: 0, m: 0, ml: 1 }, children: _jsx(CardMedia, { component: "img", image: "/v1/svg/dots-editor.svg", alt: "Options" }) })] }, preset.id))), _jsx(Button, { variant: "text", sx: { color: colors.surface, border: "1px solid",
55
43
  borderColor: colors.surface,
56
44
  borderRadius: "40px",
57
45
  textTransform: 'none',
@@ -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, CardMedia, CardContent, Checkbox, Chip, Alert, CircularProgress, ButtonGroup, Paper, Divider, FormControl, InputLabel, Select, MenuItem, Stack } from '@mui/material';
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://picsum.photos/800/600?random=${id}`,
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://picsum.photos/800/600?random=${id}`,
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://picsum.photos/300/200?random=${id}`,
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://picsum.photos/300/200?random=${id}`,
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
- const mockImages = [
68
- createMockGallery('1', { tempScore: 5, exposureScore: 2 }),
69
- createMockGallery('2'),
70
- createMockGallery('3', { contrastScore: -3, clarityScore: 8 }),
71
- createMockGallery('4'),
72
- createMockGallery('5', { vibranceScore: 10 }),
73
- createMockGallery('6'),
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 = 4;
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
- // Debug logging for presets
209
- console.log('[HonchoEditorBulkDemo] Preset state:', {
210
- presets: presets.map(p => ({ id: p.id, name: p.name })),
211
- selectedBulkPreset,
212
- activePreset: activePreset ? { id: activePreset.id, name: activePreset.name } : null,
213
- selectedCount
214
- });
215
- const getAdjustmentSummary = (image) => {
216
- const adjustments = [];
217
- if (image.tempScore !== 0)
218
- adjustments.push(`Temp: ${image.tempScore > 0 ? '+' : ''}${image.tempScore}`);
219
- if (image.exposureScore !== 0)
220
- adjustments.push(`Exp: ${image.exposureScore > 0 ? '+' : ''}${image.exposureScore}`);
221
- if (image.contrastScore !== 0)
222
- adjustments.push(`Con: ${image.contrastScore > 0 ? '+' : ''}${image.contrastScore}`);
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') {
@@ -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
- imagesLength: images.length,
143
- selectedIds,
144
- currentBatch_allImages: currentBatch.allImages,
145
- currentBatch_allImages_keys: Object.keys(currentBatch.allImages)
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
- console.debug(`Processing image ${item.id}:`, {
151
- basePhoto,
152
- batchAdjustment,
153
- hasBatchAdjustment: !!batchAdjustment
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, ...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
- item.resolve(result);
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
- let cancelled = false;
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 (!cancelled) {
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 (!cancelled) {
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
- cancelled = true;
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
- }, [photoSrc, adjustmentsString, frameMemoized, enableEditor, isEditorReady, processImage, photoId]);
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
- // Convert blob to File for HonchoEditor
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yogiswara/honcho-editor-ui",
3
- "version": "2.6.15",
3
+ "version": "2.7.1",
4
4
  "description": "A complete UI component library for the Honcho photo editor.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",