@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,5 +1,6 @@
1
1
  import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
2
2
  import { mapAdjustmentStateToColorAdjustment, mapColorAdjustmentToAdjustmentState } from "../utils/adjustment";
3
+ import { log } from '../utils/logger';
3
4
  /**
4
5
  * Create default adjustment state
5
6
  */
@@ -26,7 +27,7 @@ const compareBatchStates = (a, b) => {
26
27
  return JSON.stringify(a) === JSON.stringify(b);
27
28
  }
28
29
  catch (error) {
29
- console.warn('Failed to compare batch states with JSON.stringify:', error);
30
+ log.warn({ error }, 'Failed to compare batch states with JSON.stringify');
30
31
  return false;
31
32
  }
32
33
  };
@@ -72,7 +73,7 @@ const generateTaskId = () => {
72
73
  *
73
74
  * **Typical Usage Flow:**
74
75
  * ```typescript
75
- * const { actions, currentBatch, selectedIds } = useAdjustmentHistoryBatch();
76
+ * const { state, actions } = useAdjustmentHistoryBatch(controller, firebaseUid, eventId);
76
77
  *
77
78
  * // Select images (new images get default state automatically)
78
79
  * actions.setSelection(['img1', 'img2', 'img3']);
@@ -95,69 +96,78 @@ const generateTaskId = () => {
95
96
  * - `currentBatch.allImages`: All images that have been selected/adjusted (persistent)
96
97
  * - `selectedIds`: Array of currently selected image IDs
97
98
  *
99
+ * @param controller - Controller for backend operations
100
+ * @param firebaseUid - Firebase UID for backend operations
101
+ * @param eventId - Event ID for backend operations
98
102
  * @param options - Configuration options for history behavior
99
- * @returns Object with current batch, selection, history info, actions, and config
103
+ * @returns Object with state and actions
100
104
  */
101
- export function useAdjustmentHistoryBatch(options = {}) {
105
+ export function useAdjustmentHistoryBatch(controller, firebaseUid, eventId, options = {}) {
102
106
  // Internal stabilization
103
107
  const internalOptions = useMemo(() => ({
104
108
  maxSize: options.maxSize ?? 'unlimited',
105
109
  devWarnings: options.devWarnings ?? false,
106
110
  defaultAdjustmentState: options.defaultAdjustmentState ?? {},
107
- controller: options.controller,
108
- firebaseUid: options.firebaseUid,
109
- eventId: options.eventId
111
+ controller,
112
+ firebaseUid,
113
+ eventId
110
114
  }), [
111
115
  options.maxSize,
112
116
  options.devWarnings,
113
117
  options.defaultAdjustmentState,
114
- options.controller,
115
- options.firebaseUid,
116
- options.eventId
118
+ controller,
119
+ firebaseUid,
120
+ eventId
117
121
  ]);
118
122
  // Core state management - using per-image history instead of batch history
119
123
  const [allImageIds, setAllImageIds] = useState([]);
120
124
  const [selectedIds, setSelectedIds] = useState([]);
121
125
  const [imageHistories, setImageHistories] = useState([]);
122
126
  const [currentBatch, setCurrentBatch] = useState(createEmptyBatchState());
123
- const [lastUpdateTimestamp, setLastUpdateTimestamp] = useState(Date.now());
127
+ const lastUpdateTimestampRef = useRef(Date.now());
128
+ // Track synced image IDs to avoid re-syncing history
129
+ const syncedImageHistoryIds = useRef(new Set());
124
130
  // Configuration refs
125
131
  const maxSizeRef = useRef(internalOptions.maxSize);
126
132
  const devWarningsRef = useRef(internalOptions.devWarnings);
127
133
  // Helper function to rebuild currentBatch from imageHistories
128
134
  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
+ log.debug({
136
+ imageHistories: imageHistories.map(h => ({
137
+ imageId: h.imageId,
138
+ currentHistoryEntryId: h.currentHistoryEntryId,
139
+ historyLength: h.history.length,
140
+ historyIds: h.history.map(entry => entry.id)
141
+ }))
142
+ }, '[useAdjustmentHistoryBatch] 🔧 rebuildCurrentBatch called with imageHistories');
135
143
  const newBatch = createEmptyBatchState();
136
144
  imageHistories.forEach(imageHistory => {
137
145
  // Find current adjustment using currentHistoryEntryId
138
146
  const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
139
- console.log(`[useAdjustmentHistoryBatch] 🔍 rebuildCurrentBatch for image ${imageHistory.imageId}:`, {
147
+ log.debug({
148
+ imageId: imageHistory.imageId,
140
149
  currentHistoryEntryId: imageHistory.currentHistoryEntryId,
141
150
  foundCurrentEntry: !!currentEntry,
142
151
  historyLength: imageHistory.history.length,
143
152
  historyIds: imageHistory.history.map(entry => entry.id)
144
- });
153
+ }, `[useAdjustmentHistoryBatch] 🔍 rebuildCurrentBatch for image ${imageHistory.imageId}`);
145
154
  if (currentEntry) {
146
155
  newBatch.allImages[imageHistory.imageId] = currentEntry.adjustment;
147
156
  if (selectedIds.includes(imageHistory.imageId)) {
148
157
  newBatch.currentSelection[imageHistory.imageId] = currentEntry.adjustment;
149
158
  }
150
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully rebuilt batch for image ${imageHistory.imageId}`);
159
+ log.debug({ imageId: imageHistory.imageId }, `[useAdjustmentHistoryBatch] ✅ Successfully rebuilt batch for image`);
151
160
  }
152
161
  else {
153
- console.error(`[useAdjustmentHistoryBatch] ❌ CRITICAL: Current entry not found for image ${imageHistory.imageId}!`, {
162
+ log.error({
163
+ imageId: imageHistory.imageId,
154
164
  searchingFor: imageHistory.currentHistoryEntryId,
155
165
  availableIds: imageHistory.history.map(entry => entry.id),
156
166
  historyLength: imageHistory.history.length
157
- });
167
+ }, `[useAdjustmentHistoryBatch] ❌ CRITICAL: Current entry not found for image`);
158
168
  }
159
169
  });
