@yogiswara/honcho-editor-ui 3.3.4 โ†’ 3.4.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.
Files changed (34) hide show
  1. package/dist/components/editor/GalleryAlbum/AlbumImageGallery.d.ts +14 -7
  2. package/dist/components/editor/GalleryAlbum/AlbumImageGallery.js +207 -5
  3. package/dist/components/editor/GalleryAlbum/ImageItemComponents.d.ts +25 -0
  4. package/dist/components/editor/GalleryAlbum/ImageItemComponents.js +179 -0
  5. package/dist/components/editor/GalleryAlbum/colorsGallery.d.ts +9 -0
  6. package/dist/components/editor/GalleryAlbum/colorsGallery.js +9 -0
  7. package/dist/components/editor/GalleryAlbum/svg/Tick.d.ts +2 -0
  8. package/dist/components/editor/GalleryAlbum/svg/Tick.js +6 -0
  9. package/dist/components/editor/HBulkAccordionColorAdjustment.js +1 -2
  10. package/dist/components/editor/HBulkAccordionColorAdjustmentColors.js +1 -1
  11. package/dist/components/editor/HBulkPresetMobile.d.ts +2 -2
  12. package/dist/components/editor/HBulkPresetMobile.js +2 -2
  13. package/dist/components/editor/HImageEditorBulkMobile.d.ts +2 -2
  14. package/dist/hooks/demo/HonchoEditorBulkDemo.d.ts +0 -3
  15. package/dist/hooks/demo/HonchoEditorBulkDemo.js +770 -411
  16. package/dist/hooks/demo/HonchoEditorSingleCleanDemo.d.ts +0 -3
  17. package/dist/hooks/demo/HonchoEditorSingleCleanDemo.js +882 -354
  18. package/dist/hooks/demo/index.d.ts +0 -2
  19. package/dist/hooks/demo/index.js +3 -2
  20. package/dist/hooks/editor/type.d.ts +15 -13
  21. package/dist/hooks/editor/useHonchoEditorBulk.d.ts +47 -5
  22. package/dist/hooks/editor/useHonchoEditorBulk.js +252 -133
  23. package/dist/hooks/useAdjustmentHistory.js +12 -12
  24. package/dist/hooks/useAdjustmentHistoryBatch.d.ts +33 -31
  25. package/dist/hooks/useAdjustmentHistoryBatch.js +703 -170
  26. package/dist/hooks/usePreset.js +12 -12
  27. package/dist/index.d.ts +5 -7
  28. package/dist/index.js +5 -4
  29. package/dist/services/type.d.ts +14 -0
  30. package/dist/utils/adjustment.d.ts +1 -1
  31. package/dist/utils/adjustment.js +15 -14
  32. package/dist/utils/logger.d.ts +3 -0
  33. package/dist/utils/logger.js +11 -0
  34. package/package.json +4 -2
