@umituz/react-native-image 1.2.5 → 1.3.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/package.json +6 -6
- package/src/index.ts +5 -34
- package/src/infrastructure/services/ImageAnnotationService.ts +0 -189
- package/src/infrastructure/services/ImageFilterService.ts +0 -168
- package/src/infrastructure/utils/CanvasRenderingService.ts +0 -134
- package/src/infrastructure/utils/CropTool.ts +0 -260
- package/src/infrastructure/utils/DrawingEngine.ts +0 -210
- package/src/infrastructure/utils/FilterEffects.ts +0 -51
- package/src/infrastructure/utils/ShapeRenderer.ts +0 -168
- package/src/infrastructure/utils/TextEditor.ts +0 -273
- package/src/presentation/components/CropComponent.tsx +0 -183
- package/src/presentation/components/Editor.tsx +0 -261
- package/src/presentation/components/EditorCanvas.tsx +0 -135
- package/src/presentation/components/EditorPanel.tsx +0 -321
- package/src/presentation/components/EditorToolbar.tsx +0 -180
- package/src/presentation/components/FilterSlider.tsx +0 -123
- package/src/presentation/hooks/useEditorTools.ts +0 -188
- package/src/presentation/hooks/useImageAnnotation.ts +0 -32
- package/src/presentation/hooks/useImageEditor.ts +0 -182
- package/src/presentation/hooks/useImageFilter.ts +0 -38
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Infrastructure - Text Editor
|
|
3
|
-
*
|
|
4
|
-
* Advanced text editing with rich formatting
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export interface TextFormatting {
|
|
8
|
-
fontSize: number;
|
|
9
|
-
fontFamily: string;
|
|
10
|
-
fontWeight: 'normal' | 'bold';
|
|
11
|
-
fontStyle: 'normal' | 'italic';
|
|
12
|
-
textAlign: 'left' | 'center' | 'right';
|
|
13
|
-
color: string;
|
|
14
|
-
backgroundColor?: string;
|
|
15
|
-
opacity: number;
|
|
16
|
-
lineHeight?: number;
|
|
17
|
-
letterSpacing?: number;
|
|
18
|
-
textShadow?: {
|
|
19
|
-
color: string;
|
|
20
|
-
blur: number;
|
|
21
|
-
offsetX: number;
|
|
22
|
-
offsetY: number;
|
|
23
|
-
};
|
|
24
|
-
textStroke?: {
|
|
25
|
-
color: string;
|
|
26
|
-
width: number;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface TextMeasurement {
|
|
31
|
-
width: number;
|
|
32
|
-
height: number;
|
|
33
|
-
baseline: number;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export class TextEditor {
|
|
37
|
-
private static systemFonts = [
|
|
38
|
-
'Arial',
|
|
39
|
-
'Helvetica',
|
|
40
|
-
'Times New Roman',
|
|
41
|
-
'Georgia',
|
|
42
|
-
'Courier New',
|
|
43
|
-
'Verdana',
|
|
44
|
-
'Comic Sans MS',
|
|
45
|
-
'Impact',
|
|
46
|
-
'Palatino',
|
|
47
|
-
'Garamond',
|
|
48
|
-
'Bookman',
|
|
49
|
-
'Trebuchet MS',
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
private static getFontFamily(fontFamily: string): string {
|
|
53
|
-
// Check if font is available, fallback to system font
|
|
54
|
-
return this.systemFonts.includes(fontFamily) ? fontFamily : 'Arial';
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
static measureText(
|
|
58
|
-
text: string,
|
|
59
|
-
formatting: TextFormatting,
|
|
60
|
-
ctx: CanvasRenderingContext2D
|
|
61
|
-
): TextMeasurement {
|
|
62
|
-
const {
|
|
63
|
-
fontSize,
|
|
64
|
-
fontFamily,
|
|
65
|
-
fontWeight,
|
|
66
|
-
fontStyle,
|
|
67
|
-
lineHeight = 1.2,
|
|
68
|
-
} = formatting;
|
|
69
|
-
|
|
70
|
-
// Build font string
|
|
71
|
-
const fontString = `${fontStyle} ${fontWeight} ${fontSize}px ${this.getFontFamily(fontFamily)}`;
|
|
72
|
-
ctx.font = fontString;
|
|
73
|
-
|
|
74
|
-
// Measure text
|
|
75
|
-
const metrics = ctx.measureText(text);
|
|
76
|
-
const width = metrics.width;
|
|
77
|
-
const height = fontSize * lineHeight;
|
|
78
|
-
const baseline = fontSize * 0.8;
|
|
79
|
-
|
|
80
|
-
return { width, height, baseline };
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
static renderText(
|
|
84
|
-
ctx: CanvasRenderingContext2D,
|
|
85
|
-
text: string,
|
|
86
|
-
position: { x: number; y: number },
|
|
87
|
-
formatting: TextFormatting,
|
|
88
|
-
maxWidth?: number
|
|
89
|
-
): TextMeasurement {
|
|
90
|
-
const measurement = this.measureText(text, formatting, ctx);
|
|
91
|
-
|
|
92
|
-
ctx.save();
|
|
93
|
-
|
|
94
|
-
// Set text properties
|
|
95
|
-
const fontString = `${formatting.fontStyle} ${formatting.fontWeight} ${formatting.fontSize}px ${this.getFontFamily(formatting.fontFamily)}`;
|
|
96
|
-
ctx.font = fontString;
|
|
97
|
-
ctx.fillStyle = formatting.color;
|
|
98
|
-
ctx.globalAlpha = formatting.opacity;
|
|
99
|
-
ctx.textAlign = formatting.textAlign;
|
|
100
|
-
ctx.textBaseline = 'alphabetic';
|
|
101
|
-
|
|
102
|
-
// Apply text shadow
|
|
103
|
-
if (formatting.textShadow) {
|
|
104
|
-
ctx.shadowColor = formatting.textShadow.color;
|
|
105
|
-
ctx.shadowBlur = formatting.textShadow.blur;
|
|
106
|
-
ctx.shadowOffsetX = formatting.textShadow.offsetX;
|
|
107
|
-
ctx.shadowOffsetY = formatting.textShadow.offsetY;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// Draw background if specified
|
|
111
|
-
if (formatting.backgroundColor) {
|
|
112
|
-
const padding = 4;
|
|
113
|
-
let x = position.x;
|
|
114
|
-
let y = position.y - measurement.baseline - padding;
|
|
115
|
-
|
|
116
|
-
if (formatting.textAlign === 'center') {
|
|
117
|
-
x -= measurement.width / 2;
|
|
118
|
-
} else if (formatting.textAlign === 'right') {
|
|
119
|
-
x -= measurement.width;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
ctx.fillStyle = formatting.backgroundColor;
|
|
123
|
-
ctx.fillRect(
|
|
124
|
-
x - padding,
|
|
125
|
-
y - padding,
|
|
126
|
-
measurement.width + padding * 2,
|
|
127
|
-
measurement.height + padding * 2
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
// Reset fill style for text
|
|
131
|
-
ctx.fillStyle = formatting.color;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Draw text
|
|
135
|
-
let drawX = position.x;
|
|
136
|
-
if (formatting.textAlign === 'center') {
|
|
137
|
-
drawX = position.x;
|
|
138
|
-
} else if (formatting.textAlign === 'right') {
|
|
139
|
-
drawX = position.x;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (maxWidth && measurement.width > maxWidth) {
|
|
143
|
-
// Text wrapping
|
|
144
|
-
const words = text.split(' ');
|
|
145
|
-
let currentLine = '';
|
|
146
|
-
let currentY = position.y;
|
|
147
|
-
|
|
148
|
-
for (const word of words) {
|
|
149
|
-
const testLine = currentLine + (currentLine ? ' ' : '') + word;
|
|
150
|
-
const testMetrics = this.measureText(testLine, formatting, ctx);
|
|
151
|
-
|
|
152
|
-
if (testMetrics.width > maxWidth && currentLine) {
|
|
153
|
-
ctx.fillText(currentLine, drawX, currentY);
|
|
154
|
-
currentLine = word;
|
|
155
|
-
currentY += measurement.height;
|
|
156
|
-
} else {
|
|
157
|
-
currentLine = testLine;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
ctx.fillText(currentLine, drawX, currentY);
|
|
161
|
-
} else {
|
|
162
|
-
ctx.fillText(text, drawX, position.y);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Apply text stroke if specified
|
|
166
|
-
if (formatting.textStroke) {
|
|
167
|
-
ctx.strokeStyle = formatting.textStroke.color;
|
|
168
|
-
ctx.lineWidth = formatting.textStroke.width;
|
|
169
|
-
ctx.strokeText(text, drawX, position.y);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
ctx.restore();
|
|
173
|
-
|
|
174
|
-
return measurement;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
static wrapText(
|
|
178
|
-
text: string,
|
|
179
|
-
maxWidth: number,
|
|
180
|
-
formatting: TextFormatting,
|
|
181
|
-
ctx: CanvasRenderingContext2D
|
|
182
|
-
): string[] {
|
|
183
|
-
const words = text.split(' ');
|
|
184
|
-
const lines: string[] = [];
|
|
185
|
-
let currentLine = '';
|
|
186
|
-
|
|
187
|
-
for (const word of words) {
|
|
188
|
-
const testLine = currentLine + (currentLine ? ' ' : '') + word;
|
|
189
|
-
const testMetrics = this.measureText(testLine, formatting, ctx);
|
|
190
|
-
|
|
191
|
-
if (testMetrics.width > maxWidth && currentLine) {
|
|
192
|
-
lines.push(currentLine);
|
|
193
|
-
currentLine = word;
|
|
194
|
-
} else {
|
|
195
|
-
currentLine = testLine;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (currentLine) {
|
|
200
|
-
lines.push(currentLine);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return lines;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
static getAvailableFonts(): string[] {
|
|
207
|
-
return [...this.systemFonts];
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
static calculateTextBounds(
|
|
211
|
-
text: string,
|
|
212
|
-
formatting: TextFormatting,
|
|
213
|
-
ctx: CanvasRenderingContext2D,
|
|
214
|
-
maxWidth?: number
|
|
215
|
-
): {
|
|
216
|
-
x: number;
|
|
217
|
-
y: number;
|
|
218
|
-
width: number;
|
|
219
|
-
height: number;
|
|
220
|
-
} {
|
|
221
|
-
const measurement = this.measureText(text, formatting, ctx);
|
|
222
|
-
|
|
223
|
-
if (maxWidth && measurement.width > maxWidth) {
|
|
224
|
-
const lines = this.wrapText(text, maxWidth, formatting, ctx);
|
|
225
|
-
const lineHeight = measurement.height;
|
|
226
|
-
const totalHeight = lines.length * lineHeight;
|
|
227
|
-
|
|
228
|
-
return {
|
|
229
|
-
x: 0,
|
|
230
|
-
y: 0,
|
|
231
|
-
width: maxWidth,
|
|
232
|
-
height: totalHeight,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return {
|
|
237
|
-
x: 0,
|
|
238
|
-
y: 0,
|
|
239
|
-
width: measurement.width,
|
|
240
|
-
height: measurement.height,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
static createTextPath(
|
|
245
|
-
text: string,
|
|
246
|
-
position: { x: number; y: number },
|
|
247
|
-
formatting: TextFormatting,
|
|
248
|
-
ctx: CanvasRenderingContext2D
|
|
249
|
-
): Path2D | any {
|
|
250
|
-
const measurement = this.measureText(text, formatting, ctx);
|
|
251
|
-
|
|
252
|
-
ctx.save();
|
|
253
|
-
const fontString = `${formatting.fontStyle} ${formatting.fontWeight} ${formatting.fontSize}px ${this.getFontFamily(formatting.fontFamily)}`;
|
|
254
|
-
ctx.font = fontString;
|
|
255
|
-
|
|
256
|
-
let x = position.x;
|
|
257
|
-
if (formatting.textAlign === 'center') {
|
|
258
|
-
x = position.x;
|
|
259
|
-
} else if (formatting.textAlign === 'right') {
|
|
260
|
-
x = position.x;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const path = new Path2D();
|
|
264
|
-
path.moveTo(x, position.y);
|
|
265
|
-
|
|
266
|
-
// This is a simplified version - in a real implementation,
|
|
267
|
-
// you would use more sophisticated text-to-path conversion
|
|
268
|
-
path.rect(x - measurement.width/2, position.y - measurement.baseline, measurement.width, measurement.height);
|
|
269
|
-
|
|
270
|
-
ctx.restore();
|
|
271
|
-
return path;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Presentation - Crop Component
|
|
3
|
-
*
|
|
4
|
-
* Advanced cropping interface with aspect ratios and grid
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React, { useState, useCallback } from 'react';
|
|
8
|
-
import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
|
|
9
|
-
import { CropTool, type CropConfig, type CropArea } from '../../infrastructure/utils/CropTool';
|
|
10
|
-
|
|
11
|
-
interface CropComponentProps {
|
|
12
|
-
imageWidth: number;
|
|
13
|
-
imageHeight: number;
|
|
14
|
-
onCropChange?: (area: CropArea) => void;
|
|
15
|
-
onCropComplete?: (area: CropArea) => void;
|
|
16
|
-
initialArea?: CropArea;
|
|
17
|
-
config?: CropConfig;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function CropComponent({
|
|
21
|
-
imageWidth,
|
|
22
|
-
imageHeight,
|
|
23
|
-
onCropChange,
|
|
24
|
-
onCropComplete,
|
|
25
|
-
initialArea,
|
|
26
|
-
config = {},
|
|
27
|
-
}: CropComponentProps) {
|
|
28
|
-
const [cropArea, setCropArea] = useState<CropArea>(
|
|
29
|
-
initialArea || {
|
|
30
|
-
x: 0,
|
|
31
|
-
y: 0,
|
|
32
|
-
width: imageWidth,
|
|
33
|
-
height: imageHeight,
|
|
34
|
-
}
|
|
35
|
-
);
|
|
36
|
-
const [isDragging, setIsDragging] = useState(false);
|
|
37
|
-
const [dragHandle, setDragHandle] = useState<string | null>(null);
|
|
38
|
-
|
|
39
|
-
const presets = CropTool.getPresets();
|
|
40
|
-
|
|
41
|
-
const handleCropChange = useCallback((newArea: CropArea) => {
|
|
42
|
-
setCropArea(newArea);
|
|
43
|
-
onCropChange?.(newArea);
|
|
44
|
-
}, [onCropChange]);
|
|
45
|
-
|
|
46
|
-
const handlePresetSelect = useCallback((preset: any) => {
|
|
47
|
-
const constrainedArea = CropTool.constrainToAspectRatio(
|
|
48
|
-
cropArea.width,
|
|
49
|
-
cropArea.height,
|
|
50
|
-
preset.aspectRatio
|
|
51
|
-
);
|
|
52
|
-
|
|
53
|
-
const centeredArea = {
|
|
54
|
-
...constrainedArea,
|
|
55
|
-
x: (imageWidth - constrainedArea.width) / 2,
|
|
56
|
-
y: (imageHeight - constrainedArea.height) / 2,
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
handleCropChange(centeredArea);
|
|
60
|
-
}, [cropArea, imageWidth, imageHeight, handleCropChange]);
|
|
61
|
-
|
|
62
|
-
const handleCenterCrop = useCallback(() => {
|
|
63
|
-
const centerCrop = CropTool.centerCrop(imageWidth, imageHeight);
|
|
64
|
-
handleCropChange(centerCrop);
|
|
65
|
-
}, [imageWidth, imageHeight, handleCropChange]);
|
|
66
|
-
|
|
67
|
-
const renderPresets = () => (
|
|
68
|
-
<View style={styles.presetsContainer}>
|
|
69
|
-
<Text style={styles.presetsLabel}>Aspect Ratio</Text>
|
|
70
|
-
<View style={styles.presetButtons}>
|
|
71
|
-
{presets.map((preset) => (
|
|
72
|
-
<TouchableOpacity
|
|
73
|
-
key={preset.name}
|
|
74
|
-
style={[
|
|
75
|
-
styles.presetButton,
|
|
76
|
-
config.aspectRatio === preset.aspectRatio && styles.selectedPreset,
|
|
77
|
-
]}
|
|
78
|
-
onPress={() => handlePresetSelect(preset)}
|
|
79
|
-
>
|
|
80
|
-
<Text style={[
|
|
81
|
-
styles.presetText,
|
|
82
|
-
config.aspectRatio === preset.aspectRatio && styles.presetTextSelected
|
|
83
|
-
]}>{preset.name}</Text>
|
|
84
|
-
</TouchableOpacity>
|
|
85
|
-
))}
|
|
86
|
-
</View>
|
|
87
|
-
</View>
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const renderActions = () => (
|
|
91
|
-
<View style={styles.actionsContainer}>
|
|
92
|
-
<TouchableOpacity
|
|
93
|
-
style={styles.actionButton}
|
|
94
|
-
onPress={handleCenterCrop}
|
|
95
|
-
>
|
|
96
|
-
<Text style={styles.actionButtonText}>Center Crop</Text>
|
|
97
|
-
</TouchableOpacity>
|
|
98
|
-
|
|
99
|
-
<TouchableOpacity
|
|
100
|
-
style={[styles.actionButton, styles.primaryButton]}
|
|
101
|
-
onPress={() => onCropComplete?.(cropArea)}
|
|
102
|
-
>
|
|
103
|
-
<Text style={styles.primaryButtonText}>Apply Crop</Text>
|
|
104
|
-
</TouchableOpacity>
|
|
105
|
-
</View>
|
|
106
|
-
);
|
|
107
|
-
|
|
108
|
-
return (
|
|
109
|
-
<View style={styles.container}>
|
|
110
|
-
{renderPresets()}
|
|
111
|
-
{renderActions()}
|
|
112
|
-
</View>
|
|
113
|
-
);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const styles = StyleSheet.create({
|
|
117
|
-
container: {
|
|
118
|
-
padding: 16,
|
|
119
|
-
gap: 16,
|
|
120
|
-
},
|
|
121
|
-
presetsContainer: {
|
|
122
|
-
gap: 8,
|
|
123
|
-
},
|
|
124
|
-
presetsLabel: {
|
|
125
|
-
fontSize: 16,
|
|
126
|
-
fontWeight: '600',
|
|
127
|
-
color: '#333333',
|
|
128
|
-
marginBottom: 8,
|
|
129
|
-
},
|
|
130
|
-
presetButtons: {
|
|
131
|
-
flexDirection: 'row',
|
|
132
|
-
flexWrap: 'wrap',
|
|
133
|
-
gap: 8,
|
|
134
|
-
},
|
|
135
|
-
presetButton: {
|
|
136
|
-
paddingHorizontal: 12,
|
|
137
|
-
paddingVertical: 8,
|
|
138
|
-
borderRadius: 6,
|
|
139
|
-
borderWidth: 1,
|
|
140
|
-
borderColor: '#d0d0d0',
|
|
141
|
-
backgroundColor: '#ffffff',
|
|
142
|
-
},
|
|
143
|
-
selectedPreset: {
|
|
144
|
-
backgroundColor: '#007bff',
|
|
145
|
-
borderColor: '#007bff',
|
|
146
|
-
},
|
|
147
|
-
presetText: {
|
|
148
|
-
fontSize: 14,
|
|
149
|
-
fontWeight: '500',
|
|
150
|
-
color: '#333333',
|
|
151
|
-
},
|
|
152
|
-
presetTextSelected: {
|
|
153
|
-
color: '#ffffff',
|
|
154
|
-
},
|
|
155
|
-
actionsContainer: {
|
|
156
|
-
flexDirection: 'row',
|
|
157
|
-
justifyContent: 'space-between',
|
|
158
|
-
gap: 12,
|
|
159
|
-
},
|
|
160
|
-
actionButton: {
|
|
161
|
-
flex: 1,
|
|
162
|
-
paddingVertical: 12,
|
|
163
|
-
borderRadius: 8,
|
|
164
|
-
borderWidth: 1,
|
|
165
|
-
borderColor: '#d0d0d0',
|
|
166
|
-
backgroundColor: '#ffffff',
|
|
167
|
-
alignItems: 'center',
|
|
168
|
-
},
|
|
169
|
-
primaryButton: {
|
|
170
|
-
backgroundColor: '#007bff',
|
|
171
|
-
borderColor: '#007bff',
|
|
172
|
-
},
|
|
173
|
-
actionButtonText: {
|
|
174
|
-
fontSize: 16,
|
|
175
|
-
fontWeight: '600',
|
|
176
|
-
color: '#333333',
|
|
177
|
-
},
|
|
178
|
-
primaryButtonText: {
|
|
179
|
-
fontSize: 16,
|
|
180
|
-
fontWeight: '600',
|
|
181
|
-
color: '#ffffff',
|
|
182
|
-
},
|
|
183
|
-
});
|
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Presentation - Editor Component
|
|
3
|
-
*
|
|
4
|
-
* Main image editor interface
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import React, { useState, useCallback } from 'react';
|
|
8
|
-
import { View, StyleSheet, Alert, Text, TouchableOpacity } from 'react-native';
|
|
9
|
-
import { EditorCanvas } from './EditorCanvas';
|
|
10
|
-
import { EditorToolbar } from './EditorToolbar';
|
|
11
|
-
import { EditorPanel } from './EditorPanel';
|
|
12
|
-
import { useImageEditor } from '../hooks/useImageEditor';
|
|
13
|
-
import { EditorTool, EditorExportOptions } from '../../domain/entities/EditorTypes';
|
|
14
|
-
import type { ImageManipulationResult } from '../../domain/entities/ImageTypes';
|
|
15
|
-
|
|
16
|
-
interface EditorProps {
|
|
17
|
-
imageUri: string;
|
|
18
|
-
width?: number;
|
|
19
|
-
height?: number;
|
|
20
|
-
onSave?: (result: ImageManipulationResult) => void | Promise<void>;
|
|
21
|
-
onCancel?: () => void;
|
|
22
|
-
backgroundColor?: string;
|
|
23
|
-
toolbarBackgroundColor?: string;
|
|
24
|
-
panelBackgroundColor?: string;
|
|
25
|
-
exportOptions?: EditorExportOptions;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function Editor({
|
|
29
|
-
imageUri,
|
|
30
|
-
width = 800,
|
|
31
|
-
height = 600,
|
|
32
|
-
onSave,
|
|
33
|
-
onCancel,
|
|
34
|
-
backgroundColor = '#ffffff',
|
|
35
|
-
toolbarBackgroundColor = '#f8f9fa',
|
|
36
|
-
panelBackgroundColor = '#f8f9fa',
|
|
37
|
-
exportOptions,
|
|
38
|
-
}: EditorProps) {
|
|
39
|
-
const [selectedTool, setSelectedTool] = useState<EditorTool>(EditorTool.MOVE);
|
|
40
|
-
const [toolConfig, setToolConfig] = useState<any>({});
|
|
41
|
-
const [canvas, setCanvas] = useState<any>(null);
|
|
42
|
-
|
|
43
|
-
const {
|
|
44
|
-
editorState,
|
|
45
|
-
isProcessing,
|
|
46
|
-
error,
|
|
47
|
-
canUndo,
|
|
48
|
-
canRedo,
|
|
49
|
-
initializeEditor,
|
|
50
|
-
setTool,
|
|
51
|
-
addLayer,
|
|
52
|
-
removeLayer,
|
|
53
|
-
undo,
|
|
54
|
-
redo,
|
|
55
|
-
exportImage,
|
|
56
|
-
cancel,
|
|
57
|
-
} = useImageEditor({
|
|
58
|
-
onSave,
|
|
59
|
-
onCancel,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const handleCanvasReady = useCallback((canvasElement: any) => {
|
|
63
|
-
setCanvas(canvasElement);
|
|
64
|
-
if (imageUri && !editorState) {
|
|
65
|
-
initializeEditor(imageUri);
|
|
66
|
-
}
|
|
67
|
-
}, [imageUri, editorState, initializeEditor]);
|
|
68
|
-
|
|
69
|
-
const handleToolSelect = useCallback((tool: EditorTool) => {
|
|
70
|
-
setSelectedTool(tool);
|
|
71
|
-
setTool(tool);
|
|
72
|
-
setToolConfig({});
|
|
73
|
-
}, [setTool]);
|
|
74
|
-
|
|
75
|
-
const handleToolConfigChange = useCallback((config: any) => {
|
|
76
|
-
setToolConfig(config);
|
|
77
|
-
}, []);
|
|
78
|
-
|
|
79
|
-
const handleSave = useCallback(async () => {
|
|
80
|
-
if (isProcessing) return;
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
await exportImage(exportOptions);
|
|
84
|
-
} catch (err) {
|
|
85
|
-
Alert.alert('Error', 'Failed to save image');
|
|
86
|
-
}
|
|
87
|
-
}, [isProcessing, exportImage, exportImage]);
|
|
88
|
-
|
|
89
|
-
const handleCancel = useCallback(() => {
|
|
90
|
-
cancel();
|
|
91
|
-
}, [cancel]);
|
|
92
|
-
|
|
93
|
-
const handleUndo = useCallback(() => {
|
|
94
|
-
undo();
|
|
95
|
-
}, [undo]);
|
|
96
|
-
|
|
97
|
-
const handleRedo = useCallback(() => {
|
|
98
|
-
redo();
|
|
99
|
-
}, [redo]);
|
|
100
|
-
|
|
101
|
-
return (
|
|
102
|
-
<View style={[styles.container, { backgroundColor }]}>
|
|
103
|
-
{/* Toolbar */}
|
|
104
|
-
<EditorToolbar
|
|
105
|
-
selectedTool={selectedTool}
|
|
106
|
-
onToolSelect={handleToolSelect}
|
|
107
|
-
backgroundColor={toolbarBackgroundColor}
|
|
108
|
-
/>
|
|
109
|
-
|
|
110
|
-
{/* Main Editor Area */}
|
|
111
|
-
<View style={styles.editorArea}>
|
|
112
|
-
<EditorCanvas
|
|
113
|
-
width={width}
|
|
114
|
-
height={height}
|
|
115
|
-
onCanvasReady={handleCanvasReady}
|
|
116
|
-
onToolChange={handleToolSelect}
|
|
117
|
-
onStateChange={handleToolConfigChange}
|
|
118
|
-
backgroundColor={backgroundColor}
|
|
119
|
-
/>
|
|
120
|
-
|
|
121
|
-
{/* Error Display */}
|
|
122
|
-
{error && (
|
|
123
|
-
<View style={styles.errorContainer}>
|
|
124
|
-
<Text style={styles.errorText}>{error}</Text>
|
|
125
|
-
</View>
|
|
126
|
-
)}
|
|
127
|
-
</View>
|
|
128
|
-
|
|
129
|
-
{/* Configuration Panel */}
|
|
130
|
-
{(selectedTool === EditorTool.BRUSH ||
|
|
131
|
-
selectedTool === EditorTool.ERASER ||
|
|
132
|
-
selectedTool === EditorTool.TEXT ||
|
|
133
|
-
selectedTool === EditorTool.CROP) && (
|
|
134
|
-
<EditorPanel
|
|
135
|
-
selectedTool={selectedTool}
|
|
136
|
-
onToolConfigChange={handleToolConfigChange}
|
|
137
|
-
backgroundColor={panelBackgroundColor}
|
|
138
|
-
/>
|
|
139
|
-
)}
|
|
140
|
-
|
|
141
|
-
{/* Action Buttons */}
|
|
142
|
-
<View style={styles.actionBar}>
|
|
143
|
-
<View style={styles.undoRedoContainer}>
|
|
144
|
-
<TouchableOpacity
|
|
145
|
-
style={[styles.actionButton, !canUndo && styles.disabledButton]}
|
|
146
|
-
onPress={handleUndo}
|
|
147
|
-
disabled={!canUndo || isProcessing}
|
|
148
|
-
>
|
|
149
|
-
<Text style={styles.actionButtonText}>↶</Text>
|
|
150
|
-
</TouchableOpacity>
|
|
151
|
-
|
|
152
|
-
<TouchableOpacity
|
|
153
|
-
style={[styles.actionButton, !canRedo && styles.disabledButton]}
|
|
154
|
-
onPress={handleRedo}
|
|
155
|
-
disabled={!canRedo || isProcessing}
|
|
156
|
-
>
|
|
157
|
-
<Text style={styles.actionButtonText}>↷</Text>
|
|
158
|
-
</TouchableOpacity>
|
|
159
|
-
</View>
|
|
160
|
-
|
|
161
|
-
<View style={styles.saveCancelContainer}>
|
|
162
|
-
<TouchableOpacity
|
|
163
|
-
style={[styles.actionButton, styles.cancelButton]}
|
|
164
|
-
onPress={handleCancel}
|
|
165
|
-
disabled={isProcessing}
|
|
166
|
-
>
|
|
167
|
-
<Text style={styles.cancelButtonText}>Cancel</Text>
|
|
168
|
-
</TouchableOpacity>
|
|
169
|
-
|
|
170
|
-
<TouchableOpacity
|
|
171
|
-
style={[styles.actionButton, styles.saveButton, isProcessing && styles.processingButton]}
|
|
172
|
-
onPress={handleSave}
|
|
173
|
-
disabled={isProcessing}
|
|
174
|
-
>
|
|
175
|
-
<Text style={styles.saveButtonText}>
|
|
176
|
-
{isProcessing ? 'Saving...' : 'Save'}
|
|
177
|
-
</Text>
|
|
178
|
-
</TouchableOpacity>
|
|
179
|
-
</View>
|
|
180
|
-
</View>
|
|
181
|
-
</View>
|
|
182
|
-
);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const styles = StyleSheet.create({
|
|
186
|
-
container: {
|
|
187
|
-
flex: 1,
|
|
188
|
-
backgroundColor: '#ffffff',
|
|
189
|
-
},
|
|
190
|
-
editorArea: {
|
|
191
|
-
flex: 1,
|
|
192
|
-
position: 'relative',
|
|
193
|
-
},
|
|
194
|
-
errorContainer: {
|
|
195
|
-
position: 'absolute',
|
|
196
|
-
top: 16,
|
|
197
|
-
left: 16,
|
|
198
|
-
right: 16,
|
|
199
|
-
backgroundColor: '#ff4444',
|
|
200
|
-
padding: 12,
|
|
201
|
-
borderRadius: 8,
|
|
202
|
-
},
|
|
203
|
-
errorText: {
|
|
204
|
-
color: '#ffffff',
|
|
205
|
-
fontSize: 14,
|
|
206
|
-
fontWeight: '500',
|
|
207
|
-
},
|
|
208
|
-
actionBar: {
|
|
209
|
-
flexDirection: 'row',
|
|
210
|
-
justifyContent: 'space-between',
|
|
211
|
-
alignItems: 'center',
|
|
212
|
-
padding: 16,
|
|
213
|
-
borderTopWidth: 1,
|
|
214
|
-
borderTopColor: '#e0e0e0',
|
|
215
|
-
backgroundColor: '#f8f9fa',
|
|
216
|
-
},
|
|
217
|
-
undoRedoContainer: {
|
|
218
|
-
flexDirection: 'row',
|
|
219
|
-
gap: 8,
|
|
220
|
-
},
|
|
221
|
-
saveCancelContainer: {
|
|
222
|
-
flexDirection: 'row',
|
|
223
|
-
gap: 8,
|
|
224
|
-
},
|
|
225
|
-
actionButton: {
|
|
226
|
-
paddingHorizontal: 16,
|
|
227
|
-
paddingVertical: 8,
|
|
228
|
-
borderRadius: 6,
|
|
229
|
-
borderWidth: 1,
|
|
230
|
-
borderColor: '#d0d0d0',
|
|
231
|
-
backgroundColor: '#ffffff',
|
|
232
|
-
},
|
|
233
|
-
disabledButton: {
|
|
234
|
-
opacity: 0.5,
|
|
235
|
-
},
|
|
236
|
-
cancelButton: {
|
|
237
|
-
borderColor: '#6c757d',
|
|
238
|
-
},
|
|
239
|
-
saveButton: {
|
|
240
|
-
backgroundColor: '#28a745',
|
|
241
|
-
borderColor: '#28a745',
|
|
242
|
-
},
|
|
243
|
-
processingButton: {
|
|
244
|
-
opacity: 0.7,
|
|
245
|
-
},
|
|
246
|
-
actionButtonText: {
|
|
247
|
-
fontSize: 16,
|
|
248
|
-
fontWeight: '600',
|
|
249
|
-
color: '#333333',
|
|
250
|
-
},
|
|
251
|
-
cancelButtonText: {
|
|
252
|
-
fontSize: 16,
|
|
253
|
-
fontWeight: '600',
|
|
254
|
-
color: '#6c757d',
|
|
255
|
-
},
|
|
256
|
-
saveButtonText: {
|
|
257
|
-
fontSize: 16,
|
|
258
|
-
fontWeight: '600',
|
|
259
|
-
color: '#ffffff',
|
|
260
|
-
},
|
|
261
|
-
});
|