160
- console.log('[useAdjustmentHistoryBatch] 🔧 rebuildCurrentBatch result:', {
170
+ log.debug({
161
171
  allImagesCount: Object.keys(newBatch.allImages).length,
162
172
  currentSelectionCount: Object.keys(newBatch.currentSelection).length,
163
173
  allImagesIds: Object.keys(newBatch.allImages),
@@ -176,7 +186,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
176
186
  return historiesString.length * 2; // Rough estimate: 2 bytes per character
177
187
  }
178
188
  catch (error) {
179
- console.warn('Failed to estimate memory usage:', error);
189
+ log.warn({ error }, 'Failed to estimate memory usage');
180
190
  return imageHistories.length * 100 * 1000; // Fallback estimate
181
191
  }
182
192
  }, [imageHistories]);
@@ -207,7 +217,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
207
217
  const adjustSelected = useCallback(async (delta) => {
208
218
  if (selectedIds.length === 0) {
209
219
  if (devWarningsRef.current) {
210
- console.warn('[useAdjustmentHistoryBatch] adjustSelected called with no selection');
220
+ log.warn('[useAdjustmentHistoryBatch] adjustSelected called with no selection');
211
221
  }
212
222
  return;
213
223
  }
@@ -226,7 +236,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
226
236
  const newAdjustment = { ...currentAdjustment };
227
237
  Object.keys(delta).forEach(key => {
228
238
  const deltaValue = delta[key];
229
- if (typeof deltaValue === 'number') {
239
+ if (typeof deltaValue === 'number' && key !== 'preset_id') {
230
240
  const currentValue = newAdjustment[key];
231
241
  newAdjustment[key] = Math.max(-100, Math.min(100, currentValue + deltaValue));
232
242
  }
@@ -235,7 +245,8 @@ export function useAdjustmentHistoryBatch(options = {}) {
235
245
  const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
236
246
  const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
237
247
  let replaceFromTaskId;
238
- console.log(`[useAdjustmentHistoryBatch] 🎯 Delta replace logic for image ${imageHistory.imageId}:`, {
248
+ log.debug({
249
+ imageId: imageHistory.imageId,
239
250
  currentEntryIndex,
240
251
  historyLength: imageHistory.history.length,
241
252
  isInMiddleOfHistory,
@@ -244,10 +255,10 @@ export function useAdjustmentHistoryBatch(options = {}) {
244
255
  if (isInMiddleOfHistory) {
245
256
  // If user is in middle of history, get the task ID of current position
246
257
  replaceFromTaskId = currentEntry?.id;
247
- console.log(`[useAdjustmentHistoryBatch] 🔄 DELTA will REPLACE current position. replaceFromTaskId=${replaceFromTaskId}`);
258
+ log.debug({ replaceFromTaskId }, `[useAdjustmentHistoryBatch] 🔄 DELTA will REPLACE current position`);
248
259
  }
249
260
  else {
250
- console.log(`[useAdjustmentHistoryBatch] ➕ DELTA will ADD new entry (at end of history)`);
261
+ log.debug(`[useAdjustmentHistoryBatch] ➕ DELTA will ADD new entry (at end of history)`);
251
262
  }
252
263
  // Generate new task ID for backend (same as entry ID)
253
264
  const taskId = generateTaskId();
@@ -296,7 +307,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
296
307
  if (!operation) {
297
308
  return imageHistory; // No change for images not being adjusted
298
309
  }
299
- console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for delta on image ${imageHistory.imageId}`);
310
+ log.debug({ imageId: imageHistory.imageId }, `[useAdjustmentHistoryBatch] 📝 Applying state update for delta on image`);
300
311
  return {
301
312
  ...imageHistory,
302
313
  history: operation.newHistory,
@@ -307,36 +318,43 @@ export function useAdjustmentHistoryBatch(options = {}) {
307
318
  // Perform backend operations asynchronously (already prepared)
308
319
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
309
320
  try {
310
- console.log(`[useAdjustmentHistoryBatch] 📤 Syncing ${backendOperations.length} adjustments to backend (createEditorConfig for each image)`);
321
+ log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] 📤 Syncing adjustments to backend (createEditorConfig for each image)`);
311
322
  const promises = backendOperations.map(async (operation) => {
312
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for image ${operation.imageId} with taskId ${operation.taskId}`);
323
+ log.debug({
324
+ imageId: operation.imageId,
325
+ taskId: operation.taskId,
326
+ hasPresetId: !!operation.adjustment.preset_id,
327
+ presetId: operation.adjustment.preset_id,
328
+ hasReplaceFrom: !!operation.replaceFromTaskId
329
+ }, `[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for image`);
313
330
  await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, {
314
331
  gallery_id: operation.imageId,
315
332
  task_id: operation.taskId,
316
333
  color_adjustment: mapAdjustmentStateToColorAdjustment(operation.adjustment),
317
- replace_from: operation.replaceFromTaskId
334
+ replace_from: operation.replaceFromTaskId,
335
+ preset_id: operation.adjustment.preset_id
318
336
  });
319
337
  });
320
338
  await Promise.all(promises);
321
339
  if (devWarningsRef.current) {
322
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} adjustments to backend`);
340
+ log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced adjustments to backend`);
323
341
  }
324
342
  }
325
343
  catch (error) {
326
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync adjustments to backend:', error);
344
+ log.error({ error }, '[useAdjustmentHistoryBatch] ❌ Failed to sync adjustments to backend');
327
345
  }
328
346
  }
329
347
  }, [selectedIds, internalOptions, imageHistories]);
330
348
  // Shared function for applying adjustments (used by both reset and preset)
331
349
  const applyAdjustmentToSelected = useCallback(async (adjustment, operationType, targetImageIds) => {
332
- const idsToProcess = targetImageIds || selectedIds;
350
+ const idsToProcess = Array.isArray(targetImageIds) ? targetImageIds : selectedIds;
333
351
  if (idsToProcess.length === 0) {
334
352
  if (devWarningsRef.current) {
335
- console.warn(`[useAdjustmentHistoryBatch] ❌ ${operationType} called with no images to process`);
353
+ log.warn({ operationType }, `[useAdjustmentHistoryBatch] ❌ Operation called with no images to process`);
336
354
  }
337
355
  return;
338
356
  }
339
- console.log(`[useAdjustmentHistoryBatch] 🔄 ${operationType.toUpperCase()} called for images:`, idsToProcess);
357
+ log.info({ operationType: operationType.toUpperCase(), idsToProcess }, `[useAdjustmentHistoryBatch] 🔄 Operation called for images`);
340
358
  // Prepare backend operations BEFORE state update (outside setImageHistories)
341
359
  const backendOperations = [];
342
360
  // Process each image to prepare operations
@@ -345,13 +363,15 @@ export function useAdjustmentHistoryBatch(options = {}) {
345
363
  if (!idsToProcess.includes(imageHistory.imageId)) {
346
364
  return; // Skip images not being processed
347
365
  }
348
- console.log(`[useAdjustmentHistoryBatch] 🔄 Processing ${operationType} for image ${imageHistory.imageId}`);
366
+ log.debug({ operationType, imageId: imageHistory.imageId }, `[useAdjustmentHistoryBatch] 🔄 Processing operation for image`);
349
367
  // Get current entry for replace_from logic
350
368
  const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
351
369
  const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
352
370
  const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
353
371
  let replaceFromTaskId;
354
- console.log(`[useAdjustmentHistoryBatch] 🎯 ${operationType.toUpperCase()} replace logic for image ${imageHistory.imageId}:`, {
372
+ log.debug({
373
+ operationType: operationType.toUpperCase(),
374
+ imageId: imageHistory.imageId,
355
375
  currentEntryIndex,
356
376
  historyLength: imageHistory.history.length,
357
377
  isInMiddleOfHistory,
@@ -360,10 +380,10 @@ export function useAdjustmentHistoryBatch(options = {}) {
360
380
  if (isInMiddleOfHistory && currentEntry?.id) {
361
381
  // Only use replace_from when truly in middle of history (not at latest)
362
382
  replaceFromTaskId = currentEntry.id;
363
- console.log(`[useAdjustmentHistoryBatch] 🔄 ${operationType.toUpperCase()} will REPLACE current position. replaceFromTaskId=${replaceFromTaskId}`);
383
+ log.debug({ operationType: operationType.toUpperCase(), replaceFromTaskId }, `[useAdjustmentHistoryBatch] 🔄 Operation will REPLACE current position`);
364
384
  }
365
385
  else {
366
- console.log(`[useAdjustmentHistoryBatch] ${operationType.toUpperCase()} will ADD new entry (at end of history)`);
386
+ log.debug({ operationType: operationType.toUpperCase() }, `[useAdjustmentHistoryBatch] ➕ Operation will ADD new entry (at end of history)`);
367
387
  }
368
388
  // Create new entry with adjustment
369
389
  const newEntryId = generateEntryId();
@@ -394,10 +414,13 @@ export function useAdjustmentHistoryBatch(options = {}) {
394
414
  });
395
415
  // Prepare backend operation (OUTSIDE state setter)
396
416
  if (internalOptions.controller && internalOptions.firebaseUid) {
397
- console.log(`[useAdjustmentHistoryBatch] ✅ Adding ${operationType.toUpperCase()} backend operation for image ${imageHistory.imageId} with taskId ${newEntryId}`, {
417
+ log.debug({
418
+ operationType: operationType.toUpperCase(),
419
+ imageId: imageHistory.imageId,
420
+ taskId: newEntryId,
398
421
  replaceFromTaskId,
399
422
  willReplace: !!replaceFromTaskId
400
- });
423
+ }, `[useAdjustmentHistoryBatch] ✅ Adding backend operation for image`);
401
424
  backendOperations.push({
402
425
  imageId: imageHistory.imageId,
403
426
  taskId: newEntryId,
@@ -414,7 +437,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
414
437
  if (!operation) {
415
438
  return imageHistory; // No change for images not being processed
416
439
  }
417
- console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for ${operationType} on image ${imageHistory.imageId}`);
440
+ log.debug({ operationType, imageId: imageHistory.imageId }, `[useAdjustmentHistoryBatch] 📝 Applying state update for operation on image`);
418
441
  return {
419
442
  ...imageHistory,
420
443
  history: operation.newHistory,
@@ -425,50 +448,58 @@ export function useAdjustmentHistoryBatch(options = {}) {
425
448
  // Perform backend operations asynchronously (already prepared)
426
449
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
427
450
  try {
428
- console.log(`[useAdjustmentHistoryBatch] 📤 Syncing ${backendOperations.length} ${operationType} operations to backend (createEditorConfig for each image)`);
451
+ log.info({ operationType, operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] 📤 Syncing operations to backend (createEditorConfig for each image)`);
429
452
  const promises = backendOperations.map(async (operation) => {
430
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for ${operationType} on image ${operation.imageId} with taskId ${operation.taskId}`, {
453
+ log.debug({
454
+ operationType,
455
+ imageId: operation.imageId,
456
+ taskId: operation.taskId,
431
457
  replaceFrom: operation.replaceFromTaskId,
432
- hasReplaceFrom: !!operation.replaceFromTaskId
433
- });
458
+ hasReplaceFrom: !!operation.replaceFromTaskId,
459
+ hasPresetId: !!operation.adjustment.preset_id,
460
+ presetId: operation.adjustment.preset_id
461
+ }, `[useAdjustmentHistoryBatch] 🔄 Calling createEditorConfig for operation on image`);
434
462
  await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, {
435
463
  gallery_id: operation.imageId,
436
464
  task_id: operation.taskId,
437
465
  color_adjustment: mapAdjustmentStateToColorAdjustment(operation.adjustment),
438
- replace_from: operation.replaceFromTaskId
466
+ replace_from: operation.replaceFromTaskId,
467
+ preset_id: operation.adjustment.preset_id
439
468
  });
440
469
  });
441
470
  await Promise.all(promises);
442
471
  if (devWarningsRef.current) {
443
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} ${operationType} operations to backend`);
472
+ log.info({ operationType, operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced operations to backend`);
444
473
  }
445
474
  }
446
475
  catch (error) {
447
- console.error(`[useAdjustmentHistoryBatch] ❌ Failed to sync ${operationType} to backend:`, error);
476
+ log.error({ error, operationType }, '[useAdjustmentHistoryBatch] ❌ Failed to sync operations to backend');
448
477
  }
449
478
  }
450
- console.log(`[useAdjustmentHistoryBatch] 📊 ${operationType.toUpperCase()} backend operations prepared: ${backendOperations.length}`);
479
+ log.debug({ operationType: operationType.toUpperCase(), operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] 📊 Backend operations prepared`);
451
480
  }, [selectedIds, internalOptions, imageHistories]);
452
481
  // Apply preset values directly to selected images - now uses shared logic
453
- const adjustSelectedWithPreset = useCallback(async (presetAdjustments) => {
454
- console.log('[useAdjustmentHistoryBatch] 🎨 adjustSelectedWithPreset called with:', {
482
+ const adjustSelectedWithPreset = useCallback(async (presetAdjustments, presetId) => {
483
+ log.info({
455
484
  selectedIds,
456
485
  selectedCount: selectedIds.length,
457
486
  presetAdjustments,
458
487
  hasController: !!internalOptions.controller,
459
488
  hasFirebaseUid: !!internalOptions.firebaseUid,
460
489
  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
- })));
490
+ imageHistoriesIds: imageHistories.map(h => h.imageId),
491
+ presetId: presetId
492
+ }, '[useAdjustmentHistoryBatch] 🎨 adjustSelectedWithPreset called with');
493
+ log.debug({
494
+ imageHistories: imageHistories.map(h => ({
495
+ imageId: h.imageId,
496
+ currentHistoryEntryId: h.currentHistoryEntryId,
497
+ historyLength: h.history.length
498
+ }))
499
+ }, '[useAdjustmentHistoryBatch] 🔍 PRESET: Current imageHistories state at start');
469
500
  if (selectedIds.length === 0) {
470
501
  if (devWarningsRef.current) {
471
- console.warn('[useAdjustmentHistoryBatch] ❌ adjustSelectedWithPreset called with no selection');
502
+ log.warn('[useAdjustmentHistoryBatch] ❌ adjustSelectedWithPreset called with no selection');
472
503
  }
473
504
  return;
474
505
  }
@@ -476,27 +507,208 @@ export function useAdjustmentHistoryBatch(options = {}) {
476
507
  const clampedPreset = { ...presetAdjustments };
477
508
  Object.keys(clampedPreset).forEach(key => {
478
509
  const presetValue = clampedPreset[key];
479
- clampedPreset[key] = Math.max(-100, Math.min(100, presetValue));
510
+ if (typeof presetValue === 'number' && key !== 'preset_id') {
511
+ clampedPreset[key] = Math.max(-100, Math.min(100, presetValue));
512
+ }
480
513
  });
481
- console.log('[useAdjustmentHistoryBatch] 🎨 Using SHARED logic for preset (same as reset)');
514
+ // Include the preset_id in the adjustment state
515
+ clampedPreset.preset_id = presetId;
516
+ log.debug({
517
+ hasPresetId: !!presetId,
518
+ presetId: presetId,
519
+ clampedPreset
520
+ }, '[useAdjustmentHistoryBatch] 🎨 Using SHARED logic for preset (same as reset)');
482
521
  // Use the same logic as reset, just with preset values instead of default values
483
522
  await applyAdjustmentToSelected(clampedPreset, 'preset');
484
523
  }, [selectedIds, internalOptions, imageHistories, applyAdjustmentToSelected]);
524
+ // Apply partial adjustment values directly to selected images (paste-like behavior)
525
+ const adjustSelectedWithPaste = useCallback(async (adjustments) => {
526
+ log.info({
527
+ selectedIds,
528
+ selectedCount: selectedIds.length,
529
+ adjustments,
530
+ hasController: !!internalOptions.controller,
531
+ hasFirebaseUid: !!internalOptions.firebaseUid,
532
+ imageHistoriesCount: imageHistories.length,
533
+ imageHistoriesIds: imageHistories.map(h => h.imageId),
534
+ }, '[useAdjustmentHistoryBatch] 📋 adjustSelectedWithPaste called with');
535
+ if (selectedIds.length === 0) {
536
+ if (devWarningsRef.current) {
537
+ log.warn('[useAdjustmentHistoryBatch] ❌ adjustSelectedWithPaste called with no selection');
538
+ }
539
+ return;
540
+ }
541
+ // Prepare backend operations BEFORE state update (outside setImageHistories)
542
+ const backendOperations = [];
543
+ // Process each image to prepare operations
544
+ const operationsToApply = [];
545
+ imageHistories.forEach(imageHistory => {
546
+ if (!selectedIds.includes(imageHistory.imageId)) {
547
+ return; // Skip images not selected
548
+ }
549
+ // Get current adjustment from current entry
550
+ const currentEntry = imageHistory.history.find(entry => entry.id === imageHistory.currentHistoryEntryId);
551
+ const currentAdjustment = currentEntry?.adjustment || createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
552
+ // Merge current values with partial adjustments (only update specified properties)
553
+ const newAdjustment = { ...currentAdjustment };
554
+ Object.keys(adjustments).forEach(key => {
555
+ const pasteValue = adjustments[key];
556
+ if (typeof pasteValue === 'number' && key !== 'preset_id') {
557
+ // Apply clamping to the pasted value
558
+ newAdjustment[key] = Math.max(-100, Math.min(100, pasteValue));
559
+ }
560
+ else if (key === 'preset_id' && typeof pasteValue === 'string') {
561
+ newAdjustment[key] = pasteValue;
562
+ }
563
+ });
564
+ log.debug({
565
+ imageId: imageHistory.imageId,
566
+ currentAdjustment,
567
+ adjustments,
568
+ newAdjustment
569
+ }, '[useAdjustmentHistoryBatch] 📋 PASTE: Merging partial adjustments');
570
+ // Check if user is in the middle of history (not at latest state)
571
+ const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
572
+ const isInMiddleOfHistory = currentEntryIndex < imageHistory.history.length - 1;
573
+ let replaceFromTaskId;
574
+ if (isInMiddleOfHistory && currentEntry?.id) {
575
+ replaceFromTaskId = currentEntry.id;
576
+ log.debug({ replaceFromTaskId }, `[useAdjustmentHistoryBatch] 📋 PASTE will REPLACE current position`);
577
+ }
578
+ else {
579
+ log.debug(`[useAdjustmentHistoryBatch] ➕ PASTE will ADD new entry (at end of history)`);
580
+ }
581
+ // Generate new task ID for backend (same as entry ID)
582
+ const taskId = generateTaskId();
583
+ const newEntryId = taskId;
584
+ const newEntry = {
585
+ id: newEntryId,
586
+ adjustment: newAdjustment
587
+ };
588
+ // Build new history
589
+ let newHistory;
590
+ if (isInMiddleOfHistory) {
591
+ // If in middle of history, truncate from current position and add new entry
592
+ newHistory = [...imageHistory.history.slice(0, currentEntryIndex + 1), newEntry];
593
+ }
594
+ else {
595
+ // If at end of history, just add new entry
596
+ newHistory = [...imageHistory.history, newEntry];
597
+ }
598
+ // Trim if needed
599
+ const maxSize = maxSizeRef.current;
600
+ const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
601
+ ? newHistory.slice(-maxSize)
602
+ : newHistory;
603
+ // Store operation for state update
604
+ operationsToApply.push({
605
+ imageId: imageHistory.imageId,
606
+ newEntryId,
607
+ newHistory: trimmedHistory
608
+ });
609
+ // Prepare backend operation
610
+ if (internalOptions.controller && internalOptions.firebaseUid) {
611
+ log.debug({
612
+ imageId: imageHistory.imageId,
613
+ taskId: newEntryId,
614
+ replaceFromTaskId,
615
+ willReplace: !!replaceFromTaskId
616
+ }, `[useAdjustmentHistoryBatch] ✅ Adding backend operation for PASTE`);
617
+ backendOperations.push({
618
+ imageId: imageHistory.imageId,
619
+ taskId: newEntryId,
620
+ adjustment: newAdjustment,
621
+ replaceFromTaskId
622
+ });
623
+ }
624
+ });
625
+ if (operationsToApply.length === 0) {
626
+ log.warn('[useAdjustmentHistoryBatch] 📋 No valid images found for paste operation');
627
+ return;
628
+ }
629
+ // Apply state changes
630
+ setImageHistories(prev => {
631
+ const updated = prev.map(imageHistory => {
632
+ const operation = operationsToApply.find(op => op.imageId === imageHistory.imageId);
633
+ if (!operation) {
634
+ return imageHistory;
635
+ }
636
+ return {
637
+ ...imageHistory,
638
+ currentHistoryEntryId: operation.newEntryId,
639
+ history: operation.newHistory
640
+ };
641
+ });
642
+ log.debug({
643
+ updatedImages: updated.filter(h => selectedIds.includes(h.imageId)).map(h => ({
644
+ imageId: h.imageId,
645
+ currentHistoryEntryId: h.currentHistoryEntryId,
646
+ historyLength: h.history.length
647
+ }))
648
+ }, '[useAdjustmentHistoryBatch] 📋 PASTE: Updated imageHistories state');
649
+ return updated;
650
+ });
651
+ // Execute backend operations
652
+ if (backendOperations.length > 0) {
653
+ log.info({
654
+ operationCount: backendOperations.length,
655
+ operations: backendOperations.map(op => ({
656
+ imageId: op.imageId,
657
+ taskId: op.taskId,
658
+ hasReplaceFrom: !!op.replaceFromTaskId
659
+ }))
660
+ }, '[useAdjustmentHistoryBatch] 📋 PASTE: Starting backend operations');
661
+ // Execute all backend operations concurrently
662
+ const promises = backendOperations.map(async (operation) => {
663
+ try {
664
+ log.debug({
665
+ imageId: operation.imageId,
666
+ taskId: operation.taskId,
667
+ hasPresetId: !!operation.adjustment.preset_id,
668
+ presetId: operation.adjustment.preset_id,
669
+ hasReplaceFrom: !!operation.replaceFromTaskId
670
+ }, `[useAdjustmentHistoryBatch] 📋 PASTE: Calling createEditorConfig for image`);
671
+ await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, {
672
+ gallery_id: operation.imageId,
673
+ task_id: operation.taskId,
674
+ color_adjustment: mapAdjustmentStateToColorAdjustment(operation.adjustment),
675
+ replace_from: operation.replaceFromTaskId,
676
+ preset_id: operation.adjustment.preset_id
677
+ });
678
+ log.debug({
679
+ imageId: operation.imageId,
680
+ taskId: operation.taskId
681
+ }, '[useAdjustmentHistoryBatch] 📋 PASTE: Backend operation completed');
682
+ }
683
+ catch (error) {
684
+ log.error({
685
+ imageId: operation.imageId,
686
+ taskId: operation.taskId,
687
+ error
688
+ }, '[useAdjustmentHistoryBatch] 📋 PASTE: Backend operation failed');
689
+ }
690
+ });
691
+ await Promise.all(promises);
692
+ log.info('[useAdjustmentHistoryBatch] 📋 PASTE: All backend operations completed');
693
+ }
694
+ }, [selectedIds, internalOptions, imageHistories]);
485
695
  // Set specific adjustment states for specified images (removed since not needed)
486
696
  // Undo last changes to selected images - entry-based history version with backend sync
487
697
  const undo = useCallback(async () => {
488
698
  if (selectedIds.length === 0) {
489
699
  if (devWarningsRef.current) {
490
- console.warn('[useAdjustmentHistoryBatch] Cannot undo - no images selected');
700
+ log.warn('[useAdjustmentHistoryBatch] Cannot undo - no images selected');
491
701
  }
492
702
  return;
493
703
  }
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
- })));
704
+ log.debug({
705
+ imageHistories: imageHistories.map(h => ({
706
+ imageId: h.imageId,
707
+ currentHistoryEntryId: h.currentHistoryEntryId,
708
+ historyLength: h.history.length,
709
+ historyIds: h.history.map(entry => entry.id)
710
+ }))
711
+ }, '[useAdjustmentHistoryBatch] 🔍 UNDO: Before undo, current imageHistories state');
500
712
  // Prepare backend operations BEFORE state update (outside setImageHistories)
501
713
  let anyChanges = false;
502
714
  const backendOperations = [];
@@ -506,25 +718,29 @@ export function useAdjustmentHistoryBatch(options = {}) {
506
718
  if (!selectedIds.includes(imageHistory.imageId)) {
507
719
  return; // Skip images not selected
508
720
  }
509
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Processing image ${imageHistory.imageId} with history:`, {
721
+ log.debug({
722
+ imageId: imageHistory.imageId,
510
723
  currentHistoryEntryId: imageHistory.currentHistoryEntryId,
511
724
  historyLength: imageHistory.history.length,
512
725
  historyIds: imageHistory.history.map(entry => entry.id)
513
- });
726
+ }, `[useAdjustmentHistoryBatch] 🔍 UNDO: Processing image with history`);
514
727
  // Find current entry index
515
728
  const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
516
729
  if (currentEntryIndex <= 0) {
517
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Cannot undo image ${imageHistory.imageId} - at first entry or not found`);
730
+ log.debug({ imageId: imageHistory.imageId }, `[useAdjustmentHistoryBatch] 🔍 UNDO: Cannot undo image - at first entry or not found`);
518
731
  return; // Cannot undo if at first entry or entry not found
519
732
  }
520
733
  // Move to previous entry
521
734
  const previousEntry = imageHistory.history[currentEntryIndex - 1];
522
735
  anyChanges = true;
523
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Moving image ${imageHistory.imageId} from index ${currentEntryIndex} to ${currentEntryIndex - 1}`, {
736
+ log.debug({
737
+ imageId: imageHistory.imageId,
738
+ fromIndex: currentEntryIndex,
739
+ toIndex: currentEntryIndex - 1,
524
740
  fromId: imageHistory.currentHistoryEntryId,
525
741
  toId: previousEntry.id,
526
742
  preservedHistoryLength: imageHistory.history.length
527
- });
743
+ }, `[useAdjustmentHistoryBatch] 🔍 UNDO: Moving image from index to index`);
528
744
  // Store operation for state update
529
745
  operationsToApply.push({
530
746
  imageId: imageHistory.imageId,
@@ -546,52 +762,55 @@ export function useAdjustmentHistoryBatch(options = {}) {
546
762
  if (!operation) {
547
763
  return imageHistory; // No change for images not selected
548
764
  }
549
- console.log(`[useAdjustmentHistoryBatch] 📝 Applying state update for undo on image ${imageHistory.imageId}`);
765
+ log.debug({ imageId: imageHistory.imageId }, `[useAdjustmentHistoryBatch] 📝 Applying state update for undo on image`);
550
766
  const updatedHistory = {
551
767
  ...imageHistory,
552
768
  currentHistoryEntryId: operation.newCurrentHistoryEntryId
553
769
  };
554
- console.log(`[useAdjustmentHistoryBatch] 🔍 UNDO: Updated image ${imageHistory.imageId} history:`, {
770
+ log.debug({
771
+ imageId: imageHistory.imageId,
555
772
  currentHistoryEntryId: updatedHistory.currentHistoryEntryId,
556
773
  historyLength: updatedHistory.history.length,
557
774
  historyIds: updatedHistory.history.map(entry => entry.id)
558
- });
775
+ }, `[useAdjustmentHistoryBatch] 🔍 UNDO: Updated image history`);
559
776
  return updatedHistory;
560
777
  });
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
- })));
778
+ log.debug({
779
+ imageHistories: result.map(h => ({
780
+ imageId: h.imageId,
781
+ currentHistoryEntryId: h.currentHistoryEntryId,
782
+ historyLength: h.history.length,
783
+ historyIds: h.history.map(entry => entry.id)
784
+ }))
785
+ }, '[useAdjustmentHistoryBatch] 🔍 UNDO: After undo, final imageHistories state');
567
786
  return result;
568
787
  });
569
788
  // Sync with backend (already prepared)
570
789
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
571
790
  try {
572
- console.log(`[useAdjustmentHistoryBatch] ⏪ Syncing ${backendOperations.length} undo operations to backend (setHistoryIndex for each image)`);
791
+ log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ⏪ Syncing undo operations to backend (setHistoryIndex for each image)`);
573
792
  const promises = backendOperations.map(async (operation) => {
574
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for undo on image ${operation.imageId} to taskId ${operation.taskId}`);
793
+ log.debug({ imageId: operation.imageId, taskId: operation.taskId }, `[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for undo on image`);
575
794
  await internalOptions.controller.setHistoryIndex(internalOptions.firebaseUid, operation.imageId, operation.taskId);
576
795
  });
577
796
  await Promise.all(promises);
578
797
  if (devWarningsRef.current) {
579
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} undo operations to backend`);
798
+ log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced undo operations to backend`);
580
799
  }
