@yogiswara/honcho-editor-ui 2.2.0 → 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/editor/GalleryAlbum/AlbumImageGallery.d.ts +5 -5
- package/dist/components/editor/GalleryAlbum/AlbumImageGallery.js +2 -3
- package/dist/hooks/editor/useHonchoEditorBulk.d.ts +5 -28
- package/dist/hooks/editor/useHonchoEditorBulk.js +38 -103
- package/dist/index.d.ts +4 -1
- package/dist/index.js +4 -1
- package/dist/lib/context/EditorContext.d.ts +18 -0
- package/dist/lib/context/EditorContext.js +58 -0
- package/dist/lib/context/EditorProcessingService.d.ts +34 -0
- package/dist/lib/context/EditorProcessingService.js +190 -0
- package/dist/lib/hooks/useEditor.d.ts +22 -0
- package/dist/lib/hooks/useEditor.js +35 -0
- package/dist/lib/hooks/useEditorHeadless.d.ts +18 -0
- package/dist/lib/hooks/useEditorHeadless.js +128 -0
- package/dist/lib/hooks/useImageProcessor.d.ts +15 -0
- package/dist/lib/hooks/useImageProcessor.js +57 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { AdjustmentValues } from "../../../lib/editor/honcho-editor";
|
|
3
3
|
import type { PhotoData as BulkPhotoData } from "../../../hooks/editor/useHonchoEditorBulk";
|
|
4
|
-
interface ExtendedPhotoData extends BulkPhotoData {
|
|
4
|
+
export interface ExtendedPhotoData extends BulkPhotoData {
|
|
5
5
|
adjustments?: Partial<AdjustmentValues>;
|
|
6
6
|
frame?: string;
|
|
7
7
|
}
|
|
@@ -10,9 +10,9 @@ interface ImageGalleryProps {
|
|
|
10
10
|
isSelectedMode: boolean;
|
|
11
11
|
isHiddenGallery: boolean;
|
|
12
12
|
enableEditor: boolean;
|
|
13
|
-
onPreview: (photo: ExtendedPhotoData) =>
|
|
13
|
+
onPreview: (photo: ExtendedPhotoData) => void;
|
|
14
14
|
onSelectedMode: () => void;
|
|
15
|
-
onToggleSelect: (photo: ExtendedPhotoData) =>
|
|
15
|
+
onToggleSelect: (photo: ExtendedPhotoData) => void;
|
|
16
16
|
}
|
|
17
|
-
declare const AlbumImageGallery: React.FC<ImageGalleryProps>;
|
|
18
|
-
export
|
|
17
|
+
export declare const AlbumImageGallery: React.FC<ImageGalleryProps>;
|
|
18
|
+
export {};
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { Box, Stack } from "@mui/material";
|
|
4
4
|
import Masonry, { ResponsiveMasonry } from "react-responsive-masonry";
|
|
5
5
|
import GalleryImageItem from "./ImageItem";
|
|
6
|
-
const AlbumImageGallery = (props) => {
|
|
6
|
+
export const AlbumImageGallery = (props) => {
|
|
7
7
|
const { imageCollection, isSelectedMode, isHiddenGallery, enableEditor, // Destructure the new prop
|
|
8
8
|
onPreview, onSelectedMode, onToggleSelect, } = props;
|
|
9
9
|
console.log("imageCollection: ", imageCollection);
|
|
@@ -27,7 +27,6 @@ const AlbumImageGallery = (props) => {
|
|
|
27
27
|
};
|
|
28
28
|
return (_jsx(Box, { sx: { m: 0.5 }, children: _jsx(GalleryImageItem, { margin: "0px", index: index,
|
|
29
29
|
// UPDATED: Pass the new, correctly-typed object.
|
|
30
|
-
photo: imageItemPhotoProps, direction: "column", isFullScreenMode: false, isSelected: photo.isSelected, isSelectedMode: isSelectedMode, isHiddenGallery: isHiddenGallery, onPreview: onPreview(photo), onSelectedMode: onSelectedMode, onToggleSelect: onToggleSelect(photo), enableEditor: enableEditor, adjustments: photo.adjustments, frame: photo.frame, data: photo }) }, photo.key));
|
|
30
|
+
photo: imageItemPhotoProps, direction: "column", isFullScreenMode: false, isSelected: photo.isSelected, isSelectedMode: isSelectedMode, isHiddenGallery: isHiddenGallery, onPreview: () => { onPreview(photo); }, onSelectedMode: onSelectedMode, onToggleSelect: () => { onToggleSelect(photo); }, enableEditor: enableEditor, adjustments: photo.adjustments, frame: photo.frame, data: photo }) }, photo.key));
|
|
31
31
|
}) }) }) }));
|
|
32
32
|
};
|
|
33
|
-
export default AlbumImageGallery;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SelectChangeEvent } from "@mui/material";
|
|
2
|
-
import { AdjustmentState,
|
|
2
|
+
import { AdjustmentState, Controller, Preset } from './useHonchoEditor';
|
|
3
3
|
import { Gallery, ResponseGalleryPaging } from '../../hooks/editor/type';
|
|
4
|
+
import { AdjustmentValues } from "../../lib/editor/honcho-editor";
|
|
4
5
|
export interface PhotoData {
|
|
5
6
|
key: string;
|
|
6
7
|
src: string;
|
|
@@ -9,7 +10,7 @@ export interface PhotoData {
|
|
|
9
10
|
height: number;
|
|
10
11
|
alt: string;
|
|
11
12
|
isSelected: boolean;
|
|
12
|
-
|
|
13
|
+
adjustments?: Partial<AdjustmentValues>;
|
|
13
14
|
}
|
|
14
15
|
export interface ControllerBulk {
|
|
15
16
|
onGetImage(firebaseUid: string, imageID: string): Promise<Gallery>;
|
|
@@ -20,39 +21,15 @@ export interface ControllerBulk {
|
|
|
20
21
|
createPreset(firebaseUid: string, name: string, settings: AdjustmentState): Promise<Preset>;
|
|
21
22
|
deletePreset(firebaseUid: string, presetId: string): Promise<void>;
|
|
22
23
|
}
|
|
23
|
-
export declare function useHonchoEditorBulk(
|
|
24
|
+
export declare function useHonchoEditorBulk(controller: Controller, eventID: string, firebaseUid: string): {
|
|
24
25
|
imageCollection: PhotoData[];
|
|
25
|
-
isSelectedMode: boolean;
|
|
26
26
|
isLoading: boolean;
|
|
27
27
|
error: string | null;
|
|
28
|
-
selectedImageIds: string[];
|
|
29
|
-
handleSelectedMode: () => void;
|
|
30
|
-
handleToggleSelect: (photoToToggle: PhotoData) => () => void;
|
|
31
|
-
handlePreview: (photo: PhotoData) => () => void;
|
|
32
|
-
handleBackCallbackBulk: () => void;
|
|
33
|
-
isBulkEditing: boolean;
|
|
34
|
-
selectedImages: string;
|
|
35
|
-
imageList: ImageItem[];
|
|
36
|
-
currentBatch: import("../useAdjustmentHistoryBatch").BatchAdjustmentState;
|
|
37
28
|
selectedIds: string[];
|
|
38
|
-
|
|
39
|
-
adjustmentsMap: Map<string, AdjustmentState>;
|
|
29
|
+
handleBackCallbackBulk: () => void;
|
|
40
30
|
selectedBulkPreset: string;
|
|
41
31
|
handleToggleImageSelection: (imageId: string) => void;
|
|
42
|
-
toggleBulkEditing: () => void;
|
|
43
32
|
handleSelectBulkPreset: (event: SelectChangeEvent<string>) => void;
|
|
44
|
-
setTempScore: (value: number) => void;
|
|
45
|
-
setTintScore: (value: number) => void;
|
|
46
|
-
setVibranceScore: (value: number) => void;
|
|
47
|
-
setSaturationScore: (value: number) => void;
|
|
48
|
-
setExposureScore: (value: number) => void;
|
|
49
|
-
setHighlightsScore: (value: number) => void;
|
|
50
|
-
setShadowsScore: (value: number) => void;
|
|
51
|
-
setWhitesScore: (value: number) => void;
|
|
52
|
-
setBlacksScore: (value: number) => void;
|
|
53
|
-
setContrastScore: (value: number) => void;
|
|
54
|
-
setClarityScore: (value: number) => void;
|
|
55
|
-
setSharpnessScore: (value: number) => void;
|
|
56
33
|
handleBulkTempDecreaseMax: () => void;
|
|
57
34
|
handleBulkTempDecrease: () => void;
|
|
58
35
|
handleBulkTempIncrease: () => void;
|
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { useState, useCallback, useEffect
|
|
3
|
-
import { useAdjustmentHistory } from '../useAdjustmentHistory';
|
|
2
|
+
import { useState, useCallback, useEffect } from 'react';
|
|
4
3
|
import { useAdjustmentHistoryBatch } from '../useAdjustmentHistoryBatch';
|
|
5
4
|
// Helper function to map the API response to the format our UI component needs
|
|
6
5
|
const mapGalleryToPhotoData = (gallery) => {
|
|
7
|
-
// Use thumbnail as the primary source, with fallbacks for safety
|
|
8
|
-
const bestImage = gallery.thumbnail || gallery.download || { path: '', width: 1, height: 1, key: gallery.id, size: 0 };
|
|
9
6
|
return {
|
|
10
7
|
key: gallery.id,
|
|
11
|
-
src:
|
|
12
|
-
original: gallery.download?.path ||
|
|
13
|
-
width:
|
|
14
|
-
height:
|
|
8
|
+
src: gallery.raw_thumbnail?.path ? gallery.raw_thumbnail.path : gallery.thumbnail?.path,
|
|
9
|
+
original: gallery.download?.path || gallery.thumbnail?.path,
|
|
10
|
+
width: gallery.thumbnail?.width,
|
|
11
|
+
height: gallery.thumbnail?.height,
|
|
15
12
|
alt: gallery.id || 'gallery image',
|
|
16
13
|
isSelected: false, // Default to not selected
|
|
17
|
-
|
|
14
|
+
adjustments: gallery.editor_config?.color_adjustment,
|
|
18
15
|
};
|
|
19
16
|
};
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
const mapToImageAdjustmentConfig = (gallery) => {
|
|
18
|
+
return {
|
|
19
|
+
imageId: gallery.id,
|
|
20
|
+
adjustment: mapColorAdjustmentToAdjustmentState(gallery.editor_config?.color_adjustment),
|
|
21
|
+
};
|
|
23
22
|
};
|
|
24
|
-
const clamp = (value) => Math.max(-100, Math.min(100, value));
|
|
25
23
|
function mapColorAdjustmentToAdjustmentState(adj) {
|
|
24
|
+
if (!adj)
|
|
25
|
+
return undefined;
|
|
26
26
|
return {
|
|
27
27
|
tempScore: adj.temperature || 0,
|
|
28
28
|
tintScore: adj.tint || 0,
|
|
@@ -38,76 +38,22 @@ function mapColorAdjustmentToAdjustmentState(adj) {
|
|
|
38
38
|
sharpnessScore: adj.sharpness || 0,
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
|
-
export function useHonchoEditorBulk(
|
|
42
|
-
const {
|
|
43
|
-
const { currentBatch, selectedIds, allImageIds, actions: batchActions, historyInfo } = useAdjustmentHistoryBatch({});
|
|
41
|
+
export function useHonchoEditorBulk(controller, eventID, firebaseUid) {
|
|
42
|
+
const { currentBatch, selectedIds, actions: batchActions } = useAdjustmentHistoryBatch();
|
|
44
43
|
// State for Bulk Editing
|
|
45
44
|
const [imageCollection, setImageCollection] = useState([]);
|
|
46
|
-
const [isSelectedMode, setIsSelectedMode] = useState(false);
|
|
47
45
|
const [isLoading, setIsLoading] = useState(true);
|
|
48
46
|
const [error, setError] = useState(null);
|
|
49
|
-
const [isBulkEditing, setIsBulkEditing] = useState(false);
|
|
50
|
-
const [selectedImages, setSelectedImages] = useState('Select');
|
|
51
|
-
const [imageList, setImageList] = useState([]);
|
|
52
|
-
const [adjustmentsMap, setAdjustmentsMap] = useState(new Map());
|
|
53
47
|
const [selectedBulkPreset, setSelectedBulkPreset] = useState('preset1');
|
|
54
|
-
const [isEditorReady, setIsEditorReady] = useState(false);
|
|
55
|
-
const selectedImageIds = useMemo(() => imageCollection.filter(p => p.isSelected).map(p => p.key), [imageCollection]);
|
|
56
48
|
const handleBackCallbackBulk = useCallback(() => {
|
|
57
|
-
const lastSelectedId =
|
|
58
|
-
|
|
59
|
-
}, [
|
|
60
|
-
const handleSelectedMode = useCallback(() => setIsSelectedMode(true), []);
|
|
61
|
-
const handleToggleSelect = useCallback((photoToToggle) => () => {
|
|
62
|
-
setImageCollection(current => current.map(p => p.key === photoToToggle.key ? { ...p, isSelected: !p.isSelected } : p));
|
|
63
|
-
if (!isSelectedMode)
|
|
64
|
-
setIsSelectedMode(true);
|
|
65
|
-
}, [isSelectedMode]);
|
|
66
|
-
const handlePreview = useCallback((photo) => () => {
|
|
67
|
-
console.log("Previewing image:", photo.key);
|
|
68
|
-
}, []);
|
|
69
|
-
// const handleToggleImageSelection = useCallback((imageId: string) => {
|
|
70
|
-
// const newSelectedIds = new Set(selectedImageIds);
|
|
71
|
-
// if (newSelectedIds.has(imageId)) {
|
|
72
|
-
// if (newSelectedIds.size > 1) { // Prevent deselecting the last image
|
|
73
|
-
// newSelectedIds.delete(imageId);
|
|
74
|
-
// }
|
|
75
|
-
// } else {
|
|
76
|
-
// newSelectedIds.add(imageId);
|
|
77
|
-
// }
|
|
78
|
-
// setSelectedImageIds(newSelectedIds);
|
|
79
|
-
// }, [selectedImageIds]);
|
|
80
|
-
const toggleBulkEditing = () => {
|
|
81
|
-
setIsBulkEditing(prev => {
|
|
82
|
-
const isNowBulk = !prev;
|
|
83
|
-
setSelectedImages(isNowBulk ? 'Selected' : 'Select');
|
|
84
|
-
return isNowBulk;
|
|
85
|
-
});
|
|
86
|
-
};
|
|
49
|
+
const lastSelectedId = selectedIds.length > 0 ? selectedIds[selectedIds.length - 1] : eventID;
|
|
50
|
+
controller.handleBack(firebaseUid, lastSelectedId);
|
|
51
|
+
}, [controller, firebaseUid, selectedIds, eventID]);
|
|
87
52
|
const handleSelectBulkPreset = (event) => setSelectedBulkPreset(event.target.value);
|
|
88
53
|
// This factory creates functions that adjust a value for all selected images
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.log('Updated adjustments:', newState);
|
|
93
|
-
}, [currentState, historyActions]);
|
|
94
|
-
const createRelativeAdjuster = (key, amount) => () => {
|
|
95
|
-
const currentValue = currentState[key];
|
|
96
|
-
const newValue = clamp(currentValue + amount);
|
|
97
|
-
updateAdjustments({ [key]: newValue });
|
|
98
|
-
};
|
|
99
|
-
const setTempScore = (value) => updateAdjustments({ tempScore: value });
|
|
100
|
-
const setTintScore = (value) => updateAdjustments({ tintScore: value });
|
|
101
|
-
const setVibranceScore = (value) => updateAdjustments({ vibranceScore: value });
|
|
102
|
-
const setSaturationScore = (value) => updateAdjustments({ saturationScore: value });
|
|
103
|
-
const setExposureScore = (value) => updateAdjustments({ exposureScore: value });
|
|
104
|
-
const setHighlightsScore = (value) => updateAdjustments({ highlightsScore: value });
|
|
105
|
-
const setShadowsScore = (value) => updateAdjustments({ shadowsScore: value });
|
|
106
|
-
const setWhitesScore = (value) => updateAdjustments({ whitesScore: value });
|
|
107
|
-
const setBlacksScore = (value) => updateAdjustments({ blacksScore: value });
|
|
108
|
-
const setContrastScore = (value) => updateAdjustments({ contrastScore: value });
|
|
109
|
-
const setClarityScore = (value) => updateAdjustments({ clarityScore: value });
|
|
110
|
-
const setSharpnessScore = (value) => updateAdjustments({ sharpnessScore: value });
|
|
54
|
+
const createRelativeAdjuster = useCallback((key, amount) => () => {
|
|
55
|
+
batchActions.adjustSelected({ [key]: amount });
|
|
56
|
+
}, [batchActions]);
|
|
111
57
|
const handleBulkTempDecreaseMax = createRelativeAdjuster('tempScore', -20);
|
|
112
58
|
const handleBulkTempDecrease = createRelativeAdjuster('tempScore', -5);
|
|
113
59
|
const handleBulkTempIncrease = createRelativeAdjuster('tempScore', 5);
|
|
@@ -161,8 +107,9 @@ export function useHonchoEditorBulk(controllerBulk, eventID, firebaseUid) {
|
|
|
161
107
|
if (eventID && firebaseUid) {
|
|
162
108
|
setIsLoading(true);
|
|
163
109
|
setError(null);
|
|
164
|
-
|
|
110
|
+
controller.getImageList(firebaseUid, eventID, 1)
|
|
165
111
|
.then(response => {
|
|
112
|
+
batchActions.syncAdjustment(response.gallery.map(mapToImageAdjustmentConfig));
|
|
166
113
|
const mappedData = response.gallery.map(mapGalleryToPhotoData);
|
|
167
114
|
setImageCollection(mappedData);
|
|
168
115
|
})
|
|
@@ -174,42 +121,30 @@ export function useHonchoEditorBulk(controllerBulk, eventID, firebaseUid) {
|
|
|
174
121
|
setIsLoading(false);
|
|
175
122
|
});
|
|
176
123
|
}
|
|
177
|
-
}, [eventID, firebaseUid,
|
|
124
|
+
}, [eventID, firebaseUid, controller]);
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
// react the changes in the batch state
|
|
127
|
+
if (currentBatch.allImages) {
|
|
128
|
+
setImageCollection((current) => {
|
|
129
|
+
const updated = current.map((image) => {
|
|
130
|
+
const adjustment = currentBatch.allImages[image.key];
|
|
131
|
+
return adjustment ? { ...image, ...adjustment } : image;
|
|
132
|
+
});
|
|
133
|
+
return updated;
|
|
134
|
+
});
|
|
135
|
+
console.log("Adjustment changed detected");
|
|
136
|
+
}
|
|
137
|
+
}, [currentBatch.allImages]);
|
|
178
138
|
return {
|
|
179
139
|
imageCollection,
|
|
180
|
-
isSelectedMode,
|
|
181
140
|
isLoading,
|
|
182
141
|
error,
|
|
183
|
-
|
|
142
|
+
selectedIds,
|
|
184
143
|
// Gallery Handlers
|
|
185
|
-
handleSelectedMode,
|
|
186
|
-
handleToggleSelect,
|
|
187
|
-
handlePreview,
|
|
188
144
|
handleBackCallbackBulk,
|
|
189
|
-
isBulkEditing,
|
|
190
|
-
selectedImages,
|
|
191
|
-
imageList,
|
|
192
|
-
currentBatch,
|
|
193
|
-
selectedIds,
|
|
194
|
-
allImageIds,
|
|
195
|
-
adjustmentsMap,
|
|
196
145
|
selectedBulkPreset,
|
|
197
146
|
handleToggleImageSelection: batchActions.toggleSelection,
|
|
198
|
-
toggleBulkEditing,
|
|
199
147
|
handleSelectBulkPreset,
|
|
200
|
-
// Bulk Adjustment Handlers
|
|
201
|
-
setTempScore,
|
|
202
|
-
setTintScore,
|
|
203
|
-
setVibranceScore,
|
|
204
|
-
setSaturationScore,
|
|
205
|
-
setExposureScore,
|
|
206
|
-
setHighlightsScore,
|
|
207
|
-
setShadowsScore,
|
|
208
|
-
setWhitesScore,
|
|
209
|
-
setBlacksScore,
|
|
210
|
-
setContrastScore,
|
|
211
|
-
setClarityScore,
|
|
212
|
-
setSharpnessScore,
|
|
213
148
|
// Adjustment
|
|
214
149
|
handleBulkTempDecreaseMax,
|
|
215
150
|
handleBulkTempDecrease,
|
package/dist/index.d.ts
CHANGED
|
@@ -21,10 +21,13 @@ export { default as HWatermarkView } from './components/editor/HWatermarkView';
|
|
|
21
21
|
export { default as HModalMobile } from './components/editor/HModalMobile';
|
|
22
22
|
export { default as HPresetOptionsMenu } from './components/editor/HPresetOptionMenu';
|
|
23
23
|
export { HAlertInternetBox, HAlertCopyBox, HAlertInternetConnectionBox, HAlertPresetSave } from './components/editor/HAlertBox';
|
|
24
|
-
export {
|
|
24
|
+
export { AlbumImageGallery, ExtendedPhotoData } from './components/editor/GalleryAlbum/AlbumImageGallery';
|
|
25
25
|
export { default as GalleryImageItem } from './components/editor/GalleryAlbum/ImageItem';
|
|
26
26
|
export { default as SimplifiedAlbumGallery } from './components/editor/GalleryAlbum/SimplifiedAlbumGallery';
|
|
27
27
|
export { default as SimplifiedImageItem } from './components/editor/GalleryAlbum/SimplifiedImageItem';
|
|
28
|
+
export { EditorProvider, useEditorContext } from './lib/context/EditorContext';
|
|
29
|
+
export { useImageProcessor } from './lib/hooks/useImageProcessor';
|
|
30
|
+
export { useEditorHeadless } from './lib/hooks/useEditorHeadless';
|
|
28
31
|
export { useAdjustmentHistory, type UseAdjustmentHistoryReturn, type HistoryInfo, type HistoryActions, type HistoryConfig } from './hooks/useAdjustmentHistory';
|
|
29
32
|
export { useAdjustmentHistoryBatch, type UseAdjustmentHistoryBatchReturn, type BatchAdjustmentState, type ImageAdjustmentConfig, type BatchHistoryInfo, type BatchHistoryActions, type BatchHistoryConfig } from './hooks/useAdjustmentHistoryBatch';
|
|
30
33
|
export { default as useColors } from './themes/colors';
|
package/dist/index.js
CHANGED
|
@@ -18,10 +18,13 @@ export { default as HWatermarkView } from './components/editor/HWatermarkView';
|
|
|
18
18
|
export { default as HModalMobile } from './components/editor/HModalMobile';
|
|
19
19
|
export { default as HPresetOptionsMenu } from './components/editor/HPresetOptionMenu';
|
|
20
20
|
export { HAlertInternetBox, HAlertCopyBox, HAlertInternetConnectionBox, HAlertPresetSave } from './components/editor/HAlertBox';
|
|
21
|
-
export {
|
|
21
|
+
export { AlbumImageGallery } from './components/editor/GalleryAlbum/AlbumImageGallery';
|
|
22
22
|
export { default as GalleryImageItem } from './components/editor/GalleryAlbum/ImageItem';
|
|
23
23
|
export { default as SimplifiedAlbumGallery } from './components/editor/GalleryAlbum/SimplifiedAlbumGallery';
|
|
24
24
|
export { default as SimplifiedImageItem } from './components/editor/GalleryAlbum/SimplifiedImageItem';
|
|
25
|
+
export { EditorProvider, useEditorContext } from './lib/context/EditorContext';
|
|
26
|
+
export { useImageProcessor } from './lib/hooks/useImageProcessor';
|
|
27
|
+
export { useEditorHeadless } from './lib/hooks/useEditorHeadless';
|
|
25
28
|
// --- History Hooks ---
|
|
26
29
|
export { useAdjustmentHistory } from './hooks/useAdjustmentHistory';
|
|
27
30
|
export { useAdjustmentHistoryBatch } from './hooks/useAdjustmentHistoryBatch';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { EditorProcessingService } from '../context/EditorProcessingService';
|
|
3
|
+
interface EditorContextValue {
|
|
4
|
+
isReady: boolean;
|
|
5
|
+
error: Error | null;
|
|
6
|
+
processingService: EditorProcessingService;
|
|
7
|
+
queueStatus: {
|
|
8
|
+
queueLength: number;
|
|
9
|
+
isProcessing: boolean;
|
|
10
|
+
hasProcessor: boolean;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
interface EditorProviderProps {
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
export declare const EditorProvider: React.FC<EditorProviderProps>;
|
|
17
|
+
export declare const useEditorContext: () => EditorContextValue;
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
4
|
+
import { useEditorHeadless } from '../hooks/useEditorHeadless';
|
|
5
|
+
import { EditorProcessingService } from '../context/EditorProcessingService';
|
|
6
|
+
const EditorContext = createContext(null);
|
|
7
|
+
export const EditorProvider = ({ children }) => {
|
|
8
|
+
// Single editor instance for the entire app
|
|
9
|
+
const { editor, isReady, error, processImage } = useEditorHeadless();
|
|
10
|
+
// Single processing service instance
|
|
11
|
+
const [processingService] = useState(() => new EditorProcessingService());
|
|
12
|
+
const [queueStatus, setQueueStatus] = useState(processingService.getQueueStatus());
|
|
13
|
+
// Connect the editor to the processing service when ready
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (isReady && processImage) {
|
|
16
|
+
console.debug('Connecting editor to processing service - editor ready:', isReady);
|
|
17
|
+
processingService.setProcessor(processImage);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.debug('Editor not ready yet - isReady:', isReady, 'processImage:', !!processImage);
|
|
21
|
+
}
|
|
22
|
+
}, [isReady, processImage, processingService]);
|
|
23
|
+
// Update queue status periodically - now event-driven instead of polling
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
const updateStatus = () => {
|
|
26
|
+
setQueueStatus(processingService.getQueueStatus());
|
|
27
|
+
};
|
|
28
|
+
// Add listener for immediate updates
|
|
29
|
+
processingService.addStatusChangeListener(updateStatus);
|
|
30
|
+
// Optional: Keep a fallback interval for safety (much less frequent)
|
|
31
|
+
const interval = setInterval(updateStatus, 5000); // 5 seconds instead of 1
|
|
32
|
+
return () => {
|
|
33
|
+
processingService.removeStatusChangeListener(updateStatus);
|
|
34
|
+
clearInterval(interval);
|
|
35
|
+
};
|
|
36
|
+
}, [processingService]);
|
|
37
|
+
// Cleanup on unmount
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
return () => {
|
|
40
|
+
processingService.cleanup(); // Use new cleanup method
|
|
41
|
+
};
|
|
42
|
+
}, [processingService]);
|
|
43
|
+
const contextValue = {
|
|
44
|
+
isReady,
|
|
45
|
+
error,
|
|
46
|
+
processingService,
|
|
47
|
+
queueStatus,
|
|
48
|
+
};
|
|
49
|
+
return (_jsx(EditorContext.Provider, { value: contextValue, children: children }));
|
|
50
|
+
};
|
|
51
|
+
// Custom hook to use the editor context
|
|
52
|
+
export const useEditorContext = () => {
|
|
53
|
+
const context = useContext(EditorContext);
|
|
54
|
+
if (!context) {
|
|
55
|
+
throw new Error('useEditorContext must be used within an EditorProvider');
|
|
56
|
+
}
|
|
57
|
+
return context;
|
|
58
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AdjustmentValues } from '../editor/honcho-editor';
|
|
2
|
+
export interface EditorTask {
|
|
3
|
+
id: string;
|
|
4
|
+
path: string;
|
|
5
|
+
frame: string | null;
|
|
6
|
+
adjustments?: Partial<AdjustmentValues>;
|
|
7
|
+
priority?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface EditorResponse {
|
|
10
|
+
id: string;
|
|
11
|
+
path: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class EditorProcessingService {
|
|
14
|
+
private processingQueue;
|
|
15
|
+
private isProcessing;
|
|
16
|
+
private processImage?;
|
|
17
|
+
private pendingProcessingTimeout?;
|
|
18
|
+
private statusChangeListeners;
|
|
19
|
+
constructor();
|
|
20
|
+
setProcessor(processImage: (task: EditorTask) => Promise<EditorResponse>): void;
|
|
21
|
+
addStatusChangeListener(listener: () => void): void;
|
|
22
|
+
removeStatusChangeListener(listener: () => void): void;
|
|
23
|
+
private notifyStatusChange;
|
|
24
|
+
requestProcessing(task: EditorTask): Promise<EditorResponse>;
|
|
25
|
+
private scheduleProcessing;
|
|
26
|
+
private processQueue;
|
|
27
|
+
getQueueStatus(): {
|
|
28
|
+
queueLength: number;
|
|
29
|
+
isProcessing: boolean;
|
|
30
|
+
hasProcessor: boolean;
|
|
31
|
+
};
|
|
32
|
+
clearQueue(): void;
|
|
33
|
+
cleanup(): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Simple priority queue implementation using binary heap
|
|
2
|
+
class PriorityQueue {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.heap = [];
|
|
5
|
+
}
|
|
6
|
+
// Insert item maintaining priority order - O(log n)
|
|
7
|
+
enqueue(item) {
|
|
8
|
+
this.heap.push(item);
|
|
9
|
+
this.heapifyUp(this.heap.length - 1);
|
|
10
|
+
}
|
|
11
|
+
// Remove highest priority item - O(log n)
|
|
12
|
+
dequeue() {
|
|
13
|
+
if (this.heap.length === 0)
|
|
14
|
+
return undefined;
|
|
15
|
+
if (this.heap.length === 1)
|
|
16
|
+
return this.heap.pop();
|
|
17
|
+
const root = this.heap[0];
|
|
18
|
+
this.heap[0] = this.heap.pop();
|
|
19
|
+
this.heapifyDown(0);
|
|
20
|
+
return root;
|
|
21
|
+
}
|
|
22
|
+
get length() {
|
|
23
|
+
return this.heap.length;
|
|
24
|
+
}
|
|
25
|
+
clear() {
|
|
26
|
+
return this.heap.splice(0);
|
|
27
|
+
}
|
|
28
|
+
heapifyUp(index) {
|
|
29
|
+
while (index > 0) {
|
|
30
|
+
const parentIndex = Math.floor((index - 1) / 2);
|
|
31
|
+
if (this.compare(this.heap[index], this.heap[parentIndex]) <= 0)
|
|
32
|
+
break;
|
|
33
|
+
[this.heap[index], this.heap[parentIndex]] = [this.heap[parentIndex], this.heap[index]];
|
|
34
|
+
index = parentIndex;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
heapifyDown(index) {
|
|
38
|
+
while (true) {
|
|
39
|
+
let maxIndex = index;
|
|
40
|
+
const leftChild = 2 * index + 1;
|
|
41
|
+
const rightChild = 2 * index + 2;
|
|
42
|
+
if (leftChild < this.heap.length && this.compare(this.heap[leftChild], this.heap[maxIndex]) > 0) {
|
|
43
|
+
maxIndex = leftChild;
|
|
44
|
+
}
|
|
45
|
+
if (rightChild < this.heap.length && this.compare(this.heap[rightChild], this.heap[maxIndex]) > 0) {
|
|
46
|
+
maxIndex = rightChild;
|
|
47
|
+
}
|
|
48
|
+
if (maxIndex === index)
|
|
49
|
+
break;
|
|
50
|
+
[this.heap[index], this.heap[maxIndex]] = [this.heap[maxIndex], this.heap[index]];
|
|
51
|
+
index = maxIndex;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Compare function: higher priority first, then older timestamp
|
|
55
|
+
compare(a, b) {
|
|
56
|
+
if (a.priority !== b.priority) {
|
|
57
|
+
return (a.priority || 0) - (b.priority || 0);
|
|
58
|
+
}
|
|
59
|
+
return b.timestamp - a.timestamp; // Older first (reversed for min-heap behavior)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class EditorProcessingService {
|
|
63
|
+
constructor() {
|
|
64
|
+
this.processingQueue = new PriorityQueue();
|
|
65
|
+
this.isProcessing = false;
|
|
66
|
+
this.statusChangeListeners = [];
|
|
67
|
+
console.debug('EditorProcessingService created');
|
|
68
|
+
}
|
|
69
|
+
// Set the processing function from the editor
|
|
70
|
+
setProcessor(processImage) {
|
|
71
|
+
this.processImage = processImage;
|
|
72
|
+
console.debug('Editor processor set, queue length:', this.processingQueue.length);
|
|
73
|
+
// Start processing if there are queued items
|
|
74
|
+
if (this.processingQueue.length > 0) {
|
|
75
|
+
this.scheduleProcessing();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// Add listener for status changes
|
|
79
|
+
addStatusChangeListener(listener) {
|
|
80
|
+
this.statusChangeListeners.push(listener);
|
|
81
|
+
}
|
|
82
|
+
// Remove status change listener
|
|
83
|
+
removeStatusChangeListener(listener) {
|
|
84
|
+
const index = this.statusChangeListeners.indexOf(listener);
|
|
85
|
+
if (index > -1) {
|
|
86
|
+
this.statusChangeListeners.splice(index, 1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Notify status change listeners
|
|
90
|
+
notifyStatusChange() {
|
|
91
|
+
this.statusChangeListeners.forEach(listener => listener());
|
|
92
|
+
}
|
|
93
|
+
// Add task to processing queue
|
|
94
|
+
async requestProcessing(task) {
|
|
95
|
+
return new Promise((resolve, reject) => {
|
|
96
|
+
// Validate that we have a processor
|
|
97
|
+
if (!this.processImage) {
|
|
98
|
+
console.warn('No processor available, rejecting task:', task.id);
|
|
99
|
+
reject(new Error('Editor not ready - processor not set'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const queueItem = {
|
|
103
|
+
...task,
|
|
104
|
+
priority: task.priority || 0,
|
|
105
|
+
resolve,
|
|
106
|
+
reject,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
// Add to priority queue - O(log n)
|
|
110
|
+
this.processingQueue.enqueue(queueItem);
|
|
111
|
+
console.debug(`Added task ${task.id} to queue. Queue length: ${this.processingQueue.length}`);
|
|
112
|
+
// Notify status change
|
|
113
|
+
this.notifyStatusChange();
|
|
114
|
+
// Schedule processing with debouncing
|
|
115
|
+
this.scheduleProcessing();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
// Debounced processing to avoid starting processing on every single addition
|
|
119
|
+
scheduleProcessing() {
|
|
120
|
+
if (this.pendingProcessingTimeout) {
|
|
121
|
+
clearTimeout(this.pendingProcessingTimeout);
|
|
122
|
+
}
|
|
123
|
+
this.pendingProcessingTimeout = setTimeout(() => {
|
|
124
|
+
this.processQueue();
|
|
125
|
+
}, 5); // 5ms debounce
|
|
126
|
+
}
|
|
127
|
+
// Process queue sequentially
|
|
128
|
+
async processQueue() {
|
|
129
|
+
if (this.isProcessing || this.processingQueue.length === 0 || !this.processImage) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.isProcessing = true;
|
|
133
|
+
this.notifyStatusChange();
|
|
134
|
+
console.debug('Starting queue processing...');
|
|
135
|
+
while (this.processingQueue.length > 0) {
|
|
136
|
+
// Get highest priority item - O(log n) vs O(n log n) sorting
|
|
137
|
+
const item = this.processingQueue.dequeue();
|
|
138
|
+
console.debug(`Processing task ${item.id} (priority: ${item.priority || 0}, queue remaining: ${this.processingQueue.length})`);
|
|
139
|
+
try {
|
|
140
|
+
const result = await this.processImage(item);
|
|
141
|
+
item.resolve(result);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(`Failed to process task ${item.id}:`, error);
|
|
145
|
+
item.reject(error instanceof Error ? error : new Error(String(error)));
|
|
146
|
+
}
|
|
147
|
+
// Yield control to browser for UI updates - more efficient than setTimeout
|
|
148
|
+
await new Promise(resolve => {
|
|
149
|
+
if (typeof requestIdleCallback !== 'undefined') {
|
|
150
|
+
requestIdleCallback(resolve);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Fallback for environments without requestIdleCallback
|
|
154
|
+
setTimeout(resolve, 0);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
// Notify status change after each item
|
|
158
|
+
this.notifyStatusChange();
|
|
159
|
+
}
|
|
160
|
+
this.isProcessing = false;
|
|
161
|
+
this.notifyStatusChange();
|
|
162
|
+
console.debug('Queue processing complete');
|
|
163
|
+
}
|
|
164
|
+
// Get current queue status
|
|
165
|
+
getQueueStatus() {
|
|
166
|
+
return {
|
|
167
|
+
queueLength: this.processingQueue.length,
|
|
168
|
+
isProcessing: this.isProcessing,
|
|
169
|
+
hasProcessor: !!this.processImage,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
// Clear the queue (useful for cleanup)
|
|
173
|
+
clearQueue() {
|
|
174
|
+
const clearedItems = this.processingQueue.clear();
|
|
175
|
+
clearedItems.forEach(item => {
|
|
176
|
+
item.reject(new Error('Queue cleared'));
|
|
177
|
+
});
|
|
178
|
+
this.notifyStatusChange();
|
|
179
|
+
console.debug(`Cleared ${clearedItems.length} items from queue`);
|
|
180
|
+
}
|
|
181
|
+
// Cleanup method for removing timeouts
|
|
182
|
+
cleanup() {
|
|
183
|
+
if (this.pendingProcessingTimeout) {
|
|
184
|
+
clearTimeout(this.pendingProcessingTimeout);
|
|
185
|
+
this.pendingProcessingTimeout = undefined;
|
|
186
|
+
}
|
|
187
|
+
this.clearQueue();
|
|
188
|
+
this.statusChangeListeners.length = 0;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { EditorTask, EditorResponse } from '../context/EditorProcessingService';
|
|
2
|
+
interface UseEditorOptions {
|
|
3
|
+
priority?: number;
|
|
4
|
+
}
|
|
5
|
+
interface UseEditorResult {
|
|
6
|
+
processImage: (task: Omit<EditorTask, 'priority'>) => Promise<EditorResponse>;
|
|
7
|
+
isEditorReady: boolean;
|
|
8
|
+
editorError: Error | null;
|
|
9
|
+
queueStatus: {
|
|
10
|
+
queueLength: number;
|
|
11
|
+
isProcessing: boolean;
|
|
12
|
+
hasProcessor: boolean;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Lightweight hook for components to request image processing
|
|
17
|
+
* Uses the global editor instance via context
|
|
18
|
+
*/
|
|
19
|
+
export declare const useEditor: (options?: UseEditorOptions) => UseEditorResult;
|
|
20
|
+
export declare const useEditorHighPriority: () => UseEditorResult;
|
|
21
|
+
export declare const useEditorLowPriority: () => UseEditorResult;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { useEditorContext } from '../context/EditorContext';
|
|
4
|
+
/**
|
|
5
|
+
* Lightweight hook for components to request image processing
|
|
6
|
+
* Uses the global editor instance via context
|
|
7
|
+
*/
|
|
8
|
+
export const useEditor = (options = {}) => {
|
|
9
|
+
const { isReady, error, processingService, queueStatus } = useEditorContext();
|
|
10
|
+
const { priority = 0 } = options;
|
|
11
|
+
const processImage = useCallback(async (task) => {
|
|
12
|
+
if (!isReady) {
|
|
13
|
+
throw new Error('Editor not ready');
|
|
14
|
+
}
|
|
15
|
+
const taskWithPriority = {
|
|
16
|
+
...task,
|
|
17
|
+
priority,
|
|
18
|
+
};
|
|
19
|
+
return processingService.requestProcessing(taskWithPriority);
|
|
20
|
+
}, [isReady, processingService, priority]);
|
|
21
|
+
return {
|
|
22
|
+
processImage,
|
|
23
|
+
isEditorReady: isReady,
|
|
24
|
+
editorError: error,
|
|
25
|
+
queueStatus,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
// Convenience hook for high-priority processing (e.g., visible images)
|
|
29
|
+
export const useEditorHighPriority = () => {
|
|
30
|
+
return useEditor({ priority: 10 });
|
|
31
|
+
};
|
|
32
|
+
// Convenience hook for low-priority processing (e.g., off-screen images)
|
|
33
|
+
export const useEditorLowPriority = () => {
|
|
34
|
+
return useEditor({ priority: 1 });
|
|
35
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { AdjustmentValues, HonchoEditor } from '../editor/honcho-editor';
|
|
2
|
+
interface EditorTask {
|
|
3
|
+
id: string;
|
|
4
|
+
path: string;
|
|
5
|
+
frame: string | null;
|
|
6
|
+
adjustments?: Partial<AdjustmentValues>;
|
|
7
|
+
}
|
|
8
|
+
interface EditorResponse {
|
|
9
|
+
id: string;
|
|
10
|
+
path: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function useEditorHeadless(): {
|
|
13
|
+
editor: HonchoEditor | null;
|
|
14
|
+
isReady: boolean;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
processImage: (task: EditorTask) => Promise<EditorResponse>;
|
|
17
|
+
};
|
|
18
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useState, useEffect, useRef, useCallback } from 'react';
|
|
3
|
+
import { HonchoEditor, HonchoEditorUtils } from '../editor/honcho-editor';
|
|
4
|
+
export function useEditorHeadless() {
|
|
5
|
+
const editorRef = useRef(null);
|
|
6
|
+
const [isReady, setIsReady] = useState(false);
|
|
7
|
+
const [error, setError] = useState(null);
|
|
8
|
+
const [isScriptLoaded, setIsScriptLoaded] = useState(false);
|
|
9
|
+
// Load script dynamically without component
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const loadScript = () => {
|
|
12
|
+
// Check if script is already loaded
|
|
13
|
+
if (document.querySelector('script[src="/honcho-photo-editor.js"]')) {
|
|
14
|
+
setIsScriptLoaded(true);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const script = document.createElement('script');
|
|
18
|
+
script.src = '/honcho-photo-editor.js';
|
|
19
|
+
script.async = true;
|
|
20
|
+
script.onload = () => {
|
|
21
|
+
console.debug('Honcho photo editor script loaded');
|
|
22
|
+
setIsScriptLoaded(true);
|
|
23
|
+
};
|
|
24
|
+
script.onerror = () => {
|
|
25
|
+
console.error('Failed to load honcho photo editor script');
|
|
26
|
+
setError(new Error('Failed to load editor script'));
|
|
27
|
+
};
|
|
28
|
+
document.head.appendChild(script);
|
|
29
|
+
};
|
|
30
|
+
loadScript();
|
|
31
|
+
// Cleanup script on unmount
|
|
32
|
+
return () => {
|
|
33
|
+
const script = document.querySelector('script[src="/honcho-photo-editor.js"]');
|
|
34
|
+
if (script) {
|
|
35
|
+
script.remove();
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}, []);
|
|
39
|
+
// Initialize editor when script is loaded
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!isScriptLoaded)
|
|
42
|
+
return;
|
|
43
|
+
const initialize = async () => {
|
|
44
|
+
try {
|
|
45
|
+
console.debug('Script loaded, initializing editor...');
|
|
46
|
+
if (!editorRef.current) {
|
|
47
|
+
editorRef.current = new HonchoEditor();
|
|
48
|
+
}
|
|
49
|
+
await editorRef.current.initialize(false);
|
|
50
|
+
setIsReady(true);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
console.error("Failed to initialize editor:", e);
|
|
54
|
+
setError(e instanceof Error ? e : new Error(String(e)));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
initialize();
|
|
58
|
+
return () => {
|
|
59
|
+
editorRef.current?.cleanup();
|
|
60
|
+
};
|
|
61
|
+
}, [isScriptLoaded]);
|
|
62
|
+
// Helper function to load image as blob with fallback
|
|
63
|
+
const loadImageAsBlob = async (url) => {
|
|
64
|
+
try {
|
|
65
|
+
// Try direct fetch first (faster, no server load)
|
|
66
|
+
const response = await fetch(url, {
|
|
67
|
+
mode: 'cors',
|
|
68
|
+
credentials: 'omit'
|
|
69
|
+
});
|
|
70
|
+
if (!response.ok) {
|
|
71
|
+
throw new Error(`Direct fetch failed: ${response.statusText}`);
|
|
72
|
+
}
|
|
73
|
+
return response.blob();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.warn(`Direct fetch failed for ${url}, trying proxy fallback:`, error);
|
|
77
|
+
// Fallback to proxy API if CORS or other fetch issues
|
|
78
|
+
try {
|
|
79
|
+
const proxyUrl = `/api/image?imageUrl=${encodeURIComponent(url)}`;
|
|
80
|
+
const response = await fetch(proxyUrl);
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
throw new Error(`Proxy fetch failed: ${response.statusText}`);
|
|
83
|
+
}
|
|
84
|
+
return response.blob();
|
|
85
|
+
}
|
|
86
|
+
catch (proxyError) {
|
|
87
|
+
console.error(`Both direct and proxy fetch failed for ${url}:`, proxyError);
|
|
88
|
+
throw new Error(`Failed to load image: ${proxyError instanceof Error ? proxyError.message : 'Unknown error'}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
// Process single image task
|
|
93
|
+
const processImage = useCallback(async (task) => {
|
|
94
|
+
if (!editorRef.current || !isReady) {
|
|
95
|
+
throw new Error('Editor not ready');
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
console.debug(`Processing image: ${task.id}`);
|
|
99
|
+
// Load original image as blob first
|
|
100
|
+
const imageBlob = await loadImageAsBlob(task.path);
|
|
101
|
+
// Convert blob to File for HonchoEditor
|
|
102
|
+
const imageFile = new File([imageBlob], `image-${task.id}`, { type: imageBlob.type });
|
|
103
|
+
// Load frame if provided
|
|
104
|
+
let frameFile = null;
|
|
105
|
+
if (task.frame) {
|
|
106
|
+
const frameBlob = await loadImageAsBlob(task.frame);
|
|
107
|
+
frameFile = new File([frameBlob], `frame-${task.id}`, { type: frameBlob.type });
|
|
108
|
+
}
|
|
109
|
+
// Process image using HonchoEditor's one-shot method
|
|
110
|
+
const processedImageData = await editorRef.current.processImageOneShot(imageFile, task.adjustments, frameFile);
|
|
111
|
+
// Convert ImageData to Blob
|
|
112
|
+
const processedBlob = await HonchoEditorUtils.imageDataToBlob(processedImageData);
|
|
113
|
+
// Create blob URL for processed result
|
|
114
|
+
const blobUrl = URL.createObjectURL(processedBlob);
|
|
115
|
+
return { id: task.id, path: blobUrl };
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(`Failed to process image ${task.id}:`, error);
|
|
119
|
+
throw new Error(`Failed to process image: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
120
|
+
}
|
|
121
|
+
}, [isReady, loadImageAsBlob]);
|
|
122
|
+
return {
|
|
123
|
+
editor: editorRef.current,
|
|
124
|
+
isReady,
|
|
125
|
+
error,
|
|
126
|
+
processImage
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AdjustmentValues } from '../editor/honcho-editor';
|
|
2
|
+
interface UseImageProcessorProps {
|
|
3
|
+
photoId: string;
|
|
4
|
+
photoSrc: string;
|
|
5
|
+
enableEditor?: boolean;
|
|
6
|
+
adjustments?: Partial<AdjustmentValues>;
|
|
7
|
+
frame?: string | null;
|
|
8
|
+
priority?: 'high' | 'low';
|
|
9
|
+
}
|
|
10
|
+
interface UseImageProcessorReturn {
|
|
11
|
+
processedImageSrc: string;
|
|
12
|
+
isProcessingComplete: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function useImageProcessor({ photoId, photoSrc, enableEditor, adjustments, frame, priority, }: UseImageProcessorProps): UseImageProcessorReturn;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
2
|
+
import { useEditorHighPriority, useEditorLowPriority } from '../hooks/useEditor';
|
|
3
|
+
export function useImageProcessor({ photoId, photoSrc, enableEditor = true, adjustments, frame, priority = 'low', }) {
|
|
4
|
+
const { processImage, isEditorReady } = priority === 'high'
|
|
5
|
+
? useEditorHighPriority()
|
|
6
|
+
: useEditorLowPriority();
|
|
7
|
+
// State for processed image and processing status
|
|
8
|
+
const [processedImageSrc, setProcessedImageSrc] = useState(photoSrc);
|
|
9
|
+
const [isProcessingComplete, setIsProcessingComplete] = useState(false);
|
|
10
|
+
// Memoize adjustments to prevent unnecessary effect triggers
|
|
11
|
+
const adjustmentsString = useMemo(() => adjustments ? JSON.stringify(adjustments) : '', [adjustments]);
|
|
12
|
+
const frameMemoized = useMemo(() => frame ? frame : null, [frame]);
|
|
13
|
+
// Process image when adjustments change
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!enableEditor || !isEditorReady || !adjustments) {
|
|
16
|
+
console.debug("Skipping editor processing:", {
|
|
17
|
+
enableEditor,
|
|
18
|
+
isEditorReady,
|
|
19
|
+
hasAdjustments: !!adjustments,
|
|
20
|
+
hasFrame: frameMemoized
|
|
21
|
+
});
|
|
22
|
+
setProcessedImageSrc(photoSrc);
|
|
23
|
+
setIsProcessingComplete(true);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Reset processing state when starting new processing
|
|
27
|
+
setIsProcessingComplete(false);
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
const processImageWithEditor = async () => {
|
|
30
|
+
try {
|
|
31
|
+
const result = await processImage({
|
|
32
|
+
id: photoId,
|
|
33
|
+
path: photoSrc,
|
|
34
|
+
frame: frameMemoized,
|
|
35
|
+
adjustments: adjustments
|
|
36
|
+
});
|
|
37
|
+
if (!cancelled) {
|
|
38
|
+
setProcessedImageSrc(result.path);
|
|
39
|
+
setIsProcessingComplete(true);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (!cancelled) {
|
|
44
|
+
console.error({ error, photoKey: photoId, isEditorReady }, 'Failed to process image with editor');
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
processImageWithEditor();
|
|
49
|
+
return () => {
|
|
50
|
+
cancelled = true;
|
|
51
|
+
};
|
|
52
|
+
}, [photoSrc, adjustmentsString, frameMemoized, enableEditor, isEditorReady, processImage, photoId]);
|
|
53
|
+
return {
|
|
54
|
+
processedImageSrc,
|
|
55
|
+
isProcessingComplete
|
|
56
|
+
};
|
|
57
|
+
}
|