@yogiswara/honcho-editor-ui 3.3.3 → 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.
- package/dist/components/editor/GalleryAlbum/AlbumImageGallery.d.ts +14 -7
- package/dist/components/editor/GalleryAlbum/AlbumImageGallery.js +207 -5
- package/dist/components/editor/GalleryAlbum/ImageItemComponents.d.ts +25 -0
- package/dist/components/editor/GalleryAlbum/ImageItemComponents.js +179 -0
- package/dist/components/editor/GalleryAlbum/colorsGallery.d.ts +9 -0
- package/dist/components/editor/GalleryAlbum/colorsGallery.js +9 -0
- package/dist/components/editor/GalleryAlbum/svg/Tick.d.ts +2 -0
- package/dist/components/editor/GalleryAlbum/svg/Tick.js +6 -0
- package/dist/components/editor/HBulkAccordionColorAdjustment.js +1 -2
- package/dist/components/editor/HBulkAccordionColorAdjustmentColors.js +1 -1
- package/dist/components/editor/HBulkPresetMobile.d.ts +2 -2
- package/dist/components/editor/HBulkPresetMobile.js +2 -2
- package/dist/components/editor/HImageEditorBulkMobile.d.ts +2 -2
- package/dist/hooks/demo/HonchoEditorBulkDemo.d.ts +0 -3
- package/dist/hooks/demo/HonchoEditorBulkDemo.js +770 -411
- package/dist/hooks/demo/HonchoEditorSingleCleanDemo.d.ts +0 -3
- package/dist/hooks/demo/HonchoEditorSingleCleanDemo.js +882 -354
- package/dist/hooks/demo/index.d.ts +0 -2
- package/dist/hooks/demo/index.js +3 -2
- package/dist/hooks/editor/type.d.ts +15 -12
- package/dist/hooks/editor/useHonchoEditorBulk.d.ts +47 -5
- package/dist/hooks/editor/useHonchoEditorBulk.js +252 -133
- package/dist/hooks/useAdjustmentHistory.js +12 -12
- package/dist/hooks/useAdjustmentHistoryBatch.d.ts +33 -31
- package/dist/hooks/useAdjustmentHistoryBatch.js +703 -170
- package/dist/hooks/usePreset.js +12 -12
- package/dist/index.d.ts +5 -7
- package/dist/index.js +5 -4
- package/dist/services/type.d.ts +14 -0
- package/dist/utils/adjustment.d.ts +1 -1
- package/dist/utils/adjustment.js +15 -14
- package/dist/utils/logger.d.ts +3 -0
- package/dist/utils/logger.js +11 -0
- 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
|
-
|
|
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 {
|
|
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
|
|
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
|
|
108
|
-
firebaseUid
|
|
109
|
-
eventId
|
|
111
|
+
controller,
|
|
112
|
+
firebaseUid,
|
|
113
|
+
eventId
|
|
110
114
|
}), [
|
|
111
115
|
options.maxSize,
|
|
112
116
|
options.devWarnings,
|
|
113
117
|
options.defaultAdjustmentState,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
log.debug({ imageId: imageHistory.imageId }, `[useAdjustmentHistoryBatch] ✅ Successfully rebuilt batch for image`);
|
|
151
160
|
}
|
|
152
161
|
else {
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
258
|
+
log.debug({ replaceFromTaskId }, `[useAdjustmentHistoryBatch] 🔄 DELTA will REPLACE current position`);
|
|
248
259
|
}
|
|
249
260
|
else {
|
|
250
|
-
|
|
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
|
-
|
|
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
|
-
|
|
321
|
+
log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] 📤 Syncing adjustments to backend (createEditorConfig for each image)`);
|
|
311
322
|
const promises = backendOperations.map(async (operation) => {
|
|
312
|
-
|
|
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
|
-
|
|
340
|
+
log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced adjustments to backend`);
|
|
323
341
|
}
|
|
324
342
|
}
|
|
325
343
|
catch (error) {
|
|
326
|
-
|
|
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
|
|
350
|
+
const idsToProcess = Array.isArray(targetImageIds) ? targetImageIds : selectedIds;
|
|
333
351
|
if (idsToProcess.length === 0) {
|
|
334
352
|
if (devWarningsRef.current) {
|
|
335
|
-
|
|
353
|
+
log.warn({ operationType }, `[useAdjustmentHistoryBatch] ❌ Operation called with no images to process`);
|
|
336
354
|
}
|
|
337
355
|
return;
|
|
338
356
|
}
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
383
|
+
log.debug({ operationType: operationType.toUpperCase(), replaceFromTaskId }, `[useAdjustmentHistoryBatch] 🔄 Operation will REPLACE current position`);
|
|
364
384
|
}
|
|
365
385
|
else {
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
472
|
+
log.info({ operationType, operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced operations to backend`);
|
|
444
473
|
}
|
|
445
474
|
}
|
|
446
475
|
catch (error) {
|
|
447
|
-
|
|
476
|
+
log.error({ error, operationType }, '[useAdjustmentHistoryBatch] ❌ Failed to sync operations to backend');
|
|
448
477
|
}
|
|
449
478
|
}
|
|
450
|
-
|
|
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
|
-
|
|
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
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
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
|
-
|
|
510
|
+
if (typeof presetValue === 'number' && key !== 'preset_id') {
|
|
511
|
+
clampedPreset[key] = Math.max(-100, Math.min(100, presetValue));
|
|
512
|
+
}
|
|
480
513
|
});
|
|
481
|
-
|
|
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
|
-
|
|
700
|
+
log.warn('[useAdjustmentHistoryBatch] Cannot undo - no images selected');
|
|
491
701
|
}
|
|
492
702
|
return;
|
|
493
703
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
798
|
+
log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced undo operations to backend`);
|
|
580
799
|
}
|
|
581
800
|
}
|
|
582
801
|
catch (error) {
|
|
583
|
-
|
|
802
|
+
log.error({ error }, '[useAdjustmentHistoryBatch] ❌ Failed to sync undo to backend');
|
|
584
803
|
}
|
|
585
804
|
}
|
|
586
805
|
if (!anyChanges && devWarningsRef.current) {
|
|
587
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
870
|
+
log.info({ operationCount: backendOperations.length }, `[useAdjustmentHistoryBatch] ✅ Successfully synced redo operations to backend`);
|
|
652
871
|
}
|
|
653
872
|
}
|
|
654
873
|
catch (error) {
|
|
655
|
-
|
|
874
|
+
log.error({ error }, '[useAdjustmentHistoryBatch] ❌ Failed to sync redo to backend');
|
|
656
875
|
}
|
|
657
876
|
}
|
|
658
877
|
if (!anyChanges && devWarningsRef.current) {
|
|
659
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 -
|
|
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
|
|
685
|
-
|
|
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
|
-
|
|
689
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1036
|
-
const updateResponse = await internalOptions.controller.getGalleryUpdateTimestamp(internalOptions.firebaseUid, internalOptions.eventId,
|
|
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
|
-
|
|
1376
|
+
log.debug('[useAdjustmentHistoryBatch] ✅ No gallery updates found');
|
|
1040
1377
|
}
|
|
1041
1378
|
return;
|
|
1042
1379
|
}
|
|
1043
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1407
|
+
log.debug('[useAdjustmentHistoryBatch] ℹ️ No valid history updates to apply');
|
|
1071
1408
|
}
|
|
1072
1409
|
// Update timestamp even if no valid updates
|
|
1073
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1459
|
+
lastUpdateTimestampRef.current = Date.now();
|
|
1119
1460
|
if (devWarningsRef.current) {
|
|
1120
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
//
|
|
1141
|
-
const
|
|
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
|
-
}), [
|
|
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,
|
|
1170
|
-
|
|
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
|
-
//
|
|
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
|
-
}, [
|
|
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
|
-
|
|
1183
|
-
|
|
1184
|
-
allImageIds,
|
|
1185
|
-
historyInfo,
|
|
1186
|
-
actions,
|
|
1187
|
-
config
|
|
1719
|
+
state,
|
|
1720
|
+
actions
|
|
1188
1721
|
};
|
|
1189
1722
|
}
|