581
800
  }
582
801
  catch (error) {
583
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync undo to backend:', error);
802
+ log.error({ error }, '[useAdjustmentHistoryBatch] ❌ Failed to sync undo to backend');
584
803
  }
585
804
  }
586
805
  if (!anyChanges && devWarningsRef.current) {
587
- console.warn('[useAdjustmentHistoryBatch] Undo skipped - no changes to undo for selected images');
806
+ log.warn('[useAdjustmentHistoryBatch] Undo skipped - no changes to undo for selected images');
588
807
  }
589
808
  }, [selectedIds, internalOptions, imageHistories]);
590
809
  // Redo next changes to selected images - entry-based history version with backend sync
591
810
  const redo = useCallback(async () => {
592
811
  if (selectedIds.length === 0) {
593
812
  if (devWarningsRef.current) {
594
- console.warn('[useAdjustmentHistoryBatch] Cannot redo - no images selected');
813
+ log.warn('[useAdjustmentHistoryBatch] Cannot redo - no images selected');
595
814
  }
596
815
  return;
597
816
  }
@@ -641,52 +860,76 @@ export function useAdjustmentHistoryBatch(options = {}) {
641
860
  // Step 3: Sync with backend
642
861
  if (backendOperations.length > 0 && internalOptions.controller && internalOptions.firebaseUid) {
643
862
  try {
644
- console.log(`[useAdjustmentHistoryBatch] ⏩ Syncing ${backendOperations.length} redo operations to backend (setHistoryIndex for each image)`);
863
+ log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ⏩ Syncing redo operations to backend (setHistoryIndex for each image)`);
645
864
  const promises = backendOperations.map(async (operation) => {
646
- console.log(`[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for redo on image ${operation.imageId} to taskId ${operation.taskId}`);
865
+ log.debug({ imageId: operation.imageId, taskId: operation.taskId }, `[useAdjustmentHistoryBatch] 🔄 Calling setHistoryIndex for redo on image`);
647
866
  await internalOptions.controller.setHistoryIndex(internalOptions.firebaseUid, operation.imageId, operation.taskId);
648
867
  });
649
868
  await Promise.all(promises);
650
869
  if (devWarningsRef.current) {
651
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully synced ${backendOperations.length} redo operations to backend`);
870
+ log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced redo operations to backend`);
652
871
  }
653
872
  }
654
873
  catch (error) {
655
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync redo to backend:', error);
874
+ log.error({ error }, '[useAdjustmentHistoryBatch] ❌ Failed to sync redo to backend');
656
875
  }
657
876
  }
658
877
  if (!anyChanges && devWarningsRef.current) {
659
- console.warn('[useAdjustmentHistoryBatch] Redo skipped - no changes to redo for selected images');
878
+ log.warn('[useAdjustmentHistoryBatch] Redo skipped - no changes to redo for selected images');
660
879
  }
661
880
  }, [selectedIds, internalOptions, imageHistories]);
662
881
  // Check if any selected image can be undone
663
882
  const canUndoSelected = useCallback(() => {
664
- return selectedIds.some(imageId => {
883
+ const canUndoResults = selectedIds.map(imageId => {
665
884
  const imageHistory = imageHistories.find(h => h.imageId === imageId);
666
885
  if (!imageHistory)
667
- return false;
886
+ return { imageId, canUndo: false, reason: 'no history' };
668
887
  const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
669
- return currentEntryIndex > 0;
888
+ const canUndo = currentEntryIndex > 0;
889
+ return {
890
+ imageId,
891
+ canUndo,
892
+ currentEntryIndex,
893
+ historyLength: imageHistory.history.length,
894
+ currentHistoryEntryId: imageHistory.currentHistoryEntryId
895
+ };
670
896
  });
897
+ const result = canUndoResults.some(r => r.canUndo);
898
+ log.debug({ canUndoResults, finalResult: result }, '[useAdjustmentHistoryBatch] 🔍 canUndoSelected check');
899
+ return result;
671
900
  }, [selectedIds, imageHistories]);
672
901
  // Check if any selected image can be redone
673
902
  const canRedoSelected = useCallback(() => {
674
- return selectedIds.some(imageId => {
903
+ const canRedoResults = selectedIds.map(imageId => {
675
904
  const imageHistory = imageHistories.find(h => h.imageId === imageId);
676
905
  if (!imageHistory)
677
- return false;
906
+ return { imageId, canRedo: false, reason: 'no history' };
678
907
  const currentEntryIndex = imageHistory.history.findIndex(entry => entry.id === imageHistory.currentHistoryEntryId);
679
- return currentEntryIndex >= 0 && currentEntryIndex < imageHistory.history.length - 1;
908
+ const canRedo = currentEntryIndex >= 0 && currentEntryIndex < imageHistory.history.length - 1;
909
+ return {
910
+ imageId,
911
+ canRedo,
912
+ currentEntryIndex,
913
+ historyLength: imageHistory.history.length,
914
+ currentHistoryEntryId: imageHistory.currentHistoryEntryId
915
+ };
680
916
  });
917
+ const result = canRedoResults.some(r => r.canRedo);
918
+ log.debug({ canRedoResults, finalResult: result }, '[useAdjustmentHistoryBatch] 🔍 canRedoSelected check');
919
+ return result;
681
920
  }, [selectedIds, imageHistories]);
682
- // Reset selected images to default state - now uses shared logic
921
+ // Reset selected images to default state - creates new history entry like regular adjustment
683
922
  const reset = useCallback(async (imageIds) => {
684
- const idsToReset = imageIds || selectedIds;
685
- console.log('[useAdjustmentHistoryBatch] 🔄 reset called for images:', idsToReset);
686
- // Create default state
923
+ const idsToReset = imageIds && imageIds.length > 0 ? imageIds : selectedIds;
924
+ log.info({ idsToReset }, '[useAdjustmentHistoryBatch] 🔄 reset called for images');
925
+ // Create default state (all zeros, no preset_id)
687
926
  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
927
+ if (!idsToReset || idsToReset.length === 0) {
928
+ log.warn('[useAdjustmentHistoryBatch] ⚠️ No images to reset - no selection available');
929
+ return;
930
+ }
931
+ log.info('[useAdjustmentHistoryBatch] 🔄 Reset creating NEW history entry with all zeros (no preset_id) - acts like regular adjustment');
932
+ // Use the same logic as preset, creating a new history entry that can be undone
690
933
  await applyAdjustmentToSelected(defaultState, 'reset', idsToReset);
691
934
  }, [selectedIds, internalOptions.defaultAdjustmentState, applyAdjustmentToSelected]);
692
935
  // Selection management with initial adjustments - single state update
@@ -733,14 +976,74 @@ export function useAdjustmentHistoryBatch(options = {}) {
733
976
  // Single state update to prevent multiple re-renders
734
977
  setCurrentBatch(newBatch);
735
978
  if (internalOptions.devWarnings) {
736
- console.log('useAdjustmentHistoryBatch: Selection updated with initial adjustments', {
979
+ log.debug({
737
980
  selected: imageIds,
738
981
  totalImages: newAllImageIds.length,
739
982
  newImages: imageIds.filter(id => !allImageIds.includes(id)),
740
983
  withInitialAdjustments: configs.filter(c => c.adjustment).length
741
- });
984
+ }, 'useAdjustmentHistoryBatch: Selection updated with initial adjustments');
742
985
  }
743
986
  }, [allImageIds, currentBatch, internalOptions.defaultAdjustmentState, internalOptions.devWarnings]);
987
+ // Initialize images from pagination data without backend sync - creates initial history entries
988
+ const initializeFromPagination = useCallback((configs) => {
989
+ if (configs.length === 0)
990
+ return;
991
+ if (devWarningsRef.current) {
992
+ log.debug({
993
+ imageCount: configs.length,
994
+ imageIds: configs.map(c => c.imageId),
995
+ imagesWithAdjustments: configs.filter(c => c.adjustment).length,
996
+ scenario: 'PAGINATION_INIT'
997
+ }, '[useAdjustmentHistoryBatch] 🔄 PAGINATION INIT: Initializing images from pagination data (no backend sync)');
998
+ }
999
+ // Update allImageIds to include new images
1000
+ const newImageIds = configs.map(config => config.imageId);
1001
+ setAllImageIds(prevAllIds => {
1002
+ const uniqueIds = Array.from(new Set([...prevAllIds, ...newImageIds]));
1003
+ return uniqueIds;
1004
+ });
1005
+ // Create initial history entries for images from pagination data
1006
+ setImageHistories(prevHistories => {
1007
+ const updatedHistories = [...prevHistories];
1008
+ configs.forEach(config => {
1009
+ // Check if image already has history
1010
+ const existingHistoryIndex = updatedHistories.findIndex(h => h.imageId === config.imageId);
1011
+ if (existingHistoryIndex === -1) {
1012
+ // Create new history for image with pagination data as initial entry
1013
+ const initialAdjustment = config.adjustment || createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
1014
+ const initialEntryId = generateEntryId();
1015
+ const initialEntry = {
1016
+ id: initialEntryId,
1017
+ adjustment: initialAdjustment
1018
+ };
1019
+ const newImageHistory = {
1020
+ imageId: config.imageId,
1021
+ currentHistoryEntryId: initialEntryId,
1022
+ history: [initialEntry]
1023
+ };
1024
+ updatedHistories.push(newImageHistory);
1025
+ if (devWarningsRef.current) {
1026
+ log.debug({
1027
+ imageId: config.imageId,
1028
+ entryId: initialEntryId,
1029
+ hasAdjustments: !!config.adjustment,
1030
+ adjustment: initialAdjustment,
1031
+ scenario: 'PAGINATION_INIT'
1032
+ }, `[useAdjustmentHistoryBatch] ✅ PAGINATION INIT: Created initial history for image`);
1033
+ }
1034
+ }
1035
+ else {
1036
+ if (devWarningsRef.current) {
1037
+ log.debug({ imageId: config.imageId }, `[useAdjustmentHistoryBatch] ⏭️ PAGINATION INIT: Skipping image - already initialized`);
1038
+ }
1039
+ }
1040
+ });
1041
+ return updatedHistories;
1042
+ });
1043
+ if (devWarningsRef.current) {
1044
+ log.debug('[useAdjustmentHistoryBatch] ✅ PAGINATION INIT: All images initialized with pagination data (undo/redo available when selected)');
1045
+ }
1046
+ }, [internalOptions.defaultAdjustmentState]);
744
1047
  // Sync adjustments for specific images - loads full history from backend
745
1048
  const syncAdjustment = useCallback(async (configs) => {
746
1049
  if (configs.length === 0)
@@ -754,14 +1057,16 @@ export function useAdjustmentHistoryBatch(options = {}) {
754
1057
  return {
755
1058
  imageId: config.imageId,
756
1059
  backendHistory: historyResponse.history || [],
1060
+ currentTaskId: historyResponse.current_task_id,
757
1061
  fallbackAdjustment: config.adjustment
758
1062
  };
759
1063
  }
760
1064
  catch (error) {
761
- console.warn(`[useAdjustmentHistoryBatch] Failed to load history for image ${config.imageId}:`, error);
1065
+ log.warn({ imageId: config.imageId, error }, `[useAdjustmentHistoryBatch] Failed to load history for image`);
762
1066
  return {
763
1067
  imageId: config.imageId,
764
1068
  backendHistory: [],
1069
+ currentTaskId: undefined,
765
1070
  fallbackAdjustment: config.adjustment
766
1071
  };
767
1072
  }
@@ -770,19 +1075,34 @@ export function useAdjustmentHistoryBatch(options = {}) {
770
1075
  setImageHistories(prevHistories => {
771
1076
  const updatedHistories = [...prevHistories];
772
1077
  for (const result of historyResults) {
773
- const { imageId, backendHistory, fallbackAdjustment } = result;
1078
+ const { imageId, backendHistory, currentTaskId, fallbackAdjustment } = result;
774
1079
  const existingIndex = updatedHistories.findIndex(h => h.imageId === imageId);
775
1080
  if (backendHistory.length > 0) {
1081
+ // Backend history comes in DESC order (newest first), but we need ASC order (oldest first)
1082
+ // So we reverse the array to get the correct chronological order
1083
+ const reversedHistory = [...backendHistory].reverse();
776
1084
  // Convert backend history to local history entries
777
- const historyEntries = backendHistory.map((entry, index) => ({
1085
+ const historyEntries = reversedHistory.map((entry, index) => ({
778
1086
  id: entry.task_id, // Use backend task_id as our entry id
779
1087
  adjustment: mapColorAdjustmentToAdjustmentState ?
780
- mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment) :
1088
+ mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment, entry.editor_config.preset_id) :
781
1089
  createDefaultAdjustmentState(internalOptions.defaultAdjustmentState)
782
1090
  }));
1091
+ // Use backend current_task_id if available, otherwise default to latest entry
1092
+ const currentEntryId = currentTaskId || historyEntries[historyEntries.length - 1].id;
1093
+ log.debug({
1094
+ imageId,
1095
+ backendCurrentTaskId: currentTaskId,
1096
+ historyLength: historyEntries.length,
1097
+ selectedCurrentEntryId: currentEntryId,
1098
+ isUsingBackendPosition: !!currentTaskId,
1099
+ allEntryIds: historyEntries.map(e => e.id),
1100
+ presetIds: historyEntries.map(e => ({ entryId: e.id, preset_id: e.adjustment.preset_id })),
1101
+ currentEntryPresetId: historyEntries.find(e => e.id === currentEntryId)?.adjustment.preset_id
1102
+ });
783
1103
  const newImageHistory = {
784
1104
  imageId,
785
- currentHistoryEntryId: historyEntries[historyEntries.length - 1].id, // Point to latest entry
1105
+ currentHistoryEntryId: currentEntryId,
786
1106
  history: historyEntries
787
1107
  };
788
1108
  if (existingIndex >= 0) {
@@ -821,15 +1141,15 @@ export function useAdjustmentHistoryBatch(options = {}) {
821
1141
  if (internalOptions.devWarnings) {
822
1142
  const syncedImageIds = configs.map(c => c.imageId);
823
1143
  const totalHistoryEntries = historyResults.reduce((sum, result) => sum + result.backendHistory.length, 0);
824
- console.log('[useAdjustmentHistoryBatch] Synced adjustments with backend history', {
1144
+ log.debug({
825
1145
  syncedImages: syncedImageIds,
826
1146
  totalHistoryEntries,
827
1147
  historyLoaded: true
828
- });
1148
+ }, '[useAdjustmentHistoryBatch] Synced adjustments with backend history');
829
1149
  }
830
1150
  }
831
1151
  catch (error) {
832
- console.error('[useAdjustmentHistoryBatch] Failed to sync with backend, falling back to local only:', error);
1152
+ log.error({ error }, '[useAdjustmentHistoryBatch] Failed to sync with backend, falling back to local only');
833
1153
  // Fall back to local-only sync
834
1154
  syncAdjustmentLocal(configs);
835
1155
  }
@@ -897,7 +1217,9 @@ export function useAdjustmentHistoryBatch(options = {}) {
897
1217
  return updatedHistories;
898
1218
  });
899
1219
  }, [internalOptions]);
900
- const toggleSelection = useCallback((imageId) => {
1220
+ const toggleSelection = useCallback(async (imageId) => {
1221
+ const wasSelected = selectedIds.includes(imageId);
1222
+ const isBeingSelected = !wasSelected;
901
1223
  setSelectedIds(prev => {
902
1224
  const isCurrentlySelected = prev.includes(imageId);
903
1225
  const newSelectedIds = isCurrentlySelected
@@ -914,22 +1236,35 @@ export function useAdjustmentHistoryBatch(options = {}) {
914
1236
  delete newBatch.currentSelection[imageId];
915
1237
  }
916
1238
  else {
917
- // Add to currentSelection - use existing state from allImages or create default
1239
+ // Add to currentSelection - use existing state from allImages
918
1240
  if (currentBatch.allImages[imageId]) {
919
1241
  newBatch.currentSelection[imageId] = { ...currentBatch.allImages[imageId] };
920
1242
  }
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 };
1243
+ // Update allImageIds to include new image if not already present
1244
+ if (!allImageIds.includes(imageId)) {
1245
+ setAllImageIds(prevAllIds => [...prevAllIds, imageId]);
927
1246
  }
928
1247
  }
929
1248
  setCurrentBatch(newBatch);
930
1249
  return newSelectedIds;
931
1250
  });
932
- }, [currentBatch, internalOptions.defaultAdjustmentState]);
1251
+ // Sync backend history for newly selected image
1252
+ if (isBeingSelected && internalOptions.controller && internalOptions.firebaseUid) {
1253
+ try {
1254
+ if (devWarningsRef.current) {
1255
+ log.debug({ imageId }, `[useAdjustmentHistoryBatch] 🔄 SELECTION SYNC: Loading backend history for selected image`);
1256
+ }
1257
+ // Use syncAdjustment to load backend history for the selected image
1258
+ await syncAdjustment([{ imageId }]);
1259
+ if (devWarningsRef.current) {
1260
+ log.debug({ imageId }, `[useAdjustmentHistoryBatch] ✅ SELECTION SYNC: Backend history loaded for image - undo/redo now available`);
1261
+ }
1262
+ }
1263
+ catch (error) {
1264
+ log.error({ imageId, error }, `[useAdjustmentHistoryBatch] ❌ SELECTION SYNC: Failed to load backend history for image`);
1265
+ }
1266
+ }
1267
+ }, [selectedIds, currentBatch, allImageIds, internalOptions, syncAdjustment]);
933
1268
  const selectAll = useCallback(() => {
934
1269
  setSelectedIds([...allImageIds]);
935
1270
  // Update currentSelection to include all images
@@ -965,7 +1300,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
965
1300
  // Jump to specific index - not applicable in per-image history
966
1301
  const jumpToIndex = useCallback((index) => {
967
1302
  if (devWarningsRef.current) {
968
- console.warn('[useAdjustmentHistoryBatch] jumpToIndex not supported in per-image history mode');
1303
+ log.warn('[useAdjustmentHistoryBatch] jumpToIndex not supported in per-image history mode');
969
1304
  }
970
1305
  }, []);
971
1306
  // Clear all history and start fresh
@@ -974,6 +1309,8 @@ export function useAdjustmentHistoryBatch(options = {}) {
974
1309
  setCurrentBatch(createEmptyBatchState());
975
1310
  setAllImageIds([]);
976
1311
  setSelectedIds([]);
1312
+ // Also clear synced history cache when clearing all history
1313
+ syncedImageHistoryIds.current.clear();
977
1314
  }, []);
978
1315
  const getCurrentBatch = useCallback(() => {
979
1316
  return {
@@ -986,7 +1323,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
986
1323
  const syncBatch = useCallback((newBatch, targetIndex) => {
987
1324
  // Validate input
988
1325
  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');
1326
+ log.warn('syncBatch: newBatch must be a valid BatchAdjustmentState object with currentSelection, allImages, and initialStates');
990
1327
  return;
991
1328
  }
992
1329
  // Convert batch state to entry-based histories
@@ -1016,35 +1353,35 @@ export function useAdjustmentHistoryBatch(options = {}) {
1016
1353
  setAllImageIds(allImageIds);
1017
1354
  setSelectedIds(selectedImageIds);
1018
1355
  if (devWarningsRef.current) {
1019
- console.log('[useAdjustmentHistoryBatch] Synchronized batch state to entry-based history', {
1356
+ log.debug({
1020
1357
  totalImages: allImageIds.length,
1021
1358
  selectedImages: selectedImageIds.length
1022
- });
1359
+ }, '[useAdjustmentHistoryBatch] Synchronized batch state to entry-based history');
1023
1360
  }
1024
1361
  }, []);
1025
1362
  // Sync gallery updates - check for updated galleries and refresh their history
1026
1363
  const syncGalleryUpdates = useCallback(async () => {
1027
1364
  if (!internalOptions.controller || !internalOptions.firebaseUid || !internalOptions.eventId) {
1028
1365
  if (devWarningsRef.current) {
1029
- console.warn('[useAdjustmentHistoryBatch] syncGalleryUpdates: Missing required options (controller, firebaseUid, or eventId)');
1366
+ log.warn('[useAdjustmentHistoryBatch] syncGalleryUpdates: Missing required options (controller, firebaseUid, or eventId)');
1030
1367
  }
1031
1368
  return;
1032
1369
  }
1033
1370
  try {
1034
1371
  // 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);
1372
+ log.info({ timestamp: lastUpdateTimestampRef.current }, `[useAdjustmentHistoryBatch] 🔄 Checking for gallery updates since timestamp`);
1373
+ const updateResponse = await internalOptions.controller.getGalleryUpdateTimestamp(internalOptions.firebaseUid, internalOptions.eventId, lastUpdateTimestampRef.current);
1037
1374
  if (!updateResponse.gallery || updateResponse.gallery.length === 0) {
1038
1375
  if (devWarningsRef.current) {
1039
- console.log('[useAdjustmentHistoryBatch] ✅ No gallery updates found');
1376
+ log.debug('[useAdjustmentHistoryBatch] ✅ No gallery updates found');
1040
1377
  }
1041
1378
  return;
1042
1379
  }
1043
- console.log(`[useAdjustmentHistoryBatch] 📥 Found ${updateResponse.gallery.length} updated galleries:`, updateResponse.gallery);
1380
+ log.info({ galleryCount: updateResponse.gallery.length, galleries: updateResponse.gallery }, `[useAdjustmentHistoryBatch] 📥 Found updated galleries`);
1044
1381
  // Step 2: Fetch history for each updated gallery
1045
1382
  const historyPromises = updateResponse.gallery.map(async (galleryId) => {
1046
1383
  try {
1047
- console.log(`[useAdjustmentHistoryBatch] 🔄 Fetching history for gallery: ${galleryId}`);
1384
+ log.debug({ galleryId }, `[useAdjustmentHistoryBatch] 🔄 Fetching history for gallery`);
1048
1385
  const historyResponse = await internalOptions.controller.getEditorHistory(internalOptions.firebaseUid, galleryId);
1049
1386
  return {
1050
1387
  imageId: galleryId,
@@ -1054,7 +1391,7 @@ export function useAdjustmentHistoryBatch(options = {}) {
1054
1391
  };
1055
1392
  }
1056
1393
  catch (error) {
1057
- console.error(`[useAdjustmentHistoryBatch] ❌ Failed to fetch history for gallery ${galleryId}:`, error);
1394
+ log.error({ galleryId, error }, `[useAdjustmentHistoryBatch] ❌ Failed to fetch history for gallery`);
1058
1395
  return {
1059
1396
  imageId: galleryId,
1060
1397
  currentTaskId: '',
@@ -1067,10 +1404,10 @@ export function useAdjustmentHistoryBatch(options = {}) {
1067
1404
  const successfulUpdates = historyResults.filter(result => result.success && result.backendHistory.length > 0);
1068
1405
  if (successfulUpdates.length === 0) {
1069
1406
  if (devWarningsRef.current) {
1070
- console.log('[useAdjustmentHistoryBatch] ℹ️ No valid history updates to apply');
1407
+ log.debug('[useAdjustmentHistoryBatch] ℹ️ No valid history updates to apply');
1071
1408
  }
1072
1409
  // Update timestamp even if no valid updates
1073
- setLastUpdateTimestamp(Date.now());
1410
+ lastUpdateTimestampRef.current = Date.now();
1074
1411
  return;
1075
1412
  }
1076
1413
  // Step 3: Prepare history updates from backend data
@@ -1078,11 +1415,14 @@ export function useAdjustmentHistoryBatch(options = {}) {
1078
1415
  successfulUpdates.forEach(result => {
1079
1416
  const { imageId, currentTaskId, backendHistory } = result;
1080
1417
  if (backendHistory.length > 0) {
1418
+ // Backend history comes in DESC order (newest first), but we need ASC order (oldest first)
1419
+ // So we reverse the array to get the correct chronological order
1420
+ const reversedHistory = [...backendHistory].reverse();
1081
1421
  // Convert backend history to local history entries
1082
- const historyEntries = backendHistory.map((entry) => ({
1422
+ const historyEntries = reversedHistory.map((entry) => ({
1083
1423
  id: entry.task_id, // Use backend task_id as our entry id
1084
1424
  adjustment: mapColorAdjustmentToAdjustmentState ?
1085
- mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment) :
1425
+ mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment, entry.editor_config.preset_id) :
1086
1426
  createDefaultAdjustmentState(internalOptions.defaultAdjustmentState)
1087
1427
  }));
1088
1428
  historyUpdates.push({
@@ -1090,7 +1430,8 @@ export function useAdjustmentHistoryBatch(options = {}) {
1090
1430
  newHistory: historyEntries,
1091
1431
  newCurrentEntryId: currentTaskId // Use current_task_id as current history entry
1092
1432
  });
1093
- console.log(`[useAdjustmentHistoryBatch] 📝 Prepared history update for gallery ${imageId}:`, {
1433
+ log.debug({
1434
+ imageId,
1094
1435
  historyEntries: historyEntries.length,
1095
1436
  currentEntryId: currentTaskId
1096
1437
  });
@@ -1112,24 +1453,115 @@ export function useAdjustmentHistoryBatch(options = {}) {
1112
1453
  };
1113
1454
  });
1114
1455
  });
1115
- console.log(`[useAdjustmentHistoryBatch] ✅ Successfully updated history for ${historyUpdates.length} galleries`);
1456
+ log.info({ updateCount: historyUpdates.length }, `[useAdjustmentHistoryBatch] ✅ Successfully updated history for galleries`);
1116
1457
  }
1117
1458
  // Step 5: Update timestamp to mark successful sync
1118
- setLastUpdateTimestamp(Date.now());
1459
+ lastUpdateTimestampRef.current = Date.now();
1119
1460
  if (devWarningsRef.current) {
1120
- console.log('[useAdjustmentHistoryBatch] 🔄 Gallery sync completed', {
1461
+ log.debug({
1121
1462
  totalChecked: updateResponse.gallery.length,
1122
1463
  successfulUpdates: successfulUpdates.length,
1123
1464
  appliedUpdates: historyUpdates.length,
1124
1465
  newTimestamp: Date.now()
1466
+ }, '[useAdjustmentHistoryBatch] 🔄 Gallery sync completed');
1467
+ }
1468
+ }
1469
+ catch (error) {
1470
+ log.error({ error }, '[useAdjustmentHistoryBatch] ❌ Failed to sync gallery updates');
1471
+ // Don't update timestamp on error, so we can retry
1472
+ }
1473
+ }, [internalOptions, mapColorAdjustmentToAdjustmentState]);
1474
+ // Sync history for a specific image from backend
1475
+ const syncFromBackend = useCallback(async (imageId) => {
1476
+ if (!internalOptions.controller || !internalOptions.firebaseUid) {
1477
+ if (devWarningsRef.current) {
1478
+ log.warn('[useAdjustmentHistoryBatch] syncFromBackend: Missing required options (controller or firebaseUid)');
1479
+ }
1480
+ return;
1481
+ }
1482
+ try {
1483
+ log.info({ imageId }, `[useAdjustmentHistoryBatch] 🔄 Syncing history for image`);
1484
+ const historyResponse = await internalOptions.controller.getEditorHistory(internalOptions.firebaseUid, imageId);
1485
+ if (!historyResponse.history || historyResponse.history.length === 0) {
1486
+ if (devWarningsRef.current) {
1487
+ log.debug({ imageId }, `[useAdjustmentHistoryBatch] ℹ️ No history found for image`);
1488
+ }
1489
+ return;
1490
+ }
1491
+ // Backend history comes in DESC order (newest first), but we need ASC order (oldest first)
1492
+ // So we reverse the array to get the correct chronological order
1493
+ const reversedHistory = [...historyResponse.history].reverse();
1494
+ // Convert backend history to local history entries
1495
+ const historyEntries = reversedHistory.map((entry) => ({
1496
+ id: entry.task_id,
1497
+ adjustment: mapColorAdjustmentToAdjustmentState ?
1498
+ mapColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment, entry.editor_config.preset_id) :
1499
+ createDefaultAdjustmentState(internalOptions.defaultAdjustmentState)
1500
+ }));
1501
+ // Update the specific image's history
1502
+ setImageHistories(prevHistories => {
1503
+ return prevHistories.map(imageHistory => {
1504
+ if (imageHistory.imageId === imageId) {
1505
+ return {
1506
+ ...imageHistory,
1507
+ history: historyEntries,
1508
+ currentHistoryEntryId: historyResponse.current_task_id
1509
+ };
1510
+ }
1511
+ return imageHistory;
1125
1512
  });
1513
+ });
1514
+ log.info({
1515
+ imageId,
1516
+ historyEntries: historyEntries.length,
1517
+ currentEntryId: historyResponse.current_task_id,
1518
+ presetIds: historyEntries.map(e => ({ entryId: e.id, preset_id: e.adjustment.preset_id })),
1519
+ currentEntryPresetId: historyEntries.find(e => e.id === historyResponse.current_task_id)?.adjustment.preset_id
1520
+ });
1521
+ }
1522
+ catch (error) {
1523
+ log.error({ imageId, error }, `[useAdjustmentHistoryBatch] ❌ Failed to sync history for image`);
1524
+ }
1525
+ }, [internalOptions, mapColorAdjustmentToAdjustmentState]);
1526
+ // Sync history for all images that have updates since last timestamp
1527
+ const syncHistoryFromBackendAll = useCallback(async () => {
1528
+ if (!internalOptions.controller || !internalOptions.firebaseUid || !internalOptions.eventId) {
1529
+ if (devWarningsRef.current) {
1530
+ log.warn('[useAdjustmentHistoryBatch] syncHistoryFromBackendAll: Missing required options (controller, firebaseUid, or eventId)');
1531
+ }
1532
+ return;
1533
+ }
1534
+ try {
1535
+ log.info({ timestamp: lastUpdateTimestampRef.current }, `[useAdjustmentHistoryBatch] 🔄 Checking for gallery updates since timestamp`);
1536
+ const updateResponse = await internalOptions.controller.getGalleryUpdateTimestamp(internalOptions.firebaseUid, internalOptions.eventId, lastUpdateTimestampRef.current);
1537
+ if (!updateResponse.gallery || updateResponse.gallery.length === 0) {
1538
+ if (devWarningsRef.current) {
1539
+ log.debug('[useAdjustmentHistoryBatch] ✅ No gallery updates found');
1540
+ }
1541
+ return;
1542
+ }
1543
+ log.info({ galleryCount: updateResponse.gallery.length, galleries: updateResponse.gallery }, `[useAdjustmentHistoryBatch] 📥 Found updated galleries`);
1544
+ // Sync history for each updated image one by one
1545
+ for (const imageId of updateResponse.gallery) {
1546
+ await syncFromBackend(imageId);
1126
1547
  }
1548
+ // Update timestamp to mark successful sync
1549
+ lastUpdateTimestampRef.current = Date.now();
1550
+ log.info({
1551
+ totalUpdated: updateResponse.gallery.length,
1552
+ newTimestamp: Date.now()
1553
+ }, '[useAdjustmentHistoryBatch] ✅ Successfully synced all updated galleries');
1127
1554
  }
1128
1555
  catch (error) {
1129
- console.error('[useAdjustmentHistoryBatch] ❌ Failed to sync gallery updates:', error);
1556
+ log.error({ error }, '[useAdjustmentHistoryBatch] ❌ Failed to sync all gallery updates');
1130
1557
  // Don't update timestamp on error, so we can retry
1131
1558
  }
1132
- }, [internalOptions, lastUpdateTimestamp, mapColorAdjustmentToAdjustmentState]);
1559
+ }, [internalOptions, syncFromBackend]);
1560
+ // Clear synced history cache - forces re-sync of history for all images
1561
+ const clearSyncedHistoryCache = useCallback(() => {
1562
+ syncedImageHistoryIds.current.clear();
1563
+ log.debug('[useAdjustmentHistoryBatch] 🗑️ Cleared synced history cache - next selection will trigger history sync');
1564
+ }, []);
1133
1565
  // Configuration actions
1134
1566
  const setMaxSize = useCallback((size) => {
1135
1567
  maxSizeRef.current = size;
@@ -1137,8 +1569,11 @@ export function useAdjustmentHistoryBatch(options = {}) {
1137
1569
  enforceMaxSize();
1138
1570
  }
1139
1571
  }, [enforceMaxSize]);
1140
- // History info object - updated for per-image history
1141
- const historyInfo = useMemo(() => ({
1572
+ // State object - combines all state information
1573
+ const state = useMemo(() => ({
1574
+ currentBatch,
1575
+ selectedIds,
1576
+ allImageIds,
1142
1577
  canUndo: canUndoSelected(),
1143
1578
  canRedo: canRedoSelected(),
1144
1579
  currentIndex: 0, // Not applicable in per-image history
@@ -1146,44 +1581,142 @@ export function useAdjustmentHistoryBatch(options = {}) {
1146
1581
  selectedCount: selectedIds.length,
1147
1582
  totalImages: allImageIds.length,
1148
1583
  historySize: getMemoryUsage()
1149
- }), [canUndoSelected, canRedoSelected, imageHistories, selectedIds.length, allImageIds.length, getMemoryUsage]);
1584
+ }), [currentBatch, selectedIds, allImageIds, canUndoSelected, canRedoSelected, imageHistories, getMemoryUsage]);
1585
+ // Update state from socket - handles both selected and non-selected images
1586
+ const updateFromSocket = useCallback(async (imageId, adjustmentTaskId, adjustment, preset_id) => {
1587
+ log.info({
1588
+ imageId,
1589
+ taskId: adjustmentTaskId,
1590
+ isSelected: selectedIds.includes(imageId),
1591
+ hasPresetId: !!preset_id
1592
+ }, `[useAdjustmentHistoryBatch] 🔌 Socket update received for image`);
1593
+ if (selectedIds.includes(imageId)) {
1594
+ // Image is selected - sync full history to get correct position
1595
+ log.debug({ imageId }, `[useAdjustmentHistoryBatch] 🔄 Image is SELECTED - syncing full history`);
1596
+ await syncFromBackend(imageId);
1597
+ }
1598
+ else {
1599
+ // Image is not selected - update state directly without full sync
1600
+ log.debug({ imageId }, `[useAdjustmentHistoryBatch] 📝 Image is NOT SELECTED - updating state directly`);
1601
+ // Convert ColorAdjustment to AdjustmentState
1602
+ const adjustmentState = mapColorAdjustmentToAdjustmentState
1603
+ ? mapColorAdjustmentToAdjustmentState(adjustment, preset_id)
1604
+ : createDefaultAdjustmentState(internalOptions.defaultAdjustmentState);
1605
+ // Create new history entry
1606
+ const newEntry = {
1607
+ id: adjustmentTaskId,
1608
+ adjustment: adjustmentState
1609
+ };
1610
+ // Update the image's history and current position
1611
+ setImageHistories(prevHistories => {
1612
+ return prevHistories.map(imageHistory => {
1613
+ if (imageHistory.imageId === imageId) {
1614
+ // Add new entry to history and update current position
1615
+ const newHistory = [...imageHistory.history, newEntry];
1616
+ // Trim if needed
1617
+ const maxSize = maxSizeRef.current;
1618
+ const trimmedHistory = typeof maxSize === 'number' && newHistory.length > maxSize
1619
+ ? newHistory.slice(-maxSize)
1620
+ : newHistory;
1621
+ return {
1622
+ ...imageHistory,
1623
+ history: trimmedHistory,
1624
+ currentHistoryEntryId: adjustmentTaskId
1625
+ };
1626
+ }
1627
+ return imageHistory;
1628
+ });
1629
+ });
1630
+ // Update batch state for this image if it exists in allImages
1631
+ setCurrentBatch(prevBatch => {
1632
+ if (prevBatch.allImages[imageId]) {
1633
+ return {
1634
+ ...prevBatch,
1635
+ allImages: {
1636
+ ...prevBatch.allImages,
1637
+ [imageId]: adjustmentState
1638
+ }
1639
+ };
1640
+ }
1641
+ return prevBatch;
1642
+ });
1643
+ }
1644
+ }, [selectedIds, syncFromBackend, mapColorAdjustmentToAdjustmentState, internalOptions.defaultAdjustmentState]);
1150
1645
  // Actions object - stabilized with useMemo
1151
1646
  const actions = useMemo(() => ({
1152
1647
  adjustSelected,
1153
1648
  adjustSelectedWithPreset,
1649
+ adjustSelectedWithPaste,
1154
1650
  undo,
1155
1651
  redo,
1156
1652
  reset,
1157
1653
  setSelection,
1654
+ initializeFromPagination,
1158
1655
  syncAdjustment,
1159
1656
  syncGalleryUpdates,
1657
+ syncFromBackend,
1658
+ syncHistoryFromBackendAll,
1659
+ clearSyncedHistoryCache,
1160
1660
  toggleSelection,
1161
1661
  selectAll,
1162
1662
  clearSelection,
1163
1663
  jumpToIndex,
1164
1664
  clearHistory,
1165
1665
  getCurrentBatch,
1166
- syncBatch
1666
+ syncBatch,
1667
+ setMaxSize,
1668
+ getMemoryUsage,
1669
+ updateFromSocket
1167
1670
  }), [
1168
- adjustSelected, adjustSelectedWithPreset, undo, redo, reset,
1169
- setSelection, syncAdjustment, syncGalleryUpdates, toggleSelection, selectAll, clearSelection,
1170
- jumpToIndex, clearHistory, getCurrentBatch, syncBatch
1671
+ adjustSelected, adjustSelectedWithPreset, adjustSelectedWithPaste, undo, redo, reset,
1672
+ setSelection, initializeFromPagination, syncAdjustment, syncGalleryUpdates, syncFromBackend, syncHistoryFromBackendAll,
1673
+ clearSyncedHistoryCache, toggleSelection, selectAll, clearSelection, jumpToIndex, clearHistory,
1674
+ getCurrentBatch, syncBatch, setMaxSize, getMemoryUsage, updateFromSocket
1171
1675
  ]);
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
1676
+ // Apply max size enforcement when history data actually changes
1178
1677
  useEffect(() => {
1179
1678
  enforceMaxSize();
1180
- }, [enforceMaxSize]);
1679
+ }, [imageHistories]);
1680
+ // Smart history sync: handles both selection changes and pagination scenarios
1681
+ useEffect(() => {
1682
+ if (selectedIds.length === 0)
1683
+ return;
1684
+ // Find selected images that need history syncing
1685
+ const imagesToSync = selectedIds.filter(imageId => {
1686
+ // Check if already synced OR if already exists in imageHistories
1687
+ const alreadySynced = syncedImageHistoryIds.current.has(imageId);
1688
+ const alreadyInHistories = imageHistories.some(h => h.imageId === imageId);
1689
+ return !alreadySynced && !alreadyInHistories;
1690
+ });
1691
+ if (imagesToSync.length === 0) {
1692
+ log.debug('[useAdjustmentHistoryBatch] ✅ All selected images already have synced history');
1693
+ return;
1694
+ }
1695
+ log.info({ imagesToSync }, '[useAdjustmentHistoryBatch] 🔄 Syncing history for selected images (selection or pagination change)');
1696
+ // Sync history for each selected image that needs it
1697
+ const syncPromises = imagesToSync.map(async (imageId) => {
1698
+ try {
1699
+ await syncFromBackend(imageId);
1700
+ syncedImageHistoryIds.current.add(imageId);
1701
+ log.debug({ imageId }, `[useAdjustmentHistoryBatch] ✅ History synced for selected image`);
1702
+ }
1703
+ catch (error) {
1704
+ log.error({ imageId, error }, `[useAdjustmentHistoryBatch] ❌ Failed to sync history for selected image`);
1705
+ }
1706
+ });
1707
+ // Execute all syncs concurrently (but don't await to avoid blocking)
1708
+ Promise.allSettled(syncPromises).then((results) => {
1709
+ const successful = results.filter(r => r.status === 'fulfilled').length;
1710
+ const failed = results.filter(r => r.status === 'rejected').length;
1711
+ log.info({
1712
+ successful,
1713
+ failed,
1714
+ total: imagesToSync.length
1715
+ }, `[useAdjustmentHistoryBatch] 📊 History sync completed for selected images`);
1716
+ });
1717
+ }, [selectedIds, syncFromBackend, imageHistories]);
1181
1718
  return {
1182
- currentBatch,
1183
- selectedIds,
1184
- allImageIds,
1185
- historyInfo,
1186
- actions,
1187
- config
1719
+ state,
1720
+ actions
1188
1721
  };
1189
1722
  }