@yogiswara/honcho-editor-ui 2.7.13 → 2.7.15

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.
@@ -27,21 +27,23 @@ export default function HAccordionPreset(props) {
27
27
  };
28
28
  const isPanelExpanded = (panelName) => props.expandedPanels.includes(panelName);
29
29
  return (_jsx(_Fragment, { children: _jsx(Box, { children: _jsxs(Accordion, { sx: accordionStyle, expanded: isPanelExpanded('preset'), onChange: props.onChange('preset'), disableGutters: true, children: [_jsx(AccordionSummary, { sx: { pr: 0 }, children: _jsxs(Stack, { direction: "row", justifyContent: "space-between", alignItems: "center", sx: { width: '100%' }, children: [_jsx(Typography, { sx: { ...typography.titleMedium, color: colors.surface }, children: "Preset" }), _jsx(CardMedia, { component: "img", image: isPanelExpanded('preset') ? "/v1/svg/expanded-editor.svg" : "/v1/svg/expand-editor.svg", sx: { width: "11.67px", height: "5.83px" } })] }) }), _jsx(AccordionDetails, { sx: { pr: "4px" }, children: _jsxs(Stack, { direction: "column", gap: "8px", sx: { pt: '0px', pb: '0px', mx: '0px', width: '100%' }, children: [props.presets.map((preset) => (_jsxs(Stack, { direction: "row", alignItems: "center", sx: { width: "100%" }, children: [_jsx(Box, { sx: { width: 100, flexShrink: 0 }, children: _jsx(Button, { sx: {
30
- flex: 1,
31
- minWidth: 0, // allow ellipsis to kick in
32
- textTransform: "none",
33
- display: "block", // keep flex for left alignment
34
- justifyContent: "flex-start",
35
- alignItems: "center",
30
+ width: '100px',
31
+ textWrap: 'nowrap',
32
+ overflow: 'hidden',
33
+ textOverflow: 'ellipsis',
34
+ display: 'block',
35
+ textTransform: 'none',
36
36
  color: colors.surface,
37
- pl: 0,
38
- ml: 0,
39
- ...typography.bodyMedium,
37
+ // pr: "120px", // No longer needed since the button is full width
38
+ pl: "0px",
39
+ ml: "0px",
40
+ justifyContent: 'flex-start',
41
+ ...typography.bodyMedium
40
42
  }, onClick: () => props.onSelectPreset(preset.id), children: _jsx(Box, { component: "span", sx: {
41
43
  overflow: "hidden",
42
44
  textOverflow: "ellipsis",
43
45
  whiteSpace: "nowrap",
44
- minWidth: 0, // critical for flex children
46
+ minWidth: 0,
45
47
  }, children: preset.name }) }) }), _jsxs(Stack, { direction: "row", alignItems: "center", spacing: 1, sx: { ml: "auto" }, children: [props.selectedPreset === preset.id && (_jsx(CardMedia, { component: "img", image: "v1/svg/check-ratio-editor.svg", sx: { width: "20px", height: "20px" } })), _jsx(IconButton, { "aria-label": preset.name, onClick: (event) => props.onPresetMenuClick(event, preset.id), sx: { p: 0, m: 0 }, children: _jsx(CardMedia, { component: "img", image: "/v1/svg/dots-editor.svg", alt: "Options" }) })] })] }, preset.id))), _jsx(Button, { variant: "text", sx: { color: colors.surface, border: "1px solid",
46
48
  borderColor: colors.surface,
47
49
  borderRadius: "40px",
@@ -11,10 +11,10 @@ export default function HHeaderEditor(props) {
11
11
  return (_jsx(_Fragment, { children: _jsxs(Stack, { direction: "row", justifyContent: "space-between", width: "100%", sx: {
12
12
  id: 'HHeaderEditor',
13
13
  pr: !isMobile ? "24px" : "6px",
14
- position: 'fixed',
15
- top: 0,
16
- left: 0,
17
- right: 0,
14
+ position: isMobile ? 'fixed' : 'relative',
15
+ top: isMobile ? 0 : 'auto',
16
+ left: isMobile ? 0 : 'auto',
17
+ right: isMobile ? 0 : 'auto',
18
18
  zIndex: 1300,
19
19
  backgroundColor: 'transparent',
20
20
  }, children: [_jsx(Stack, { direction: "row", justifyContent: "flex-start", sx: { pl: !isMobile ? "0px" : "14px" }, children: _jsx(IconButton, { "aria-label": "back", onClick: props.onBack, sx: {
@@ -37,7 +37,7 @@ export function useHonchoEditorSingle({ controller, initImageId, firebaseUid })
37
37
  [field]: value
38
38
  };
39
39
  adjustmentHistory.actions.pushState(newState);
40
- }, [adjustmentHistory.currentState, adjustmentHistory.actions.pushState]);
40
+ }, [adjustmentHistory.currentState, adjustmentHistory.actions.pushState, adjustmentHistory.historyInfo.isBatchMode]);
41
41
  const setBatchMode = useCallback((enabled) => {
42
42
  adjustmentHistory.config.setBatchMode(enabled);
43
43
  }, [adjustmentHistory.config.setBatchMode]);
@@ -57,17 +57,17 @@ export function useHonchoEditorSingle({ controller, initImageId, firebaseUid })
57
57
  // Reset means setting all adjustments to 0 and adding it as new history entry
58
58
  // This allows users to undo the reset operation
59
59
  // Reset acts like normal adjustment - each reset creates a new history entry
60
- console.log('Resetting adjustments to 0 - adding to history and sending to backend');
61
- // First add reset to history (this creates local history entry)
60
+ console.log('Resetting adjustments to 0 - adding to history');
61
+ // Always add reset to history (this automatically syncs to backend via batch mode)
62
+ await adjustmentHistory.config.setBatchMode(true);
62
63
  adjustmentHistory.actions.pushState(initialAdjustments);
63
- // Then sync to backend
64
- await adjustmentHistory.config.syncToBackend();
65
- }, [adjustmentHistory.actions.pushState, adjustmentHistory.config.syncToBackend]);
64
+ await adjustmentHistory.config.setBatchMode(false, true); // forceHistory = true
65
+ }, [adjustmentHistory.actions.pushState, adjustmentHistory.config.setBatchMode]);
66
66
  const loadPresets = useCallback(async () => {
67
67
  await presetHook.actions.load();
68
68
  }, [presetHook.actions.load]);
69
69
  const applyPreset = useCallback(async (preset) => {
70
- console.log('Applying preset:', preset.name, '- saving to backend history');
70
+ console.log('Applying preset:', preset.name);
71
71
  const adjustmentState = {
72
72
  tempScore: preset.temperature,
73
73
  tintScore: preset.tint,
@@ -82,11 +82,12 @@ export function useHonchoEditorSingle({ controller, initImageId, firebaseUid })
82
82
  clarityScore: preset.clarity,
83
83
  sharpnessScore: preset.sharpness,
84
84
  };
85
- // Apply preset directly and add to history
85
+ console.log('Applying preset:', preset.name, 'with adjustments:', adjustmentState);
86
+ // Always apply preset and add to history (this automatically syncs to backend via batch mode)
87
+ await adjustmentHistory.config.setBatchMode(true);
86
88
  adjustmentHistory.actions.pushState(adjustmentState);
87
- // Then sync to backend
88
- await adjustmentHistory.config.syncToBackend();
89
- }, [adjustmentHistory.actions.pushState, adjustmentHistory.config.syncToBackend]);
89
+ await adjustmentHistory.config.setBatchMode(false, true); // forceHistory = true
90
+ }, [adjustmentHistory.actions.pushState, adjustmentHistory.config.setBatchMode]);
90
91
  const createPreset = useCallback(async (name, adjustments) => {
91
92
  return await presetHook.actions.create(name, adjustments);
92
93
  }, [presetHook.actions.create]);
@@ -101,16 +102,16 @@ export function useHonchoEditorSingle({ controller, initImageId, firebaseUid })
101
102
  return {
102
103
  temperature: adjustments.tempScore,
103
104
  tint: adjustments.tintScore,
104
- vibrance: adjustments.vibranceScore / 100,
105
- saturation: adjustments.saturationScore / 100,
106
- exposure: (adjustments.exposureScore / 100) * 3,
107
- highlights: adjustments.highlightsScore / 100,
108
- shadows: adjustments.shadowsScore / 100,
109
- whites: adjustments.whitesScore / 100,
110
- blacks: adjustments.blacksScore / 100,
111
- contrast: adjustments.contrastScore / 100,
112
- clarity: adjustments.clarityScore / 100,
113
- sharpness: adjustments.sharpnessScore / 100,
105
+ vibrance: adjustments.vibranceScore,
106
+ saturation: adjustments.saturationScore,
107
+ exposure: adjustments.exposureScore,
108
+ highlights: adjustments.highlightsScore,
109
+ shadows: adjustments.shadowsScore,
110
+ whites: adjustments.whitesScore,
111
+ blacks: adjustments.blacksScore,
112
+ contrast: adjustments.contrastScore,
113
+ clarity: adjustments.clarityScore,
114
+ sharpness: adjustments.sharpnessScore,
114
115
  };
115
116
  }, [adjustmentHistory.currentState]);
116
117
  const actions = {
@@ -49,6 +49,8 @@ export interface HistoryActions {
49
49
  trimHistory: (keepLast: number) => void;
50
50
  /** Replace entire history with new list of adjustment states */
51
51
  syncHistory: (newHistory: AdjustmentState[], targetIndex?: number) => void;
52
+ /** Sync history from backend using getEditorHistory */
53
+ syncFromBackend: () => Promise<void>;
52
54
  }
53
55
  /**
54
56
  * Configuration actions for runtime adjustment
@@ -57,11 +59,9 @@ export interface HistoryConfig {
57
59
  /** Set maximum history size */
58
60
  setMaxSize: (size: number | 'unlimited') => void;
59
61
  /** Enable or disable batch mode */
60
- setBatchMode: (enabled: boolean) => Promise<void>;
62
+ setBatchMode: (enabled: boolean, forceHistory?: boolean) => Promise<void>;
61
63
  /** Get current memory usage estimate */
62
64
  getMemoryUsage: () => number;
63
- /** Force sync current state to backend */
64
- syncToBackend: () => Promise<void>;
65
65
  }
66
66
  /**
67
67
  * Return type for the useAdjustmentHistory hook
@@ -18,6 +18,25 @@ const convertAdjustmentStateToColorAdjustment = (adjustmentState) => {
18
18
  sharpness: adjustmentState.sharpnessScore,
19
19
  };
20
20
  };
21
+ /**
22
+ * Convert ColorAdjustment from backend to AdjustmentState format
23
+ */
24
+ const convertColorAdjustmentToAdjustmentState = (colorAdjustment) => {
25
+ return {
26
+ tempScore: colorAdjustment.temperature,
27
+ tintScore: colorAdjustment.tint,
28
+ saturationScore: colorAdjustment.saturation,
29
+ vibranceScore: colorAdjustment.vibrance,
30
+ exposureScore: colorAdjustment.exposure,
31
+ contrastScore: colorAdjustment.contrast,
32
+ highlightsScore: colorAdjustment.highlights,
33
+ shadowsScore: colorAdjustment.shadows,
34
+ whitesScore: colorAdjustment.whites,
35
+ blacksScore: colorAdjustment.blacks,
36
+ clarityScore: colorAdjustment.clarity,
37
+ sharpnessScore: colorAdjustment.sharpness,
38
+ };
39
+ };
21
40
  /**
22
41
  * Compare two AdjustmentState objects for equality
23
42
  * Uses JSON.stringify for deep comparison of all adjustment values
@@ -79,7 +98,7 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
79
98
  currentImageId
80
99
  ]);
81
100
  // Core state management
82
- const [history, setHistory] = useState([{ state: initialState }]);
101
+ const [history, setHistory] = useState([{ state: initialState, taskId: `initial_${Date.now()}` }]);
83
102
  const [currentIndex, setCurrentIndex] = useState(0);
84
103
  const [currentState, setCurrentState] = useState(initialState);
85
104
  // Batch mode state - ref to avoid triggering effects
@@ -97,11 +116,17 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
97
116
  largeHistoryWarningShown: false
98
117
  });
99
118
  // Sync currentState with history when not in batch mode
119
+ // useEffect(() => {
120
+ // if (!batchModeRef.current) {
121
+ // console.log(`[useAdjustmentHistory] Syncing currentState with history:`, history[currentIndex]?.state);
122
+ // setCurrentState(history[currentIndex]?.state || initialState);
123
+ // }
124
+ // }, [history, currentIndex, initialState]);
100
125
  useEffect(() => {
101
- if (!batchModeRef.current) {
102
- setCurrentState(history[currentIndex]?.state || initialState);
126
+ if (internalOptions.currentImageId) {
127
+ syncFromBackend().catch(console.error);
103
128
  }
104
- }, [history, currentIndex, initialState]);
129
+ }, [internalOptions.currentImageId, internalOptions.firebaseUid, internalOptions.controller]);
105
130
  const getMemoryUsage = useCallback(() => {
106
131
  try {
107
132
  const historyString = JSON.stringify(history);
@@ -160,12 +185,15 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
160
185
  if (batchModeRef.current) {
161
186
  // In batch mode: Don't update history yet, just update UI state
162
187
  // History will be updated when batch mode ends
188
+ console.log(`[useAdjustmentHistory] Pushing state in batch mode:`, newState);
163
189
  return;
164
190
  }
165
191
  // Normal mode: Update history immediately
166
192
  setHistory(prevHistory => {
167
193
  const truncatedHistory = prevHistory.slice(0, currentIndex + 1);
168
- const newHistory = [...truncatedHistory, { state: newState }];
194
+ // const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
195
+ // we put the taskId empty to check this state no need to save it
196
+ const newHistory = [...truncatedHistory, { state: newState, taskId: "" }];
169
197
  setCurrentIndex(newHistory.length - 1);
170
198
  return newHistory;
171
199
  });
@@ -178,6 +206,7 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
178
206
  const newState = historyEntry.state;
179
207
  setCurrentIndex(newIndex);
180
208
  setCurrentState(newState);
209
+ console.log(`[useAdjustmentHistory] Undoing to index ${newIndex}:`, newState);
181
210
  // Call controller to set history index in backend if taskId exists
182
211
  if (historyEntry.taskId && internalOptions.controller && internalOptions.firebaseUid && internalOptions.currentImageId) {
183
212
  try {
@@ -189,6 +218,9 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
189
218
  console.error('❌ Failed to set history index for undo:', error);
190
219
  }
191
220
  }
221
+ else {
222
+ console.log("🔙 Undo: No taskId available for setting history index", historyEntry.taskId, internalOptions.firebaseUid, internalOptions.currentImageId);
223
+ }
192
224
  // Exit batch mode when undoing
193
225
  if (batchModeRef.current) {
194
226
  batchModeRef.current = false;
@@ -205,6 +237,7 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
205
237
  const newState = historyEntry.state;
206
238
  setCurrentIndex(newIndex);
207
239
  setCurrentState(newState);
240
+ console.log(`[useAdjustmentHistory] Redoing to index ${newIndex}:`, newState);
208
241
  // Call controller to set history index in backend if taskId exists
209
242
  if (historyEntry.taskId && internalOptions.controller && internalOptions.firebaseUid && internalOptions.currentImageId) {
210
243
  try {
@@ -216,11 +249,14 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
216
249
  console.error('❌ Failed to set history index for redo:', error);
217
250
  }
218
251
  }
252
+ else {
253
+ console.warn('🔄 Redo: No taskId available for setting history index', historyEntry.taskId, internalOptions.firebaseUid, internalOptions.currentImageId);
254
+ }
219
255
  }
220
256
  }, [currentIndex, history, internalOptions]);
221
257
  // Reset history with new initial state
222
258
  const reset = useCallback((newInitialState) => {
223
- setHistory([{ state: newInitialState }]);
259
+ setHistory([{ state: newInitialState, taskId: `reset_${Date.now()}` }]);
224
260
  setCurrentIndex(0);
225
261
  setCurrentState(newInitialState);
226
262
  batchModeRef.current = internalOptions.enableBatching;
@@ -244,7 +280,7 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
244
280
  }, [history]);
245
281
  // Clear all history and start fresh
246
282
  const clearHistory = useCallback(() => {
247
- setHistory([{ state: currentState }]);
283
+ setHistory([{ state: currentState, taskId: `clear_${Date.now()}` }]);
248
284
  setCurrentIndex(0);
249
285
  batchModeRef.current = internalOptions.enableBatching;
250
286
  batchStartIndexRef.current = null;
@@ -260,62 +296,69 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
260
296
  }, [trimHistoryToSize]);
261
297
  // Sync/replace entire history with new list
262
298
  const syncHistory = useCallback((newHistory, targetIndex) => {
263
- // Validate input
264
- if (!Array.isArray(newHistory) || newHistory.length === 0) {
265
- console.warn('syncHistory: newHistory must be a non-empty array');
266
- return;
267
- }
268
- // Validate all items are AdjustmentState objects
269
- const isValidHistory = newHistory.every(state => state && typeof state === 'object' &&
270
- typeof state.tempScore === 'number' &&
271
- typeof state.tintScore === 'number' &&
272
- typeof state.vibranceScore === 'number' &&
273
- typeof state.saturationScore === 'number' &&
274
- typeof state.exposureScore === 'number' &&
275
- typeof state.highlightsScore === 'number' &&
276
- typeof state.shadowsScore === 'number' &&
277
- typeof state.whitesScore === 'number' &&
278
- typeof state.blacksScore === 'number' &&
279
- typeof state.contrastScore === 'number' &&
280
- typeof state.clarityScore === 'number' &&
281
- typeof state.sharpnessScore === 'number');
282
- if (!isValidHistory) {
283
- console.warn('syncHistory: All items in newHistory must be valid AdjustmentState objects');
284
- return;
285
- }
286
- // Exit batch mode if active
287
- if (batchModeRef.current) {
288
- batchModeRef.current = false;
289
- batchStartIndexRef.current = null;
290
- batchStartStateRef.current = null;
291
- }
292
- // Determine target index
293
- let finalIndex = targetIndex ?? newHistory.length - 1; // Default to last item
294
- finalIndex = Math.max(0, Math.min(finalIndex, newHistory.length - 1)); // Clamp to valid range
295
- // Create a copy of the new history to avoid mutations and convert to HistoryEntry format
296
- const historyToSet = newHistory.map(state => ({ state: { ...state } }));
297
- // Apply max size limit if needed
298
- if (maxSizeRef.current !== 'unlimited' && historyToSet.length > maxSizeRef.current) {
299
- const trimAmount = historyToSet.length - maxSizeRef.current;
300
- const trimmedHistory = historyToSet.slice(trimAmount);
301
- // Adjust target index
302
- finalIndex = Math.max(0, finalIndex - trimAmount);
303
- setHistory(trimmedHistory);
304
- setCurrentIndex(finalIndex);
305
- setCurrentState(trimmedHistory[finalIndex].state);
306
- if (devWarningsRef.current) {
307
- console.warn(`syncHistory: Trimmed ${trimAmount} entries to respect maxSize of ${maxSizeRef.current}`);
308
- }
309
- }
310
- else {
311
- // Set history as-is
312
- setHistory(historyToSet);
313
- setCurrentIndex(finalIndex);
314
- setCurrentState(historyToSet[finalIndex].state);
315
- }
316
- if (devWarningsRef.current) {
317
- console.log(`syncHistory: Synchronized ${historyToSet.length} states, current index: ${finalIndex}`);
318
- }
299
+ throw Error('syncHistory is not implemented yet');
300
+ // // Validate input
301
+ // if (!Array.isArray(newHistory) || newHistory.length === 0) {
302
+ // console.warn('syncHistory: newHistory must be a non-empty array');
303
+ // return;
304
+ // }
305
+ // // Validate all items are AdjustmentState objects
306
+ // const isValidHistory = newHistory.every(state =>
307
+ // state && typeof state === 'object' &&
308
+ // typeof state.tempScore === 'number' &&
309
+ // typeof state.tintScore === 'number' &&
310
+ // typeof state.vibranceScore === 'number' &&
311
+ // typeof state.saturationScore === 'number' &&
312
+ // typeof state.exposureScore === 'number' &&
313
+ // typeof state.highlightsScore === 'number' &&
314
+ // typeof state.shadowsScore === 'number' &&
315
+ // typeof state.whitesScore === 'number' &&
316
+ // typeof state.blacksScore === 'number' &&
317
+ // typeof state.contrastScore === 'number' &&
318
+ // typeof state.clarityScore === 'number' &&
319
+ // typeof state.sharpnessScore === 'number'
320
+ // );
321
+ // if (!isValidHistory) {
322
+ // console.warn('syncHistory: All items in newHistory must be valid AdjustmentState objects');
323
+ // return;
324
+ // }
325
+ // // Exit batch mode if active
326
+ // if (batchModeRef.current) {
327
+ // batchModeRef.current = false;
328
+ // batchStartIndexRef.current = null;
329
+ // batchStartStateRef.current = null;
330
+ // }
331
+ // // Determine target index
332
+ // let finalIndex = targetIndex ?? newHistory.length - 1; // Default to last item
333
+ // finalIndex = Math.max(0, Math.min(finalIndex, newHistory.length - 1)); // Clamp to valid range
334
+ // // Create a copy of the new history to avoid mutations and convert to HistoryEntry format
335
+ // const historyToSet = newHistory.map((state, index) => ({
336
+ // state: { ...state },
337
+ // taskId: `sync_${Date.now()}_${index}`
338
+ // }));
339
+ // // Apply max size limit if needed
340
+ // if (maxSizeRef.current !== 'unlimited' && historyToSet.length > maxSizeRef.current) {
341
+ // const trimAmount = historyToSet.length - maxSizeRef.current;
342
+ // const trimmedHistory = historyToSet.slice(trimAmount);
343
+ // // Adjust target index
344
+ // finalIndex = Math.max(0, finalIndex - trimAmount);
345
+ // setHistory(trimmedHistory);
346
+ // setCurrentIndex(finalIndex);
347
+ // console.log(`syncHistory: Trimmed ${trimAmount} entries to respect maxSize of ${maxSizeRef.current}`);
348
+ // setCurrentState(trimmedHistory[finalIndex].state);
349
+ // if (devWarningsRef.current) {
350
+ // console.warn(`syncHistory: Trimmed ${trimAmount} entries to respect maxSize of ${maxSizeRef.current}`);
351
+ // }
352
+ // } else {
353
+ // // Set history as-is
354
+ // setHistory(historyToSet);
355
+ // setCurrentIndex(finalIndex);
356
+ // console.log(`syncHistory: Synchronized ${historyToSet.length} states, current index: ${finalIndex}`);
357
+ // setCurrentState(historyToSet[finalIndex].state);
358
+ // }
359
+ // if (devWarningsRef.current) {
360
+ // console.log(`syncHistory: Synchronized ${historyToSet.length} states, current index: ${finalIndex}`);
361
+ // }
319
362
  }, []);
320
363
  // Configuration setters
321
364
  const setMaxSize = useCallback((size) => {
@@ -324,7 +367,7 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
324
367
  enforceMaxSize();
325
368
  }
326
369
  }, [enforceMaxSize]);
327
- const setBatchMode = useCallback(async (enabled) => {
370
+ const setBatchMode = useCallback(async (enabled, forceHistory = false) => {
328
371
  const wasInBatch = batchModeRef.current;
329
372
  console.log(`🔧 setBatchMode called: enabled=${enabled}, wasInBatch=${wasInBatch}, currentIndex=${currentIndex}, historyLength=${history.length}`);
330
373
  if (enabled && !wasInBatch) {
@@ -332,27 +375,40 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
332
375
  batchModeRef.current = true;
333
376
  batchStartIndexRef.current = currentIndex;
334
377
  batchStartStateRef.current = currentState;
335
- console.log("Current start batch ", currentState, `batchStartIndex=${currentIndex}`);
378
+ console.log("🔧 setBatchMode(true): Starting batch mode", {
379
+ currentState,
380
+ currentIndex,
381
+ batchStartState: currentState
382
+ });
336
383
  }
337
384
  else if (!enabled && wasInBatch) {
338
385
  // Guard against double execution
386
+ console.log("🔧 setBatchMode: Ending batch mode", batchModeProcessingRef.current);
339
387
  if (batchModeProcessingRef.current) {
340
388
  console.log("⚠️ setBatchMode(false) already processing, skipping duplicate call");
341
389
  return;
342
390
  }
391
+ console.log("🔧 setBatchMode: Ending batch mode", currentState, `batchStartIndex=${batchStartIndexRef.current}`);
343
392
  // Ending batch mode - commit final state to history
344
393
  batchModeProcessingRef.current = true;
345
394
  batchModeRef.current = false;
346
395
  // Only add to history if state actually changed from batch start
347
- if (batchStartStateRef.current &&
348
- !compareAdjustmentStates(currentState, batchStartStateRef.current)) {
396
+ const statesEqual = batchStartStateRef.current ?
397
+ compareAdjustmentStates(currentState, batchStartStateRef.current) : true;
398
+ console.log("🔧 setBatchMode(false): Comparing states", {
399
+ currentState,
400
+ batchStartState: batchStartStateRef.current,
401
+ statesEqual,
402
+ forceHistory,
403
+ willAddToHistory: batchStartStateRef.current && (!statesEqual || forceHistory)
404
+ });
405
+ if (batchStartStateRef.current && (!statesEqual || forceHistory)) {
349
406
  // Generate a unique task ID for this history entry
350
407
  const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
351
408
  setHistory(prevHistory => {
352
409
  // Check if we were in the middle of history BEFORE any truncation
353
410
  // Handle case where batchStartIndexRef.current might be null (e.g., after undo)
354
411
  const batchStartIndex = batchStartIndexRef.current ?? currentIndex;
355
- const wasInMiddleOfHistory = batchStartIndex < prevHistory.length - 1;
356
412
  const truncatedHistory = prevHistory.slice(0, batchStartIndex + 1);
357
413
  const newHistoryEntry = {
358
414
  state: currentState,
@@ -401,11 +457,78 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
401
457
  }
402
458
  }
403
459
  }
460
+ else {
461
+ if (forceHistory) {
462
+ console.log("🔧 setBatchMode: Force history requested but no batch start state");
463
+ }
464
+ else {
465
+ console.log("🔧 setBatchMode: No changes since batch start, not adding to history");
466
+ }
467
+ }
404
468
  batchStartIndexRef.current = null;
405
469
  batchStartStateRef.current = null;
406
470
  batchModeProcessingRef.current = false; // Reset processing flag
407
471
  }
408
472
  }, [currentIndex, currentState, internalOptions]);
473
+ // Sync history from backend using getEditorHistory
474
+ const syncFromBackend = useCallback(async () => {
475
+ if (!internalOptions.controller || !internalOptions.firebaseUid || !internalOptions.currentImageId) {
476
+ console.warn('⚠️ syncFromBackend: Controller, firebaseUid, or currentImageId not provided - cannot sync from backend');
477
+ return;
478
+ }
479
+ try {
480
+ console.log('🔄 Syncing history from backend using getEditorHistory');
481
+ const historyResponse = await internalOptions.controller.getEditorHistory(internalOptions.firebaseUid, internalOptions.currentImageId);
482
+ console.log('📥 Received history response from backend:', historyResponse);
483
+ // Sort history by timestamp (oldest first) before processing
484
+ const sortedHistory = [...historyResponse.history].sort((a, b) => {
485
+ const timeA = new Date(a.log.created_at).getTime();
486
+ const timeB = new Date(b.log.created_at).getTime();
487
+ return timeA - timeB; // Ascending order (oldest first)
488
+ });
489
+ console.log('📋 Sorted history by timestamp:', sortedHistory.map(entry => ({
490
+ task_id: entry.task_id,
491
+ timestamp: entry.log.created_at
492
+ })));
493
+ // Convert backend history to AdjustmentState format with taskIds
494
+ const backendHistoryEntries = sortedHistory.map((entry) => ({
495
+ state: convertColorAdjustmentToAdjustmentState(entry.editor_config.color_adjustment),
496
+ taskId: entry.task_id
497
+ }));
498
+ // Find the index of the current task
499
+ let currentTaskIndex = -1;
500
+ if (historyResponse.current_task_id) {
501
+ currentTaskIndex = backendHistoryEntries.findIndex(entry => entry.taskId === historyResponse.current_task_id);
502
+ }
503
+ // If current_task_id is not found in history, default to the last entry
504
+ if (currentTaskIndex === -1 && backendHistoryEntries.length > 0) {
505
+ currentTaskIndex = backendHistoryEntries.length - 1;
506
+ console.warn(`⚠️ Current task ID "${historyResponse.current_task_id}" not found in history, defaulting to last entry`);
507
+ }
508
+ // Handle empty history case
509
+ if (backendHistoryEntries.length === 0) {
510
+ console.log('📝 Backend history is empty, keeping current local history');
511
+ return;
512
+ }
513
+ // Exit batch mode if active
514
+ if (batchModeRef.current) {
515
+ batchModeRef.current = false;
516
+ batchStartIndexRef.current = null;
517
+ batchStartStateRef.current = null;
518
+ }
519
+ // Update history state with backend data
520
+ setHistory(backendHistoryEntries);
521
+ setCurrentIndex(Math.max(0, currentTaskIndex));
522
+ console.log(`📍 Setting current index to: ${Math.max(0, currentTaskIndex)} (task_id: ${historyResponse.current_task_id})`);
523
+ setCurrentState(backendHistoryEntries[Math.max(0, currentTaskIndex)].state);
524
+ console.log(`✅ Successfully synced ${backendHistoryEntries.length} history entries from backend`);
525
+ console.log(`📍 Set current index to: ${Math.max(0, currentTaskIndex)} (task_id: ${historyResponse.current_task_id})`);
526
+ }
527
+ catch (error) {
528
+ console.error('❌ Failed to sync history from backend:', error);
529
+ throw error;
530
+ }
531
+ }, [internalOptions]);
409
532
  // History info object
410
533
  const historyInfo = useMemo(() => ({
411
534
  canUndo: currentIndex > 0,
@@ -425,60 +548,15 @@ export function useAdjustmentHistory(initialState, controller, firebaseUid, curr
425
548
  clearHistory,
426
549
  getHistory,
427
550
  trimHistory,
428
- syncHistory
429
- }), [pushState, undo, redo, reset, jumpToIndex, clearHistory, getHistory, trimHistory, syncHistory]);
430
- // Force sync current state to backend
431
- const syncToBackend = useCallback(async () => {
432
- if (internalOptions.controller && internalOptions.firebaseUid && internalOptions.currentImageId) {
433
- try {
434
- console.log('🔄 Force syncing current state to backend');
435
- // Check if we're in middle of history (not at the latest)
436
- const wasInMiddleOfHistory = currentIndex < history.length - 1;
437
- let replaceFromTaskId;
438
- if (wasInMiddleOfHistory) {
439
- // Get the task_id from the current history entry to use as replace_from
440
- const currentHistoryEntry = history[currentIndex];
441
- replaceFromTaskId = currentHistoryEntry?.taskId;
442
- console.log(`📍 In middle of history (index ${currentIndex}), using replace_from: ${replaceFromTaskId}`);
443
- }
444
- // Generate a unique task ID for this sync
445
- const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
446
- const createEditorConfigPayload = {
447
- gallery_id: internalOptions.currentImageId,
448
- task_id: taskId,
449
- color_adjustment: convertAdjustmentStateToColorAdjustment(currentState),
450
- ...(replaceFromTaskId && { replace_from: replaceFromTaskId })
451
- };
452
- await internalOptions.controller.createEditorConfig(internalOptions.firebaseUid, createEditorConfigPayload);
453
- console.log('✅ Successfully synced current state to backend');
454
- // Update the current history entry with the new task ID
455
- setHistory(prevHistory => {
456
- const newHistory = [...prevHistory];
457
- if (newHistory[currentIndex]) {
458
- newHistory[currentIndex] = {
459
- ...newHistory[currentIndex],
460
- taskId: taskId
461
- };
462
- }
463
- return newHistory;
464
- });
465
- }
466
- catch (error) {
467
- console.error('❌ Failed to sync current state to backend:', error);
468
- throw error;
469
- }
470
- }
471
- else {
472
- console.warn('⚠️ Controller, firebaseUid, or currentImageId not provided - skipping backend sync');
473
- }
474
- }, [currentState, currentIndex, history, internalOptions]);
551
+ syncHistory,
552
+ syncFromBackend
553
+ }), [pushState, undo, redo, reset, jumpToIndex, clearHistory, getHistory, trimHistory, syncHistory, syncFromBackend]);
475
554
  // Config object - stabilized with useMemo
476
555
  const config = useMemo(() => ({
477
556
  setMaxSize,
478
557
  setBatchMode,
479
- getMemoryUsage,
480
- syncToBackend
481
- }), [setMaxSize, setBatchMode, getMemoryUsage, syncToBackend]);
558
+ getMemoryUsage
559
+ }), [setMaxSize, setBatchMode, getMemoryUsage]);
482
560
  // Apply max size enforcement when history changes
483
561
  useEffect(() => {
484
562
  enforceMaxSize();
@@ -125,7 +125,7 @@ export function useEditorHeadless(options = {}) {
125
125
  console.debug('HonchoEditor instance created successfully');
126
126
  }
127
127
  console.debug('Initializing HonchoEditor...');
128
- await editorRef.current.initialize(false);
128
+ await editorRef.current.initialize(true);
129
129
  console.debug('HonchoEditor initialized successfully');
130
130
  setIsReady(true);
131
131
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yogiswara/honcho-editor-ui",
3
- "version": "2.7.13",
3
+ "version": "2.7.15",
4
4
  "description": "A complete UI component library for the Honcho photo editor.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",