@@ -1,411 +1,770 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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
- import { useHonchoEditorBulk } from '../editor/useHonchoEditorBulk';
5
- import { useImageProcessor } from "../../lib/hooks/useImageProcessor";
6
- // Mock data for demonstration
7
- const createMockGallery = (id, adjustments) => ({
8
- id,
9
- uid: 'demo-user',
10
- event_id: 'demo-event',
11
- download: {
12
- key: `${id}-download`,
13
- path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
14
- size: 1024000,
15
- width: 800,
16
- height: 600,
17
- },
18
- download_edited: {
19
- key: `${id}-download-edited`,
20
- path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
21
- size: 1024000,
22
- width: 800,
23
- height: 600,
24
- },
25
- thumbnail: {
26
- key: `${id}-thumb`,
27
- path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
28
- size: 50000,
29
- width: 300,
30
- height: 200,
31
- },
32
- thumbnail_edited: {
33
- key: `${id}-thumb-edited`,
34
- path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/thumbnail.jpeg`,
35
- size: 50000,
36
- width: 300,
37
- height: 200,
38
- },
39
- is_original: true,
40
- available: true,
41
- show_gallery: true,
42
- editor_config: {
43
- color_adjustment: {
44
- temperature: adjustments?.tempScore || 0,
45
- tint: adjustments?.tintScore || 0,
46
- vibrance: adjustments?.vibranceScore || 0,
47
- saturation: adjustments?.saturationScore || 0,
48
- exposure: adjustments?.exposureScore || 0,
49
- highlights: adjustments?.highlightsScore || 0,
50
- shadows: adjustments?.shadowsScore || 0,
51
- whites: adjustments?.whitesScore || 0,
52
- blacks: adjustments?.blacksScore || 0,
53
- contrast: adjustments?.contrastScore || 0,
54
- clarity: adjustments?.clarityScore || 0,
55
- sharpness: adjustments?.sharpnessScore || 0,
56
- },
57
- transformation_adjustment: [],
58
- watermarks: [],
59
- },
60
- log: {
61
- created_at: new Date().toISOString(),
62
- updated_at: new Date().toISOString(),
63
- },
64
- });
65
- // Mock Controller implementation
66
- const createMockController = () => {
67
- console.log('[Controller] ๐Ÿญ createMockController() called - Creating new mock controller instance for bulk editor');
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
- }
75
- return {
76
- onGetImage: async (uid, imageId) => {
77
- console.log(`[Controller] ๐Ÿ“ท onGetImage called: uid=${uid}, imageId=${imageId}`);
78
- await new Promise(resolve => setTimeout(resolve, 500));
79
- const image = mockImages.find(img => img.id === imageId);
80
- if (!image) {
81
- console.error(`[Controller] โŒ Image ${imageId} not found`);
82
- throw new Error(`Image ${imageId} not found`);
83
- }
84
- console.log(`[Controller] ๐Ÿ“ท onGetImage returning image:`, image.id);
85
- return image;
86
- },
87
- getImageList: async (uid, eventId, page) => {
88
- console.log(`[Controller] ๐Ÿ“‹ getImageList called: uid=${uid}, eventId=${eventId}, page=${page}`);
89
- await new Promise(resolve => setTimeout(resolve, 800));
90
- const pageSize = 1000; // Increased page size for better UX with more images
91
- const startIndex = (page - 1) * pageSize;
92
- const endIndex = startIndex + pageSize;
93
- const pageImages = mockImages.slice(startIndex, endIndex);
94
- const result = {
95
- gallery: pageImages,
96
- limit: pageSize,
97
- current_page: page,
98
- prev_page: page > 1 ? page - 1 : 0,
99
- next_page: endIndex < mockImages.length ? page + 1 : 0,
100
- sum_of_image: pageImages.length,
101
- };
102
- console.log(`[Controller] ๐Ÿ“‹ getImageList returning ${pageImages.length} images for page ${page} (total available: ${mockImages.length})`, result);
103
- return result;
104
- },
105
- syncConfig: async (uid) => {
106
- console.log(`[Controller] ๐Ÿ”„ syncConfig called: uid=${uid}`);
107
- await new Promise(resolve => setTimeout(resolve, 200));
108
- console.log(`[Controller] ๐Ÿ”„ syncConfig completed for uid=${uid}`);
109
- },
110
- handleBack: (uid, imageId) => {
111
- console.log(`[Controller] โฌ…๏ธ handleBack called: uid=${uid}, imageId=${imageId}`);
112
- console.log(`[Controller] โฌ…๏ธ Back to: ${imageId}`);
113
- },
114
- getPresets: async (uid) => {
115
- console.log(`[Controller] ๐ŸŽจ getPresets called: uid=${uid}`);
116
- await new Promise(resolve => setTimeout(resolve, 300));
117
- const presets = [
118
- { id: '1', name: 'Warm Sunset', is_default: false, temperature: 15, tint: 5, saturation: 8, vibrance: 12, exposure: 2, contrast: 5, highlights: -10, shadows: 8, whites: 3, blacks: -5, clarity: 4, sharpness: 6 },
119
- { id: '2', name: 'Cool Morning', is_default: false, temperature: -12, tint: -3, saturation: -2, vibrance: 5, exposure: 1, contrast: 3, highlights: -5, shadows: 12, whites: 8, blacks: -8, clarity: 6, sharpness: 4 },
120
- { id: '3', name: 'High Contrast', is_default: false, temperature: 0, tint: 0, saturation: 5, vibrance: 8, exposure: 0, contrast: 20, highlights: -15, shadows: 15, whites: 10, blacks: -10, clarity: 15, sharpness: 8 },
121
- ];
122
- console.log(`[Controller] ๐ŸŽจ getPresets returning ${presets.length} presets`, presets.map(p => ({ id: p.id, name: p.name })));
123
- return presets;
124
- },
125
- createPreset: async (uid, name, settings) => {
126
- console.log(`[Controller] โž• createPreset called: uid=${uid}, name=${name}`, settings);
127
- await new Promise(resolve => setTimeout(resolve, 500));
128
- console.log(`[Controller] โž• createPreset completed: ${name}`);
129
- },
130
- deletePreset: async (uid, presetId) => {
131
- console.log(`[Controller] ๐Ÿ—‘๏ธ deletePreset called: uid=${uid}, presetId=${presetId}`);
132
- await new Promise(resolve => setTimeout(resolve, 300));
133
- console.log(`[Controller] ๐Ÿ—‘๏ธ deletePreset completed: ${presetId}`);
134
- },
135
- updatePreset: async (uid, data) => {
136
- console.log(`[Controller] ๐Ÿ”„ updatePreset called: uid=${uid}`, data);
137
- await new Promise(resolve => setTimeout(resolve, 300));
138
- console.log(`[Controller] ๐Ÿ”„ updatePreset completed:`, data.name);
139
- },
140
- createEditorConfig: async (uid, payload) => {
141
- console.log(`[Controller] โš™๏ธ createEditorConfig called: uid=${uid}`, payload);
142
- await new Promise(resolve => setTimeout(resolve, 300));
143
- console.log(`[Controller] โš™๏ธ createEditorConfig completed for gallery_id=${payload.gallery_id}, task_id=${payload.task_id}`);
144
- },
145
- getEditorHistory: async (uid, imageId) => {
146
- console.log(`[Controller] ๐Ÿ“š getEditorHistory called: uid=${uid}, imageId=${imageId}`);
147
- await new Promise(resolve => setTimeout(resolve, 200));
148
- const result = {
149
- current_task_id: "",
150
- history: []
151
- };
152
- console.log(`[Controller] ๐Ÿ“š getEditorHistory returning empty history for demo:`, result);
153
- return result;
154
- },
155
- getGalleryUpdateTimestamp: async (uid, eventId) => {
156
- console.log(`[Controller] โฐ getGalleryUpdateTimestamp called: uid=${uid}, eventId=${eventId}`);
157
- await new Promise(resolve => setTimeout(resolve, 100));
158
- const result = {
159
- gallery: []
160
- };
161
- console.log(`[Controller] โฐ getGalleryUpdateTimestamp returning:`, result);
162
- return result;
163
- },
164
- setHistoryIndex: async (uid, imageId, taskId) => {
165
- console.log(`[Controller] ๐Ÿ“ setHistoryIndex called: uid=${uid}, imageId=${imageId}, taskId=${taskId}`);
166
- await new Promise(resolve => setTimeout(resolve, 100));
167
- console.log(`[Controller] ๐Ÿ“ setHistoryIndex completed - Set history index for image ${imageId} to task ${taskId}`);
168
- },
169
- };
170
- };
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
- frame: null,
269
- }), [image.key, image.src, image.adjustments, shouldLoadImage, isInView]);
270
- // Only call useImageProcessor when the card is visible AND we should load the image AND it's in view
271
- const { processedImageSrc, isProcessingComplete, cancelProcessing, isProcessing } = useImageProcessor(imageProcessorParams);
272
- // Cancel processing when card goes out of view
273
- useEffect(() => {
274
- if (!isInView && isProcessing) {
275
- console.debug(`[ImageCard] Cancelling processing for image ${image.key} - no longer in view`);
276
- cancelProcessing();
277
- }
278
- }, [isInView, isProcessing, cancelProcessing, image.key]);
279
- // Memoize the toggle selection handler to prevent unnecessary re-renders
280
- const handleToggleSelection = useMemo(() => {
281
- return () => onToggleSelection(image.key);
282
- }, [onToggleSelection, image.key]);
283
- const handleCheckboxClick = useMemo(() => {
284
- return (e) => {
285
- e.stopPropagation();
286
- onToggleSelection(image.key);
287
- };
288
- }, [onToggleSelection, image.key]);
289
- // Create placeholder content for non-visible cards
290
- const renderPlaceholder = () => (_jsxs(Box, { sx: {
291
- height: 200,
292
- backgroundColor: 'grey.100',
293
- display: 'flex',
294
- alignItems: 'center',
295
- justifyContent: 'center',
296
- flexDirection: 'column'
297
- }, children: [_jsx(CircularProgress, { size: 24, sx: { mb: 1 } }), _jsx(Typography, { variant: "caption", color: "text.secondary", children: "Loading..." })] }));
298
- // Create processing indicator
299
- const renderProcessingOverlay = () => {
300
- return isProcessing && shouldLoadImage && isInView ? (_jsxs(Box, { sx: {
301
- position: 'absolute',
302
- top: 8,
303
- right: 8,
304
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
305
- color: 'white',
306
- borderRadius: 1,
307
- px: 1,
308
- py: 0.5,
309
- display: 'flex',
310
- alignItems: 'center',
311
- gap: 0.5,
312
- zIndex: 1,
313
- maxWidth: '120px'
314
- }, children: [_jsx(CircularProgress, { size: 12, color: "inherit" }), _jsx(Typography, { variant: "caption", children: "Processing" })] })) : null;
315
- };
316
- return (_jsxs(Card, { ref: cardRef, sx: {
317
- border: image.isSelected ? 2 : 1,
318
- borderColor: image.isSelected ? 'primary.main' : 'divider',
319
- cursor: 'pointer',
320
- transition: 'all 0.2s',
321
- position: 'relative',
322
- opacity: 1,
323
- '&:hover': {
324
- transform: 'translateY(-2px)',
325
- boxShadow: 2,
326
- }
327
- }, onClick: handleToggleSelection, children: [shouldLoadImage ? (_jsxs(Box, { sx: { position: 'relative', width: '100%', height: '200px' }, children: [_jsx("img", { id: "image-card-media", src: processedImageSrc, alt: "", style: {
328
- width: '100%',
329
- height: '200px',
330
- objectFit: 'cover',
331
- opacity: 1,
332
- display: 'block'
333
- }, 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 })] }) })] }));
334
- }, (prevProps, nextProps) => {
335
- // Custom comparison function for React.memo - be more strict to prevent unnecessary re-renders
336
- const prevAdj = prevProps.image.adjustments || {};
337
- const nextAdj = nextProps.image.adjustments || {};
338
- // Only re-render if key properties actually change
339
- const shouldUpdate = (prevProps.image.key !== nextProps.image.key ||
340
- prevProps.image.isSelected !== nextProps.image.isSelected ||
341
- prevProps.image.src !== nextProps.image.src ||
342
- // Compare individual adjustment properties to avoid JSON.stringify issues
343
- prevAdj.temperature !== nextAdj.temperature ||
344
- prevAdj.tint !== nextAdj.tint ||
345
- prevAdj.exposure !== nextAdj.exposure ||
346
- prevAdj.contrast !== nextAdj.contrast ||
347
- prevAdj.clarity !== nextAdj.clarity ||
348
- prevAdj.vibrance !== nextAdj.vibrance ||
349
- prevAdj.saturation !== nextAdj.saturation ||
350
- prevAdj.highlights !== nextAdj.highlights ||
351
- prevAdj.shadows !== nextAdj.shadows ||
352
- prevAdj.whites !== nextAdj.whites ||
353
- prevAdj.blacks !== nextAdj.blacks ||
354
- prevAdj.sharpness !== nextAdj.sharpness);
355
- // Return false to re-render, true to skip re-render
356
- return !shouldUpdate;
357
- });
358
- export const HonchoEditorBulkDemo = () => {
359
- const [controller] = useState(() => createMockController());
360
- const [showAdjustments, setShowAdjustments] = useState(false);
361
- const { imageData, isLoading, error, selectedIds, hasMore, selectedBulkPreset, presets, // Add presets
362
- activePreset, // Add activePreset
363
- handleToggleImageSelection, handleLoadMore, handleRefresh, handleSelectBulkPreset, handleBackCallbackBulk,
364
- // Temperature adjustments
365
- handleBulkTempDecreaseMax, handleBulkTempDecrease, handleBulkTempIncrease, handleBulkTempIncreaseMax,
366
- // Tint adjustments
367
- handleBulkTintDecreaseMax, handleBulkTintDecrease, handleBulkTintIncrease, handleBulkTintIncreaseMax,
368
- // Exposure adjustments
369
- handleBulkExposureDecreaseMax, handleBulkExposureDecrease, handleBulkExposureIncrease, handleBulkExposureIncreaseMax,
370
- // Contrast adjustments
371
- handleBulkContrastDecreaseMax, handleBulkContrastDecrease, handleBulkContrastIncrease, handleBulkContrastIncreaseMax,
372
- // Clarity adjustments
373
- handleBulkClarityDecreaseMax, handleBulkClarityDecrease, handleBulkClarityIncrease, handleBulkClarityIncreaseMax,
374
- // History actions
375
- handleUndo, handleRedo, handleReset, historyInfo, } = useHonchoEditorBulk(controller, 'demo-event', 'demo-user');
376
- const selectedCount = selectedIds.length;
377
- const totalCount = imageData.length;
378
- 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: () => {
379
- // Select all images on current page
380
- imageData.forEach(image => {
381
- if (!image.isSelected) {
382
- handleToggleImageSelection(image.key);
383
- }
384
- });
385
- }, disabled: isLoading || imageData.length === 0, children: "Select All" }), _jsx(Button, { variant: "outlined", onClick: () => {
386
- // Deselect all images
387
- imageData.forEach(image => {
388
- if (image.isSelected) {
389
- handleToggleImageSelection(image.key);
390
- }
391
- });
392
- }, disabled: isLoading || selectedCount === 0, children: "Clear Selection" }), _jsx(Button, { variant: "outlined", onClick: () => {
393
- console.log('Debug Info:', {
394
- selectedIds,
395
- selectedCount,
396
- showAdjustments,
397
- presets: presets.map(p => ({ id: p.id, name: p.name })),
398
- selectedBulkPreset,
399
- activePreset: activePreset ? { id: activePreset.id, name: activePreset.name } : null,
400
- imageData: imageData.map(img => ({ key: img.key, isSelected: img.isSelected }))
401
- });
402
- }, 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." })] })] }));
403
- };
404
- // Add debugging function to global scope for manual testing
405
- if (typeof window !== 'undefined') {
406
- window.testPresetSelection = () => {
407
- console.log("๐Ÿงช Manual preset test - this would normally be called by the UI");
408
- console.log("๐Ÿงช To test properly, use the actual preset dropdown in the UI above");
409
- };
410
- }
411
- export default HonchoEditorBulkDemo;
1
+ "use strict";
2
+ // import React, { useState, useMemo, useRef, useEffect } from 'react';
3
+ // import {
4
+ // Box,
5
+ // Container,
6
+ // Typography,
7
+ // Button,
8
+ // Grid,
9
+ // Card,
10
+ // CardMedia,
11
+ // CardContent,
12
+ // CardActions,
13
+ // Checkbox,
14
+ // Chip,
15
+ // Alert,
16
+ // CircularProgress,
17
+ // ButtonGroup,
18
+ // Paper,
19
+ // Divider,
20
+ // FormControl,
21
+ // InputLabel,
22
+ // Select,
23
+ // MenuItem,
24
+ // Stack
25
+ // } from '@mui/material';
26
+ // import { PhotoData, useHonchoEditorBulk } from '../editor/useHonchoEditorBulk';
27
+ // import { Controller, AdjustmentState, Preset } from '../editor/type';
28
+ // import { Gallery, ResponseGalleryPaging, CreateEditorTaskRequest, GetHistoryResponse, GetGalleryUpdateTimestampResponse } from '../editor/type';
29
+ // import { useEditorHeadless } from "../../lib/hooks/useEditorHeadless";
30
+ // import { useImageProcessor } from "../../lib/hooks/useImageProcessor";
31
+ // // Mock data for demonstration
32
+ // const createMockGallery = (id: string, adjustments?: Partial<AdjustmentState>): Gallery => ({
33
+ // id,
34
+ // uid: 'demo-user',
35
+ // event_id: 'demo-event',
36
+ // download: {
37
+ // key: `${id}-download`,
38
+ // path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
39
+ // size: 1024000,
40
+ // width: 800,
41
+ // height: 600,
42
+ // },
43
+ // download_edited: {
44
+ // key: `${id}-download-edited`,
45
+ // path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
46
+ // size: 1024000,
47
+ // width: 800,
48
+ // height: 600,
49
+ // },
50
+ // thumbnail: {
51
+ // key: `${id}-thumb`,
52
+ // path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/original.jpeg`,
53
+ // size: 50000,
54
+ // width: 300,
55
+ // height: 200,
56
+ // },
57
+ // thumbnail_edited: {
58
+ // key: `${id}-thumb-edited`,
59
+ // path: `https://s3.ap-southeast-1.amazonaws.com/dev.pronto.ubersnap/event/689c514d225024b1172ed297/media/95d811da-72a1-4e97-91cc-94446f0a10ad/thumbnail.jpeg`,
60
+ // size: 50000,
61
+ // width: 300,
62
+ // height: 200,
63
+ // },
64
+ // is_original: true,
65
+ // available: true,
66
+ // show_gallery: true,
67
+ // editor_config: {
68
+ // color_adjustment: {
69
+ // temperature: adjustments?.tempScore || 0,
70
+ // tint: adjustments?.tintScore || 0,
71
+ // vibrance: adjustments?.vibranceScore || 0,
72
+ // saturation: adjustments?.saturationScore || 0,
73
+ // exposure: adjustments?.exposureScore || 0,
74
+ // highlights: adjustments?.highlightsScore || 0,
75
+ // shadows: adjustments?.shadowsScore || 0,
76
+ // whites: adjustments?.whitesScore || 0,
77
+ // blacks: adjustments?.blacksScore || 0,
78
+ // contrast: adjustments?.contrastScore || 0,
79
+ // clarity: adjustments?.clarityScore || 0,
80
+ // sharpness: adjustments?.sharpnessScore || 0,
81
+ // },
82
+ // transformation_adjustment: [],
83
+ // watermarks: [],
84
+ // },
85
+ // log: {
86
+ // created_at: new Date().toISOString(),
87
+ // updated_at: new Date().toISOString(),
88
+ // },
89
+ // });
90
+ // // Mock Controller implementation
91
+ // const createMockController = (): Controller => {
92
+ // console.log('[Controller] ๐Ÿญ createMockController() called - Creating new mock controller instance for bulk editor');
93
+ // // Generate mock images from 1 to 1277 using picsum.dev static URLs
94
+ // const mockImages: Gallery[] = [];
95
+ // for (let i = 1; i <= 1000; i++) {
96
+ // const id = i.toString();
97
+ // // let adjustments: Partial<AdjustmentState> | undefined;
98
+ // mockImages.push(createMockGallery(id, undefined));
99
+ // }
100
+ // return {
101
+ // onGetImage: async (uid: string, imageId: string) => {
102
+ // console.log(`[Controller] ๐Ÿ“ท onGetImage called: uid=${uid}, imageId=${imageId}`);
103
+ // await new Promise(resolve => setTimeout(resolve, 500));
104
+ // const image = mockImages.find(img => img.id === imageId);
105
+ // if (!image) {
106
+ // console.error(`[Controller] โŒ Image ${imageId} not found`);
107
+ // throw new Error(`Image ${imageId} not found`);
108
+ // }
109
+ // console.log(`[Controller] ๐Ÿ“ท onGetImage returning image:`, image.id);
110
+ // return image;
111
+ // },
112
+ // getImageList: async (uid: string, eventId: string, page: number) => {
113
+ // console.log(`[Controller] ๐Ÿ“‹ getImageList called: uid=${uid}, eventId=${eventId}, page=${page}`);
114
+ // await new Promise(resolve => setTimeout(resolve, 800));
115
+ // const pageSize = 1000; // Increased page size for better UX with more images
116
+ // const startIndex = (page - 1) * pageSize;
117
+ // const endIndex = startIndex + pageSize;
118
+ // const pageImages = mockImages.slice(startIndex, endIndex);
119
+ // const result = {
120
+ // gallery: pageImages,
121
+ // limit: pageSize,
122
+ // current_page: page,
123
+ // prev_page: page > 1 ? page - 1 : 0,
124
+ // next_page: endIndex < mockImages.length ? page + 1 : 0,
125
+ // sum_of_image: pageImages.length,
126
+ // } as ResponseGalleryPaging;
127
+ // console.log(`[Controller] ๐Ÿ“‹ getImageList returning ${pageImages.length} images for page ${page} (total available: ${mockImages.length})`, result);
128
+ // return result;
129
+ // },
130
+ // syncConfig: async (uid: string) => {
131
+ // console.log(`[Controller] ๐Ÿ”„ syncConfig called: uid=${uid}`);
132
+ // await new Promise(resolve => setTimeout(resolve, 200));
133
+ // console.log(`[Controller] ๐Ÿ”„ syncConfig completed for uid=${uid}`);
134
+ // },
135
+ // handleBack: (uid: string, imageId: string) => {
136
+ // console.log(`[Controller] โฌ…๏ธ handleBack called: uid=${uid}, imageId=${imageId}`);
137
+ // console.log(`[Controller] โฌ…๏ธ Back to: ${imageId}`);
138
+ // },
139
+ // getPresets: async (uid: string) => {
140
+ // console.log(`[Controller] ๐ŸŽจ getPresets called: uid=${uid}`);
141
+ // await new Promise(resolve => setTimeout(resolve, 300));
142
+ // const presets = [
143
+ // { id: '1', name: 'Warm Sunset', is_default: false, temperature: 15, tint: 5, saturation: 8, vibrance: 12, exposure: 2, contrast: 5, highlights: -10, shadows: 8, whites: 3, blacks: -5, clarity: 4, sharpness: 6 },
144
+ // { id: '2', name: 'Cool Morning', is_default: false, temperature: -12, tint: -3, saturation: -2, vibrance: 5, exposure: 1, contrast: 3, highlights: -5, shadows: 12, whites: 8, blacks: -8, clarity: 6, sharpness: 4 },
145
+ // { id: '3', name: 'High Contrast', is_default: false, temperature: 0, tint: 0, saturation: 5, vibrance: 8, exposure: 0, contrast: 20, highlights: -15, shadows: 15, whites: 10, blacks: -10, clarity: 15, sharpness: 8 },
146
+ // ] as Preset[];
147
+ // console.log(`[Controller] ๐ŸŽจ getPresets returning ${presets.length} presets`, presets.map(p => ({ id: p.id, name: p.name })));
148
+ // return presets;
149
+ // },
150
+ // createPreset: async (uid: string, name: string, settings: AdjustmentState) => {
151
+ // console.log(`[Controller] โž• createPreset called: uid=${uid}, name=${name}`, settings);
152
+ // await new Promise(resolve => setTimeout(resolve, 500));
153
+ // console.log(`[Controller] โž• createPreset completed: ${name}`);
154
+ // },
155
+ // deletePreset: async (uid: string, presetId: string) => {
156
+ // console.log(`[Controller] ๐Ÿ—‘๏ธ deletePreset called: uid=${uid}, presetId=${presetId}`);
157
+ // await new Promise(resolve => setTimeout(resolve, 300));
158
+ // console.log(`[Controller] ๐Ÿ—‘๏ธ deletePreset completed: ${presetId}`);
159
+ // },
160
+ // updatePreset: async (uid: string, data: Preset) => {
161
+ // console.log(`[Controller] ๐Ÿ”„ updatePreset called: uid=${uid}`, data);
162
+ // await new Promise(resolve => setTimeout(resolve, 300));
163
+ // console.log(`[Controller] ๐Ÿ”„ updatePreset completed:`, data.name);
164
+ // },
165
+ // createEditorConfig: async (uid: string, payload: CreateEditorTaskRequest) => {
166
+ // console.log(`[Controller] โš™๏ธ createEditorConfig called: uid=${uid}`, payload);
167
+ // await new Promise(resolve => setTimeout(resolve, 300));
168
+ // console.log(`[Controller] โš™๏ธ createEditorConfig completed for gallery_id=${payload.gallery_id}, task_id=${payload.task_id}`);
169
+ // },
170
+ // getEditorHistory: async (uid: string, imageId: string): Promise<GetHistoryResponse> => {
171
+ // console.log(`[Controller] ๐Ÿ“š getEditorHistory called: uid=${uid}, imageId=${imageId}`);
172
+ // await new Promise(resolve => setTimeout(resolve, 200));
173
+ // const result = {
174
+ // current_task_id: "",
175
+ // history: []
176
+ // };
177
+ // console.log(`[Controller] ๐Ÿ“š getEditorHistory returning empty history for demo:`, result);
178
+ // return result;
179
+ // },
180
+ // getGalleryUpdateTimestamp: async (uid: string, eventId: string): Promise<GetGalleryUpdateTimestampResponse> => {
181
+ // console.log(`[Controller] โฐ getGalleryUpdateTimestamp called: uid=${uid}, eventId=${eventId}`);
182
+ // await new Promise(resolve => setTimeout(resolve, 100));
183
+ // const result = {
184
+ // gallery: []
185
+ // };
186
+ // console.log(`[Controller] โฐ getGalleryUpdateTimestamp returning:`, result);
187
+ // return result;
188
+ // },
189
+ // setHistoryIndex: async (uid: string, imageId: string, taskId: string) => {
190
+ // console.log(`[Controller] ๐Ÿ“ setHistoryIndex called: uid=${uid}, imageId=${imageId}, taskId=${taskId}`);
191
+ // await new Promise(resolve => setTimeout(resolve, 100));
192
+ // console.log(`[Controller] ๐Ÿ“ setHistoryIndex completed - Set history index for image ${imageId} to task ${taskId}`);
193
+ // },
194
+ // };
195
+ // };
196
+ // const AdjustmentControls: React.FC<{
197
+ // label: string;
198
+ // onDecreaseMax: () => void;
199
+ // onDecrease: () => void;
200
+ // onIncrease: () => void;
201
+ // onIncreaseMax: () => void;
202
+ // disabled?: boolean;
203
+ // }> = ({ label, onDecreaseMax, onDecrease, onIncrease, onIncreaseMax, disabled = false }) => (
204
+ // <Box mb={2}>
205
+ // <Typography variant="body2" gutterBottom>{label}</Typography>
206
+ // <ButtonGroup size="small" variant="outlined">
207
+ // <Button onClick={onDecreaseMax} disabled={disabled}>--</Button>
208
+ // <Button onClick={onDecrease} disabled={disabled}>-</Button>
209
+ // <Button onClick={onIncrease} disabled={disabled}>+</Button>
210
+ // <Button onClick={onIncreaseMax} disabled={disabled}>++</Button>
211
+ // </ButtonGroup>
212
+ // </Box>
213
+ // );
214
+ // const ImageCard = React.memo<{
215
+ // image: PhotoData;
216
+ // onToggleSelection: (key: string) => void;
217
+ // }>(({ image, onToggleSelection }) => {
218
+ // const [isVisible, setIsVisible] = useState(false);
219
+ // const [shouldLoadImage, setShouldLoadImage] = useState(false);
220
+ // const [isInView, setIsInView] = useState(false);
221
+ // const cardRef = useRef<HTMLDivElement>(null);
222
+ // const abortControllerRef = useRef<AbortController | null>(null);
223
+ // useEffect(() => {
224
+ // const visibilityObserver = new IntersectionObserver(
225
+ // ([entry]) => {
226
+ // if (entry.isIntersecting) {
227
+ // setIsVisible(true);
228
+ // setIsInView(true);
229
+ // // Delay image loading slightly to ensure smooth scrolling
230
+ // setTimeout(() => setShouldLoadImage(true), 100);
231
+ // // Don't disconnect here as we want to track when it goes out of view
232
+ // } else {
233
+ // setIsInView(false);
234
+ // // Cancel processing when going out of view
235
+ // if (abortControllerRef.current) {
236
+ // console.debug(`[ImageCard] Cancelling processing for image ${image.key} - out of view`);
237
+ // abortControllerRef.current.abort();
238
+ // }
239
+ // }
240
+ // },
241
+ // {
242
+ // threshold: 0.1, // Trigger when 10% of the element is visible
243
+ // rootMargin: '100px', // Start loading 100px before the element comes into view
244
+ // }
245
+ // );
246
+ // if (cardRef.current) {
247
+ // visibilityObserver.observe(cardRef.current);
248
+ // }
249
+ // return () => {
250
+ // visibilityObserver.disconnect();
251
+ // // Cancel any ongoing processing when component unmounts
252
+ // if (abortControllerRef.current) {
253
+ // abortControllerRef.current.abort();
254
+ // }
255
+ // };
256
+ // }, [image.key]);
257
+ // // Track adjustments changes for debugging
258
+ // useEffect(() => {
259
+ // console.log("[ImageCard] Adjustments changed for image", image.key, ":", image.adjustments);
260
+ // }, [image.adjustments, image.key]);
261
+ // // Create a key that changes when adjustments change to force abort controller recreation
262
+ // const adjustmentsKey = useMemo(() => {
263
+ // return JSON.stringify(image.adjustments);
264
+ // }, [image.adjustments]);
265
+ // // Store the current abort signal in a ref to prevent it from changing on every render
266
+ // const currentAbortSignalRef = useRef<AbortSignal | undefined>();
267
+ // const isInitializedRef = useRef(false);
268
+ // // Much more conservative abort controller management - only recreate when absolutely necessary
269
+ // useEffect(() => {
270
+ // if (shouldLoadImage && isInView) {
271
+ // // Only create abort controller if we don't have one OR if this is the first time
272
+ // if (!abortControllerRef.current || !isInitializedRef.current) {
273
+ // console.debug(`[ImageCard] Creating initial abort controller for image ${image.key}`);
274
+ // if (abortControllerRef.current) {
275
+ // abortControllerRef.current.abort();
276
+ // }
277
+ // abortControllerRef.current = new AbortController();
278
+ // currentAbortSignalRef.current = abortControllerRef.current.signal;
279
+ // isInitializedRef.current = true;
280
+ // }
281
+ // // For adjustment changes, don't recreate the controller - let useImageProcessor handle it
282
+ // } else {
283
+ // // Only clean up when not visible or not loading
284
+ // if (abortControllerRef.current && isInitializedRef.current) {
285
+ // console.debug(`[ImageCard] Cleaning up abort controller for image ${image.key} - not visible or not loading`);
286
+ // abortControllerRef.current.abort();
287
+ // abortControllerRef.current = null;
288
+ // currentAbortSignalRef.current = undefined;
289
+ // isInitializedRef.current = false;
290
+ // }
291
+ // }
292
+ // return () => {
293
+ // if (abortControllerRef.current) {
294
+ // abortControllerRef.current.abort();
295
+ // abortControllerRef.current = null;
296
+ // currentAbortSignalRef.current = undefined;
297
+ // isInitializedRef.current = false;
298
+ // }
299
+ // };
300
+ // }, [shouldLoadImage, isInView, image.key]); // Remove adjustmentsKey from dependencies
301
+ // // Separate effect to handle adjustment changes without recreating abort controller
302
+ // useEffect(() => {
303
+ // if (shouldLoadImage && isInView && isInitializedRef.current) {
304
+ // console.debug(`[ImageCard] Adjustments changed for image ${image.key} - letting useImageProcessor handle the change`);
305
+ // }
306
+ // }, [adjustmentsKey, shouldLoadImage, isInView, image.key]);
307
+ // // Memoize the useImageProcessor call to prevent unnecessary hook calls
308
+ // const imageProcessorParams = useMemo(() => ({
309
+ // photoId: image.key,
310
+ // photoSrc: image.src,
311
+ // adjustments: image.adjustments,
312
+ // enableEditor: shouldLoadImage && isInView, // Only enable when we should load the image AND it's in view
313
+ // abortSignal: currentAbortSignalRef.current, // Use stable abort signal
314
+ // frame: null,
315
+ // }), [image.key, image.src, image.adjustments, shouldLoadImage, isInView]);
316
+ // // Only call useImageProcessor when the card is visible AND we should load the image AND it's in view
317
+ // const { processedImageSrc, isProcessingComplete, cancelProcessing, isProcessing } = useImageProcessor(imageProcessorParams);
318
+ // // Cancel processing when card goes out of view
319
+ // useEffect(() => {
320
+ // if (!isInView && isProcessing) {
321
+ // console.debug(`[ImageCard] Cancelling processing for image ${image.key} - no longer in view`);
322
+ // cancelProcessing();
323
+ // }
324
+ // }, [isInView, isProcessing, cancelProcessing, image.key]);
325
+ // // Memoize the toggle selection handler to prevent unnecessary re-renders
326
+ // const handleToggleSelection = useMemo(() => {
327
+ // return () => onToggleSelection(image.key);
328
+ // }, [onToggleSelection, image.key]);
329
+ // const handleCheckboxClick = useMemo(() => {
330
+ // return (e: React.MouseEvent) => {
331
+ // e.stopPropagation();
332
+ // onToggleSelection(image.key);
333
+ // };
334
+ // }, [onToggleSelection, image.key]);
335
+ // // Create placeholder content for non-visible cards
336
+ // const renderPlaceholder = () => (
337
+ // <Box
338
+ // sx={{
339
+ // height: 200,
340
+ // backgroundColor: 'grey.100',
341
+ // display: 'flex',
342
+ // alignItems: 'center',
343
+ // justifyContent: 'center',
344
+ // flexDirection: 'column'
345
+ // }}
346
+ // >
347
+ // <CircularProgress size={24} sx={{ mb: 1 }} />
348
+ // <Typography variant="caption" color="text.secondary">
349
+ // Loading...
350
+ // </Typography>
351
+ // </Box>
352
+ // );
353
+ // // Create processing indicator
354
+ // const renderProcessingOverlay = () => {
355
+ // return isProcessing && shouldLoadImage && isInView ? (
356
+ // <Box
357
+ // sx={{
358
+ // position: 'absolute',
359
+ // top: 8,
360
+ // right: 8,
361
+ // backgroundColor: 'rgba(0, 0, 0, 0.8)',
362
+ // color: 'white',
363
+ // borderRadius: 1,
364
+ // px: 1,
365
+ // py: 0.5,
366
+ // display: 'flex',
367
+ // alignItems: 'center',
368
+ // gap: 0.5,
369
+ // zIndex: 1,
370
+ // maxWidth: '120px'
371
+ // }}
372
+ // >
373
+ // <CircularProgress size={12} color="inherit" />
374
+ // <Typography variant="caption">Processing</Typography>
375
+ // </Box>
376
+ // ) : null;
377
+ // };
378
+ // return (
379
+ // <Card
380
+ // ref={cardRef}
381
+ // sx={{
382
+ // border: image.isSelected ? 2 : 1,
383
+ // borderColor: image.isSelected ? 'primary.main' : 'divider',
384
+ // cursor: 'pointer',
385
+ // transition: 'all 0.2s',
386
+ // position: 'relative',
387
+ // opacity: 1,
388
+ // '&:hover': {
389
+ // transform: 'translateY(-2px)',
390
+ // boxShadow: 2,
391
+ // }
392
+ // }}
393
+ // onClick={handleToggleSelection}
394
+ // >
395
+ // {shouldLoadImage ? (
396
+ // <Box sx={{ position: 'relative', width: '100%', height: '200px' }}>
397
+ // <img
398
+ // id="image-card-media"
399
+ // src={processedImageSrc}
400
+ // alt=""
401
+ // style={{
402
+ // width: '100%',
403
+ // height: '200px',
404
+ // objectFit: 'cover',
405
+ // opacity: 1,
406
+ // display: 'block'
407
+ // }}
408
+ // loading="lazy"
409
+ // />
410
+ // {renderProcessingOverlay()}
411
+ // </Box>
412
+ // ) : (
413
+ // renderPlaceholder()
414
+ // )}
415
+ // <CardContent sx={{ pb: 1 }}>
416
+ // <Box display="flex" alignItems="center" justifyContent="space-between">
417
+ // <Typography variant="body2" color="text.secondary">
418
+ // Image {image.key}
419
+ // {!shouldLoadImage && (
420
+ // <Typography
421
+ // component="span"
422
+ // variant="caption"
423
+ // sx={{ ml: 1 }}
424
+ // >
425
+ // (waiting...)
426
+ // </Typography>
427
+ // )}
428
+ // {!isInView && shouldLoadImage && (
429
+ // <Typography
430
+ // component="span"
431
+ // variant="caption"
432
+ // sx={{ ml: 1, color: 'warning.main' }}
433
+ // >
434
+ // (paused)
435
+ // </Typography>
436
+ // )}
437
+ // </Typography>
438
+ // <Checkbox
439
+ // checked={image.isSelected}
440
+ // size="small"
441
+ // onClick={handleCheckboxClick}
442
+ // />
443
+ // </Box>
444
+ // </CardContent>
445
+ // </Card>
446
+ // );
447
+ // }, (prevProps, nextProps) => {
448
+ // // Custom comparison function for React.memo - be more strict to prevent unnecessary re-renders
449
+ // const prevAdj = prevProps.image.adjustments || {};
450
+ // const nextAdj = nextProps.image.adjustments || {};
451
+ // // Only re-render if key properties actually change
452
+ // const shouldUpdate = (
453
+ // prevProps.image.key !== nextProps.image.key ||
454
+ // prevProps.image.isSelected !== nextProps.image.isSelected ||
455
+ // prevProps.image.src !== nextProps.image.src ||
456
+ // // Compare individual adjustment properties to avoid JSON.stringify issues
457
+ // prevAdj.temperature !== nextAdj.temperature ||
458
+ // prevAdj.tint !== nextAdj.tint ||
459
+ // prevAdj.exposure !== nextAdj.exposure ||
460
+ // prevAdj.contrast !== nextAdj.contrast ||
461
+ // prevAdj.clarity !== nextAdj.clarity ||
462
+ // prevAdj.vibrance !== nextAdj.vibrance ||
463
+ // prevAdj.saturation !== nextAdj.saturation ||
464
+ // prevAdj.highlights !== nextAdj.highlights ||
465
+ // prevAdj.shadows !== nextAdj.shadows ||
466
+ // prevAdj.whites !== nextAdj.whites ||
467
+ // prevAdj.blacks !== nextAdj.blacks ||
468
+ // prevAdj.sharpness !== nextAdj.sharpness
469
+ // );
470
+ // // Return false to re-render, true to skip re-render
471
+ // return !shouldUpdate;
472
+ // });
473
+ // export const HonchoEditorBulkDemo: React.FC = () => {
474
+ // const [controller] = useState(() => createMockController());
475
+ // const [showAdjustments, setShowAdjustments] = useState(false);
476
+ // const {
477
+ // imageData,
478
+ // isLoading,
479
+ // error,
480
+ // selectedIds,
481
+ // hasMore,
482
+ // selectedBulkPreset,
483
+ // presets, // Add presets
484
+ // activePreset, // Add activePreset
485
+ // handleToggleImageSelection,
486
+ // handleLoadMore,
487
+ // handleRefresh,
488
+ // handleSelectBulkPreset,
489
+ // handleBackCallbackBulk,
490
+ // // Temperature adjustments
491
+ // handleBulkTempDecreaseMax,
492
+ // handleBulkTempDecrease,
493
+ // handleBulkTempIncrease,
494
+ // handleBulkTempIncreaseMax,
495
+ // // Tint adjustments
496
+ // handleBulkTintDecreaseMax,
497
+ // handleBulkTintDecrease,
498
+ // handleBulkTintIncrease,
499
+ // handleBulkTintIncreaseMax,
500
+ // // Exposure adjustments
501
+ // handleBulkExposureDecreaseMax,
502
+ // handleBulkExposureDecrease,
503
+ // handleBulkExposureIncrease,
504
+ // handleBulkExposureIncreaseMax,
505
+ // // Contrast adjustments
506
+ // handleBulkContrastDecreaseMax,
507
+ // handleBulkContrastDecrease,
508
+ // handleBulkContrastIncrease,
509
+ // handleBulkContrastIncreaseMax,
510
+ // // Clarity adjustments
511
+ // handleBulkClarityDecreaseMax,
512
+ // handleBulkClarityDecrease,
513
+ // handleBulkClarityIncrease,
514
+ // handleBulkClarityIncreaseMax,
515
+ // // History actions
516
+ // handleUndo,
517
+ // handleRedo,
518
+ // handleReset,
519
+ // historyInfo,
520
+ // } = useHonchoEditorBulk(controller, 'demo-event', 'demo-user');
521
+ // const selectedCount = selectedIds.length;
522
+ // const totalCount = imageData.length;
523
+ // return (
524
+ // <Container maxWidth="xl" sx={{ py: 4 }}>
525
+ // <Typography variant="h3" gutterBottom align="center">
526
+ // Honcho Editor Bulk Demo
527
+ // </Typography>
528
+ // <Typography variant="subtitle1" align="center" color="text.secondary" gutterBottom>
529
+ // This demo shows the useHonchoEditorBulk hook with 1,277 mock images from picsum.dev
530
+ // </Typography>
531
+ // {error && (
532
+ // <Alert severity="error" sx={{ mb: 3 }}>
533
+ // {error}
534
+ // </Alert>
535
+ // )}
536
+ // <Paper sx={{ p: 3, mb: 3 }}>
537
+ // <Stack direction="row" spacing={2} alignItems="center" flexWrap="wrap">
538
+ // <Button
539
+ // variant="contained"
540
+ // onClick={handleRefresh}
541
+ // disabled={isLoading}
542
+ // >
543
+ // {isLoading ? <CircularProgress size={20} /> : 'Refresh Images'}
544
+ // </Button>
545
+ // <Button
546
+ // variant="outlined"
547
+ // onClick={() => setShowAdjustments(!showAdjustments)}
548
+ // >
549
+ // {showAdjustments ? 'Hide' : 'Show'} Adjustments
550
+ // </Button>
551
+ // <Button
552
+ // variant="outlined"
553
+ // onClick={() => {
554
+ // // Select all images on current page
555
+ // imageData.forEach(image => {
556
+ // if (!image.isSelected) {
557
+ // handleToggleImageSelection(image.key);
558
+ // }
559
+ // });
560
+ // }}
561
+ // disabled={isLoading || imageData.length === 0}
562
+ // >
563
+ // Select All
564
+ // </Button>
565
+ // <Button
566
+ // variant="outlined"
567
+ // onClick={() => {
568
+ // // Deselect all images
569
+ // imageData.forEach(image => {
570
+ // if (image.isSelected) {
571
+ // handleToggleImageSelection(image.key);
572
+ // }
573
+ // });
574
+ // }}
575
+ // disabled={isLoading || selectedCount === 0}
576
+ // >
577
+ // Clear Selection
578
+ // </Button>
579
+ // <Button
580
+ // variant="outlined"
581
+ // onClick={() => {
582
+ // console.log('Debug Info:', {
583
+ // selectedIds,
584
+ // selectedCount,
585
+ // showAdjustments,
586
+ // presets: presets.map(p => ({ id: p.id, name: p.name })),
587
+ // selectedBulkPreset,
588
+ // activePreset: activePreset ? { id: activePreset.id, name: activePreset.name } : null,
589
+ // imageData: imageData.map(img => ({ key: img.key, isSelected: img.isSelected }))
590
+ // });
591
+ // }}
592
+ // >
593
+ // Debug Selection
594
+ // </Button>
595
+ // <Button
596
+ // variant="outlined"
597
+ // onClick={handleBackCallbackBulk}
598
+ // >
599
+ // Back
600
+ // </Button>
601
+ // <Chip
602
+ // label={`${selectedCount} of ${totalCount} selected`}
603
+ // color={selectedCount > 0 ? "primary" : "default"}
604
+ // />
605
+ // {hasMore && (
606
+ // <Button
607
+ // variant="outlined"
608
+ // onClick={handleLoadMore}
609
+ // disabled={isLoading}
610
+ // >
611
+ // Load More
612
+ // </Button>
613
+ // )}
614
+ // <FormControl size="small" sx={{ minWidth: 150 }}>
615
+ // <InputLabel>Bulk Preset</InputLabel>
616
+ // <Select
617
+ // value={selectedBulkPreset}
618
+ // onChange={handleSelectBulkPreset}
619
+ // label="Bulk Preset"
620
+ // displayEmpty
621
+ // >
622
+ // <MenuItem value="">
623
+ // <em>No Preset</em>
624
+ // </MenuItem>
625
+ // {presets.map((preset) => (
626
+ // <MenuItem key={preset.id} value={preset.id}>
627
+ // {preset.name}
628
+ // {activePreset?.id === preset.id && ' โœ“'}
629
+ // </MenuItem>
630
+ // ))}
631
+ // </Select>
632
+ // </FormControl>
633
+ // <Typography variant="body2" sx={{ ml: 2, opacity: 0.7 }}>
634
+ // Active: {activePreset ? activePreset.name : 'None'} | Presets: {presets.length}
635
+ // </Typography>
636
+ // </Stack>
637
+ // </Paper>
638
+ // {showAdjustments && (
639
+ // <Paper sx={{ p: 3, mb: 3 }}>
640
+ // <Stack direction="row" justifyContent="space-between" alignItems="center" sx={{ mb: 2 }}>
641
+ // <Typography variant="h6">
642
+ // Bulk Adjustments ({selectedCount} images selected)
643
+ // </Typography>
644
+ // <Stack direction="row" spacing={1}>
645
+ // <Button
646
+ // variant="outlined"
647
+ // size="small"
648
+ // onClick={handleUndo}
649
+ // disabled={!historyInfo.canUndo}
650
+ // sx={{ minWidth: 80 }}
651
+ // >
652
+ // Undo
653
+ // </Button>
654
+ // <Button
655
+ // variant="outlined"
656
+ // size="small"
657
+ // onClick={handleRedo}
658
+ // disabled={!historyInfo.canRedo}
659
+ // sx={{ minWidth: 80 }}
660
+ // >
661
+ // Redo
662
+ // </Button>
663
+ // <Button
664
+ // variant="outlined"
665
+ // size="small"
666
+ // onClick={() => handleReset()}
667
+ // disabled={selectedCount === 0}
668
+ // color="warning"
669
+ // sx={{ minWidth: 80 }}
670
+ // >
671
+ // Reset
672
+ // </Button>
673
+ // </Stack>
674
+ // </Stack>
675
+ // {selectedCount === 0 && (
676
+ // <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
677
+ // Select one or more images below to enable bulk adjustments
678
+ // </Typography>
679
+ // )}
680
+ // <Typography variant="body2" color="text.secondary" sx={{ mb: 2 }}>
681
+ // History: {historyInfo.currentIndex + 1}/{historyInfo.totalStates} states |
682
+ // Can Undo: {historyInfo.canUndo ? 'Yes' : 'No'} |
683
+ // Can Redo: {historyInfo.canRedo ? 'Yes' : 'No'}
684
+ // </Typography>
685
+ // <Grid container spacing={3}>
686
+ // <Grid item xs={12} md={3}>
687
+ // <Typography variant="subtitle2" gutterBottom>Color</Typography>
688
+ // <AdjustmentControls
689
+ // label="Temperature"
690
+ // onDecreaseMax={handleBulkTempDecreaseMax}
691
+ // onDecrease={handleBulkTempDecrease}
692
+ // onIncrease={handleBulkTempIncrease}
693
+ // onIncreaseMax={handleBulkTempIncreaseMax}
694
+ // disabled={selectedCount === 0}
695
+ // />
696
+ // <AdjustmentControls
697
+ // label="Tint"
698
+ // onDecreaseMax={handleBulkTintDecreaseMax}
699
+ // onDecrease={handleBulkTintDecrease}
700
+ // onIncrease={handleBulkTintIncrease}
701
+ // onIncreaseMax={handleBulkTintIncreaseMax}
702
+ // disabled={selectedCount === 0}
703
+ // />
704
+ // </Grid>
705
+ // <Grid item xs={12} md={3}>
706
+ // <Typography variant="subtitle2" gutterBottom>Light</Typography>
707
+ // <AdjustmentControls
708
+ // label="Exposure"
709
+ // onDecreaseMax={handleBulkExposureDecreaseMax}
710
+ // onDecrease={handleBulkExposureDecrease}
711
+ // onIncrease={handleBulkExposureIncrease}
712
+ // onIncreaseMax={handleBulkExposureIncreaseMax}
713
+ // disabled={selectedCount === 0}
714
+ // />
715
+ // <AdjustmentControls
716
+ // label="Contrast"
717
+ // onDecreaseMax={handleBulkContrastDecreaseMax}
718
+ // onDecrease={handleBulkContrastDecrease}
719
+ // onIncrease={handleBulkContrastIncrease}
720
+ // onIncreaseMax={handleBulkContrastIncreaseMax}
721
+ // disabled={selectedCount === 0}
722
+ // />
723
+ // </Grid>
724
+ // <Grid item xs={12} md={3}>
725
+ // <Typography variant="subtitle2" gutterBottom>Details</Typography>
726
+ // <AdjustmentControls
727
+ // label="Clarity"
728
+ // onDecreaseMax={handleBulkClarityDecreaseMax}
729
+ // onDecrease={handleBulkClarityDecrease}
730
+ // onIncrease={handleBulkClarityIncrease}
731
+ // onIncreaseMax={handleBulkClarityIncreaseMax}
732
+ // disabled={selectedCount === 0}
733
+ // />
734
+ // </Grid>
735
+ // </Grid>
736
+ // </Paper>
737
+ // )}
738
+ // <Grid container spacing={2}>
739
+ // {imageData.map((image) => (
740
+ // <Grid item xs={12} sm={6} md={4} lg={3} key={image.key}>
741
+ // <ImageCard
742
+ // image={image}
743
+ // onToggleSelection={handleToggleImageSelection}
744
+ // />
745
+ // </Grid>
746
+ // ))}
747
+ // </Grid>
748
+ // {isLoading && imageData.length === 0 && (
749
+ // <Box display="flex" justifyContent="center" py={4}>
750
+ // <CircularProgress />
751
+ // </Box>
752
+ // )}
753
+ // <Box mt={4}>
754
+ // <Divider />
755
+ // <Typography variant="body2" color="text.secondary" align="center" sx={{ mt: 2 }}>
756
+ // Demo Notes: This uses mock data and simulated API calls.
757
+ // Select images and try the bulk adjustment controls above.
758
+ // </Typography>
759
+ // </Box>
760
+ // </Container>
761
+ // );
762
+ // };
763
+ // // Add debugging function to global scope for manual testing
764
+ // if (typeof window !== 'undefined') {
765
+ // (window as any).testPresetSelection = () => {
766
+ // console.log("๐Ÿงช Manual preset test - this would normally be called by the UI");
767
+ // console.log("๐Ÿงช To test properly, use the actual preset dropdown in the UI above");
768
+ // };
769
+ // }
770
+ // export default HonchoEditorBulkDemo;