@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.
@@ -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) => () => void;
13
+ onPreview: (photo: ExtendedPhotoData) => void;
14
14
  onSelectedMode: () => void;
15
- onToggleSelect: (photo: ExtendedPhotoData) => () => void;
15
+ onToggleSelect: (photo: ExtendedPhotoData) => void;
16
16
  }
17
- declare const AlbumImageGallery: React.FC<ImageGalleryProps>;
18
- export default AlbumImageGallery;
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, ImageItem, Controller, Preset } from './useHonchoEditor';
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
- originalData: Gallery;
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(controllerBulk: Controller, eventID: string, firebaseUid: string): {
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
- allImageIds: string[];
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, useMemo } from 'react';
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: bestImage.path,
12
- original: gallery.download?.path || bestImage.path,
13
- width: bestImage.width || 1,
14
- height: bestImage.height || 1,
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
- originalData: gallery,
14
+ adjustments: gallery.editor_config?.color_adjustment,
18
15
  };
19
16
  };
20
- const initialAdjustments = {
21
- tempScore: 0, tintScore: 0, vibranceScore: 0, exposureScore: 0, highlightsScore: 0, shadowsScore: 0,
22
- whitesScore: 0, blacksScore: 0, saturationScore: 0, contrastScore: 0, clarityScore: 0, sharpnessScore: 0,
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(controllerBulk, eventID, firebaseUid) {
42
- const { currentState, actions: historyActions, } = useAdjustmentHistory(initialAdjustments);
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 = selectedImageIds.length > 0 ? selectedImageIds[selectedImageIds.length - 1] : eventID;
58
- controllerBulk.handleBack(firebaseUid, lastSelectedId);
59
- }, [controllerBulk, firebaseUid, selectedImageIds, eventID]);
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 updateAdjustments = useCallback((newValues) => {
90
- const newState = { ...currentState, ...newValues };
91
- historyActions.pushState(newState);
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
- controllerBulk.getImageList(firebaseUid, eventID, 1)
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, controllerBulk]);
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
- selectedImageIds,
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 { default as AlbumImageGallery } from './components/editor/GalleryAlbum/AlbumImageGallery';
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 { default as AlbumImageGallery } from './components/editor/GalleryAlbum/AlbumImageGallery';
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yogiswara/honcho-editor-ui",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
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",