@umituz/react-native-image 1.1.5 → 1.2.1
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/package.json +25 -10
- package/src/domain/entities/EditorTypes.ts +180 -0
- package/src/domain/entities/ImageFilterTypes.ts +70 -0
- package/src/index.ts +54 -0
- package/src/infrastructure/services/ImageAIEnhancementService.ts +136 -0
- package/src/infrastructure/services/ImageAnnotationService.ts +189 -0
- package/src/infrastructure/services/ImageBatchService.ts +199 -0
- package/src/infrastructure/services/ImageEditorService.ts +274 -0
- package/src/infrastructure/services/ImageFilterService.ts +168 -0
- package/src/infrastructure/services/ImageMetadataService.ts +187 -0
- package/src/infrastructure/services/ImageSpecializedEnhancementService.ts +57 -0
- package/src/infrastructure/utils/AIImageAnalysisUtils.ts +122 -0
- package/src/infrastructure/utils/CanvasRenderingService.ts +134 -0
- package/src/infrastructure/utils/CropTool.ts +260 -0
- package/src/infrastructure/utils/DrawingEngine.ts +210 -0
- package/src/infrastructure/utils/FilterEffects.ts +51 -0
- package/src/infrastructure/utils/FilterProcessor.ts +361 -0
- package/src/infrastructure/utils/ImageQualityPresets.ts +110 -0
- package/src/infrastructure/utils/LayerManager.ts +158 -0
- package/src/infrastructure/utils/ShapeRenderer.ts +168 -0
- package/src/infrastructure/utils/TextEditor.ts +273 -0
- package/src/presentation/components/CropComponent.tsx +183 -0
- package/src/presentation/components/Editor.tsx +261 -0
- package/src/presentation/components/EditorCanvas.tsx +135 -0
- package/src/presentation/components/EditorPanel.tsx +321 -0
- package/src/presentation/components/EditorToolbar.tsx +180 -0
- package/src/presentation/components/FilterSlider.tsx +123 -0
- package/src/presentation/components/GalleryHeader.tsx +87 -25
- package/src/presentation/components/ImageGallery.tsx +97 -48
- package/src/presentation/hooks/useEditorTools.ts +188 -0
- package/src/presentation/hooks/useImage.ts +33 -2
- package/src/presentation/hooks/useImageAIEnhancement.ts +33 -0
- package/src/presentation/hooks/useImageAnnotation.ts +32 -0
- package/src/presentation/hooks/useImageBatch.ts +33 -0
- package/src/presentation/hooks/useImageEditor.ts +165 -38
- package/src/presentation/hooks/useImageFilter.ts +38 -0
- package/src/presentation/hooks/useImageMetadata.ts +28 -0
|
@@ -1,32 +1,66 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Presentation - Gallery Header Component
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* High-performance, premium header for the Image Gallery.
|
|
5
|
+
* Uses design system tokens and handles safe areas.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import React from 'react';
|
|
9
|
-
import { View, TouchableOpacity, StyleSheet
|
|
9
|
+
import { View, TouchableOpacity, StyleSheet } from 'react-native';
|
|
10
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
11
|
+
import { useAppDesignTokens } from '@umituz/react-native-design-system-theme';
|
|
12
|
+
import { AtomicText } from '@umituz/react-native-design-system-atoms';
|
|
10
13
|
|
|
11
14
|
interface GalleryHeaderProps {
|
|
12
15
|
onEdit?: () => void;
|
|
13
16
|
onClose: () => void;
|
|
17
|
+
title?: string;
|
|
14
18
|
}
|
|
15
19
|
|
|
16
|
-
export function GalleryHeader({ onEdit, onClose }: GalleryHeaderProps) {
|
|
20
|
+
export function GalleryHeader({ onEdit, onClose, title }: GalleryHeaderProps) {
|
|
21
|
+
const insets = useSafeAreaInsets();
|
|
22
|
+
const tokens = useAppDesignTokens();
|
|
23
|
+
|
|
17
24
|
return (
|
|
18
|
-
<View style={
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
<View style={[
|
|
26
|
+
styles.container,
|
|
27
|
+
{
|
|
28
|
+
paddingTop: Math.max(insets.top, 24),
|
|
29
|
+
backgroundColor: 'rgba(0, 0, 0, 0.4)'
|
|
30
|
+
}
|
|
31
|
+
]}>
|
|
32
|
+
<View style={styles.leftSection}>
|
|
33
|
+
{onEdit ? (
|
|
34
|
+
<TouchableOpacity
|
|
35
|
+
style={[styles.actionButton, { backgroundColor: 'rgba(255, 255, 255, 0.15)' }]}
|
|
36
|
+
onPress={onEdit}
|
|
37
|
+
activeOpacity={0.7}
|
|
38
|
+
>
|
|
39
|
+
<AtomicText style={styles.buttonText}>Edit</AtomicText>
|
|
40
|
+
</TouchableOpacity>
|
|
41
|
+
) : (
|
|
42
|
+
<View style={styles.spacer} />
|
|
43
|
+
)}
|
|
44
|
+
</View>
|
|
45
|
+
|
|
46
|
+
<View style={styles.centerSection}>
|
|
47
|
+
{title ? (
|
|
48
|
+
<AtomicText type="bodyMedium" style={styles.titleText}>
|
|
49
|
+
{title}
|
|
50
|
+
</AtomicText>
|
|
51
|
+
) : null}
|
|
52
|
+
</View>
|
|
26
53
|
|
|
27
|
-
<
|
|
28
|
-
<
|
|
29
|
-
|
|
54
|
+
<View style={styles.rightSection}>
|
|
55
|
+
<TouchableOpacity
|
|
56
|
+
style={[styles.closeButton, { backgroundColor: 'rgba(0, 0, 0, 0.4)' }]}
|
|
57
|
+
onPress={onClose}
|
|
58
|
+
activeOpacity={0.7}
|
|
59
|
+
hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
|
|
60
|
+
>
|
|
61
|
+
<AtomicText style={styles.closeIcon}>✕</AtomicText>
|
|
62
|
+
</TouchableOpacity>
|
|
63
|
+
</View>
|
|
30
64
|
</View>
|
|
31
65
|
);
|
|
32
66
|
}
|
|
@@ -37,28 +71,56 @@ const styles = StyleSheet.create({
|
|
|
37
71
|
top: 0,
|
|
38
72
|
left: 0,
|
|
39
73
|
right: 0,
|
|
40
|
-
|
|
41
|
-
paddingHorizontal: 16,
|
|
74
|
+
paddingHorizontal: 20,
|
|
42
75
|
paddingBottom: 16,
|
|
43
76
|
flexDirection: 'row',
|
|
44
77
|
justifyContent: 'space-between',
|
|
45
78
|
alignItems: 'center',
|
|
46
|
-
|
|
79
|
+
zIndex: 1000,
|
|
80
|
+
},
|
|
81
|
+
leftSection: {
|
|
82
|
+
flex: 1,
|
|
83
|
+
alignItems: 'flex-start',
|
|
84
|
+
},
|
|
85
|
+
centerSection: {
|
|
86
|
+
flex: 2,
|
|
87
|
+
alignItems: 'center',
|
|
88
|
+
},
|
|
89
|
+
rightSection: {
|
|
90
|
+
flex: 1,
|
|
91
|
+
alignItems: 'flex-end',
|
|
47
92
|
},
|
|
48
93
|
spacer: {
|
|
49
|
-
width:
|
|
94
|
+
width: 44,
|
|
95
|
+
height: 44,
|
|
50
96
|
},
|
|
51
|
-
|
|
52
|
-
|
|
97
|
+
actionButton: {
|
|
98
|
+
paddingVertical: 8,
|
|
99
|
+
paddingHorizontal: 16,
|
|
100
|
+
borderRadius: 20,
|
|
101
|
+
justifyContent: 'center',
|
|
102
|
+
alignItems: 'center',
|
|
103
|
+
},
|
|
104
|
+
closeButton: {
|
|
105
|
+
width: 50,
|
|
106
|
+
height: 50,
|
|
107
|
+
borderRadius: 25,
|
|
108
|
+
justifyContent: 'center',
|
|
53
109
|
alignItems: 'center',
|
|
54
|
-
gap: 8,
|
|
55
|
-
padding: 12,
|
|
56
|
-
borderRadius: 8,
|
|
57
|
-
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
58
110
|
},
|
|
59
111
|
buttonText: {
|
|
60
112
|
fontSize: 14,
|
|
113
|
+
color: '#FFFFFF',
|
|
114
|
+
fontWeight: 'bold',
|
|
115
|
+
},
|
|
116
|
+
closeIcon: {
|
|
117
|
+
fontSize: 28,
|
|
118
|
+
color: '#FFFFFF',
|
|
119
|
+
fontWeight: '300',
|
|
120
|
+
},
|
|
121
|
+
titleText: {
|
|
61
122
|
color: '#FFFFFF',
|
|
62
123
|
fontWeight: '600',
|
|
124
|
+
textAlign: 'center',
|
|
63
125
|
},
|
|
64
126
|
});
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Presentation - Image Gallery Component
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* High-performance, premium image gallery using expo-image.
|
|
5
|
+
* Replaces slow standard image components for instant loading.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import React, { useCallback } from 'react';
|
|
8
|
-
import
|
|
9
|
-
|
|
8
|
+
import React, { useCallback, useMemo, useState, useEffect } from 'react';
|
|
9
|
+
import { Modal, View, StyleSheet, FlatList, Dimensions, TouchableOpacity } from 'react-native';
|
|
10
|
+
import { Image } from 'expo-image';
|
|
11
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
10
12
|
import type { ImageViewerItem, ImageGalleryOptions } from '../../domain/entities/ImageTypes';
|
|
11
13
|
import { GalleryHeader } from './GalleryHeader';
|
|
12
14
|
|
|
15
|
+
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
|
|
16
|
+
|
|
13
17
|
export interface ImageGalleryProps extends ImageGalleryOptions {
|
|
14
18
|
images: ImageViewerItem[];
|
|
15
19
|
visible: boolean;
|
|
@@ -17,6 +21,7 @@ export interface ImageGalleryProps extends ImageGalleryOptions {
|
|
|
17
21
|
index?: number;
|
|
18
22
|
onImageChange?: (uri: string, index: number) => void | Promise<void>;
|
|
19
23
|
enableEditing?: boolean;
|
|
24
|
+
title?: string;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
@@ -24,66 +29,110 @@ export const ImageGallery: React.FC<ImageGalleryProps> = ({
|
|
|
24
29
|
visible,
|
|
25
30
|
onDismiss,
|
|
26
31
|
index = 0,
|
|
27
|
-
backgroundColor,
|
|
28
|
-
swipeToCloseEnabled = true,
|
|
29
|
-
doubleTapToZoomEnabled = true,
|
|
32
|
+
backgroundColor = '#000000',
|
|
30
33
|
onIndexChange,
|
|
31
34
|
onImageChange,
|
|
32
35
|
enableEditing = false,
|
|
36
|
+
title,
|
|
33
37
|
}) => {
|
|
34
|
-
|
|
35
|
-
const [currentIndex, setCurrentIndex] =
|
|
36
|
-
|
|
37
|
-
React.useEffect(() => {
|
|
38
|
-
setCurrentIndex(index);
|
|
39
|
-
}, [index]);
|
|
40
|
-
|
|
41
|
-
const bg = backgroundColor || '#000000';
|
|
38
|
+
const insets = useSafeAreaInsets();
|
|
39
|
+
const [currentIndex, setCurrentIndex] = useState(index);
|
|
42
40
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
if (visible) setCurrentIndex(index);
|
|
43
|
+
}, [visible, index]);
|
|
47
44
|
|
|
48
45
|
const handleEdit = useCallback(async () => {
|
|
49
46
|
const currentImage = images[currentIndex];
|
|
50
47
|
if (!currentImage || !onImageChange) return;
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
await onImageChange(currentImage.uri, currentIndex);
|
|
54
|
-
} catch (error) {
|
|
55
|
-
// Consumer should handle editing logic
|
|
56
|
-
}
|
|
48
|
+
await onImageChange(currentImage.uri, currentIndex);
|
|
57
49
|
}, [images, currentIndex, onImageChange]);
|
|
58
50
|
|
|
59
|
-
const
|
|
60
|
-
(
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
51
|
+
const handleScroll = useCallback((event: any) => {
|
|
52
|
+
const nextIndex = Math.round(event.nativeEvent.contentOffset.x / SCREEN_WIDTH);
|
|
53
|
+
if (nextIndex !== currentIndex) {
|
|
54
|
+
setCurrentIndex(nextIndex);
|
|
55
|
+
onIndexChange?.(nextIndex);
|
|
56
|
+
}
|
|
57
|
+
}, [currentIndex, onIndexChange]);
|
|
66
58
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
59
|
+
const renderItem = useCallback(({ item }: { item: ImageViewerItem }) => (
|
|
60
|
+
<View style={styles.imageWrapper}>
|
|
61
|
+
<Image
|
|
62
|
+
source={{ uri: item.uri }}
|
|
63
|
+
style={styles.fullImage}
|
|
64
|
+
contentFit="contain"
|
|
65
|
+
transition={200}
|
|
66
|
+
cachePolicy="memory-disk"
|
|
72
67
|
/>
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
</View>
|
|
69
|
+
), []);
|
|
70
|
+
|
|
71
|
+
if (!visible && !currentIndex) return null;
|
|
75
72
|
|
|
76
73
|
return (
|
|
77
|
-
<
|
|
78
|
-
images={viewerImages}
|
|
79
|
-
imageIndex={currentIndex}
|
|
74
|
+
<Modal
|
|
80
75
|
visible={visible}
|
|
76
|
+
transparent
|
|
77
|
+
animationType="fade"
|
|
81
78
|
onRequestClose={onDismiss}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
79
|
+
statusBarTranslucent
|
|
80
|
+
>
|
|
81
|
+
<View style={[styles.container, { backgroundColor }]}>
|
|
82
|
+
<GalleryHeader
|
|
83
|
+
onClose={onDismiss}
|
|
84
|
+
onEdit={enableEditing ? handleEdit : undefined}
|
|
85
|
+
title={title || `${currentIndex + 1} / ${images.length}`}
|
|
86
|
+
/>
|
|
87
|
+
|
|
88
|
+
<FlatList
|
|
89
|
+
data={images}
|
|
90
|
+
renderItem={renderItem}
|
|
91
|
+
horizontal
|
|
92
|
+
pagingEnabled
|
|
93
|
+
showsHorizontalScrollIndicator={false}
|
|
94
|
+
initialScrollIndex={index}
|
|
95
|
+
getItemLayout={(_, i) => ({
|
|
96
|
+
length: SCREEN_WIDTH,
|
|
97
|
+
offset: SCREEN_WIDTH * i,
|
|
98
|
+
index: i,
|
|
99
|
+
})}
|
|
100
|
+
onScroll={handleScroll}
|
|
101
|
+
scrollEventThrottle={16}
|
|
102
|
+
keyExtractor={(item, i) => `${item.uri}-${i}`}
|
|
103
|
+
style={styles.list}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
<View style={[styles.footer, { paddingBottom: Math.max(insets.bottom, 20) }]}>
|
|
107
|
+
{/* Potential for thumbnail strip or captions in future */}
|
|
108
|
+
</View>
|
|
109
|
+
</View>
|
|
110
|
+
</Modal>
|
|
88
111
|
);
|
|
89
112
|
};
|
|
113
|
+
|
|
114
|
+
const styles = StyleSheet.create({
|
|
115
|
+
container: {
|
|
116
|
+
flex: 1,
|
|
117
|
+
},
|
|
118
|
+
list: {
|
|
119
|
+
flex: 1,
|
|
120
|
+
},
|
|
121
|
+
imageWrapper: {
|
|
122
|
+
width: SCREEN_WIDTH,
|
|
123
|
+
height: SCREEN_HEIGHT,
|
|
124
|
+
justifyContent: 'center',
|
|
125
|
+
alignItems: 'center',
|
|
126
|
+
},
|
|
127
|
+
fullImage: {
|
|
128
|
+
width: '100%',
|
|
129
|
+
height: '100%',
|
|
130
|
+
},
|
|
131
|
+
footer: {
|
|
132
|
+
position: 'absolute',
|
|
133
|
+
bottom: 0,
|
|
134
|
+
left: 0,
|
|
135
|
+
right: 0,
|
|
136
|
+
alignItems: 'center',
|
|
137
|
+
}
|
|
138
|
+
});
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Editor Tool Hook
|
|
3
|
+
*
|
|
4
|
+
* Manages individual editor tools and their states
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { useState, useCallback } from 'react';
|
|
8
|
+
import { DrawingEngine, type DrawingPath, type DrawingConfig } from '../../infrastructure/utils/DrawingEngine';
|
|
9
|
+
import { CropTool, type CropConfig } from '../../infrastructure/utils/CropTool';
|
|
10
|
+
import { TextEditor, type TextFormatting } from '../../infrastructure/utils/TextEditor';
|
|
11
|
+
import { ShapeRenderer, type ShapeStyle } from '../../infrastructure/utils/ShapeRenderer';
|
|
12
|
+
|
|
13
|
+
interface ToolState {
|
|
14
|
+
isDrawing: boolean;
|
|
15
|
+
currentPath: DrawingPath;
|
|
16
|
+
startPoint?: { x: number; y: number };
|
|
17
|
+
currentTool: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface UseEditorToolsOptions {
|
|
21
|
+
onStateChange?: (state: any) => void;
|
|
22
|
+
canvas?: HTMLCanvasElement | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function useEditorTools({ onStateChange, canvas }: UseEditorToolsOptions = {}) {
|
|
26
|
+
const [toolState, setToolState] = useState<ToolState>({
|
|
27
|
+
isDrawing: false,
|
|
28
|
+
currentPath: [],
|
|
29
|
+
currentTool: 'move',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const [drawingConfig, setDrawingConfig] = useState<DrawingConfig>({
|
|
33
|
+
color: '#000000',
|
|
34
|
+
size: 5,
|
|
35
|
+
opacity: 1,
|
|
36
|
+
style: 'normal',
|
|
37
|
+
smoothing: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const [cropConfig, setCropConfig] = useState<CropConfig>({
|
|
41
|
+
aspectRatio: undefined,
|
|
42
|
+
lockAspectRatio: false,
|
|
43
|
+
showGrid: true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const [textFormatting, setTextFormatting] = useState<TextFormatting>({
|
|
47
|
+
fontSize: 16,
|
|
48
|
+
fontFamily: 'Arial',
|
|
49
|
+
fontWeight: 'normal',
|
|
50
|
+
fontStyle: 'normal',
|
|
51
|
+
color: '#000000',
|
|
52
|
+
textAlign: 'left',
|
|
53
|
+
opacity: 1,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const setTool = useCallback((tool: string) => {
|
|
57
|
+
const newState = { ...toolState, currentTool: tool };
|
|
58
|
+
setToolState(newState);
|
|
59
|
+
onStateChange?.(newState);
|
|
60
|
+
}, [toolState, onStateChange]);
|
|
61
|
+
|
|
62
|
+
const startDrawing = useCallback((point: { x: number; y: number }) => {
|
|
63
|
+
if (!canvas) return;
|
|
64
|
+
|
|
65
|
+
const ctx = canvas.getContext('2d');
|
|
66
|
+
if (!ctx) return;
|
|
67
|
+
|
|
68
|
+
const newState = {
|
|
69
|
+
...toolState,
|
|
70
|
+
isDrawing: true,
|
|
71
|
+
startPoint: point,
|
|
72
|
+
currentPath: [point],
|
|
73
|
+
};
|
|
74
|
+
setToolState(newState);
|
|
75
|
+
}, [toolState, canvas]);
|
|
76
|
+
|
|
77
|
+
const draw = useCallback((point: { x: number; y: number }) => {
|
|
78
|
+
if (!toolState.isDrawing || !canvas) return;
|
|
79
|
+
|
|
80
|
+
const ctx = canvas.getContext('2d');
|
|
81
|
+
if (!ctx) return;
|
|
82
|
+
|
|
83
|
+
const newPath = [...toolState.currentPath, point];
|
|
84
|
+
|
|
85
|
+
setToolState(prev => ({
|
|
86
|
+
...prev,
|
|
87
|
+
currentPath: newPath,
|
|
88
|
+
}));
|
|
89
|
+
|
|
90
|
+
// Real-time drawing feedback
|
|
91
|
+
const tempPath = [...toolState.currentPath, point];
|
|
92
|
+
DrawingEngine.drawStroke(ctx, tempPath, drawingConfig);
|
|
93
|
+
}, [toolState.isDrawing, toolState.currentPath, drawingConfig, canvas]);
|
|
94
|
+
|
|
95
|
+
const stopDrawing = useCallback(() => {
|
|
96
|
+
if (!toolState.isDrawing || !canvas) return;
|
|
97
|
+
|
|
98
|
+
const ctx = canvas.getContext('2d');
|
|
99
|
+
if (!ctx) return;
|
|
100
|
+
|
|
101
|
+
// Finalize the stroke
|
|
102
|
+
DrawingEngine.drawStroke(ctx, toolState.currentPath, drawingConfig);
|
|
103
|
+
|
|
104
|
+
const newState = {
|
|
105
|
+
...toolState,
|
|
106
|
+
isDrawing: false,
|
|
107
|
+
currentPath: [],
|
|
108
|
+
startPoint: undefined,
|
|
109
|
+
};
|
|
110
|
+
setToolState(newState);
|
|
111
|
+
onStateChange?.(newState);
|
|
112
|
+
}, [toolState, drawingConfig, canvas, onStateChange]);
|
|
113
|
+
|
|
114
|
+
const drawShape = useCallback((
|
|
115
|
+
type: 'rectangle' | 'circle' | 'line' | 'arrow',
|
|
116
|
+
startPoint: { x: number; y: number },
|
|
117
|
+
endPoint: { x: number; y: number },
|
|
118
|
+
style: ShapeStyle
|
|
119
|
+
) => {
|
|
120
|
+
if (!canvas) return;
|
|
121
|
+
|
|
122
|
+
const ctx = canvas.getContext('2d');
|
|
123
|
+
if (!ctx) return;
|
|
124
|
+
|
|
125
|
+
DrawingEngine.drawShape(ctx, type, startPoint, endPoint, style as any);
|
|
126
|
+
onStateChange?.({ toolState, shape: { type, startPoint, endPoint, style } });
|
|
127
|
+
}, [canvas, onStateChange, toolState]);
|
|
128
|
+
|
|
129
|
+
const addText = useCallback((
|
|
130
|
+
text: string,
|
|
131
|
+
position: { x: number; y: number },
|
|
132
|
+
formatting?: Partial<TextFormatting>
|
|
133
|
+
) => {
|
|
134
|
+
if (!canvas) return;
|
|
135
|
+
|
|
136
|
+
const ctx = canvas.getContext('2d');
|
|
137
|
+
if (!ctx) return;
|
|
138
|
+
|
|
139
|
+
const finalFormatting = { ...textFormatting, ...formatting };
|
|
140
|
+
TextEditor.renderText(ctx, text, position, finalFormatting);
|
|
141
|
+
|
|
142
|
+
setTextFormatting(finalFormatting);
|
|
143
|
+
onStateChange?.({ toolState, text: { text, position, formatting: finalFormatting } });
|
|
144
|
+
}, [textFormatting, canvas, onStateChange, toolState]);
|
|
145
|
+
|
|
146
|
+
const setCropArea = useCallback((cropArea: any) => {
|
|
147
|
+
if (!canvas) return;
|
|
148
|
+
|
|
149
|
+
const ctx = canvas.getContext('2d');
|
|
150
|
+
if (!ctx) return;
|
|
151
|
+
|
|
152
|
+
CropTool.drawCropOverlay(ctx, cropArea, cropConfig);
|
|
153
|
+
onStateChange?.({ toolState, cropArea });
|
|
154
|
+
}, [cropConfig, canvas, onStateChange, toolState]);
|
|
155
|
+
|
|
156
|
+
const clearCanvas = useCallback(() => {
|
|
157
|
+
if (!canvas) return;
|
|
158
|
+
|
|
159
|
+
const ctx = canvas.getContext('2d');
|
|
160
|
+
if (!ctx) return;
|
|
161
|
+
|
|
162
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
163
|
+
onStateChange?.({ toolState, cleared: true });
|
|
164
|
+
}, [canvas, onStateChange, toolState]);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
// State
|
|
168
|
+
toolState,
|
|
169
|
+
drawingConfig,
|
|
170
|
+
cropConfig,
|
|
171
|
+
textFormatting,
|
|
172
|
+
|
|
173
|
+
// Actions
|
|
174
|
+
setTool,
|
|
175
|
+
startDrawing,
|
|
176
|
+
draw,
|
|
177
|
+
stopDrawing,
|
|
178
|
+
drawShape,
|
|
179
|
+
addText,
|
|
180
|
+
setCropArea,
|
|
181
|
+
clearCanvas,
|
|
182
|
+
|
|
183
|
+
// Config setters
|
|
184
|
+
setDrawingConfig,
|
|
185
|
+
setCropConfig,
|
|
186
|
+
setTextFormatting,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -6,16 +6,47 @@
|
|
|
6
6
|
|
|
7
7
|
import { useImageTransform } from './useImageTransform';
|
|
8
8
|
import { useImageConversion } from './useImageConversion';
|
|
9
|
+
import { useImageFilter } from './useImageFilter';
|
|
10
|
+
import { useImageBatch } from './useImageBatch';
|
|
11
|
+
import { useImageAIEnhancement } from './useImageAIEnhancement';
|
|
12
|
+
import { useImageAnnotation } from './useImageAnnotation';
|
|
13
|
+
import { useImageMetadata } from './useImageMetadata';
|
|
9
14
|
|
|
10
15
|
export const useImage = () => {
|
|
11
16
|
const transform = useImageTransform();
|
|
12
17
|
const conversion = useImageConversion();
|
|
18
|
+
const filter = useImageFilter();
|
|
19
|
+
const batch = useImageBatch();
|
|
20
|
+
const aiEnhancement = useImageAIEnhancement();
|
|
21
|
+
const annotation = useImageAnnotation();
|
|
22
|
+
const metadata = useImageMetadata();
|
|
13
23
|
|
|
14
24
|
return {
|
|
25
|
+
// Basic operations
|
|
15
26
|
...transform,
|
|
16
27
|
...conversion,
|
|
28
|
+
// Advanced operations
|
|
29
|
+
...filter,
|
|
30
|
+
...batch,
|
|
31
|
+
...aiEnhancement,
|
|
32
|
+
...annotation,
|
|
33
|
+
...metadata,
|
|
17
34
|
// Combined state
|
|
18
|
-
isProcessing:
|
|
19
|
-
|
|
35
|
+
isProcessing:
|
|
36
|
+
transform.isTransforming ||
|
|
37
|
+
conversion.isConverting ||
|
|
38
|
+
filter.isFiltering ||
|
|
39
|
+
batch.isBatchProcessing ||
|
|
40
|
+
aiEnhancement.isEnhancing ||
|
|
41
|
+
annotation.isAnnotating ||
|
|
42
|
+
metadata.isExtracting,
|
|
43
|
+
error:
|
|
44
|
+
transform.transformError ||
|
|
45
|
+
conversion.conversionError ||
|
|
46
|
+
filter.filterError ||
|
|
47
|
+
batch.batchError ||
|
|
48
|
+
aiEnhancement.enhancementError ||
|
|
49
|
+
annotation.annotationError ||
|
|
50
|
+
metadata.metadataError,
|
|
20
51
|
};
|
|
21
52
|
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Image AI Enhancement Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
import { useImageOperation } from './useImageOperation';
|
|
7
|
+
import { ImageAIEnhancementService, type AutoEnhancementOptions, type EnhancementResult } from '../../infrastructure/services/ImageAIEnhancementService';
|
|
8
|
+
import { ImageSpecializedEnhancementService } from '../../infrastructure/services/ImageSpecializedEnhancementService';
|
|
9
|
+
|
|
10
|
+
export const useImageAIEnhancement = () => {
|
|
11
|
+
const { isProcessing, error, execute } = useImageOperation();
|
|
12
|
+
|
|
13
|
+
const autoEnhance = useCallback((uri: string, options?: AutoEnhancementOptions) =>
|
|
14
|
+
execute(() => ImageAIEnhancementService.autoEnhance(uri, options), 'Failed to auto enhance'), [execute]);
|
|
15
|
+
|
|
16
|
+
const enhancePortrait = useCallback((uri: string) =>
|
|
17
|
+
execute(() => ImageSpecializedEnhancementService.enhancePortrait(uri), 'Failed to enhance portrait'), [execute]);
|
|
18
|
+
|
|
19
|
+
const enhanceLandscape = useCallback((uri: string) =>
|
|
20
|
+
execute(() => ImageSpecializedEnhancementService.enhanceLandscape(uri), 'Failed to enhance landscape'), [execute]);
|
|
21
|
+
|
|
22
|
+
const analyzeImage = useCallback((uri: string) =>
|
|
23
|
+
execute(() => ImageAIEnhancementService.analyzeImage(uri), 'Failed to analyze image'), [execute]);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
autoEnhance,
|
|
27
|
+
enhancePortrait,
|
|
28
|
+
enhanceLandscape,
|
|
29
|
+
analyzeImage,
|
|
30
|
+
isEnhancing: isProcessing,
|
|
31
|
+
enhancementError: error,
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Image Annotation Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
import { useImageOperation } from './useImageOperation';
|
|
7
|
+
import { ImageAnnotationService, type ImageAnnotation, type TextOverlay, type DrawingElement, type WatermarkOptions } from '../../infrastructure/services/ImageAnnotationService';
|
|
8
|
+
|
|
9
|
+
export const useImageAnnotation = () => {
|
|
10
|
+
const { isProcessing, error, execute } = useImageOperation();
|
|
11
|
+
|
|
12
|
+
const addTextOverlay = useCallback((uri: string, overlay: TextOverlay) =>
|
|
13
|
+
execute(() => ImageAnnotationService.addTextOverlay(uri, overlay), 'Failed to add text'), [execute]);
|
|
14
|
+
|
|
15
|
+
const addDrawingElements = useCallback((uri: string, elements: DrawingElement[]) =>
|
|
16
|
+
execute(() => ImageAnnotationService.addDrawingElements(uri, elements), 'Failed to add drawing'), [execute]);
|
|
17
|
+
|
|
18
|
+
const addWatermark = useCallback((uri: string, options: WatermarkOptions) =>
|
|
19
|
+
execute(() => ImageAnnotationService.addWatermark(uri, options), 'Failed to add watermark'), [execute]);
|
|
20
|
+
|
|
21
|
+
const applyAnnotation = useCallback((uri: string, annotation: ImageAnnotation) =>
|
|
22
|
+
execute(() => ImageAnnotationService.applyAnnotation(uri, annotation), 'Failed to apply annotation'), [execute]);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
addTextOverlay,
|
|
26
|
+
addDrawingElements,
|
|
27
|
+
addWatermark,
|
|
28
|
+
applyAnnotation,
|
|
29
|
+
isAnnotating: isProcessing,
|
|
30
|
+
annotationError: error,
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation - Image Batch Hook
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { useCallback } from 'react';
|
|
6
|
+
import { useImageOperation } from './useImageOperation';
|
|
7
|
+
import { ImageBatchService, type BatchOperation, type BatchProcessingOptions } from '../../infrastructure/services/ImageBatchService';
|
|
8
|
+
import type { ImageFilter } from '../../domain/entities/ImageFilterTypes';
|
|
9
|
+
|
|
10
|
+
export const useImageBatch = () => {
|
|
11
|
+
const { isProcessing, error, execute } = useImageOperation();
|
|
12
|
+
|
|
13
|
+
const processBatch = useCallback((operations: BatchOperation[], options?: BatchProcessingOptions) =>
|
|
14
|
+
execute(() => ImageBatchService.processBatch(operations, options), 'Failed to process batch'), [execute]);
|
|
15
|
+
|
|
16
|
+
const resizeBatch = useCallback((uris: string[], width?: number, height?: number, options?: BatchProcessingOptions & { saveOptions?: any }) =>
|
|
17
|
+
execute(() => ImageBatchService.resizeBatch(uris, width, height, options), 'Failed to resize batch'), [execute]);
|
|
18
|
+
|
|
19
|
+
const compressBatch = useCallback((uris: string[], quality?: number, options?: BatchProcessingOptions) =>
|
|
20
|
+
execute(() => ImageBatchService.compressBatch(uris, quality, options), 'Failed to compress batch'), [execute]);
|
|
21
|
+
|
|
22
|
+
const filterBatch = useCallback((uris: string[], filter: ImageFilter, options?: BatchProcessingOptions) =>
|
|
23
|
+
execute(() => ImageBatchService.filterBatch(uris, filter, options), 'Failed to filter batch'), [execute]);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
processBatch,
|
|
27
|
+
resizeBatch,
|
|
28
|
+
compressBatch,
|
|
29
|
+
filterBatch,
|
|
30
|
+
isBatchProcessing: isProcessing,
|
|
31
|
+
batchError: error,
|
|
32
|
+
};
|
|
33
|
+
};
|