@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,36 @@
|
|
|
1
|
+
import { Controller } from "./editor/useHonchoEditor";
|
|
2
|
+
import { Gallery } from "./editor/type";
|
|
3
|
+
/**
|
|
4
|
+
* Return type for the useGallerySwipe hook
|
|
5
|
+
* Provides image navigation functionality with pagination support
|
|
6
|
+
*/
|
|
7
|
+
interface UseGallerySwipeReturn {
|
|
8
|
+
/** Current image data object containing all image information */
|
|
9
|
+
currentImageData: Gallery | null;
|
|
10
|
+
/** Whether next image navigation is available (considers pagination) */
|
|
11
|
+
isNextAvailable: boolean;
|
|
12
|
+
/** Whether previous image navigation is available */
|
|
13
|
+
isPrevAvailable: boolean;
|
|
14
|
+
/** Function to navigate to the next image (async due to potential API calls) */
|
|
15
|
+
onSwipeNext: () => Promise<void>;
|
|
16
|
+
/** Function to navigate to the previous image (async due to potential API calls) */
|
|
17
|
+
onSwipePrev: () => Promise<void>;
|
|
18
|
+
/** Loading state during image transitions and API calls */
|
|
19
|
+
isLoading: boolean;
|
|
20
|
+
/** Error message if any operation fails */
|
|
21
|
+
error: string | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Custom hook for handling image gallery navigation with automatic pagination
|
|
25
|
+
*
|
|
26
|
+
* This hook manages image swipe/navigation functionality across a paginated gallery.
|
|
27
|
+
* It handles the complexity of finding images across multiple pages and loading
|
|
28
|
+
* additional pages as needed during navigation.
|
|
29
|
+
*
|
|
30
|
+
* @param firebaseUid - User's Firebase UID (can be null during initialization)
|
|
31
|
+
* @param initImageId - Initial image ID to start with (can be null during initialization)
|
|
32
|
+
* @param controller - Controller instance for API calls (can be null during initialization)
|
|
33
|
+
* @returns Object containing current image data and navigation functions
|
|
34
|
+
*/
|
|
35
|
+
export declare function useGallerySwipe(firebaseUid: string | null, initImageId: string | null, controller: Controller | null): UseGallerySwipeReturn;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the useGallerySwipe hook
|
|
3
|
+
* This shows various integration patterns and use cases
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Basic example: Simple image gallery with navigation buttons
|
|
7
|
+
*/
|
|
8
|
+
export declare function BasicGalleryExample(): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
/**
|
|
10
|
+
* Advanced example: Gallery with keyboard navigation and swipe gestures
|
|
11
|
+
*/
|
|
12
|
+
export declare function AdvancedGalleryExample(): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
/**
|
|
14
|
+
* Integration example: Using with existing useHonchoEditor
|
|
15
|
+
*/
|
|
16
|
+
export declare function EditorIntegrationExample(): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
/**
|
|
18
|
+
* Mobile-optimized example with touch gestures
|
|
19
|
+
*/
|
|
20
|
+
export declare function MobileGalleryExample(): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
/**
|
|
22
|
+
* Error handling and loading states example
|
|
23
|
+
*/
|
|
24
|
+
export declare function ErrorHandlingExample(): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useGallerySwipe } from './useGallerySwipe';
|
|
3
|
+
import { useState, useEffect } from 'react';
|
|
4
|
+
/**
|
|
5
|
+
* Example usage of the useGallerySwipe hook
|
|
6
|
+
* This shows various integration patterns and use cases
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Basic example: Simple image gallery with navigation buttons
|
|
10
|
+
*/
|
|
11
|
+
export function BasicGalleryExample() {
|
|
12
|
+
const [firebaseUid, setFirebaseUid] = useState('user123');
|
|
13
|
+
const [currentImageId, setCurrentImageId] = useState('image_001');
|
|
14
|
+
const [controller, setController] = useState(null);
|
|
15
|
+
// Initialize controller (replace with your actual controller initialization)
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
// Your controller initialization logic here
|
|
18
|
+
const initController = async () => {
|
|
19
|
+
// const controllerInstance = new YourController();
|
|
20
|
+
// setController(controllerInstance);
|
|
21
|
+
};
|
|
22
|
+
initController();
|
|
23
|
+
}, []);
|
|
24
|
+
const { currentImageData, isNextAvailable, isPrevAvailable, onSwipeNext, onSwipePrev, isLoading, error } = useGallerySwipe(firebaseUid, currentImageId, controller);
|
|
25
|
+
return (_jsxs("div", { className: "gallery-container", children: [error && (_jsxs("div", { className: "error-message", style: { color: 'red' }, children: ["Error: ", error] })), isLoading && (_jsx("div", { className: "loading-indicator", children: "Loading..." })), currentImageData && (_jsxs("div", { className: "image-display", children: [_jsx("img", { src: currentImageData.download?.path || currentImageData.raw_edited?.path, alt: `Image ${currentImageData.id}`, style: { maxWidth: '100%', height: 'auto' } }), _jsxs("p", { children: ["Image ID: ", currentImageData.id] }), _jsxs("p", { children: ["Event ID: ", currentImageData.event_id] })] })), _jsxs("div", { className: "navigation-controls", children: [_jsx("button", { onClick: onSwipePrev, disabled: !isPrevAvailable || isLoading, className: "nav-button prev", children: "\u2190 Previous" }), _jsx("button", { onClick: onSwipeNext, disabled: !isNextAvailable || isLoading, className: "nav-button next", children: "Next \u2192" })] })] }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Advanced example: Gallery with keyboard navigation and swipe gestures
|
|
29
|
+
*/
|
|
30
|
+
export function AdvancedGalleryExample() {
|
|
31
|
+
const [firebaseUid] = useState('user456');
|
|
32
|
+
const [selectedImageId, setSelectedImageId] = useState('initial_image_id');
|
|
33
|
+
const [controller] = useState(null); // Replace with actual controller
|
|
34
|
+
const gallerySwipe = useGallerySwipe(firebaseUid, selectedImageId, controller);
|
|
35
|
+
// Keyboard navigation
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const handleKeyPress = async (event) => {
|
|
38
|
+
if (gallerySwipe.isLoading)
|
|
39
|
+
return;
|
|
40
|
+
switch (event.key) {
|
|
41
|
+
case 'ArrowLeft':
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
if (gallerySwipe.isPrevAvailable) {
|
|
44
|
+
await gallerySwipe.onSwipePrev();
|
|
45
|
+
}
|
|
46
|
+
break;
|
|
47
|
+
case 'ArrowRight':
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
if (gallerySwipe.isNextAvailable) {
|
|
50
|
+
await gallerySwipe.onSwipeNext();
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener('keydown', handleKeyPress);
|
|
56
|
+
return () => window.removeEventListener('keydown', handleKeyPress);
|
|
57
|
+
}, [gallerySwipe]);
|
|
58
|
+
// Touch/swipe gesture handlers (basic implementation)
|
|
59
|
+
const [touchStart, setTouchStart] = useState(null);
|
|
60
|
+
const [touchEnd, setTouchEnd] = useState(null);
|
|
61
|
+
const handleTouchStart = (e) => {
|
|
62
|
+
setTouchEnd(null);
|
|
63
|
+
setTouchStart(e.targetTouches[0].clientX);
|
|
64
|
+
};
|
|
65
|
+
const handleTouchMove = (e) => {
|
|
66
|
+
setTouchEnd(e.targetTouches[0].clientX);
|
|
67
|
+
};
|
|
68
|
+
const handleTouchEnd = async () => {
|
|
69
|
+
if (!touchStart || !touchEnd)
|
|
70
|
+
return;
|
|
71
|
+
const distance = touchStart - touchEnd;
|
|
72
|
+
const isLeftSwipe = distance > 50;
|
|
73
|
+
const isRightSwipe = distance < -50;
|
|
74
|
+
if (isLeftSwipe && gallerySwipe.isNextAvailable) {
|
|
75
|
+
await gallerySwipe.onSwipeNext();
|
|
76
|
+
}
|
|
77
|
+
if (isRightSwipe && gallerySwipe.isPrevAvailable) {
|
|
78
|
+
await gallerySwipe.onSwipePrev();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
return (_jsxs("div", { className: "advanced-gallery", children: [_jsxs("div", { className: "gallery-header", children: [_jsx("h2", { children: "Advanced Gallery Navigation" }), _jsxs("div", { className: "status-indicators", children: [_jsx("span", { className: `status ${gallerySwipe.isLoading ? 'loading' : 'ready'}`, children: gallerySwipe.isLoading ? 'Loading...' : 'Ready' }), _jsxs("span", { className: "navigation-status", children: ["Prev: ", gallerySwipe.isPrevAvailable ? '✓' : '✗', " | Next: ", gallerySwipe.isNextAvailable ? '✓' : '✗'] })] })] }), _jsxs("div", { className: "image-container", onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, style: {
|
|
82
|
+
touchAction: 'pan-x',
|
|
83
|
+
userSelect: 'none',
|
|
84
|
+
position: 'relative'
|
|
85
|
+
}, children: [gallerySwipe.currentImageData ? (_jsxs("div", { className: "image-wrapper", children: [_jsx("img", { src: gallerySwipe.currentImageData.raw_edited?.path ||
|
|
86
|
+
gallerySwipe.currentImageData.download?.path, alt: `Gallery image ${gallerySwipe.currentImageData.id}`, style: {
|
|
87
|
+
width: '100%',
|
|
88
|
+
height: 'auto',
|
|
89
|
+
display: 'block'
|
|
90
|
+
} }), _jsxs("div", { className: "image-info", children: [_jsxs("p", { children: ["ID: ", gallerySwipe.currentImageData.id] }), _jsxs("p", { children: ["Event: ", gallerySwipe.currentImageData.event_id] })] })] })) : (_jsx("div", { className: "no-image", children: "No image loaded" })), gallerySwipe.error && (_jsx("div", { className: "error-overlay", children: gallerySwipe.error }))] }), _jsx("div", { className: "instructions", children: _jsx("p", { children: "Use arrow keys, navigation buttons, or swipe to navigate" }) })] }));
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Integration example: Using with existing useHonchoEditor
|
|
94
|
+
*/
|
|
95
|
+
export function EditorIntegrationExample() {
|
|
96
|
+
const [firebaseUid] = useState('editor_user');
|
|
97
|
+
const [currentImageId, setCurrentImageId] = useState('editor_image_001');
|
|
98
|
+
const [controller] = useState(null);
|
|
99
|
+
// Gallery navigation hook
|
|
100
|
+
const { currentImageData, isNextAvailable, isPrevAvailable, onSwipeNext, onSwipePrev, isLoading: galleryLoading, error: galleryError } = useGallerySwipe(firebaseUid, currentImageId, controller);
|
|
101
|
+
// Update current image ID when gallery navigation changes
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
if (currentImageData?.id && currentImageData.id !== currentImageId) {
|
|
104
|
+
setCurrentImageId(currentImageData.id);
|
|
105
|
+
// Trigger any editor updates here
|
|
106
|
+
// e.g., loadImageFromId(firebaseUid, currentImageData.id);
|
|
107
|
+
}
|
|
108
|
+
}, [currentImageData, currentImageId, firebaseUid]);
|
|
109
|
+
// Example: Integrate with your existing editor handlers
|
|
110
|
+
const handleImageChange = async (direction) => {
|
|
111
|
+
try {
|
|
112
|
+
if (direction === 'next' && isNextAvailable) {
|
|
113
|
+
await onSwipeNext();
|
|
114
|
+
}
|
|
115
|
+
else if (direction === 'prev' && isPrevAvailable) {
|
|
116
|
+
await onSwipePrev();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
console.error('Navigation error:', error);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
return (_jsxs("div", { className: "editor-integration", children: [_jsxs("div", { className: "editor-header", children: [_jsx("button", { onClick: () => handleImageChange('prev'), disabled: !isPrevAvailable || galleryLoading, className: "nav-btn", children: "\u27E8 Prev Image" }), _jsx("span", { className: "current-image-info", children: currentImageData ? `Image: ${currentImageData.id}` : 'No image' }), _jsx("button", { onClick: () => handleImageChange('next'), disabled: !isNextAvailable || galleryLoading, className: "nav-btn", children: "Next Image \u27E9" })] }), galleryError && (_jsxs("div", { className: "error-banner", children: ["Gallery Error: ", galleryError] })), _jsx("div", { className: "editor-content", children: currentImageData && (_jsxs("div", { className: "editor-canvas", children: [_jsxs("p", { children: ["Editing: ", currentImageData.id] }), _jsxs("p", { children: ["Event: ", currentImageData.event_id] })] })) }), galleryLoading && (_jsx("div", { className: "loading-overlay", children: "Loading next image..." }))] }));
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Mobile-optimized example with touch gestures
|
|
127
|
+
*/
|
|
128
|
+
export function MobileGalleryExample() {
|
|
129
|
+
const [firebaseUid] = useState('mobile_user');
|
|
130
|
+
const [imageId] = useState('mobile_image_001');
|
|
131
|
+
const [controller] = useState(null);
|
|
132
|
+
const gallery = useGallerySwipe(firebaseUid, imageId, controller);
|
|
133
|
+
// Mobile-specific gesture handling
|
|
134
|
+
const [swipeThreshold] = useState(75); // Minimum distance for swipe
|
|
135
|
+
const [touchStartX, setTouchStartX] = useState(0);
|
|
136
|
+
const [touchStartY, setTouchStartY] = useState(0);
|
|
137
|
+
const handleTouchStart = (e) => {
|
|
138
|
+
setTouchStartX(e.touches[0].clientX);
|
|
139
|
+
setTouchStartY(e.touches[0].clientY);
|
|
140
|
+
};
|
|
141
|
+
const handleTouchEnd = async (e) => {
|
|
142
|
+
const touchEndX = e.changedTouches[0].clientX;
|
|
143
|
+
const touchEndY = e.changedTouches[0].clientY;
|
|
144
|
+
const deltaX = touchStartX - touchEndX;
|
|
145
|
+
const deltaY = Math.abs(touchStartY - touchEndY);
|
|
146
|
+
// Only process horizontal swipes (ignore vertical)
|
|
147
|
+
if (deltaY < 100 && Math.abs(deltaX) > swipeThreshold) {
|
|
148
|
+
if (deltaX > 0 && gallery.isNextAvailable) {
|
|
149
|
+
// Swipe left - next image
|
|
150
|
+
await gallery.onSwipeNext();
|
|
151
|
+
}
|
|
152
|
+
else if (deltaX < 0 && gallery.isPrevAvailable) {
|
|
153
|
+
// Swipe right - previous image
|
|
154
|
+
await gallery.onSwipePrev();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
return (_jsxs("div", { className: "mobile-gallery", style: { touchAction: 'pan-y' }, children: [_jsx("div", { className: "mobile-header", children: _jsxs("div", { className: "navigation-dots", children: [_jsx("span", { className: `dot ${gallery.isPrevAvailable ? 'active' : 'inactive'}`, children: "\u25CF" }), _jsx("span", { className: "current-dot", children: "\u25CF" }), _jsx("span", { className: `dot ${gallery.isNextAvailable ? 'active' : 'inactive'}`, children: "\u25CF" })] }) }), _jsxs("div", { className: "swipe-area", onTouchStart: handleTouchStart, onTouchEnd: handleTouchEnd, style: {
|
|
159
|
+
width: '100%',
|
|
160
|
+
height: '70vh',
|
|
161
|
+
position: 'relative',
|
|
162
|
+
overflow: 'hidden'
|
|
163
|
+
}, children: [gallery.currentImageData && (_jsx("img", { src: gallery.currentImageData.download?.path, alt: "Gallery image", style: {
|
|
164
|
+
width: '100%',
|
|
165
|
+
height: '100%',
|
|
166
|
+
objectFit: 'contain',
|
|
167
|
+
userSelect: 'none',
|
|
168
|
+
pointerEvents: 'none'
|
|
169
|
+
} })), gallery.isLoading && (_jsxs("div", { className: "mobile-loading", children: [_jsx("div", { className: "spinner", children: "\u25D0" }), _jsx("p", { children: "Loading..." })] })), gallery.error && (_jsx("div", { className: "mobile-error", children: _jsxs("p", { children: ["\u26A0\uFE0F ", gallery.error] }) }))] }), _jsx("div", { className: "mobile-footer", children: _jsxs("p", { className: "swipe-hint", children: ["Swipe left/right to navigate \u2022 ", gallery.currentImageData?.id || 'No image'] }) })] }));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Error handling and loading states example
|
|
173
|
+
*/
|
|
174
|
+
export function ErrorHandlingExample() {
|
|
175
|
+
const [firebaseUid] = useState('test_user');
|
|
176
|
+
const [imageId] = useState('test_image');
|
|
177
|
+
const [controller] = useState(null);
|
|
178
|
+
const { currentImageData, isNextAvailable, isPrevAvailable, onSwipeNext, onSwipePrev, isLoading, error } = useGallerySwipe(firebaseUid, imageId, controller);
|
|
179
|
+
const handleRetry = () => {
|
|
180
|
+
// Force re-initialization by changing a key prop or calling a refresh function
|
|
181
|
+
window.location.reload(); // Simple retry approach
|
|
182
|
+
};
|
|
183
|
+
return (_jsxs("div", { className: "error-handling-example", children: [_jsxs("div", { className: "status-panel", children: [_jsx("h3", { children: "Gallery Status" }), _jsxs("div", { className: "status-grid", children: [_jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Loading:" }), _jsx("span", { className: isLoading ? 'status-active' : 'status-inactive', children: isLoading ? 'Yes' : 'No' })] }), _jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Error:" }), _jsx("span", { className: error ? 'status-error' : 'status-ok', children: error || 'None' })] }), _jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Current Image:" }), _jsx("span", { children: currentImageData?.id || 'None' })] }), _jsxs("div", { className: "status-item", children: [_jsx("label", { children: "Navigation:" }), _jsxs("span", { children: ["Prev: ", isPrevAvailable ? '✓' : '✗', " | Next: ", isNextAvailable ? '✓' : '✗'] })] })] }), error && (_jsx("div", { className: "error-actions", children: _jsx("button", { onClick: handleRetry, className: "retry-button", children: "Retry" }) }))] }), _jsx("div", { className: "gallery-content", children: currentImageData ? (_jsx("div", { className: "image-preview", children: _jsx("img", { src: currentImageData.download?.path, alt: "Current", style: { maxWidth: '300px', maxHeight: '200px' } }) })) : (_jsx("div", { className: "no-content", children: isLoading ? 'Loading gallery...' : 'No image available' })) }), _jsxs("div", { className: "navigation-test", children: [_jsx("button", { onClick: onSwipePrev, disabled: !isPrevAvailable || isLoading, children: "Test Previous" }), _jsx("button", { onClick: onSwipeNext, disabled: !isNextAvailable || isLoading, children: "Test Next" })] })] }));
|
|
184
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* Custom hook for handling image gallery navigation with automatic pagination
|
|
4
|
+
*
|
|
5
|
+
* This hook manages image swipe/navigation functionality across a paginated gallery.
|
|
6
|
+
* It handles the complexity of finding images across multiple pages and loading
|
|
7
|
+
* additional pages as needed during navigation.
|
|
8
|
+
*
|
|
9
|
+
* @param firebaseUid - User's Firebase UID (can be null during initialization)
|
|
10
|
+
* @param initImageId - Initial image ID to start with (can be null during initialization)
|
|
11
|
+
* @param controller - Controller instance for API calls (can be null during initialization)
|
|
12
|
+
* @returns Object containing current image data and navigation functions
|
|
13
|
+
*/
|
|
14
|
+
export function useGallerySwipe(firebaseUid, initImageId, controller) {
|
|
15
|
+
// Core state for current image and navigation
|
|
16
|
+
const [currentImageId, setCurrentImageId] = useState(initImageId);
|
|
17
|
+
const [currentImageData, setCurrentImageData] = useState(null);
|
|
18
|
+
const [currentEventId, setCurrentEventId] = useState(null);
|
|
19
|
+
// Pagination and image list state
|
|
20
|
+
const [currentImageList, setCurrentImageList] = useState([]);
|
|
21
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
22
|
+
const [hasNextPage, setHasNextPage] = useState(false);
|
|
23
|
+
// UI state management
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
+
const [error, setError] = useState(null);
|
|
26
|
+
const [isInitialized, setIsInitialized] = useState(false);
|
|
27
|
+
// Refs to track parameter changes and avoid unnecessary re-initialization
|
|
28
|
+
// This prevents the hook from re-initializing when the same values are passed
|
|
29
|
+
const prevFirebaseUid = useRef(null);
|
|
30
|
+
const prevInitImageId = useRef(null);
|
|
31
|
+
/**
|
|
32
|
+
* Get the index of the current image in the loaded image list
|
|
33
|
+
* Returns -1 if the current image is not found in the list
|
|
34
|
+
*/
|
|
35
|
+
const getCurrentImageIndex = useCallback(() => {
|
|
36
|
+
if (!currentImageId || currentImageList.length === 0)
|
|
37
|
+
return -1;
|
|
38
|
+
return currentImageList.findIndex(img => img.id === currentImageId);
|
|
39
|
+
}, [currentImageId, currentImageList]);
|
|
40
|
+
/**
|
|
41
|
+
* Fetch image pages sequentially until the target image is found
|
|
42
|
+
* This is necessary because we don't know which page contains the initial image
|
|
43
|
+
*
|
|
44
|
+
* @param imageId - The target image ID to find
|
|
45
|
+
* @param eventId - The event ID to search within
|
|
46
|
+
* @returns Array of all gallery images up to and including the page containing the target image
|
|
47
|
+
*/
|
|
48
|
+
const getImageListUntilFound = useCallback(async (imageId, eventId) => {
|
|
49
|
+
let page = 1;
|
|
50
|
+
let allImages = [];
|
|
51
|
+
let isFound = false;
|
|
52
|
+
// Search through pages until we find the target image or reach safety limit
|
|
53
|
+
while (!isFound && page <= 100) { // Safety limit to prevent infinite loop
|
|
54
|
+
try {
|
|
55
|
+
const response = await controller.getImageList(firebaseUid, eventId, page);
|
|
56
|
+
if (response.gallery && response.gallery.length > 0) {
|
|
57
|
+
// Accumulate all images from previous pages
|
|
58
|
+
allImages = [...allImages, ...response.gallery];
|
|
59
|
+
// Check if target image is found in current page
|
|
60
|
+
isFound = response.gallery.some(img => img.id === imageId);
|
|
61
|
+
if (isFound) {
|
|
62
|
+
// Update pagination state when target is found
|
|
63
|
+
setCurrentPage(page);
|
|
64
|
+
setHasNextPage(response.next_page !== 0 && response.next_page > response.current_page);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
// If no next page exists, stop searching
|
|
68
|
+
if (response.next_page === 0 || response.next_page <= response.current_page) {
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
page++;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
// Empty response, stop searching
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
console.error(`Error fetching page ${page}:`, err);
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return allImages;
|
|
84
|
+
}, [controller, firebaseUid]);
|
|
85
|
+
/**
|
|
86
|
+
* Load the next page of images when user reaches the end of current list
|
|
87
|
+
* This enables seamless navigation across page boundaries
|
|
88
|
+
*
|
|
89
|
+
* @returns Array of new images from the next page, or empty array if no more pages
|
|
90
|
+
*/
|
|
91
|
+
const loadNextPage = useCallback(async () => {
|
|
92
|
+
// Check prerequisites before attempting to load next page
|
|
93
|
+
if (!hasNextPage || !currentEventId || !controller || !firebaseUid) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const nextPageNum = currentPage + 1;
|
|
98
|
+
const response = await controller.getImageList(firebaseUid, currentEventId, nextPageNum);
|
|
99
|
+
if (response.gallery && response.gallery.length > 0) {
|
|
100
|
+
// Update pagination state with new page information
|
|
101
|
+
setCurrentPage(nextPageNum);
|
|
102
|
+
setHasNextPage(response.next_page !== 0 && response.next_page > response.current_page);
|
|
103
|
+
return response.gallery;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
console.error('Error loading next page:', err);
|
|
108
|
+
}
|
|
109
|
+
return [];
|
|
110
|
+
}, [hasNextPage, currentEventId, controller, firebaseUid, currentPage]);
|
|
111
|
+
/**
|
|
112
|
+
* Initialize or re-initialize the gallery when parameters change
|
|
113
|
+
* This function:
|
|
114
|
+
* 1. Fetches initial image data
|
|
115
|
+
* 2. Discovers the event ID from the image
|
|
116
|
+
* 3. Loads all image pages until the initial image is found
|
|
117
|
+
* 4. Sets up pagination state
|
|
118
|
+
*
|
|
119
|
+
* Only re-initializes when parameters actually change to avoid unnecessary API calls
|
|
120
|
+
*/
|
|
121
|
+
const initializeGallery = useCallback(async () => {
|
|
122
|
+
// Early return if required parameters are missing
|
|
123
|
+
if (!firebaseUid || !initImageId || !controller) {
|
|
124
|
+
setCurrentImageData(null);
|
|
125
|
+
setCurrentImageList([]);
|
|
126
|
+
setIsInitialized(false);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Check if re-initialization is needed by comparing with previous values
|
|
130
|
+
const needsReinit = prevFirebaseUid.current !== firebaseUid ||
|
|
131
|
+
prevInitImageId.current !== initImageId ||
|
|
132
|
+
!isInitialized;
|
|
133
|
+
if (!needsReinit)
|
|
134
|
+
return;
|
|
135
|
+
setIsLoading(true);
|
|
136
|
+
setError(null);
|
|
137
|
+
try {
|
|
138
|
+
// Step 1: Get initial image data to discover event ID
|
|
139
|
+
const gallery = await controller.onGetImage(firebaseUid, initImageId);
|
|
140
|
+
if (!gallery) {
|
|
141
|
+
throw new Error('Failed to fetch initial image data');
|
|
142
|
+
}
|
|
143
|
+
// Step 2: Set up initial state with discovered data
|
|
144
|
+
setCurrentImageData(gallery);
|
|
145
|
+
setCurrentImageId(initImageId);
|
|
146
|
+
setCurrentEventId(gallery.event_id);
|
|
147
|
+
// Step 3: Get complete image list by searching through pages
|
|
148
|
+
// This ensures we have navigation context for the current image
|
|
149
|
+
const allImages = await getImageListUntilFound(initImageId, gallery.event_id);
|
|
150
|
+
setCurrentImageList(allImages);
|
|
151
|
+
// Step 4: Update tracking refs to prevent unnecessary re-initialization
|
|
152
|
+
prevFirebaseUid.current = firebaseUid;
|
|
153
|
+
prevInitImageId.current = initImageId;
|
|
154
|
+
setIsInitialized(true);
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred';
|
|
158
|
+
setError(errorMessage);
|
|
159
|
+
console.error('Error initializing gallery:', err);
|
|
160
|
+
}
|
|
161
|
+
finally {
|
|
162
|
+
setIsLoading(false);
|
|
163
|
+
}
|
|
164
|
+
}, [firebaseUid, initImageId, controller, getImageListUntilFound, isInitialized]);
|
|
165
|
+
/**
|
|
166
|
+
* Navigate to the next image in the gallery
|
|
167
|
+
* Handles two scenarios:
|
|
168
|
+
* 1. Next image exists in current loaded list - navigate directly
|
|
169
|
+
* 2. At end of current list - load next page and navigate to first image of new page
|
|
170
|
+
*
|
|
171
|
+
* @returns Promise that resolves when navigation is complete
|
|
172
|
+
*/
|
|
173
|
+
const onSwipeNext = useCallback(async () => {
|
|
174
|
+
// Prevent action if no current image or already loading
|
|
175
|
+
if (!currentImageId || isLoading)
|
|
176
|
+
return;
|
|
177
|
+
setIsLoading(true);
|
|
178
|
+
setError(null);
|
|
179
|
+
try {
|
|
180
|
+
const currentIndex = getCurrentImageIndex();
|
|
181
|
+
if (currentIndex === -1) {
|
|
182
|
+
throw new Error('Current image not found in list');
|
|
183
|
+
}
|
|
184
|
+
// Scenario 1: At the last image of current list
|
|
185
|
+
if (currentIndex === currentImageList.length - 1) {
|
|
186
|
+
// Try to load next page for more images
|
|
187
|
+
const newImages = await loadNextPage();
|
|
188
|
+
if (newImages.length > 0) {
|
|
189
|
+
// Extend current list with new images
|
|
190
|
+
const updatedList = [...currentImageList, ...newImages];
|
|
191
|
+
setCurrentImageList(updatedList);
|
|
192
|
+
// Navigate to first image of the new page
|
|
193
|
+
const nextImage = newImages[0];
|
|
194
|
+
setCurrentImageId(nextImage.id);
|
|
195
|
+
// Fetch complete data for the new current image
|
|
196
|
+
const nextImageData = await controller.onGetImage(firebaseUid, nextImage.id);
|
|
197
|
+
if (nextImageData) {
|
|
198
|
+
setCurrentImageData(nextImageData);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
// No more pages available - end of gallery reached
|
|
203
|
+
setError('No more images available');
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// Scenario 2: Navigate to next image in current list
|
|
208
|
+
const nextImage = currentImageList[currentIndex + 1];
|
|
209
|
+
setCurrentImageId(nextImage.id);
|
|
210
|
+
// Fetch complete data for the next image
|
|
211
|
+
const nextImageData = await controller.onGetImage(firebaseUid, nextImage.id);
|
|
212
|
+
if (nextImageData) {
|
|
213
|
+
setCurrentImageData(nextImageData);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const errorMessage = err instanceof Error ? err.message : 'Error moving to next image';
|
|
219
|
+
setError(errorMessage);
|
|
220
|
+
console.error('Error in onSwipeNext:', err);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
setIsLoading(false);
|
|
224
|
+
}
|
|
225
|
+
}, [currentImageId, isLoading, getCurrentImageIndex, currentImageList, loadNextPage, controller, firebaseUid]);
|
|
226
|
+
/**
|
|
227
|
+
* Navigate to the previous image in the gallery
|
|
228
|
+
* Only works within the currently loaded image list
|
|
229
|
+
* (Previous pages are not loaded on-demand for backward navigation)
|
|
230
|
+
*
|
|
231
|
+
* @returns Promise that resolves when navigation is complete
|
|
232
|
+
*/
|
|
233
|
+
const onSwipePrev = useCallback(async () => {
|
|
234
|
+
// Prevent action if no current image or already loading
|
|
235
|
+
if (!currentImageId || isLoading)
|
|
236
|
+
return;
|
|
237
|
+
setIsLoading(true);
|
|
238
|
+
setError(null);
|
|
239
|
+
try {
|
|
240
|
+
const currentIndex = getCurrentImageIndex();
|
|
241
|
+
if (currentIndex === -1) {
|
|
242
|
+
throw new Error('Current image not found in list');
|
|
243
|
+
}
|
|
244
|
+
if (currentIndex > 0) {
|
|
245
|
+
// Navigate to previous image in the current list
|
|
246
|
+
const prevImage = currentImageList[currentIndex - 1];
|
|
247
|
+
setCurrentImageId(prevImage.id);
|
|
248
|
+
// Fetch complete data for the previous image
|
|
249
|
+
const prevImageData = await controller.onGetImage(firebaseUid, prevImage.id);
|
|
250
|
+
if (prevImageData) {
|
|
251
|
+
setCurrentImageData(prevImageData);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
// Already at the first image of loaded list
|
|
256
|
+
setError('Already at the first image');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
const errorMessage = err instanceof Error ? err.message : 'Error moving to previous image';
|
|
261
|
+
setError(errorMessage);
|
|
262
|
+
console.error('Error in onSwipePrev:', err);
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
setIsLoading(false);
|
|
266
|
+
}
|
|
267
|
+
}, [currentImageId, isLoading, getCurrentImageIndex, currentImageList, controller, firebaseUid]);
|
|
268
|
+
/**
|
|
269
|
+
* Calculate if next image navigation is available
|
|
270
|
+
* Returns true if:
|
|
271
|
+
* 1. There's a next image in the current loaded list, OR
|
|
272
|
+
* 2. We're at the end of current list but more pages are available
|
|
273
|
+
*
|
|
274
|
+
* @returns Boolean indicating if next navigation is possible
|
|
275
|
+
*/
|
|
276
|
+
const isNextAvailable = useCallback(() => {
|
|
277
|
+
if (isLoading)
|
|
278
|
+
return false;
|
|
279
|
+
const currentIndex = getCurrentImageIndex();
|
|
280
|
+
if (currentIndex === -1)
|
|
281
|
+
return false;
|
|
282
|
+
// If we're not at the last image, next is definitely available
|
|
283
|
+
if (currentIndex < currentImageList.length - 1)
|
|
284
|
+
return true;
|
|
285
|
+
// If we're at the last image but there are more pages, next is still available
|
|
286
|
+
return hasNextPage;
|
|
287
|
+
}, [isLoading, getCurrentImageIndex, currentImageList.length, hasNextPage]);
|
|
288
|
+
/**
|
|
289
|
+
* Calculate if previous image navigation is available
|
|
290
|
+
* Returns true if there's a previous image in the currently loaded list
|
|
291
|
+
*
|
|
292
|
+
* @returns Boolean indicating if previous navigation is possible
|
|
293
|
+
*/
|
|
294
|
+
const isPrevAvailable = useCallback(() => {
|
|
295
|
+
if (isLoading)
|
|
296
|
+
return false;
|
|
297
|
+
const currentIndex = getCurrentImageIndex();
|
|
298
|
+
return currentIndex > 0;
|
|
299
|
+
}, [isLoading, getCurrentImageIndex]);
|
|
300
|
+
// Initialize when dependencies change
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
initializeGallery();
|
|
303
|
+
}, [initializeGallery]);
|
|
304
|
+
// Update currentImageId when initImageId changes (but only if different)
|
|
305
|
+
// This allows the hook to respond to external changes to the initial image ID
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
if (initImageId && initImageId !== currentImageId) {
|
|
308
|
+
setCurrentImageId(initImageId);
|
|
309
|
+
}
|
|
310
|
+
}, [initImageId, currentImageId]);
|
|
311
|
+
// Return all the functionality and state that components need
|
|
312
|
+
return {
|
|
313
|
+
currentImageData,
|
|
314
|
+
isNextAvailable: isNextAvailable(),
|
|
315
|
+
isPrevAvailable: isPrevAvailable(),
|
|
316
|
+
onSwipeNext,
|
|
317
|
+
onSwipePrev,
|
|
318
|
+
isLoading,
|
|
319
|
+
error
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { ColorAdjustment } from "../hooks/editor/type";
|
|
2
|
+
import { AdjustmentState } from "../hooks/editor/useHonchoEditor";
|
|
3
|
+
import { AdjustmentValues } from "../lib/editor/honcho-editor";
|
|
4
|
+
export declare function mapAdjustmentStateToAdjustmentEditor(state: AdjustmentState): AdjustmentValues;
|
|
5
|
+
export declare function mapColorAdjustmentToAdjustmentState(colors: ColorAdjustment): AdjustmentState;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function mapAdjustmentStateToAdjustmentEditor(state) {
|
|
2
|
+
return {
|
|
3
|
+
temperature: state.tempScore,
|
|
4
|
+
tint: state.tintScore,
|
|
5
|
+
saturation: state.saturationScore,
|
|
6
|
+
vibrance: state.vibranceScore,
|
|
7
|
+
exposure: state.exposureScore,
|
|
8
|
+
contrast: state.contrastScore,
|
|
9
|
+
highlights: state.highlightsScore,
|
|
10
|
+
shadows: state.shadowsScore,
|
|
11
|
+
whites: state.whitesScore,
|
|
12
|
+
blacks: state.blacksScore,
|
|
13
|
+
clarity: state.clarityScore,
|
|
14
|
+
sharpness: state.sharpnessScore,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export function mapColorAdjustmentToAdjustmentState(colors) {
|
|
18
|
+
return {
|
|
19
|
+
tempScore: colors.temperature,
|
|
20
|
+
tintScore: colors.tint,
|
|
21
|
+
vibranceScore: colors.vibrance,
|
|
22
|
+
saturationScore: colors.saturation,
|
|
23
|
+
exposureScore: colors.exposure,
|
|
24
|
+
highlightsScore: colors.highlights,
|
|
25
|
+
shadowsScore: colors.shadows,
|
|
26
|
+
whitesScore: colors.whites,
|
|
27
|
+
blacksScore: colors.blacks,
|
|
28
|
+
contrastScore: colors.contrast,
|
|
29
|
+
clarityScore: colors.clarity,
|
|
30
|
+
sharpnessScore: colors.sharpness
|
|
31
|
+
};
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yogiswara/honcho-editor-ui",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.9",
|
|
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",
|
|
@@ -10,7 +10,10 @@
|
|
|
10
10
|
"public"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "tsc"
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "jest",
|
|
15
|
+
"test:watch": "jest --watch",
|
|
16
|
+
"test:coverage": "jest --coverage"
|
|
14
17
|
},
|
|
15
18
|
"peerDependencies": {
|
|
16
19
|
"@emotion/react": "^11.11.1",
|
|
@@ -26,12 +29,18 @@
|
|
|
26
29
|
"@emotion/styled": "^11.11.0",
|
|
27
30
|
"@mui/icons-material": "^6.1.2",
|
|
28
31
|
"@mui/material": "^6.1.2",
|
|
32
|
+
"@testing-library/jest-dom": "^6.6.4",
|
|
33
|
+
"@testing-library/react": "^16.3.0",
|
|
34
|
+
"@types/jest": "^30.0.0",
|
|
29
35
|
"@types/node": "^20",
|
|
30
36
|
"@types/react": "^18",
|
|
31
37
|
"@types/react-dom": "^18",
|
|
38
|
+
"jest": "^30.0.5",
|
|
39
|
+
"jest-environment-jsdom": "^30.0.5",
|
|
32
40
|
"next": "^13.5.6 || ^14.0.0",
|
|
33
41
|
"react": "^18",
|
|
34
42
|
"react-dom": "^18",
|
|
43
|
+
"ts-jest": "^29.4.1",
|
|
35
44
|
"tsup": "^8.0.0",
|
|
36
45
|
"typescript": "^5"
|
|
37
46
|
},
|