@yogiswara/honcho-editor-ui 1.3.7 → 1.3.9
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/hooks/__tests__/useGallerySwipe.test.d.ts +0 -0
- package/dist/hooks/__tests__/useGallerySwipe.test.js +619 -0
- package/dist/hooks/editor/useHonchoEditor.d.ts +15 -83
- package/dist/hooks/editor/useHonchoEditor.js +362 -316
- package/dist/hooks/useAdjustmentHistory.d.ts +91 -0
- package/dist/hooks/useAdjustmentHistory.demo.d.ts +8 -0
- package/dist/hooks/useAdjustmentHistory.demo.js +106 -0
- package/dist/hooks/useAdjustmentHistory.example.d.ts +33 -0
- package/dist/hooks/useAdjustmentHistory.example.js +150 -0
- package/dist/hooks/useAdjustmentHistory.js +277 -0
- package/dist/hooks/useGallerySwipe.d.ts +36 -0
- package/dist/hooks/useGallerySwipe.example.d.ts +24 -0
- package/dist/hooks/useGallerySwipe.example.js +184 -0
- package/dist/hooks/useGallerySwipe.js +321 -0
- package/dist/setupTests.d.ts +1 -0
- package/dist/setupTests.js +1 -0
- package/dist/utils/adjustment.d.ts +5 -0
- package/dist/utils/adjustment.js +32 -0
- package/package.json +11 -2
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { AdjustmentState } from './editor/useHonchoEditor';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for the adjustment history hook
|
|
4
|
+
*/
|
|
5
|
+
interface HistoryOptions {
|
|
6
|
+
/** Maximum number of history entries to keep. Use 'unlimited' for no limit */
|
|
7
|
+
maxSize?: number | 'unlimited';
|
|
8
|
+
/** Whether to enable batch mode for grouping multiple changes */
|
|
9
|
+
enableBatching?: boolean;
|
|
10
|
+
/** Enable development warnings for performance issues */
|
|
11
|
+
devWarnings?: boolean;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Information about the current history state
|
|
15
|
+
*/
|
|
16
|
+
export interface HistoryInfo {
|
|
17
|
+
/** Whether undo operation is available */
|
|
18
|
+
canUndo: boolean;
|
|
19
|
+
/** Whether redo operation is available */
|
|
20
|
+
canRedo: boolean;
|
|
21
|
+
/** Current position in history (0-based index) */
|
|
22
|
+
currentIndex: number;
|
|
23
|
+
/** Total number of states in history */
|
|
24
|
+
totalStates: number;
|
|
25
|
+
/** Current size of history in memory */
|
|
26
|
+
historySize: number;
|
|
27
|
+
/** Whether batch mode is currently active */
|
|
28
|
+
isBatchMode: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Actions available for history management
|
|
32
|
+
*/
|
|
33
|
+
export interface HistoryActions {
|
|
34
|
+
/** Add a new adjustment state to history */
|
|
35
|
+
pushState: (state: AdjustmentState) => void;
|
|
36
|
+
/** Undo to previous adjustment state */
|
|
37
|
+
undo: () => void;
|
|
38
|
+
/** Redo to next adjustment state */
|
|
39
|
+
redo: () => void;
|
|
40
|
+
/** Reset history with new initial adjustment state */
|
|
41
|
+
reset: (newInitialState: AdjustmentState) => void;
|
|
42
|
+
/** Jump to specific index in history */
|
|
43
|
+
jumpToIndex: (index: number) => void;
|
|
44
|
+
/** Clear all history and start fresh */
|
|
45
|
+
clearHistory: () => void;
|
|
46
|
+
/** Get a copy of the entire history array */
|
|
47
|
+
getHistory: () => AdjustmentState[];
|
|
48
|
+
/** Trim history to specified size, keeping most recent entries */
|
|
49
|
+
trimHistory: (keepLast: number) => void;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Configuration actions for runtime adjustment
|
|
53
|
+
*/
|
|
54
|
+
export interface HistoryConfig {
|
|
55
|
+
/** Set maximum history size */
|
|
56
|
+
setMaxSize: (size: number | 'unlimited') => void;
|
|
57
|
+
/** Enable or disable batch mode */
|
|
58
|
+
setBatchMode: (enabled: boolean) => void;
|
|
59
|
+
/** Get current memory usage estimate */
|
|
60
|
+
getMemoryUsage: () => number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Return type for the useAdjustmentHistory hook
|
|
64
|
+
*/
|
|
65
|
+
export interface UseAdjustmentHistoryReturn {
|
|
66
|
+
/** Current adjustment state value */
|
|
67
|
+
currentState: AdjustmentState;
|
|
68
|
+
/** Information about history state */
|
|
69
|
+
historyInfo: HistoryInfo;
|
|
70
|
+
/** Available history actions */
|
|
71
|
+
actions: HistoryActions;
|
|
72
|
+
/** Configuration options */
|
|
73
|
+
config: HistoryConfig;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Advanced hook for managing AdjustmentState history with undo/redo functionality.
|
|
77
|
+
*
|
|
78
|
+
* Features:
|
|
79
|
+
* - Unlimited or configurable history size
|
|
80
|
+
* - Batch mode for grouping multiple changes into single undo operations
|
|
81
|
+
* - Memory usage monitoring and optimization
|
|
82
|
+
* - Internal stabilization to prevent re-render issues
|
|
83
|
+
* - Jump to any point in history
|
|
84
|
+
* - Automatic AdjustmentState comparison
|
|
85
|
+
*
|
|
86
|
+
* @param initialState - The initial AdjustmentState value
|
|
87
|
+
* @param options - Configuration options for history behavior
|
|
88
|
+
* @returns Object with current state, history info, actions, and config
|
|
89
|
+
*/
|
|
90
|
+
export declare function useAdjustmentHistory(initialState: AdjustmentState, options?: HistoryOptions): UseAdjustmentHistoryReturn;
|
|
91
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo component showing batch mode behavior
|
|
3
|
+
*/
|
|
4
|
+
export declare function BatchModeDemo(): import("react/jsx-runtime").JSX.Element;
|
|
5
|
+
/**
|
|
6
|
+
* Example showing smooth preset application
|
|
7
|
+
*/
|
|
8
|
+
export declare function SmoothPresetDemo(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import React, { useState } from 'react';
|
|
3
|
+
import { useAdjustmentHistory } from './useAdjustmentHistory';
|
|
4
|
+
const initialAdjustments = {
|
|
5
|
+
tempScore: 0, tintScore: 0, vibranceScore: 0, saturationScore: 0,
|
|
6
|
+
exposureScore: 0, highlightsScore: 0, shadowsScore: 0, whitesScore: 0,
|
|
7
|
+
blacksScore: 0, contrastScore: 0, clarityScore: 0, sharpnessScore: 0,
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Demo component showing batch mode behavior
|
|
11
|
+
*/
|
|
12
|
+
export function BatchModeDemo() {
|
|
13
|
+
const { currentState, historyInfo, actions, config } = useAdjustmentHistory(initialAdjustments);
|
|
14
|
+
const [updateCount, setUpdateCount] = useState(0);
|
|
15
|
+
// Track UI updates
|
|
16
|
+
React.useEffect(() => {
|
|
17
|
+
setUpdateCount(prev => prev + 1);
|
|
18
|
+
}, [currentState]);
|
|
19
|
+
const handleBatchUpdates = async () => {
|
|
20
|
+
setUpdateCount(0); // Reset counter
|
|
21
|
+
console.log('Starting batch mode...');
|
|
22
|
+
config.setBatchMode(true);
|
|
23
|
+
// These will update currentState 4 times (UI updates)
|
|
24
|
+
console.log('Push state 1...');
|
|
25
|
+
actions.pushState({ ...currentState, tempScore: 25 });
|
|
26
|
+
console.log('Push state 2...');
|
|
27
|
+
actions.pushState({ ...currentState, tempScore: 50 });
|
|
28
|
+
console.log('Push state 3...');
|
|
29
|
+
actions.pushState({ ...currentState, tempScore: 75 });
|
|
30
|
+
console.log('Push state 4...');
|
|
31
|
+
actions.pushState({ ...currentState, tempScore: 100 });
|
|
32
|
+
console.log('Ending batch mode...');
|
|
33
|
+
config.setBatchMode(false); // Only now will 1 history entry be created
|
|
34
|
+
console.log('Batch complete!');
|
|
35
|
+
};
|
|
36
|
+
const handleInstantUpdates = () => {
|
|
37
|
+
setUpdateCount(0); // Reset counter
|
|
38
|
+
// These will create 4 history entries (normal mode)
|
|
39
|
+
actions.pushState({ ...currentState, tintScore: 25 });
|
|
40
|
+
actions.pushState({ ...currentState, tintScore: 50 });
|
|
41
|
+
actions.pushState({ ...currentState, tintScore: 75 });
|
|
42
|
+
actions.pushState({ ...currentState, tintScore: 100 });
|
|
43
|
+
};
|
|
44
|
+
return (_jsxs("div", { style: { padding: '20px', fontFamily: 'monospace' }, children: [_jsx("h2", { children: "Batch Mode Demo" }), _jsxs("div", { style: { marginBottom: '20px', padding: '10px', backgroundColor: '#f5f5f5' }, children: [_jsx("strong", { children: "Current State:" }), _jsxs("div", { children: ["Temperature: ", currentState.tempScore] }), _jsxs("div", { children: ["Tint: ", currentState.tintScore] }), _jsxs("div", { children: ["UI Updates: ", updateCount] }), _jsxs("div", { children: ["History Size: ", historyInfo.totalStates] }), _jsxs("div", { children: ["Can Undo: ", historyInfo.canUndo ? 'Yes' : 'No'] }), _jsxs("div", { children: ["Batch Mode: ", historyInfo.isBatchMode ? 'Active' : 'Inactive'] })] }), _jsxs("div", { style: { display: 'flex', gap: '10px', marginBottom: '20px' }, children: [_jsx("button", { onClick: handleBatchUpdates, style: { padding: '10px', backgroundColor: '#4CAF50', color: 'white', border: 'none' }, children: "Batch Mode (4 UI updates, 1 history entry)" }), _jsx("button", { onClick: handleInstantUpdates, style: { padding: '10px', backgroundColor: '#2196F3', color: 'white', border: 'none' }, children: "Normal Mode (4 UI updates, 4 history entries)" })] }), _jsxs("div", { style: { display: 'flex', gap: '10px' }, children: [_jsx("button", { onClick: actions.undo, disabled: !historyInfo.canUndo, style: {
|
|
45
|
+
padding: '10px',
|
|
46
|
+
backgroundColor: historyInfo.canUndo ? '#FF9800' : '#ccc',
|
|
47
|
+
color: 'white',
|
|
48
|
+
border: 'none'
|
|
49
|
+
}, children: "Undo" }), _jsx("button", { onClick: actions.redo, disabled: !historyInfo.canRedo, style: {
|
|
50
|
+
padding: '10px',
|
|
51
|
+
backgroundColor: historyInfo.canRedo ? '#9C27B0' : '#ccc',
|
|
52
|
+
color: 'white',
|
|
53
|
+
border: 'none'
|
|
54
|
+
}, children: "Redo" }), _jsx("button", { onClick: () => actions.reset(initialAdjustments), style: { padding: '10px', backgroundColor: '#f44336', color: 'white', border: 'none' }, children: "Reset" })] }), _jsxs("div", { style: { marginTop: '20px', fontSize: '12px', color: '#666' }, children: [_jsx("h3", { children: "How it works:" }), _jsxs("ul", { children: [_jsxs("li", { children: [_jsx("strong", { children: "Batch Mode:" }), " UI updates immediately on each pushState, but only final state is saved to history when batch ends"] }), _jsxs("li", { children: [_jsx("strong", { children: "Normal Mode:" }), " Each pushState creates both UI update and history entry"] }), _jsxs("li", { children: [_jsx("strong", { children: "Result:" }), " Smooth UI animations with clean undo/redo history"] })] })] })] }));
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Example showing smooth preset application
|
|
58
|
+
*/
|
|
59
|
+
export function SmoothPresetDemo() {
|
|
60
|
+
const { currentState, historyInfo, actions, config } = useAdjustmentHistory(initialAdjustments);
|
|
61
|
+
const dramaticPreset = {
|
|
62
|
+
tempScore: 50, tintScore: -20, vibranceScore: 80, saturationScore: 60,
|
|
63
|
+
exposureScore: 30, highlightsScore: -40, shadowsScore: 40, whitesScore: 20,
|
|
64
|
+
blacksScore: -30, contrastScore: 70, clarityScore: 50, sharpnessScore: 40,
|
|
65
|
+
};
|
|
66
|
+
const handleSmoothPreset = async () => {
|
|
67
|
+
config.setBatchMode(true);
|
|
68
|
+
// Create smooth transition with multiple steps
|
|
69
|
+
const steps = 8;
|
|
70
|
+
for (let i = 1; i <= steps; i++) {
|
|
71
|
+
const progress = i / steps;
|
|
72
|
+
const intermediateState = {
|
|
73
|
+
tempScore: Math.round(currentState.tempScore + (dramaticPreset.tempScore - currentState.tempScore) * progress),
|
|
74
|
+
tintScore: Math.round(currentState.tintScore + (dramaticPreset.tintScore - currentState.tintScore) * progress),
|
|
75
|
+
vibranceScore: Math.round(currentState.vibranceScore + (dramaticPreset.vibranceScore - currentState.vibranceScore) * progress),
|
|
76
|
+
saturationScore: Math.round(currentState.saturationScore + (dramaticPreset.saturationScore - currentState.saturationScore) * progress),
|
|
77
|
+
exposureScore: Math.round(currentState.exposureScore + (dramaticPreset.exposureScore - currentState.exposureScore) * progress),
|
|
78
|
+
highlightsScore: Math.round(currentState.highlightsScore + (dramaticPreset.highlightsScore - currentState.highlightsScore) * progress),
|
|
79
|
+
shadowsScore: Math.round(currentState.shadowsScore + (dramaticPreset.shadowsScore - currentState.shadowsScore) * progress),
|
|
80
|
+
whitesScore: Math.round(currentState.whitesScore + (dramaticPreset.whitesScore - currentState.whitesScore) * progress),
|
|
81
|
+
blacksScore: Math.round(currentState.blacksScore + (dramaticPreset.blacksScore - currentState.blacksScore) * progress),
|
|
82
|
+
contrastScore: Math.round(currentState.contrastScore + (dramaticPreset.contrastScore - currentState.contrastScore) * progress),
|
|
83
|
+
clarityScore: Math.round(currentState.clarityScore + (dramaticPreset.clarityScore - currentState.clarityScore) * progress),
|
|
84
|
+
sharpnessScore: Math.round(currentState.sharpnessScore + (dramaticPreset.sharpnessScore - currentState.sharpnessScore) * progress),
|
|
85
|
+
};
|
|
86
|
+
actions.pushState(intermediateState);
|
|
87
|
+
// Small delay for smooth animation
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
89
|
+
}
|
|
90
|
+
config.setBatchMode(false); // Commit final state
|
|
91
|
+
};
|
|
92
|
+
return (_jsxs("div", { style: { padding: '20px', fontFamily: 'monospace' }, children: [_jsx("h2", { children: "Smooth Preset Application" }), _jsxs("div", { style: { display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '10px', marginBottom: '20px', fontSize: '12px' }, children: [_jsxs("div", { children: ["Temp: ", currentState.tempScore] }), _jsxs("div", { children: ["Tint: ", currentState.tintScore] }), _jsxs("div", { children: ["Vibrance: ", currentState.vibranceScore] }), _jsxs("div", { children: ["Saturation: ", currentState.saturationScore] }), _jsxs("div", { children: ["Exposure: ", currentState.exposureScore] }), _jsxs("div", { children: ["Highlights: ", currentState.highlightsScore] }), _jsxs("div", { children: ["Shadows: ", currentState.shadowsScore] }), _jsxs("div", { children: ["Whites: ", currentState.whitesScore] }), _jsxs("div", { children: ["Blacks: ", currentState.blacksScore] }), _jsxs("div", { children: ["Contrast: ", currentState.contrastScore] }), _jsxs("div", { children: ["Clarity: ", currentState.clarityScore] }), _jsxs("div", { children: ["Sharpness: ", currentState.sharpnessScore] })] }), _jsx("button", { onClick: handleSmoothPreset, style: {
|
|
93
|
+
padding: '15px 30px',
|
|
94
|
+
backgroundColor: '#4CAF50',
|
|
95
|
+
color: 'white',
|
|
96
|
+
border: 'none',
|
|
97
|
+
fontSize: '16px',
|
|
98
|
+
marginRight: '10px'
|
|
99
|
+
}, children: "Apply Dramatic Preset (Smooth)" }), _jsx("button", { onClick: actions.undo, disabled: !historyInfo.canUndo, style: {
|
|
100
|
+
padding: '15px 30px',
|
|
101
|
+
backgroundColor: historyInfo.canUndo ? '#FF9800' : '#ccc',
|
|
102
|
+
color: 'white',
|
|
103
|
+
border: 'none',
|
|
104
|
+
fontSize: '16px'
|
|
105
|
+
}, children: "Undo Preset" }), _jsx("div", { style: { marginTop: '20px', fontSize: '12px', color: '#666' }, children: _jsx("p", { children: "This demo shows smooth preset application with 8 intermediate steps, but only 1 undo operation!" }) })] }));
|
|
106
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type HistoryInfo } from './useAdjustmentHistory';
|
|
2
|
+
import { AdjustmentState } from './editor/useHonchoEditor';
|
|
3
|
+
export declare function useEditorWithHistory(): {
|
|
4
|
+
adjustments: AdjustmentState;
|
|
5
|
+
canUndo: boolean;
|
|
6
|
+
canRedo: boolean;
|
|
7
|
+
undo: () => void;
|
|
8
|
+
redo: () => void;
|
|
9
|
+
updateTemperature: (newTemp: number) => void;
|
|
10
|
+
applyPresetWithSmoothUI: (presetState: AdjustmentState) => void;
|
|
11
|
+
updateMultipleAdjustments: (updates: Partial<AdjustmentState>) => void;
|
|
12
|
+
resetAdjustments: () => void;
|
|
13
|
+
jumpToIndex: (index: number) => void;
|
|
14
|
+
getHistory: () => AdjustmentState[];
|
|
15
|
+
clearHistory: () => void;
|
|
16
|
+
setBatchMode: (enabled: boolean) => void;
|
|
17
|
+
setMaxSize: (size: number | "unlimited") => void;
|
|
18
|
+
getMemoryUsage: () => number;
|
|
19
|
+
historyInfo: HistoryInfo;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Example integration with your existing useHonchoEditor pattern
|
|
23
|
+
*/
|
|
24
|
+
export declare function integrateWithHonchoEditor(): {
|
|
25
|
+
currentAdjustments: AdjustmentState;
|
|
26
|
+
canUndo: boolean;
|
|
27
|
+
canRedo: boolean;
|
|
28
|
+
handleSliderChange: (key: keyof AdjustmentState, value: number) => void;
|
|
29
|
+
handlePresetWithAnimation: (presetState: AdjustmentState) => Promise<void>;
|
|
30
|
+
handleUndo: () => void;
|
|
31
|
+
handleRedo: () => void;
|
|
32
|
+
handleRevert: () => void;
|
|
33
|
+
};
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { useAdjustmentHistory } from './useAdjustmentHistory';
|
|
2
|
+
/**
|
|
3
|
+
* Example usage of the simplified useAdjustmentHistory hook
|
|
4
|
+
* This shows how to integrate it with your existing useHonchoEditor
|
|
5
|
+
*/
|
|
6
|
+
// Example initial adjustment state
|
|
7
|
+
const initialAdjustments = {
|
|
8
|
+
tempScore: 0,
|
|
9
|
+
tintScore: 0,
|
|
10
|
+
vibranceScore: 0,
|
|
11
|
+
saturationScore: 0,
|
|
12
|
+
exposureScore: 0,
|
|
13
|
+
highlightsScore: 0,
|
|
14
|
+
shadowsScore: 0,
|
|
15
|
+
whitesScore: 0,
|
|
16
|
+
blacksScore: 0,
|
|
17
|
+
contrastScore: 0,
|
|
18
|
+
clarityScore: 0,
|
|
19
|
+
sharpnessScore: 0,
|
|
20
|
+
};
|
|
21
|
+
export function useEditorWithHistory() {
|
|
22
|
+
// Initialize the adjustment history hook
|
|
23
|
+
const { currentState: currentAdjustments, historyInfo, actions, config } = useAdjustmentHistory(initialAdjustments, {
|
|
24
|
+
maxSize: 100, // Keep last 100 states
|
|
25
|
+
enableBatching: false, // Start with batching disabled
|
|
26
|
+
devWarnings: true // Show performance warnings in development
|
|
27
|
+
});
|
|
28
|
+
// Example: Update a single adjustment value
|
|
29
|
+
const updateTemperature = (newTemp) => {
|
|
30
|
+
const newState = {
|
|
31
|
+
...currentAdjustments,
|
|
32
|
+
tempScore: newTemp
|
|
33
|
+
};
|
|
34
|
+
actions.pushState(newState);
|
|
35
|
+
};
|
|
36
|
+
// Example: Apply preset with batch mode for smooth UI updates
|
|
37
|
+
const applyPresetWithSmoothUI = (presetState) => {
|
|
38
|
+
// Enable batch mode to group all changes into one undo operation
|
|
39
|
+
config.setBatchMode(true);
|
|
40
|
+
// These will update the UI immediately but not create history entries
|
|
41
|
+
actions.pushState({ ...currentAdjustments, tempScore: presetState.tempScore });
|
|
42
|
+
actions.pushState({ ...currentAdjustments, tempScore: presetState.tempScore, tintScore: presetState.tintScore });
|
|
43
|
+
actions.pushState({ ...currentAdjustments, tempScore: presetState.tempScore, tintScore: presetState.tintScore, exposureScore: presetState.exposureScore });
|
|
44
|
+
actions.pushState(presetState); // Final complete state
|
|
45
|
+
// Disable batch mode to commit only the final state to history
|
|
46
|
+
config.setBatchMode(false);
|
|
47
|
+
// Result: UI updated 4 times (smooth animation), but only 1 undo operation
|
|
48
|
+
};
|
|
49
|
+
// Example: Bulk update multiple adjustments
|
|
50
|
+
const updateMultipleAdjustments = (updates) => {
|
|
51
|
+
config.setBatchMode(true); // Start batching
|
|
52
|
+
const newState = {
|
|
53
|
+
...currentAdjustments,
|
|
54
|
+
...updates
|
|
55
|
+
};
|
|
56
|
+
actions.pushState(newState);
|
|
57
|
+
config.setBatchMode(false); // Commit batch
|
|
58
|
+
};
|
|
59
|
+
// Example: Reset to initial state
|
|
60
|
+
const resetAdjustments = () => {
|
|
61
|
+
actions.reset(initialAdjustments);
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
// Current adjustment values
|
|
65
|
+
adjustments: currentAdjustments,
|
|
66
|
+
// History controls
|
|
67
|
+
canUndo: historyInfo.canUndo,
|
|
68
|
+
canRedo: historyInfo.canRedo,
|
|
69
|
+
undo: actions.undo,
|
|
70
|
+
redo: actions.redo,
|
|
71
|
+
// Adjustment functions
|
|
72
|
+
updateTemperature,
|
|
73
|
+
applyPresetWithSmoothUI,
|
|
74
|
+
updateMultipleAdjustments,
|
|
75
|
+
resetAdjustments,
|
|
76
|
+
// Advanced features
|
|
77
|
+
jumpToIndex: actions.jumpToIndex,
|
|
78
|
+
getHistory: actions.getHistory,
|
|
79
|
+
clearHistory: actions.clearHistory,
|
|
80
|
+
// Configuration
|
|
81
|
+
setBatchMode: config.setBatchMode,
|
|
82
|
+
setMaxSize: config.setMaxSize,
|
|
83
|
+
getMemoryUsage: config.getMemoryUsage,
|
|
84
|
+
// History info
|
|
85
|
+
historyInfo
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Example integration with your existing useHonchoEditor pattern
|
|
90
|
+
*/
|
|
91
|
+
export function integrateWithHonchoEditor() {
|
|
92
|
+
const history = useAdjustmentHistory(initialAdjustments);
|
|
93
|
+
// Replace your current manual history management with this:
|
|
94
|
+
// Instead of:
|
|
95
|
+
// const [history, setHistory] = useState<AdjustmentState[]>([initialAdjustments]);
|
|
96
|
+
// const [historyIndex, setHistoryIndex] = useState(0);
|
|
97
|
+
// Use:
|
|
98
|
+
const currentAdjustments = history.currentState;
|
|
99
|
+
const canUndo = history.historyInfo.canUndo;
|
|
100
|
+
const canRedo = history.historyInfo.canRedo;
|
|
101
|
+
// When any adjustment changes (e.g., slider value):
|
|
102
|
+
const handleSliderChange = (key, value) => {
|
|
103
|
+
const newState = {
|
|
104
|
+
...currentAdjustments,
|
|
105
|
+
[key]: value
|
|
106
|
+
};
|
|
107
|
+
history.actions.pushState(newState);
|
|
108
|
+
};
|
|
109
|
+
// Smooth preset application with multiple UI updates
|
|
110
|
+
const handlePresetWithAnimation = async (presetState) => {
|
|
111
|
+
history.config.setBatchMode(true);
|
|
112
|
+
// Animate through intermediate states for smooth UI
|
|
113
|
+
const steps = 4;
|
|
114
|
+
for (let i = 1; i <= steps; i++) {
|
|
115
|
+
const progress = i / steps;
|
|
116
|
+
const intermediateState = {
|
|
117
|
+
tempScore: Math.round(currentAdjustments.tempScore + (presetState.tempScore - currentAdjustments.tempScore) * progress),
|
|
118
|
+
tintScore: Math.round(currentAdjustments.tintScore + (presetState.tintScore - currentAdjustments.tintScore) * progress),
|
|
119
|
+
vibranceScore: Math.round(currentAdjustments.vibranceScore + (presetState.vibranceScore - currentAdjustments.vibranceScore) * progress),
|
|
120
|
+
saturationScore: Math.round(currentAdjustments.saturationScore + (presetState.saturationScore - currentAdjustments.saturationScore) * progress),
|
|
121
|
+
exposureScore: Math.round(currentAdjustments.exposureScore + (presetState.exposureScore - currentAdjustments.exposureScore) * progress),
|
|
122
|
+
highlightsScore: Math.round(currentAdjustments.highlightsScore + (presetState.highlightsScore - currentAdjustments.highlightsScore) * progress),
|
|
123
|
+
shadowsScore: Math.round(currentAdjustments.shadowsScore + (presetState.shadowsScore - currentAdjustments.shadowsScore) * progress),
|
|
124
|
+
whitesScore: Math.round(currentAdjustments.whitesScore + (presetState.whitesScore - currentAdjustments.whitesScore) * progress),
|
|
125
|
+
blacksScore: Math.round(currentAdjustments.blacksScore + (presetState.blacksScore - currentAdjustments.blacksScore) * progress),
|
|
126
|
+
contrastScore: Math.round(currentAdjustments.contrastScore + (presetState.contrastScore - currentAdjustments.contrastScore) * progress),
|
|
127
|
+
clarityScore: Math.round(currentAdjustments.clarityScore + (presetState.clarityScore - currentAdjustments.clarityScore) * progress),
|
|
128
|
+
sharpnessScore: Math.round(currentAdjustments.sharpnessScore + (presetState.sharpnessScore - currentAdjustments.sharpnessScore) * progress),
|
|
129
|
+
};
|
|
130
|
+
history.actions.pushState(intermediateState);
|
|
131
|
+
// Small delay for smooth animation
|
|
132
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
133
|
+
}
|
|
134
|
+
history.config.setBatchMode(false); // Commit only final state to history
|
|
135
|
+
};
|
|
136
|
+
// Your existing undo/redo handlers become:
|
|
137
|
+
const handleUndo = history.actions.undo;
|
|
138
|
+
const handleRedo = history.actions.redo;
|
|
139
|
+
const handleRevert = () => history.actions.reset(initialAdjustments);
|
|
140
|
+
return {
|
|
141
|
+
currentAdjustments,
|
|
142
|
+
canUndo,
|
|
143
|
+
canRedo,
|
|
144
|
+
handleSliderChange,
|
|
145
|
+
handlePresetWithAnimation,
|
|
146
|
+
handleUndo,
|
|
147
|
+
handleRedo,
|
|
148
|
+
handleRevert
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo, useRef, useEffect } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Compare two AdjustmentState objects for equality
|
|
4
|
+
* Uses JSON.stringify for deep comparison of all adjustment values
|
|
5
|
+
*/
|
|
6
|
+
const compareAdjustmentStates = (a, b) => {
|
|
7
|
+
try {
|
|
8
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
// Fallback to manual comparison if JSON.stringify fails
|
|
12
|
+
console.warn('Failed to compare adjustment states with JSON.stringify, falling back to manual comparison:', error);
|
|
13
|
+
return (a.tempScore === b.tempScore &&
|
|
14
|
+
a.tintScore === b.tintScore &&
|
|
15
|
+
a.vibranceScore === b.vibranceScore &&
|
|
16
|
+
a.saturationScore === b.saturationScore &&
|
|
17
|
+
a.exposureScore === b.exposureScore &&
|
|
18
|
+
a.highlightsScore === b.highlightsScore &&
|
|
19
|
+
a.shadowsScore === b.shadowsScore &&
|
|
20
|
+
a.whitesScore === b.whitesScore &&
|
|
21
|
+
a.blacksScore === b.blacksScore &&
|
|
22
|
+
a.contrastScore === b.contrastScore &&
|
|
23
|
+
a.clarityScore === b.clarityScore &&
|
|
24
|
+
a.sharpnessScore === b.sharpnessScore);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Advanced hook for managing AdjustmentState history with undo/redo functionality.
|
|
29
|
+
*
|
|
30
|
+
* Features:
|
|
31
|
+
* - Unlimited or configurable history size
|
|
32
|
+
* - Batch mode for grouping multiple changes into single undo operations
|
|
33
|
+
* - Memory usage monitoring and optimization
|
|
34
|
+
* - Internal stabilization to prevent re-render issues
|
|
35
|
+
* - Jump to any point in history
|
|
36
|
+
* - Automatic AdjustmentState comparison
|
|
37
|
+
*
|
|
38
|
+
* @param initialState - The initial AdjustmentState value
|
|
39
|
+
* @param options - Configuration options for history behavior
|
|
40
|
+
* @returns Object with current state, history info, actions, and config
|
|
41
|
+
*/
|
|
42
|
+
export function useAdjustmentHistory(initialState, options = {}) {
|
|
43
|
+
// Internal stabilization - prevent re-renders from options object recreation
|
|
44
|
+
const internalOptions = useMemo(() => ({
|
|
45
|
+
maxSize: options.maxSize ?? 'unlimited',
|
|
46
|
+
enableBatching: options.enableBatching ?? false,
|
|
47
|
+
devWarnings: options.devWarnings ?? false
|
|
48
|
+
}), [
|
|
49
|
+
options.maxSize,
|
|
50
|
+
options.enableBatching,
|
|
51
|
+
options.devWarnings
|
|
52
|
+
]);
|
|
53
|
+
// Core state management
|
|
54
|
+
const [history, setHistory] = useState([initialState]);
|
|
55
|
+
const [currentIndex, setCurrentIndex] = useState(0);
|
|
56
|
+
const [currentState, setCurrentState] = useState(initialState);
|
|
57
|
+
// Batch mode state - ref to avoid triggering effects
|
|
58
|
+
const batchModeRef = useRef(internalOptions.enableBatching);
|
|
59
|
+
const batchStartIndexRef = useRef(null);
|
|
60
|
+
const batchStartStateRef = useRef(null);
|
|
61
|
+
// Configuration refs - prevent re-renders when config changes
|
|
62
|
+
const maxSizeRef = useRef(internalOptions.maxSize);
|
|
63
|
+
const devWarningsRef = useRef(internalOptions.devWarnings);
|
|
64
|
+
// Performance monitoring
|
|
65
|
+
const performanceRef = useRef({
|
|
66
|
+
lastHistorySize: 1,
|
|
67
|
+
lastUpdateTime: Date.now(),
|
|
68
|
+
largeHistoryWarningShown: false
|
|
69
|
+
});
|
|
70
|
+
// Sync currentState with history when not in batch mode
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!batchModeRef.current) {
|
|
73
|
+
setCurrentState(history[currentIndex]);
|
|
74
|
+
}
|
|
75
|
+
}, [history, currentIndex]);
|
|
76
|
+
const getMemoryUsage = useCallback(() => {
|
|
77
|
+
try {
|
|
78
|
+
const historyString = JSON.stringify(history);
|
|
79
|
+
return historyString.length * 2; // Rough estimate: 2 bytes per character
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
console.warn('Failed to estimate memory usage:', error);
|
|
83
|
+
return history.length * 1000; // Fallback estimate
|
|
84
|
+
}
|
|
85
|
+
}, [history]);
|
|
86
|
+
// Development warnings for performance
|
|
87
|
+
const checkPerformance = useCallback(() => {
|
|
88
|
+
if (!devWarningsRef.current)
|
|
89
|
+
return;
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
const perfData = performanceRef.current;
|
|
92
|
+
// Warn about large history sizes
|
|
93
|
+
if (history.length > 1000 && !perfData.largeHistoryWarningShown) {
|
|
94
|
+
console.warn(`useAdjustmentHistory: Large history size detected (${history.length} entries). Consider setting a maxSize limit.`);
|
|
95
|
+
perfData.largeHistoryWarningShown = true;
|
|
96
|
+
}
|
|
97
|
+
// Update performance tracking
|
|
98
|
+
perfData.lastHistorySize = history.length;
|
|
99
|
+
perfData.lastUpdateTime = now;
|
|
100
|
+
}, [history.length]);
|
|
101
|
+
// Trim history to specified size, keeping most recent entries
|
|
102
|
+
const trimHistoryToSize = useCallback((size) => {
|
|
103
|
+
if (size <= 0)
|
|
104
|
+
return;
|
|
105
|
+
setHistory(prevHistory => {
|
|
106
|
+
if (prevHistory.length <= size)
|
|
107
|
+
return prevHistory;
|
|
108
|
+
const startIndex = Math.max(0, prevHistory.length - size);
|
|
109
|
+
const trimmedHistory = prevHistory.slice(startIndex);
|
|
110
|
+
// Adjust current index to maintain relative position
|
|
111
|
+
setCurrentIndex(prevIndex => {
|
|
112
|
+
const adjustedIndex = prevIndex - startIndex;
|
|
113
|
+
return Math.max(0, Math.min(adjustedIndex, trimmedHistory.length - 1));
|
|
114
|
+
});
|
|
115
|
+
return trimmedHistory;
|
|
116
|
+
});
|
|
117
|
+
}, []);
|
|
118
|
+
// Apply max size limit when history grows
|
|
119
|
+
const enforceMaxSize = useCallback(() => {
|
|
120
|
+
if (maxSizeRef.current === 'unlimited')
|
|
121
|
+
return;
|
|
122
|
+
const maxSize = maxSizeRef.current;
|
|
123
|
+
if (history.length > maxSize) {
|
|
124
|
+
trimHistoryToSize(maxSize);
|
|
125
|
+
}
|
|
126
|
+
}, [history.length, trimHistoryToSize]);
|
|
127
|
+
// Push new state to history
|
|
128
|
+
const pushState = useCallback((newState) => {
|
|
129
|
+
// Skip if state hasn't changed
|
|
130
|
+
if (compareAdjustmentStates(newState, currentState)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// Always update currentState immediately for smooth UI
|
|
134
|
+
setCurrentState(newState);
|
|
135
|
+
if (batchModeRef.current) {
|
|
136
|
+
// In batch mode: Don't update history yet, just update UI state
|
|
137
|
+
// History will be updated when batch mode ends
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// Normal mode: Update history immediately
|
|
141
|
+
setHistory(prevHistory => {
|
|
142
|
+
const truncatedHistory = prevHistory.slice(0, currentIndex + 1);
|
|
143
|
+
const newHistory = [...truncatedHistory, newState];
|
|
144
|
+
setCurrentIndex(newHistory.length - 1);
|
|
145
|
+
return newHistory;
|
|
146
|
+
});
|
|
147
|
+
}, [currentState, currentIndex]);
|
|
148
|
+
// Undo to previous state
|
|
149
|
+
const undo = useCallback(() => {
|
|
150
|
+
if (currentIndex > 0) {
|
|
151
|
+
const newIndex = currentIndex - 1;
|
|
152
|
+
setCurrentIndex(newIndex);
|
|
153
|
+
setCurrentState(history[newIndex]);
|
|
154
|
+
// Exit batch mode when undoing
|
|
155
|
+
if (batchModeRef.current) {
|
|
156
|
+
batchModeRef.current = false;
|
|
157
|
+
batchStartIndexRef.current = null;
|
|
158
|
+
batchStartStateRef.current = null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}, [currentIndex, history]);
|
|
162
|
+
// Redo to next state
|
|
163
|
+
const redo = useCallback(() => {
|
|
164
|
+
if (currentIndex < history.length - 1) {
|
|
165
|
+
const newIndex = currentIndex + 1;
|
|
166
|
+
setCurrentIndex(newIndex);
|
|
167
|
+
setCurrentState(history[newIndex]);
|
|
168
|
+
}
|
|
169
|
+
}, [currentIndex, history]);
|
|
170
|
+
// Reset history with new initial state
|
|
171
|
+
const reset = useCallback((newInitialState) => {
|
|
172
|
+
setHistory([newInitialState]);
|
|
173
|
+
setCurrentIndex(0);
|
|
174
|
+
setCurrentState(newInitialState);
|
|
175
|
+
batchModeRef.current = internalOptions.enableBatching;
|
|
176
|
+
batchStartIndexRef.current = null;
|
|
177
|
+
batchStartStateRef.current = null;
|
|
178
|
+
}, [internalOptions.enableBatching]);
|
|
179
|
+
// Jump to specific index in history
|
|
180
|
+
const jumpToIndex = useCallback((index) => {
|
|
181
|
+
if (index >= 0 && index < history.length) {
|
|
182
|
+
setCurrentIndex(index);
|
|
183
|
+
setCurrentState(history[index]);
|
|
184
|
+
// Exit batch mode when jumping
|
|
185
|
+
if (batchModeRef.current) {
|
|
186
|
+
batchModeRef.current = false;
|
|
187
|
+
batchStartIndexRef.current = null;
|
|
188
|
+
batchStartStateRef.current = null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}, [history]);
|
|
192
|
+
// Clear all history and start fresh
|
|
193
|
+
const clearHistory = useCallback(() => {
|
|
194
|
+
setHistory([currentState]);
|
|
195
|
+
setCurrentIndex(0);
|
|
196
|
+
batchModeRef.current = internalOptions.enableBatching;
|
|
197
|
+
batchStartIndexRef.current = null;
|
|
198
|
+
batchStartStateRef.current = null;
|
|
199
|
+
}, [currentState, internalOptions.enableBatching]);
|
|
200
|
+
// Get copy of entire history
|
|
201
|
+
const getHistory = useCallback(() => {
|
|
202
|
+
return [...history];
|
|
203
|
+
}, [history]);
|
|
204
|
+
// Manually trim history
|
|
205
|
+
const trimHistory = useCallback((keepLast) => {
|
|
206
|
+
trimHistoryToSize(keepLast);
|
|
207
|
+
}, [trimHistoryToSize]);
|
|
208
|
+
// Configuration setters
|
|
209
|
+
const setMaxSize = useCallback((size) => {
|
|
210
|
+
maxSizeRef.current = size;
|
|
211
|
+
if (size !== 'unlimited') {
|
|
212
|
+
enforceMaxSize();
|
|
213
|
+
}
|
|
214
|
+
}, [enforceMaxSize]);
|
|
215
|
+
const setBatchMode = useCallback((enabled) => {
|
|
216
|
+
const wasInBatch = batchModeRef.current;
|
|
217
|
+
if (enabled && !wasInBatch) {
|
|
218
|
+
// Starting batch mode - save current state as batch start
|
|
219
|
+
batchModeRef.current = true;
|
|
220
|
+
batchStartIndexRef.current = currentIndex;
|
|
221
|
+
batchStartStateRef.current = currentState;
|
|
222
|
+
}
|
|
223
|
+
else if (!enabled && wasInBatch) {
|
|
224
|
+
// Ending batch mode - commit final state to history
|
|
225
|
+
batchModeRef.current = false;
|
|
226
|
+
// Only add to history if state actually changed from batch start
|
|
227
|
+
if (batchStartStateRef.current &&
|
|
228
|
+
!compareAdjustmentStates(currentState, batchStartStateRef.current)) {
|
|
229
|
+
setHistory(prevHistory => {
|
|
230
|
+
const truncatedHistory = prevHistory.slice(0, batchStartIndexRef.current + 1);
|
|
231
|
+
const newHistory = [...truncatedHistory, currentState];
|
|
232
|
+
setCurrentIndex(newHistory.length - 1);
|
|
233
|
+
return newHistory;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
batchStartIndexRef.current = null;
|
|
237
|
+
batchStartStateRef.current = null;
|
|
238
|
+
}
|
|
239
|
+
}, [currentIndex, currentState]);
|
|
240
|
+
// History info object
|
|
241
|
+
const historyInfo = useMemo(() => ({
|
|
242
|
+
canUndo: currentIndex > 0,
|
|
243
|
+
canRedo: currentIndex < history.length - 1,
|
|
244
|
+
currentIndex,
|
|
245
|
+
totalStates: history.length,
|
|
246
|
+
historySize: getMemoryUsage(),
|
|
247
|
+
isBatchMode: batchModeRef.current
|
|
248
|
+
}), [currentIndex, history.length, getMemoryUsage]);
|
|
249
|
+
// Actions object - stabilized with useMemo
|
|
250
|
+
const actions = useMemo(() => ({
|
|
251
|
+
pushState,
|
|
252
|
+
undo,
|
|
253
|
+
redo,
|
|
254
|
+
reset,
|
|
255
|
+
jumpToIndex,
|
|
256
|
+
clearHistory,
|
|
257
|
+
getHistory,
|
|
258
|
+
trimHistory
|
|
259
|
+
}), [pushState, undo, redo, reset, jumpToIndex, clearHistory, getHistory, trimHistory]);
|
|
260
|
+
// Config object - stabilized with useMemo
|
|
261
|
+
const config = useMemo(() => ({
|
|
262
|
+
setMaxSize,
|
|
263
|
+
setBatchMode,
|
|
264
|
+
getMemoryUsage
|
|
265
|
+
}), [setMaxSize, setBatchMode, getMemoryUsage]);
|
|
266
|
+
// Apply max size enforcement when history changes
|
|
267
|
+
useEffect(() => {
|
|
268
|
+
enforceMaxSize();
|
|
269
|
+
checkPerformance();
|
|
270
|
+
}, [enforceMaxSize, checkPerformance]);
|
|
271
|
+
return {
|
|
272
|
+
currentState,
|
|
273
|
+
historyInfo,
|
|
274
|
+
actions,
|
|
275
|
+
config
|
|
276
|
+
};
|
|
277
|
+
}
|