@yogiswara/honcho-editor-ui 2.7.13 → 2.7.14

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 (139) hide show
  1. package/package.json +1 -1
  2. package/dist/color.d.ts +0 -9
  3. package/dist/color.js +0 -9
  4. package/dist/components/editor/GalleryAlbum/AlbumImageGallery.d.ts +0 -8
  5. package/dist/components/editor/GalleryAlbum/AlbumImageGallery.js +0 -28
  6. package/dist/components/editor/GalleryAlbum/ImageItem.d.ts +0 -10
  7. package/dist/components/editor/GalleryAlbum/ImageItem.js +0 -81
  8. package/dist/components/editor/HAccordionAspectRatio.d.ts +0 -14
  9. package/dist/components/editor/HAccordionAspectRatio.js +0 -102
  10. package/dist/components/editor/HAccordionColor.d.ts +0 -16
  11. package/dist/components/editor/HAccordionColor.js +0 -282
  12. package/dist/components/editor/HAccordionColorAdjustment.d.ts +0 -35
  13. package/dist/components/editor/HAccordionColorAdjustment.js +0 -31
  14. package/dist/components/editor/HAccordionDetails.d.ts +0 -12
  15. package/dist/components/editor/HAccordionDetails.js +0 -157
  16. package/dist/components/editor/HAccordionLight.d.ts +0 -20
  17. package/dist/components/editor/HAccordionLight.js +0 -414
  18. package/dist/components/editor/HAccordionPreset.d.ts +0 -23
  19. package/dist/components/editor/HAccordionPreset.js +0 -50
  20. package/dist/components/editor/HAlertBox.d.ts +0 -8
  21. package/dist/components/editor/HAlertBox.js +0 -55
  22. package/dist/components/editor/HAspectRatioMobile.d.ts +0 -0
  23. package/dist/components/editor/HAspectRatioMobile.js +0 -1
  24. package/dist/components/editor/HBulkAccordionColorAdjustment.d.ts +0 -55
  25. package/dist/components/editor/HBulkAccordionColorAdjustment.js +0 -31
  26. package/dist/components/editor/HBulkAccordionColorAdjustmentColors.d.ts +0 -20
  27. package/dist/components/editor/HBulkAccordionColorAdjustmentColors.js +0 -121
  28. package/dist/components/editor/HBulkAccordionColorAdjustmentDetails.d.ts +0 -12
  29. package/dist/components/editor/HBulkAccordionColorAdjustmentDetails.js +0 -65
  30. package/dist/components/editor/HBulkAccordionColorAdjustmentLight.d.ts +0 -28
  31. package/dist/components/editor/HBulkAccordionColorAdjustmentLight.js +0 -177
  32. package/dist/components/editor/HBulkColorAdjustmentMobile.d.ts +0 -53
  33. package/dist/components/editor/HBulkColorAdjustmentMobile.js +0 -16
  34. package/dist/components/editor/HBulkColorMobile.d.ts +0 -20
  35. package/dist/components/editor/HBulkColorMobile.js +0 -121
  36. package/dist/components/editor/HBulkDetailsMobile.d.ts +0 -12
  37. package/dist/components/editor/HBulkDetailsMobile.js +0 -65
  38. package/dist/components/editor/HBulkLightMobile.d.ts +0 -28
  39. package/dist/components/editor/HBulkLightMobile.js +0 -192
  40. package/dist/components/editor/HBulkPreset.d.ts +0 -24
  41. package/dist/components/editor/HBulkPreset.js +0 -43
  42. package/dist/components/editor/HBulkPresetMobile.d.ts +0 -15
  43. package/dist/components/editor/HBulkPresetMobile.js +0 -26
  44. package/dist/components/editor/HDialogBox.d.ts +0 -18
  45. package/dist/components/editor/HDialogBox.js +0 -51
  46. package/dist/components/editor/HDialogCopy.d.ts +0 -40
  47. package/dist/components/editor/HDialogCopy.js +0 -80
  48. package/dist/components/editor/HFooter.d.ts +0 -12
  49. package/dist/components/editor/HFooter.js +0 -24
  50. package/dist/components/editor/HHeaderEditor.d.ts +0 -17
  51. package/dist/components/editor/HHeaderEditor.js +0 -36
  52. package/dist/components/editor/HImageEditorBulkDekstop.d.ts +0 -15
  53. package/dist/components/editor/HImageEditorBulkDekstop.js +0 -29
  54. package/dist/components/editor/HImageEditorBulkMobile.d.ts +0 -72
  55. package/dist/components/editor/HImageEditorBulkMobile.js +0 -81
  56. package/dist/components/editor/HImageEditorDekstop.d.ts +0 -15
  57. package/dist/components/editor/HImageEditorDekstop.js +0 -29
  58. package/dist/components/editor/HImageEditorMobile.d.ts +0 -51
  59. package/dist/components/editor/HImageEditorMobile.js +0 -92
  60. package/dist/components/editor/HImageEditorMobileLayout.d.ts +0 -14
  61. package/dist/components/editor/HImageEditorMobileLayout.js +0 -58
  62. package/dist/components/editor/HModalEditorDekstop.d.ts +0 -13
  63. package/dist/components/editor/HModalEditorDekstop.js +0 -22
  64. package/dist/components/editor/HModalMobile.d.ts +0 -13
  65. package/dist/components/editor/HModalMobile.js +0 -7
  66. package/dist/components/editor/HPresetDelete.d.ts +0 -7
  67. package/dist/components/editor/HPresetDelete.js +0 -7
  68. package/dist/components/editor/HPresetOptionMenu.d.ts +0 -9
  69. package/dist/components/editor/HPresetOptionMenu.js +0 -20
  70. package/dist/components/editor/HSliderColorMobile.d.ts +0 -16
  71. package/dist/components/editor/HSliderColorMobile.js +0 -270
  72. package/dist/components/editor/HSliderDetailsMobile.d.ts +0 -12
  73. package/dist/components/editor/HSliderDetailsMobile.js +0 -154
  74. package/dist/components/editor/HSliderLightMobile.d.ts +0 -20
  75. package/dist/components/editor/HSliderLightMobile.js +0 -420
  76. package/dist/components/editor/HTabAspectRatioMobile.d.ts +0 -0
  77. package/dist/components/editor/HTabAspectRatioMobile.js +0 -1
  78. package/dist/components/editor/HTabColorAdjustmentMobile.d.ts +0 -33
  79. package/dist/components/editor/HTabColorAdjustmentMobile.js +0 -16
  80. package/dist/components/editor/HTabPresetMobile.d.ts +0 -14
  81. package/dist/components/editor/HTabPresetMobile.js +0 -10
  82. package/dist/components/editor/HTextField.d.ts +0 -14
  83. package/dist/components/editor/HTextField.js +0 -51
  84. package/dist/components/editor/HWatermarkView.d.ts +0 -6
  85. package/dist/components/editor/HWatermarkView.js +0 -16
  86. package/dist/components/editor/svg/Tick.d.ts +0 -2
  87. package/dist/components/editor/svg/Tick.js +0 -6
  88. package/dist/components/modal/HModalDialog.d.ts +0 -12
  89. package/dist/components/modal/HModalDialog.js +0 -18
  90. package/dist/components/modal/HModalRename.d.ts +0 -14
  91. package/dist/components/modal/HModalRename.js +0 -35
  92. package/dist/hooks/demo/HonchoEditorBulkDemo.d.ts +0 -3
  93. package/dist/hooks/demo/HonchoEditorBulkDemo.js +0 -410
  94. package/dist/hooks/demo/HonchoEditorSingleCleanDemo.d.ts +0 -3
  95. package/dist/hooks/demo/HonchoEditorSingleCleanDemo.js +0 -354
  96. package/dist/hooks/demo/index.d.ts +0 -2
  97. package/dist/hooks/demo/index.js +0 -2
  98. package/dist/hooks/editor/type.d.ts +0 -174
  99. package/dist/hooks/editor/type.js +0 -1
  100. package/dist/hooks/editor/useHonchoEditorBulk.d.ts +0 -96
  101. package/dist/hooks/editor/useHonchoEditorBulk.js +0 -427
  102. package/dist/hooks/editor/useHonchoEditorSingle.d.ts +0 -44
  103. package/dist/hooks/editor/useHonchoEditorSingle.js +0 -162
  104. package/dist/hooks/useAdjustmentHistory.d.ts +0 -97
  105. package/dist/hooks/useAdjustmentHistory.js +0 -493
  106. package/dist/hooks/useAdjustmentHistoryBatch.d.ts +0 -177
  107. package/dist/hooks/useAdjustmentHistoryBatch.js +0 -1189
  108. package/dist/hooks/useGallerySwipe.d.ts +0 -36
  109. package/dist/hooks/useGallerySwipe.js +0 -344
  110. package/dist/hooks/usePaging.d.ts +0 -89
  111. package/dist/hooks/usePaging.js +0 -211
  112. package/dist/hooks/usePreset.d.ts +0 -82
  113. package/dist/hooks/usePreset.js +0 -344
  114. package/dist/index.d.ts +0 -41
  115. package/dist/index.js +0 -44
  116. package/dist/lib/context/EditorContext.d.ts +0 -28
  117. package/dist/lib/context/EditorContext.js +0 -60
  118. package/dist/lib/context/EditorProcessingService.d.ts +0 -36
  119. package/dist/lib/context/EditorProcessingService.js +0 -249
  120. package/dist/lib/editor/honcho-editor.d.ts +0 -324
  121. package/dist/lib/editor/honcho-editor.js +0 -825
  122. package/dist/lib/hooks/useEditor.d.ts +0 -22
  123. package/dist/lib/hooks/useEditor.js +0 -35
  124. package/dist/lib/hooks/useEditorHeadless.d.ts +0 -34
  125. package/dist/lib/hooks/useEditorHeadless.js +0 -207
  126. package/dist/lib/hooks/useImageProcessor.d.ts +0 -18
  127. package/dist/lib/hooks/useImageProcessor.js +0 -113
  128. package/dist/setupTests.d.ts +0 -1
  129. package/dist/setupTests.js +0 -1
  130. package/dist/themes/colors.d.ts +0 -12
  131. package/dist/themes/colors.js +0 -12
  132. package/dist/themes/honchoTheme.d.ts +0 -25
  133. package/dist/themes/honchoTheme.js +0 -94
  134. package/dist/utils/adjustment.d.ts +0 -6
  135. package/dist/utils/adjustment.js +0 -48
  136. package/dist/utils/imageLoader.d.ts +0 -11
  137. package/dist/utils/imageLoader.js +0 -48
  138. package/dist/utils/isMobile.d.ts +0 -1
  139. package/dist/utils/isMobile.js +0 -5
@@ -1,1189 +0,0 @@
1
- import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
2
- import { mapAdjustmentStateToColorAdjustment, mapColorAdjustmentToAdjustmentState } from '../utils/adjustment';
3
- /**
4
- * Create default adjustment state
5
- */
6
- const createDefaultAdjustmentState = (overrides) => ({
7
- tempScore: 0,
8
- tintScore: 0,
9
- vibranceScore: 0,
10
- saturationScore: 0,
11
- exposureScore: 0,
12
- highlightsScore: 0,
13
- shadowsScore: 0,
14
- whitesScore: 0,
15
- blacksScore: 0,
16
- contrastScore: 0,
17
- clarityScore: 0,
18
- sharpnessScore: 0,
19
- ...overrides
20
- });
21
- /**
22
- * Compare two BatchAdjustmentState objects for equality
23
- */
24
- const compareBatchStates = (a, b) => {
25
- try {
26
- return JSON.stringify(a) === JSON.stringify(b);
27
- }
28
- catch (error) {
29
- console.warn('Failed to compare batch states with JSON.stringify:', error);
30
- return false;
31
- }
32
- };
33
- /**
34
- * Create empty batch state
35
- */
36
- const createEmptyBatchState = () => ({
37
- currentSelection: {},
38
- allImages: {},
39
- initialStates: {}
40
- });
41
- /**
42
- * Generate unique ID for history entries using UUID format
43
- */
44
- const generateEntryId = () => {
45
- // Simple UUID v4 implementation
46
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
47
- const r = Math.random() * 16 | 0;
48
- const v = c === 'x' ? r : (r & 0x3 | 0x8);
49
- return v.toString(16);
50
- });
51
- };
52
- /**
53
- * Generate unique task ID for backend operations (same as entry ID)
54
- */
55
- const generateTaskId = () => {
56
- return generateEntryId();
57
- };
58
- /**
59
- * Advanced hook for managing batch AdjustmentState history with selective undo/redo functionality.
60
- *
61
- * **Pure State Management Design:**
62
- * - Starts empty, no image loading functionality
63
- * - Focus on state management and history tracking only
64
- * - Selection-based operations with persistent state
65
- * - Manual selection via `actions.setSelection()`
66
- *
67
- * **Key Features:**
68
- * - **Current Selection**: Images actively being adjusted
69
- * - **All Images**: Persistent state for every image touched
70
- * - **Selective Operations**: Undo/redo only affects selected images
71
- * - **Automatic State Persistence**: Images remain in allImages even when deselected
72
- *
73
- * **Typical Usage Flow:**
74
- * ```typescript
75
- * const { actions, currentBatch, selectedIds } = useAdjustmentHistoryBatch();
76
- *
77
- * // Select images (new images get default state automatically)
78
- * actions.setSelection(['img1', 'img2', 'img3']);
79
- *
80
- * // Apply adjustments to selected images
81
- * actions.adjustSelected({ exposureScore: 10 });
82
- *
83
- * // Change selection (img1, img2 state persists in allImages)
84
- * actions.setSelection(['img3']);
85
- *
86
- * // Adjust only img3
87
- * actions.adjustSelected({ contrastScore: 5 });
88
- *
89
- * // Undo affects only currently selected (img3)
90
- * actions.undo();
91
- * ```
92
- *
93
- * **State Structure:**
94
- * - `currentBatch.currentSelection`: Currently selected images and their states
95
- * - `currentBatch.allImages`: All images that have been selected/adjusted (persistent)
96
- * - `selectedIds`: Array of currently selected image IDs
97
- *
98
- * @param options - Configuration options for history behavior
99
- * @returns Object with current batch, selection, history info, actions, and config
100
- */
101
- export function useAdjustmentHistoryBatch(options = {}) {
102
- // Internal stabilization
103
- const internalOptions = useMemo(() => ({
104
- maxSize: options.maxSize ?? 'unlimited',
105
- devWarnings: options.devWarnings ?? false,
106
- defaultAdjustmentState: options.defaultAdjustmentState ?? {},
107
- controller: options.controller,
108
- firebaseUid: options.firebaseUid,
109
- eventId: options.eventId
110
- }), [
111
- options.maxSize,
112
- options.devWarnings,
113
- options.defaultAdjustmentState,
114
- options.controller,
115
- options.firebaseUid,
116
- options.eventId
117
- ]);
118
- // Core state management - using per-image history instead of batch history
119
- const [allImageIds, setAllImageIds] = useState([]);
120
- const [selectedIds, setSelectedIds] = useState([]);
121
- const [imageHistories, setImageHistories] = useState([]);
122
- const [currentBatch, setCurrentBatch] = useState(createEmptyBatchState());
123
- const [lastUpdateTimestamp, setLastUpdateTimestamp] = useState(Date.now());
124
- // Configuration refs
125
- const maxSizeRef = useRef(internalOptions.maxSize);
126
- const devWarningsRef = useRef(internalOptions.devWarnings);
127
- // Helper function to rebuild currentBatch from imageHistories
128
- const rebuildCurrentBatch = useCallback(() => {
129
- console.log('[useAdjustmentHistoryBatch] 🔧 rebuildCurrentBatch called with imageHistories:', imageHistories.map(h => ({
130
- imageId: h.imageId,
131
- currentHistoryEntryId: h.currentHistoryEntryId,
132
- historyLength: h.history.length,
133
- historyIds: h.history.map(entry => entry.id)
134
- })));
135
- const newBatch = createEmptyBatchState();
136
- imageHistories.forEach(imageHistory => {
137
- // Find current adjustment using currentHistoryEntryId
138
- const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
139
- console.log(`[useAdjustmentHistoryBatch] 🔍 rebuildCurrentBatch for image ${imageHistory.imageId}:`, {
140
- currentHistoryEntryId: imageHistory.currentHistoryEntryId,
141
- foundCurrentEntry: !!currentEntry,
142
- historyLength: imageHistory.history.length,
143
- historyIds: imageHistory.history.map(entry => entry.id)
144
- });
145
- if (currentEntry) {
146
- newBatch.allImages[imageHistory.imageId] = currentEntry.adjustment;
147
- if (selectedIds.includes(imageHistory.imageId)) {
148
- newBatch.currentSelection[imageHistory.imageId] = currentEntry.adjustment;
149
- }
150
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully rebuilt batch for image ${imageHistory.imageId}`);
151
- }
152
- else {
153
- console.error(`[useAdjustmentHistoryBatch] ❌ CRITICAL: Current entry not found for image ${imageHistory.imageId}!`, {
154
- searchingFor: imageHistory.currentHistoryEntryId,
155
- availableIds: imageHistory.history.map(entry => entry.id),
156
- historyLength: imageHistory.history.length
157
- });
158
- }
159
- });
160
- console.log('[useAdjustmentHistoryBatch] 🔧 rebuildCurrentBatch result:', {
161
- allImagesCount: Object.keys(newBatch.allImages).length,
162
- currentSelectionCount: Object.keys(newBatch.currentSelection).length,
163
- allImagesIds: Object.keys(newBatch.allImages),
164
- currentSelectionIds: Object.keys(newBatch.currentSelection)
165
- });
166
- return newBatch;
167
- }, [imageHistories, selectedIds]);
168
- // Sync currentBatch with imageHistories
169
- useEffect(() => {
170
- setCurrentBatch(rebuildCurrentBatch());
171
- }, [rebuildCurrentBatch]);
172
- // Memory usage calculation
173
- const getMemoryUsage = useCallback(() => {
174
- try {
175
- const historiesString = JSON.stringify(imageHistories);
176
- return historiesString.length * 2; // Rough estimate: 2 bytes per character
177
- }
178
- catch (error) {
179
- console.warn('Failed to estimate memory usage:', error);
180
- return imageHistories.length * 100 * 1000; // Fallback estimate
181
- }
182
- }, [imageHistories]);
183
- // Trim individual image histories to specified size
184
- const trimImageHistoriesToSize = useCallback((size) => {
185
- if (size <= 0)
186
- return;
187
- setImageHistories(prevHistories => prevHistories.map(imageHistory => ({
188
- ...imageHistory,
189
- history: imageHistory.history.length <= size
190
- ? imageHistory.history
191
- : imageHistory.history.slice(-size) // Keep last 'size' entries
192
- })));
193
- }, []);
194
- // Apply max size limit
195
- const enforceMaxSize = useCallback(() => {
196
- if (maxSizeRef.current === 'unlimited')
197
- return;
198
- const maxSize = maxSizeRef.current;
199
- if (typeof maxSize === 'number' && imageHistories.length > 0) {
200
- const totalHistorySize = imageHistories.reduce((sum, h) => sum + h.history.length, 0);
201
- if (totalHistorySize > maxSize * imageHistories.length) {
202
- trimImageHistoriesToSize(maxSize);
203
- }
204
- }
205
- }, [imageHistories, trimImageHistoriesToSize]);
206
- // Apply adjustment deltas to selected images - with entry-based history and backend sync
207
- const adjustSelected = useCallback(async (delta) => {
208
- if (selectedIds.length === 0) {
209
- if (devWarningsRef.current) {
210
- console.warn('[useAdjustmentHistoryBatch] adjustSelected called with no selection');
211
- }
212
- return;
213
- }
214
- // Prepare backend operations BEFORE state update (outside setImageHistories)
215
- const backendOperations = [];
216
- // Process each image to prepare operations
217
- const operationsToApply = [];
218
- imageHistories.forEach(imageHistory => {
219
- if (!selectedIds.includes(imageHistory.imageId)) {
220
- return; // Skip images not being adjusted
221
- }
222
- // Get current adjustment from current entry
223
- const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
224
- const currentAdjustment = currentEntry?.adjustment || createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
225
- // Apply deltas with clamping
226
- const newAdjustment = { ...currentAdjustment };
227
- Object.keys(delta).forEach(key => {
228
- const deltaValue = delta[key];
229
- if (typeof deltaValue === 'number') {
230
- const currentValue = newAdjustment[key];
231
- newAdjustment[key] = Math.max(-100, Math.min(100, currentValue + deltaValue));
232
- }
233
- });
234
- // Check if user is in the middle of history (not at latest state)
235
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
236
- const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
237
- let replaceFromTaskId;
238
- console.log(`[useAdjustmentHistoryBatch] đŸŽ¯ Delta replace logic for image ${imageHistory.imageId}:`, {
239
- currentEntryIndex,
240
- historyLength: imageHistory.history.length,
241
- isInMiddleOfHistory,
242
- currentEntryId: currentEntry?.id
243
- });
244
- if (isInMiddleOfHistory) {
245
- // If user is in middle of history, get the task ID of current position
246
- replaceFromTaskId = currentEntry?.id;
247
- console.log(`[useAdjustmentHistoryBatch] 🔄 DELTA will REPLACE current position. replaceFromTaskId=${replaceFromTaskId}`);
248
- }
249
- else {
250
- console.log(`[useAdjustmentHistoryBatch] ➕ DELTA will ADD new entry (at end of history)`);
251
- }
252
- // Generate new task ID for backend (same as entry ID)
253
- const taskId = generateTaskId();
254
- // Create new entry with task ID
255
- const newEntryId = taskId; // Use the same ID for both entry and task
256
- const newEntry = {
257
- id: newEntryId,
258
- adjustment: newAdjustment
259
- };
260
- // Build new history
261
- let newHistory;
262
- if (isInMiddleOfHistory) {
263
- // If in middle of history, truncate from current position and add new entry
264
- newHistory = [...imageHistory.history.slice(0, currentEntryIndex + 1), newEntry];
265
- }
266
- else {
267
- // If at end of history, just add new entry
268
- newHistory = [...imageHistory.history, newEntry];
269
- }
270
- // Trim if needed
271
- const maxSize = maxSizeRef.current;
272
- const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
273
- ? newHistory.slice(-maxSize)
274
- : newHistory;
275
- // Store operation for state update
276
- operationsToApply.push({
277
- imageId: imageHistory.imageId,
278
- newEntryId,
279
- newHistory: trimmedHistory
280
- });
281
- // Prepare backend operation (OUTSIDE state setter)
282
- if (internalOptions.controller && internalOptions.firebaseUid) {
283
- backendOperations.push({
284
- imageId: imageHistory.imageId,
285
- taskId,
286
- adjustment: newAdjustment,
287
- replaceFromTaskId
288
- });
289
- }
290
- });
291
- // Now update state with prepared operations (no backend preparation inside)
292
- setImageHistories(prevHistories => {
293
- return prevHistories.map(imageHistory => {
294
- // Find operation for this image
295
- const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
296
- if (!operation) {
297
- return imageHistory; // No change for images not being adjusted
298
- }
299
- console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for delta on image ${imageHistory.imageId}`);
300
- return {
301
- ...imageHistory,
302
- history: operation.newHistory,
303
- currentHistoryEntryId: operation.newEntryId // Update current pointer
304
- };
305
- });
306
- });
307
- // Perform backend operations asynchronously (already prepared)
308
- if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
309
- try {
310
- console.log(`[useAdjustmentHistoryBatch] 📤 Syncing ${backendOperations.length} adjustments to backend (createEditorConfig for each image)`);
311
- const promises = backendOperations.map(async (operation) => {
312
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for image ${operation.imageId} with taskId ${operation.taskId}`);
313
- await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, {
314
- gallery_id: operation.imageId,
315
- task_id: operation.taskId,
316
- color_adjustment: mapAdjustmentStateToColorAdjustment(operation.adjustment),
317
- replace_from: operation.replaceFromTaskId
318
- });
319
- });
320
- await Promise.all(promises);
321
- if (devWarningsRef.current) {
322
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} adjustments to backend`);
323
- }
324
- }
325
- catch (error) {
326
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync adjustments to backend:', error);
327
- }
328
- }
329
- }, [selectedIds, internalOptions, imageHistories]);
330
- // Shared function for applying adjustments (used by both reset and preset)
331
- const applyAdjustmentToSelected = useCallback(async (adjustment, operationType, targetImageIds) => {
332
- const idsToProcess = targetImageIds || selectedIds;
333
- if (idsToProcess.length === 0) {
334
- if (devWarningsRef.current) {
335
- console.warn(`[useAdjustmentHistoryBatch] ❌ ${operationType} called with no images to process`);
336
- }
337
- return;
338
- }
339
- console.log(`[useAdjustmentHistoryBatch] 🔄 ${operationType.toUpperCase()} called for images:`, idsToProcess);
340
- // Prepare backend operations BEFORE state update (outside setImageHistories)
341
- const backendOperations = [];
342
- // Process each image to prepare operations
343
- const operationsToApply = [];
344
- imageHistories.forEach(imageHistory => {
345
- if (!idsToProcess.includes(imageHistory.imageId)) {
346
- return; // Skip images not being processed
347
- }
348
- console.log(`[useAdjustmentHistoryBatch] 🔄 Processing ${operationType} for image ${imageHistory.imageId}`);
349
- // Get current entry for replace_from logic
350
- const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
351
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
352
- const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
353
- let replaceFromTaskId;
354
- console.log(`[useAdjustmentHistoryBatch] đŸŽ¯ ${operationType.toUpperCase()} replace logic for image ${imageHistory.imageId}:`, {
355
- currentEntryIndex,
356
- historyLength: imageHistory.history.length,
357
- isInMiddleOfHistory,
358
- currentEntryId: currentEntry?.id
359
- });
360
- if (isInMiddleOfHistory && currentEntry?.id) {
361
- // Only use replace_from when truly in middle of history (not at latest)
362
- replaceFromTaskId = currentEntry.id;
363
- console.log(`[useAdjustmentHistoryBatch] 🔄 ${operationType.toUpperCase()} will REPLACE current position. replaceFromTaskId=${replaceFromTaskId}`);
364
- }
365
- else {
366
- console.log(`[useAdjustmentHistoryBatch] ➕ ${operationType.toUpperCase()} will ADD new entry (at end of history)`);
367
- }
368
- // Create new entry with adjustment
369
- const newEntryId = generateEntryId();
370
- const newEntry = {
371
- id: newEntryId,
372
- adjustment
373
- };
374
- // Build new history
375
- let newHistory;
376
- if (isInMiddleOfHistory) {
377
- // If in middle of history, truncate from current position and add new entry
378
- newHistory = [...imageHistory.history.slice(0, currentEntryIndex + 1), newEntry];
379
- }
380
- else {
381
- // If at end of history, just add new entry
382
- newHistory = [...imageHistory.history, newEntry];
383
- }
384
- // Trim if needed
385
- const maxSize = maxSizeRef.current;
386
- const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
387
- ? newHistory.slice(-maxSize)
388
- : newHistory;
389
- // Store operation for state update
390
- operationsToApply.push({
391
- imageId: imageHistory.imageId,
392
- newEntryId,
393
- newHistory: trimmedHistory
394
- });
395
- // Prepare backend operation (OUTSIDE state setter)
396
- if (internalOptions.controller && internalOptions.firebaseUid) {
397
- console.log(`[useAdjustmentHistoryBatch] ✅ Adding ${operationType.toUpperCase()} backend operation for image ${imageHistory.imageId} with taskId ${newEntryId}`, {
398
- replaceFromTaskId,
399
- willReplace: !!replaceFromTaskId
400
- });
401
- backendOperations.push({
402
- imageId: imageHistory.imageId,
403
- taskId: newEntryId,
404
- adjustment,
405
- replaceFromTaskId
406
- });
407
- }
408
- });
409
- // Now update state with prepared operations (no backend preparation inside)
410
- setImageHistories(prevHistories => {
411
- return prevHistories.map(imageHistory => {
412
- // Find operation for this image
413
- const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
414
- if (!operation) {
415
- return imageHistory; // No change for images not being processed
416
- }
417
- console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for ${operationType} on image ${imageHistory.imageId}`);
418
- return {
419
- ...imageHistory,
420
- history: operation.newHistory,
421
- currentHistoryEntryId: operation.newEntryId // Update current pointer
422
- };
423
- });
424
- });
425
- // Perform backend operations asynchronously (already prepared)
426
- if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
427
- try {
428
- console.log(`[useAdjustmentHistoryBatch] 📤 Syncing ${backendOperations.length} ${operationType} operations to backend (createEditorConfig for each image)`);
429
- const promises = backendOperations.map(async (operation) => {
430
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for ${operationType} on image ${operation.imageId} with taskId ${operation.taskId}`, {
431
- replaceFrom: operation.replaceFromTaskId,
432
- hasReplaceFrom: !!operation.replaceFromTaskId
433
- });
434
- await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, {
435
- gallery_id: operation.imageId,
436
- task_id: operation.taskId,
437
- color_adjustment: mapAdjustmentStateToColorAdjustment(operation.adjustment),
438
- replace_from: operation.replaceFromTaskId
439
- });
440
- });
441
- await Promise.all(promises);
442
- if (devWarningsRef.current) {
443
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} ${operationType} operations to backend`);
444
- }
445
- }
446
- catch (error) {
447
- console.error(`[useAdjustmentHistoryBatch] ❌ Failed to sync ${operationType} to backend:`, error);
448
- }
449
- }
450
- console.log(`[useAdjustmentHistoryBatch] 📊 ${operationType.toUpperCase()} backend operations prepared: ${backendOperations.length}`);
451
- }, [selectedIds, internalOptions, imageHistories]);
452
- // Apply preset values directly to selected images - now uses shared logic
453
- const adjustSelectedWithPreset = useCallback(async (presetAdjustments) => {
454
- console.log('[useAdjustmentHistoryBatch] 🎨 adjustSelectedWithPreset called with:', {
455
- selectedIds,
456
- selectedCount: selectedIds.length,
457
- presetAdjustments,
458
- hasController: !!internalOptions.controller,
459
- hasFirebaseUid: !!internalOptions.firebaseUid,
460
- imageHistoriesCount: imageHistories.length,
461
- imageHistoriesIds: imageHistories.map(h => h.imageId)
462
- });
463
- console.log('[useAdjustmentHistoryBatch] 🔍 PRESET: Current imageHistories state at start:', imageHistories.map(h => ({
464
- imageId: h.imageId,
465
- currentHistoryEntryId: h.currentHistoryEntryId,
466
- historyLength: h.history.length,
467
- historyIds: h.history.map(entry => entry.id)
468
- })));
469
- if (selectedIds.length === 0) {
470
- if (devWarningsRef.current) {
471
- console.warn('[useAdjustmentHistoryBatch] ❌ adjustSelectedWithPreset called with no selection');
472
- }
473
- return;
474
- }
475
- // Apply preset values with clamping
476
- const clampedPreset = { ...presetAdjustments };
477
- Object.keys(clampedPreset).forEach(key => {
478
- const presetValue = clampedPreset[key];
479
- clampedPreset[key] = Math.max(-100, Math.min(100, presetValue));
480
- });
481
- console.log('[useAdjustmentHistoryBatch] 🎨 Using SHARED logic for preset (same as reset)');
482
- // Use the same logic as reset, just with preset values instead of default values
483
- await applyAdjustmentToSelected(clampedPreset, 'preset');
484
- }, [selectedIds, internalOptions, imageHistories, applyAdjustmentToSelected]);
485
- // Set specific adjustment states for specified images (removed since not needed)
486
- // Undo last changes to selected images - entry-based history version with backend sync
487
- const undo = useCallback(async () => {
488
- if (selectedIds.length === 0) {
489
- if (devWarningsRef.current) {
490
- console.warn('[useAdjustmentHistoryBatch] Cannot undo - no images selected');
491
- }
492
- return;
493
- }
494
- console.log('[useAdjustmentHistoryBatch] 🔍 UNDO: Before undo, current imageHistories state:', imageHistories.map(h => ({
495
- imageId: h.imageId,
496
- currentHistoryEntryId: h.currentHistoryEntryId,
497
- historyLength: h.history.length,
498
- historyIds: h.history.map(entry => entry.id)
499
- })));
500
- // Prepare backend operations BEFORE state update (outside setImageHistories)
501
- let anyChanges = false;
502
- const backendOperations = [];
503
- // Process each image to prepare operations
504
- const operationsToApply = [];
505
- imageHistories.forEach(imageHistory => {
506
- if (!selectedIds.includes(imageHistory.imageId)) {
507
- return; // Skip images not selected
508
- }
509
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Processing image ${imageHistory.imageId} with history:`, {
510
- currentHistoryEntryId: imageHistory.currentHistoryEntryId,
511
- historyLength: imageHistory.history.length,
512
- historyIds: imageHistory.history.map(entry => entry.id)
513
- });
514
- // Find current entry index
515
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
516
- if (currentEntryIndex <= 0) {
517
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Cannot undo image ${imageHistory.imageId} - at first entry or not found`);
518
- return; // Cannot undo if at first entry or entry not found
519
- }
520
- // Move to previous entry
521
- const previousEntry = imageHistory.history[currentEntryIndex - 1];
522
- anyChanges = true;
523
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Moving image ${imageHistory.imageId} from index ${currentEntryIndex} to ${currentEntryIndex - 1}`, {
524
- fromId: imageHistory.currentHistoryEntryId,
525
- toId: previousEntry.id,
526
- preservedHistoryLength: imageHistory.history.length
527
- });
528
- // Store operation for state update
529
- operationsToApply.push({
530
- imageId: imageHistory.imageId,
531
- newCurrentHistoryEntryId: previousEntry.id
532
- });
533
- // Prepare backend sync operation (OUTSIDE state setter)
534
- if (previousEntry.id && internalOptions.controller && internalOptions.firebaseUid) {
535
- backendOperations.push({
536
- imageId: imageHistory.imageId,
537
- taskId: previousEntry.id
538
- });
539
- }
540
- });
541
- // Now update state with prepared operations (no backend preparation inside)
542
- setImageHistories(prevHistories => {
543
- const result = prevHistories.map(imageHistory => {
544
- // Find operation for this image
545
- const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
546
- if (!operation) {
547
- return imageHistory; // No change for images not selected
548
- }
549
- console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for undo on image ${imageHistory.imageId}`);
550
- const updatedHistory = {
551
- ...imageHistory,
552
- currentHistoryEntryId: operation.newCurrentHistoryEntryId
553
- };
554
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Updated image ${imageHistory.imageId} history:`, {
555
- currentHistoryEntryId: updatedHistory.currentHistoryEntryId,
556
- historyLength: updatedHistory.history.length,
557
- historyIds: updatedHistory.history.map(entry => entry.id)
558
- });
559
- return updatedHistory;
560
- });
561
- console.log('[useAdjustmentHistoryBatch] 🔍 UNDO: After undo, final imageHistories state:', result.map(h => ({
562
- imageId: h.imageId,
563
- currentHistoryEntryId: h.currentHistoryEntryId,
564
- historyLength: h.history.length,
565
- historyIds: h.history.map(entry => entry.id)
566
- })));
567
- return result;
568
- });
569
- // Sync with backend (already prepared)
570
- if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
571
- try {
572
- console.log(`[useAdjustmentHistoryBatch] âĒ Syncing ${backendOperations.length} undo operations to backend (setHistoryIndex for each image)`);
573
- const promises = backendOperations.map(async (operation) => {
574
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for undo on image ${operation.imageId} to taskId ${operation.taskId}`);
575
- await internalOptions.controller.setHistoryIndex(internalOptions.firebaseUid, operation.imageId, operation.taskId);
576
- });
577
- await Promise.all(promises);
578
- if (devWarningsRef.current) {
579
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} undo operations to backend`);
580
- }
581
- }
582
- catch (error) {
583
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync undo to backend:', error);
584
- }
585
- }
586
- if (!anyChanges && devWarningsRef.current) {
587
- console.warn('[useAdjustmentHistoryBatch] Undo skipped - no changes to undo for selected images');
588
- }
589
- }, [selectedIds, internalOptions, imageHistories]);
590
- // Redo next changes to selected images - entry-based history version with backend sync
591
- const redo = useCallback(async () => {
592
- if (selectedIds.length === 0) {
593
- if (devWarningsRef.current) {
594
- console.warn('[useAdjustmentHistoryBatch] Cannot redo - no images selected');
595
- }
596
- return;
597
- }
598
- // Step 1: Prepare redo operations from current state
599
- let anyChanges = false;
600
- const operationsToApply = [];
601
- const backendOperations = [];
602
- imageHistories.forEach(imageHistory => {
603
- if (!selectedIds.includes(imageHistory.imageId)) {
604
- return; // Skip unselected images
605
- }
606
- // Find current entry index
607
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
608
- if (currentEntryIndex >= imageHistory.history.length - 1 || currentEntryIndex === -1) {
609
- return; // Cannot redo if at last entry or entry not found
610
- }
611
- // Move to next entry
612
- const nextEntry = imageHistory.history[currentEntryIndex + 1];
613
- anyChanges = true;
614
- operationsToApply.push({
615
- imageId: imageHistory.imageId,
616
- nextEntryId: nextEntry.id
617
- });
618
- // Prepare backend sync operation
619
- if (nextEntry.id && internalOptions.controller && internalOptions.firebaseUid) {
620
- backendOperations.push({
621
- imageId: imageHistory.imageId,
622
- taskId: nextEntry.id
623
- });
624
- }
625
- });
626
- // Step 2: Apply state updates cleanly
627
- if (operationsToApply.length > 0) {
628
- setImageHistories(prevHistories => {
629
- return prevHistories.map(imageHistory => {
630
- const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
631
- if (!operation) {
632
- return imageHistory; // No change for this image
633
- }
634
- return {
635
- ...imageHistory,
636
- currentHistoryEntryId: operation.nextEntryId
637
- };
638
- });
639
- });
640
- }
641
- // Step 3: Sync with backend
642
- if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
643
- try {
644
- console.log(`[useAdjustmentHistoryBatch] ⏊ Syncing ${backendOperations.length} redo operations to backend (setHistoryIndex for each image)`);
645
- const promises = backendOperations.map(async (operation) => {
646
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for redo on image ${operation.imageId} to taskId ${operation.taskId}`);
647
- await internalOptions.controller.setHistoryIndex(internalOptions.firebaseUid, operation.imageId, operation.taskId);
648
- });
649
- await Promise.all(promises);
650
- if (devWarningsRef.current) {
651
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} redo operations to backend`);
652
- }
653
- }
654
- catch (error) {
655
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync redo to backend:', error);
656
- }
657
- }
658
- if (!anyChanges && devWarningsRef.current) {
659
- console.warn('[useAdjustmentHistoryBatch] Redo skipped - no changes to redo for selected images');
660
- }
661
- }, [selectedIds, internalOptions, imageHistories]);
662
- // Check if any selected image can be undone
663
- const canUndoSelected = useCallback(() => {
664
- return selectedIds.some(imageId => {
665
- const imageHistory = imageHistories.find(h => h.imageId === imageId);
666
- if (!imageHistory)
667
- return false;
668
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
669
- return currentEntryIndex > 0;
670
- });
671
- }, [selectedIds, imageHistories]);
672
- // Check if any selected image can be redone
673
- const canRedoSelected = useCallback(() => {
674
- return selectedIds.some(imageId => {
675
- const imageHistory = imageHistories.find(h => h.imageId === imageId);
676
- if (!imageHistory)
677
- return false;
678
- const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
679
- return currentEntryIndex >= 0 && currentEntryIndex < imageHistory.history.length - 1;
680
- });
681
- }, [selectedIds, imageHistories]);
682
- // Reset selected images to default state - now uses shared logic
683
- const reset = useCallback(async (imageIds) => {
684
- const idsToReset = imageIds || selectedIds;
685
- console.log('[useAdjustmentHistoryBatch] 🔄 reset called for images:', idsToReset);
686
- // Create default state
687
- const defaultState = createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
688
- console.log('[useAdjustmentHistoryBatch] 🔄 Using SHARED logic for reset (same as preset)');
689
- // Use the same logic as preset, just with default values instead of preset values
690
- await applyAdjustmentToSelected(defaultState, 'reset', idsToReset);
691
- }, [selectedIds, internalOptions.defaultAdjustmentState, applyAdjustmentToSelected]);
692
- // Selection management with initial adjustments - single state update
693
- const setSelection = useCallback((configs) => {
694
- const imageIds = configs.map(config => config.imageId);
695
- // Update selectedIds state
696
- setSelectedIds(imageIds);
697
- // Build new batch state with initial adjustments
698
- const newBatch = {
699
- currentSelection: {},
700
- allImages: { ...currentBatch.allImages },
701
- initialStates: { ...currentBatch.initialStates }
702
- };
703
- // Process each image config
704
- for (const config of configs) {
705
- const { imageId, adjustment } = config;
706
- // If image exists in allImages, use its state
707
- if (currentBatch.allImages[imageId]) {
708
- newBatch.currentSelection[imageId] = { ...currentBatch.allImages[imageId] };
709
- }
710
- else {
711
- // New image - determine initial state
712
- let initialState;
713
- if (adjustment) {
714
- // Use provided adjustment as initial state
715
- initialState = {
716
- ...createDefaultAdjustmentState(internalOptions.defaultAdjustmentState),
717
- ...adjustment
718
- };
719
- }
720
- else {
721
- // Use default state
722
- initialState = createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
723
- }
724
- // Set initial state for new image
725
- newBatch.currentSelection[imageId] = { ...initialState };
726
- newBatch.allImages[imageId] = { ...initialState };
727
- newBatch.initialStates[imageId] = { ...initialState };
728
- }
729
- }
730
- // Update allImageIds to include any new images
731
- const newAllImageIds = Array.from(new Set([...allImageIds, ...imageIds]));
732
- setAllImageIds(newAllImageIds);
733
- // Single state update to prevent multiple re-renders
734
- setCurrentBatch(newBatch);
735
- if (internalOptions.devWarnings) {
736
- console.log('useAdjustmentHistoryBatch: Selection updated with initial adjustments', {
737
- selected: imageIds,
738
- totalImages: newAllImageIds.length,
739
- newImages: imageIds.filter(id => !allImageIds.includes(id)),
740
- withInitialAdjustments: configs.filter(c => c.adjustment).length
741
- });
742
- }
743
- }, [allImageIds, currentBatch, internalOptions.defaultAdjustmentState, internalOptions.devWarnings]);
744
- // Sync adjustments for specific images - loads full history from backend
745
- const syncAdjustment = useCallback(async (configs) => {
746
- if (configs.length === 0)
747
- return;
748
- // If controller is available, load full history from backend
749
- if (internalOptions.controller && internalOptions.firebaseUid) {
750
- try {
751
- const historyPromises = configs.map(async (config) => {
752
- try {
753
- const historyResponse = await internalOptions.controller.getEditorHistory(internalOptions.firebaseUid, config.imageId);
754
- return {
755
- imageId: config.imageId,
756
- backendHistory: historyResponse.history || [],
757
- fallbackAdjustment: config.adjustment
758
- };
759
- }
760
- catch (error) {
761
- console.warn(`[useAdjustmentHistoryBatch] Failed to load history for image ${config.imageId}:`, error);
762
- return {
763
- imageId: config.imageId,
764
- backendHistory: [],
765
- fallbackAdjustment: config.adjustment
766
- };
767
- }
768
- });
769
- const historyResults = await Promise.all(historyPromises);
770
- setImageHistories(prevHistories => {
771
- const updatedHistories = [...prevHistories];
772
- for (const result of historyResults) {
773
- const { imageId, backendHistory, fallbackAdjustment } = result;
774
- const existingIndex = updatedHistories.findIndex(h => h.imageId === imageId);
775
- if (backendHistory.length > 0) {
776
- // Convert backend history to local history entries
777
- const historyEntries = backendHistory.map((entry, index) => ({
778
- id: entry.task_id, // Use backend task_id as our entry id
779
- adjustment: mapColorAdjustmentToAdjustmentState ?
780
- mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment) :
781
- createDefaultAdjustmentState(internalOptions.defaultAdjustmentState)
782
- }));
783
- const newImageHistory = {
784
- imageId,
785
- currentHistoryEntryId: historyEntries[historyEntries.length - 1].id, // Point to latest entry
786
- history: historyEntries
787
- };
788
- if (existingIndex >= 0) {
789
- updatedHistories[existingIndex] = newImageHistory;
790
- }
791
- else {
792
- updatedHistories.push(newImageHistory);
793
- }
794
- }
795
- else {
796
- // No backend history, use fallback adjustment or default
797
- const adjustment = fallbackAdjustment ? {
798
- ...createDefaultAdjustmentState(internalOptions.defaultAdjustmentState),
799
- ...fallbackAdjustment
800
- } : createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
801
- const entryId = generateEntryId();
802
- const entry = {
803
- id: entryId,
804
- adjustment
805
- };
806
- const newImageHistory = {
807
- imageId,
808
- currentHistoryEntryId: entryId,
809
- history: [entry]
810
- };
811
- if (existingIndex >= 0) {
812
- updatedHistories[existingIndex] = newImageHistory;
813
- }
814
- else {
815
- updatedHistories.push(newImageHistory);
816
- }
817
- }
818
- }
819
- return updatedHistories;
820
- });
821
- if (internalOptions.devWarnings) {
822
- const syncedImageIds = configs.map(c => c.imageId);
823
- const totalHistoryEntries = historyResults.reduce((sum, result) => sum + result.backendHistory.length, 0);
824
- console.log('[useAdjustmentHistoryBatch] Synced adjustments with backend history', {
825
- syncedImages: syncedImageIds,
826
- totalHistoryEntries,
827
- historyLoaded: true
828
- });
829
- }
830
- }
831
- catch (error) {
832
- console.error('[useAdjustmentHistoryBatch] Failed to sync with backend, falling back to local only:', error);
833
- // Fall back to local-only sync
834
- syncAdjustmentLocal(configs);
835
- }
836
- }
837
- else {
838
- // No controller available, use local-only sync
839
- syncAdjustmentLocal(configs);
840
- }
841
- // Update allImageIds to include any new images
842
- const newImageIds = configs.map(c => c.imageId);
843
- setAllImageIds(prev => {
844
- const combined = Array.from(new Set([...prev, ...newImageIds]));
845
- return combined;
846
- });
847
- }, [internalOptions]);
848
- // Local-only sync for fallback
849
- const syncAdjustmentLocal = useCallback((configs) => {
850
- setImageHistories(prevHistories => {
851
- const updatedHistories = [...prevHistories];
852
- for (const config of configs) {
853
- const { imageId, adjustment } = config;
854
- const existingIndex = updatedHistories.findIndex(h => h.imageId === imageId);
855
- if (adjustment) {
856
- const fullAdjustment = {
857
- ...createDefaultAdjustmentState(internalOptions.defaultAdjustmentState),
858
- ...adjustment
859
- };
860
- const entryId = generateEntryId();
861
- const entry = {
862
- id: entryId,
863
- adjustment: fullAdjustment
864
- };
865
- if (existingIndex >= 0) {
866
- // Update existing image history, replace with new adjustment as initial state
867
- updatedHistories[existingIndex] = {
868
- imageId,
869
- currentHistoryEntryId: entryId,
870
- history: [entry] // Reset history with synced state
871
- };
872
- }
873
- else {
874
- // Add new image history
875
- updatedHistories.push({
876
- imageId,
877
- currentHistoryEntryId: entryId,
878
- history: [entry]
879
- });
880
- }
881
- }
882
- else if (existingIndex < 0) {
883
- // Add new image with default state
884
- const defaultState = createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
885
- const entryId = generateEntryId();
886
- const entry = {
887
- id: entryId,
888
- adjustment: defaultState
889
- };
890
- updatedHistories.push({
891
- imageId,
892
- currentHistoryEntryId: entryId,
893
- history: [entry]
894
- });
895
- }
896
- }
897
- return updatedHistories;
898
- });
899
- }, [internalOptions]);
900
- const toggleSelection = useCallback((imageId) => {
901
- setSelectedIds(prev => {
902
- const isCurrentlySelected = prev.includes(imageId);
903
- const newSelectedIds = isCurrentlySelected
904
- ? prev.filter(id => id !== imageId)
905
- : [...prev, imageId];
906
- // Update currentSelection in batch state
907
- const newBatch = {
908
- currentSelection: { ...currentBatch.currentSelection },
909
- allImages: { ...currentBatch.allImages },
910
- initialStates: { ...currentBatch.initialStates }
911
- };
912
- if (isCurrentlySelected) {
913
- // Remove from currentSelection
914
- delete newBatch.currentSelection[imageId];
915
- }
916
- else {
917
- // Add to currentSelection - use existing state from allImages or create default
918
- if (currentBatch.allImages[imageId]) {
919
- newBatch.currentSelection[imageId] = { ...currentBatch.allImages[imageId] };
920
- }
921
- else {
922
- // New image - create default state
923
- const defaultState = createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
924
- newBatch.currentSelection[imageId] = { ...defaultState };
925
- newBatch.allImages[imageId] = { ...defaultState };
926
- newBatch.initialStates[imageId] = { ...defaultState };
927
- }
928
- }
929
- setCurrentBatch(newBatch);
930
- return newSelectedIds;
931
- });
932
- }, [currentBatch, internalOptions.defaultAdjustmentState]);
933
- const selectAll = useCallback(() => {
934
- setSelectedIds([...allImageIds]);
935
- // Update currentSelection to include all images
936
- const newBatch = {
937
- currentSelection: {},
938
- allImages: { ...currentBatch.allImages },
939
- initialStates: { ...currentBatch.initialStates }
940
- };
941
- for (const imageId of allImageIds) {
942
- if (currentBatch.allImages[imageId]) {
943
- newBatch.currentSelection[imageId] = { ...currentBatch.allImages[imageId] };
944
- }
945
- else {
946
- // New image - create default state
947
- const defaultState = createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
948
- newBatch.currentSelection[imageId] = { ...defaultState };
949
- newBatch.allImages[imageId] = { ...defaultState };
950
- newBatch.initialStates[imageId] = { ...defaultState };
951
- }
952
- }
953
- setCurrentBatch(newBatch);
954
- }, [allImageIds, currentBatch, internalOptions.defaultAdjustmentState]);
955
- const clearSelection = useCallback(() => {
956
- setSelectedIds([]);
957
- // Clear currentSelection but keep allImages and initialStates
958
- const newBatch = {
959
- currentSelection: {},
960
- allImages: { ...currentBatch.allImages },
961
- initialStates: { ...currentBatch.initialStates }
962
- };
963
- setCurrentBatch(newBatch);
964
- }, [currentBatch]);
965
- // Jump to specific index - not applicable in per-image history
966
- const jumpToIndex = useCallback((index) => {
967
- if (devWarningsRef.current) {
968
- console.warn('[useAdjustmentHistoryBatch] jumpToIndex not supported in per-image history mode');
969
- }
970
- }, []);
971
- // Clear all history and start fresh
972
- const clearHistory = useCallback(() => {
973
- setImageHistories([]);
974
- setCurrentBatch(createEmptyBatchState());
975
- setAllImageIds([]);
976
- setSelectedIds([]);
977
- }, []);
978
- const getCurrentBatch = useCallback(() => {
979
- return {
980
- currentSelection: { ...currentBatch.currentSelection },
981
- allImages: { ...currentBatch.allImages },
982
- initialStates: { ...currentBatch.initialStates }
983
- };
984
- }, [currentBatch]);
985
- // Sync entire batch state - adapted for entry-based history
986
- const syncBatch = useCallback((newBatch, targetIndex) => {
987
- // Validate input
988
- if (!newBatch || typeof newBatch !== 'object' || !newBatch.currentSelection || !newBatch.allImages || !newBatch.initialStates) {
989
- console.warn('syncBatch: newBatch must be a valid BatchAdjustmentState object with currentSelection, allImages, and initialStates');
990
- return;
991
- }
992
- // Convert batch state to entry-based histories
993
- const newImageHistories = [];
994
- Object.entries(newBatch.allImages).forEach(([imageId, adjustment]) => {
995
- const entryId = generateEntryId();
996
- const entry = {
997
- id: entryId,
998
- adjustment
999
- };
1000
- newImageHistories.push({
1001
- imageId,
1002
- currentHistoryEntryId: entryId,
1003
- history: [entry] // Start with current state as single history entry
1004
- });
1005
- });
1006
- // Update state
1007
- setImageHistories(newImageHistories);
1008
- setCurrentBatch({
1009
- currentSelection: { ...newBatch.currentSelection },
1010
- allImages: { ...newBatch.allImages },
1011
- initialStates: { ...newBatch.initialStates }
1012
- });
1013
- // Update tracking
1014
- const allImageIds = Object.keys(newBatch.allImages);
1015
- const selectedImageIds = Object.keys(newBatch.currentSelection);
1016
- setAllImageIds(allImageIds);
1017
- setSelectedIds(selectedImageIds);
1018
- if (devWarningsRef.current) {
1019
- console.log('[useAdjustmentHistoryBatch] Synchronized batch state to entry-based history', {
1020
- totalImages: allImageIds.length,
1021
- selectedImages: selectedImageIds.length
1022
- });
1023
- }
1024
- }, []);
1025
- // Sync gallery updates - check for updated galleries and refresh their history
1026
- const syncGalleryUpdates = useCallback(async () => {
1027
- if (!internalOptions.controller || !internalOptions.firebaseUid || !internalOptions.eventId) {
1028
- if (devWarningsRef.current) {
1029
- console.warn('[useAdjustmentHistoryBatch] syncGalleryUpdates: Missing required options (controller, firebaseUid, or eventId)');
1030
- }
1031
- return;
1032
- }
1033
- try {
1034
- // Step 1: Check for gallery updates since last timestamp
1035
- console.log(`[useAdjustmentHistoryBatch] 🔄 Checking for gallery updates since timestamp: ${lastUpdateTimestamp}`);
1036
- const updateResponse = await internalOptions.controller.getGalleryUpdateTimestamp(internalOptions.firebaseUid, internalOptions.eventId, lastUpdateTimestamp);
1037
- if (!updateResponse.gallery || updateResponse.gallery.length === 0) {
1038
- if (devWarningsRef.current) {
1039
- console.log('[useAdjustmentHistoryBatch] ✅ No gallery updates found');
1040
- }
1041
- return;
1042
- }
1043
- console.log(`[useAdjustmentHistoryBatch] đŸ“Ĩ Found ${updateResponse.gallery.length} updated galleries:`, updateResponse.gallery);
1044
- // Step 2: Fetch history for each updated gallery
1045
- const historyPromises = updateResponse.gallery.map(async (galleryId) => {
1046
- try {
1047
- console.log(`[useAdjustmentHistoryBatch] 🔄 Fetching history for gallery: ${galleryId}`);
1048
- const historyResponse = await internalOptions.controller.getEditorHistory(internalOptions.firebaseUid, galleryId);
1049
- return {
1050
- imageId: galleryId,
1051
- currentTaskId: historyResponse.current_task_id,
1052
- backendHistory: historyResponse.history || [],
1053
- success: true
1054
- };
1055
- }
1056
- catch (error) {
1057
- console.error(`[useAdjustmentHistoryBatch] ❌ Failed to fetch history for gallery ${galleryId}:`, error);
1058
- return {
1059
- imageId: galleryId,
1060
- currentTaskId: '',
1061
- backendHistory: [],
1062
- success: false
1063
- };
1064
- }
1065
- });
1066
- const historyResults = await Promise.all(historyPromises);
1067
- const successfulUpdates = historyResults.filter(result => result.success && result.backendHistory.length > 0);
1068
- if (successfulUpdates.length === 0) {
1069
- if (devWarningsRef.current) {
1070
- console.log('[useAdjustmentHistoryBatch] â„šī¸ No valid history updates to apply');
1071
- }
1072
- // Update timestamp even if no valid updates
1073
- setLastUpdateTimestamp(Date.now());
1074
- return;
1075
- }
1076
- // Step 3: Prepare history updates from backend data
1077
- const historyUpdates = [];
1078
- successfulUpdates.forEach(result => {
1079
- const { imageId, currentTaskId, backendHistory } = result;
1080
- if (backendHistory.length > 0) {
1081
- // Convert backend history to local history entries
1082
- const historyEntries = backendHistory.map((entry) => ({
1083
- id: entry.task_id, // Use backend task_id as our entry id
1084
- adjustment: mapColorAdjustmentToAdjustmentState ?
1085
- mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment) :
1086
- createDefaultAdjustmentState(internalOptions.defaultAdjustmentState)
1087
- }));
1088
- historyUpdates.push({
1089
- imageId,
1090
- newHistory: historyEntries,
1091
- newCurrentEntryId: currentTaskId // Use current_task_id as current history entry
1092
- });
1093
- console.log(`[useAdjustmentHistoryBatch] 📝 Prepared history update for gallery ${imageId}:`, {
1094
- historyEntries: historyEntries.length,
1095
- currentEntryId: currentTaskId
1096
- });
1097
- }
1098
- });
1099
- // Step 4: Apply history updates cleanly to state
1100
- if (historyUpdates.length > 0) {
1101
- setImageHistories(prevHistories => {
1102
- return prevHistories.map(imageHistory => {
1103
- const update = historyUpdates.find(u => u.imageId === imageHistory.imageId);
1104
- if (!update) {
1105
- return imageHistory; // No update for this image
1106
- }
1107
- // Replace entire history with updated backend data
1108
- return {
1109
- ...imageHistory,
1110
- history: update.newHistory,
1111
- currentHistoryEntryId: update.newCurrentEntryId
1112
- };
1113
- });
1114
- });
1115
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully updated history for ${historyUpdates.length} galleries`);
1116
- }
1117
- // Step 5: Update timestamp to mark successful sync
1118
- setLastUpdateTimestamp(Date.now());
1119
- if (devWarningsRef.current) {
1120
- console.log('[useAdjustmentHistoryBatch] 🔄 Gallery sync completed', {
1121
- totalChecked: updateResponse.gallery.length,
1122
- successfulUpdates: successfulUpdates.length,
1123
- appliedUpdates: historyUpdates.length,
1124
- newTimestamp: Date.now()
1125
- });
1126
- }
1127
- }
1128
- catch (error) {
1129
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync gallery updates:', error);
1130
- // Don't update timestamp on error, so we can retry
1131
- }
1132
- }, [internalOptions, lastUpdateTimestamp, mapColorAdjustmentToAdjustmentState]);
1133
- // Configuration actions
1134
- const setMaxSize = useCallback((size) => {
1135
- maxSizeRef.current = size;
1136
- if (size !== 'unlimited') {
1137
- enforceMaxSize();
1138
- }
1139
- }, [enforceMaxSize]);
1140
- // History info object - updated for per-image history
1141
- const historyInfo = useMemo(() => ({
1142
- canUndo: canUndoSelected(),
1143
- canRedo: canRedoSelected(),
1144
- currentIndex: 0, // Not applicable in per-image history
1145
- totalStates: imageHistories.reduce((sum, h) => sum + h.history.length, 0),
1146
- selectedCount: selectedIds.length,
1147
- totalImages: allImageIds.length,
1148
- historySize: getMemoryUsage()
1149
- }), [canUndoSelected, canRedoSelected, imageHistories, selectedIds.length, allImageIds.length, getMemoryUsage]);
1150
- // Actions object - stabilized with useMemo
1151
- const actions = useMemo(() => ({
1152
- adjustSelected,
1153
- adjustSelectedWithPreset,
1154
- undo,
1155
- redo,
1156
- reset,
1157
- setSelection,
1158
- syncAdjustment,
1159
- syncGalleryUpdates,
1160
- toggleSelection,
1161
- selectAll,
1162
- clearSelection,
1163
- jumpToIndex,
1164
- clearHistory,
1165
- getCurrentBatch,
1166
- syncBatch
1167
- }), [
1168
- adjustSelected, adjustSelectedWithPreset, undo, redo, reset,
1169
- setSelection, syncAdjustment, syncGalleryUpdates, toggleSelection, selectAll, clearSelection,
1170
- jumpToIndex, clearHistory, getCurrentBatch, syncBatch
1171
- ]);
1172
- // Config object - stabilized with useMemo
1173
- const config = useMemo(() => ({
1174
- setMaxSize,
1175
- getMemoryUsage
1176
- }), [setMaxSize, getMemoryUsage]);
1177
- // Apply max size enforcement when history changes
1178
- useEffect(() => {
1179
- enforceMaxSize();
1180
- }, [enforceMaxSize]);
1181
- return {
1182
- currentBatch,
1183
- selectedIds,
1184
- allImageIds,
1185
- historyInfo,
1186
- actions,
1187
- config
1188
- };
1189
- }