@umituz/react-native-photo-editor 2.0.1 → 2.0.3
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 +1 -1
- package/src/components/AIMagicSheet.tsx +3 -3
- package/src/components/EditorCanvas.tsx +20 -22
- package/src/components/EditorToolbar.tsx +7 -16
- package/src/components/FilterPicker.tsx +3 -3
- package/src/components/FontControls.tsx +3 -3
- package/src/components/LayerManager.tsx +4 -4
- package/src/components/StickerPicker.tsx +3 -3
- package/src/components/TextEditorSheet.tsx +5 -5
- package/src/hooks/useImagePicker.ts +5 -5
- package/src/hooks/usePhotoEditorUI.ts +0 -3
package/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from "react";
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
2
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicText, AtomicIcon, AtomicButton } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -21,7 +21,7 @@ export const AIMagicSheet: React.FC<AIMagicSheetProps> = ({
|
|
|
21
21
|
const [selected, setSelected] = useState<string | null>(null);
|
|
22
22
|
const [loading, setLoading] = useState(false);
|
|
23
23
|
|
|
24
|
-
const styles = StyleSheet.create({
|
|
24
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
25
25
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
26
26
|
header: { flexDirection: "row", alignItems: "center", gap: tokens.spacing.sm },
|
|
27
27
|
grid: { gap: tokens.spacing.sm },
|
|
@@ -39,7 +39,7 @@ export const AIMagicSheet: React.FC<AIMagicSheetProps> = ({
|
|
|
39
39
|
backgroundColor: tokens.colors.primary + "10",
|
|
40
40
|
},
|
|
41
41
|
info: { flex: 1, marginLeft: tokens.spacing.sm },
|
|
42
|
-
});
|
|
42
|
+
}), [tokens]);
|
|
43
43
|
|
|
44
44
|
const handleGenerate = async () => {
|
|
45
45
|
if (!selected || !onGenerateCaption) return;
|
|
@@ -3,7 +3,7 @@ import { View, StyleSheet } from "react-native";
|
|
|
3
3
|
import { Image } from "expo-image";
|
|
4
4
|
import { DraggableText, LayerTransform } from "./DraggableText";
|
|
5
5
|
import { DraggableSticker } from "./DraggableSticker";
|
|
6
|
-
import { Layer,
|
|
6
|
+
import { Layer, ImageFilters } from "../types";
|
|
7
7
|
|
|
8
8
|
interface EditorCanvasProps {
|
|
9
9
|
imageUrl: string;
|
|
@@ -60,39 +60,37 @@ export const EditorCanvas: React.FC<EditorCanvasProps> = ({
|
|
|
60
60
|
|
|
61
61
|
{layers.map((layer) => {
|
|
62
62
|
if (layer.type === "text") {
|
|
63
|
-
const textLayer = layer as TextLayer;
|
|
64
63
|
return (
|
|
65
64
|
<DraggableText
|
|
66
65
|
key={layer.id}
|
|
67
|
-
text={
|
|
68
|
-
color={
|
|
69
|
-
fontSize={
|
|
70
|
-
fontFamily={
|
|
71
|
-
textAlign={
|
|
72
|
-
rotation={
|
|
73
|
-
scale={
|
|
74
|
-
opacity={
|
|
75
|
-
backgroundColor={
|
|
76
|
-
isBold={
|
|
77
|
-
isItalic={
|
|
78
|
-
initialX={
|
|
79
|
-
initialY={
|
|
66
|
+
text={layer.text || "Tap to edit"}
|
|
67
|
+
color={layer.color}
|
|
68
|
+
fontSize={layer.fontSize}
|
|
69
|
+
fontFamily={layer.fontFamily}
|
|
70
|
+
textAlign={layer.textAlign}
|
|
71
|
+
rotation={layer.rotation}
|
|
72
|
+
scale={layer.scale}
|
|
73
|
+
opacity={layer.opacity}
|
|
74
|
+
backgroundColor={layer.backgroundColor}
|
|
75
|
+
isBold={layer.isBold}
|
|
76
|
+
isItalic={layer.isItalic}
|
|
77
|
+
initialX={layer.x}
|
|
78
|
+
initialY={layer.y}
|
|
80
79
|
onTransformEnd={(t) => onLayerTransform(layer.id, t)}
|
|
81
80
|
onPress={() => onLayerTap(layer.id)}
|
|
82
81
|
isSelected={activeLayerId === layer.id}
|
|
83
82
|
/>
|
|
84
83
|
);
|
|
85
84
|
} else if (layer.type === "sticker") {
|
|
86
|
-
const stickerLayer = layer as StickerLayer;
|
|
87
85
|
return (
|
|
88
86
|
<DraggableSticker
|
|
89
87
|
key={layer.id}
|
|
90
|
-
uri={
|
|
91
|
-
initialX={
|
|
92
|
-
initialY={
|
|
93
|
-
rotation={
|
|
94
|
-
scale={
|
|
95
|
-
opacity={
|
|
88
|
+
uri={layer.uri}
|
|
89
|
+
initialX={layer.x}
|
|
90
|
+
initialY={layer.y}
|
|
91
|
+
rotation={layer.rotation}
|
|
92
|
+
scale={layer.scale}
|
|
93
|
+
opacity={layer.opacity}
|
|
96
94
|
onTransformEnd={(t) => onLayerTransform(layer.id, t)}
|
|
97
95
|
onPress={() => onLayerTap(layer.id)}
|
|
98
96
|
isSelected={activeLayerId === layer.id}
|
|
@@ -30,7 +30,6 @@ const ToolButton = ({
|
|
|
30
30
|
onPress,
|
|
31
31
|
isActive,
|
|
32
32
|
disabled,
|
|
33
|
-
tokens,
|
|
34
33
|
parentStyles,
|
|
35
34
|
}: {
|
|
36
35
|
icon: string;
|
|
@@ -38,7 +37,6 @@ const ToolButton = ({
|
|
|
38
37
|
onPress: () => void;
|
|
39
38
|
isActive?: boolean;
|
|
40
39
|
disabled?: boolean;
|
|
41
|
-
tokens: ReturnType<typeof useAppDesignTokens>;
|
|
42
40
|
parentStyles: EditorToolbarProps["styles"];
|
|
43
41
|
}) => (
|
|
44
42
|
<TouchableOpacity
|
|
@@ -84,28 +82,25 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
|
|
84
82
|
{onUndo && (
|
|
85
83
|
<ToolButton
|
|
86
84
|
icon="arrow-back"
|
|
87
|
-
label={t("
|
|
85
|
+
label={t("photo_editor.undo") || "Undo"}
|
|
88
86
|
onPress={onUndo}
|
|
89
87
|
disabled={!canUndo}
|
|
90
|
-
tokens={tokens}
|
|
91
88
|
parentStyles={parentStyles}
|
|
92
89
|
/>
|
|
93
90
|
)}
|
|
94
91
|
|
|
95
92
|
<ToolButton
|
|
96
93
|
icon="edit"
|
|
97
|
-
label={t("
|
|
94
|
+
label={t("photo_editor.text") || "Text"}
|
|
98
95
|
onPress={onAddText}
|
|
99
|
-
tokens={tokens}
|
|
100
96
|
parentStyles={parentStyles}
|
|
101
97
|
/>
|
|
102
98
|
|
|
103
99
|
{onAddSticker && (
|
|
104
100
|
<ToolButton
|
|
105
101
|
icon="sparkles"
|
|
106
|
-
label={t("
|
|
102
|
+
label={t("photo_editor.sticker") || "Sticker"}
|
|
107
103
|
onPress={onAddSticker}
|
|
108
|
-
tokens={tokens}
|
|
109
104
|
parentStyles={parentStyles}
|
|
110
105
|
/>
|
|
111
106
|
)}
|
|
@@ -124,9 +119,8 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
|
|
124
119
|
{onOpenAdjustments && (
|
|
125
120
|
<ToolButton
|
|
126
121
|
icon="flash"
|
|
127
|
-
label={t("
|
|
122
|
+
label={t("photo_editor.adjust") || "Adjust"}
|
|
128
123
|
onPress={onOpenAdjustments}
|
|
129
|
-
tokens={tokens}
|
|
130
124
|
parentStyles={parentStyles}
|
|
131
125
|
/>
|
|
132
126
|
)}
|
|
@@ -134,28 +128,25 @@ export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
|
|
134
128
|
{onOpenFilters && (
|
|
135
129
|
<ToolButton
|
|
136
130
|
icon="brush"
|
|
137
|
-
label={t("
|
|
131
|
+
label={t("photo_editor.filters") || "Filters"}
|
|
138
132
|
onPress={onOpenFilters}
|
|
139
|
-
tokens={tokens}
|
|
140
133
|
parentStyles={parentStyles}
|
|
141
134
|
/>
|
|
142
135
|
)}
|
|
143
136
|
|
|
144
137
|
<ToolButton
|
|
145
138
|
icon="copy"
|
|
146
|
-
label={t("
|
|
139
|
+
label={t("photo_editor.layers") || "Layers"}
|
|
147
140
|
onPress={onOpenLayers}
|
|
148
|
-
tokens={tokens}
|
|
149
141
|
parentStyles={parentStyles}
|
|
150
142
|
/>
|
|
151
143
|
|
|
152
144
|
{onRedo && (
|
|
153
145
|
<ToolButton
|
|
154
146
|
icon="chevron-forward"
|
|
155
|
-
label={t("
|
|
147
|
+
label={t("photo_editor.redo") || "Redo"}
|
|
156
148
|
onPress={onRedo}
|
|
157
149
|
disabled={!canRedo}
|
|
158
|
-
tokens={tokens}
|
|
159
150
|
parentStyles={parentStyles}
|
|
160
151
|
/>
|
|
161
152
|
)}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -17,7 +17,7 @@ export const FilterPicker: React.FC<FilterPickerProps> = ({
|
|
|
17
17
|
}) => {
|
|
18
18
|
const tokens = useAppDesignTokens();
|
|
19
19
|
|
|
20
|
-
const styles = StyleSheet.create({
|
|
20
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
21
21
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
22
22
|
grid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
|
|
23
23
|
filter: {
|
|
@@ -34,7 +34,7 @@ export const FilterPicker: React.FC<FilterPickerProps> = ({
|
|
|
34
34
|
borderColor: tokens.colors.primary,
|
|
35
35
|
backgroundColor: tokens.colors.primary + "10",
|
|
36
36
|
},
|
|
37
|
-
});
|
|
37
|
+
}), [tokens]);
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
<View style={styles.container}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -32,7 +32,7 @@ export const FontControls: React.FC<FontControlsProps> = ({
|
|
|
32
32
|
}) => {
|
|
33
33
|
const tokens = useAppDesignTokens();
|
|
34
34
|
|
|
35
|
-
const styles = StyleSheet.create({
|
|
35
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
36
36
|
stepRow: {
|
|
37
37
|
flexDirection: "row",
|
|
38
38
|
gap: tokens.spacing.sm,
|
|
@@ -58,7 +58,7 @@ export const FontControls: React.FC<FontControlsProps> = ({
|
|
|
58
58
|
alignItems: "center",
|
|
59
59
|
gap: tokens.spacing.xs,
|
|
60
60
|
},
|
|
61
|
-
});
|
|
61
|
+
}), [tokens]);
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
64
|
<View style={externalStyles.controlsPanel}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -27,7 +27,7 @@ export const LayerManager: React.FC<LayerManagerProps> = ({
|
|
|
27
27
|
}) => {
|
|
28
28
|
const tokens = useAppDesignTokens();
|
|
29
29
|
|
|
30
|
-
const styles = StyleSheet.create({
|
|
30
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
31
31
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
32
32
|
item: {
|
|
33
33
|
flexDirection: "row",
|
|
@@ -53,7 +53,7 @@ export const LayerManager: React.FC<LayerManagerProps> = ({
|
|
|
53
53
|
padding: tokens.spacing.xs,
|
|
54
54
|
borderRadius: tokens.borders.radius.sm,
|
|
55
55
|
},
|
|
56
|
-
});
|
|
56
|
+
}), [tokens]);
|
|
57
57
|
|
|
58
58
|
const sortedLayers = [...layers].reverse(); // top layer first in list
|
|
59
59
|
|
|
@@ -73,7 +73,7 @@ export const LayerManager: React.FC<LayerManagerProps> = ({
|
|
|
73
73
|
const isActive = activeLayerId === layer.id;
|
|
74
74
|
const label =
|
|
75
75
|
layer.type === "text"
|
|
76
|
-
? (layer as TextLayer).text || t("
|
|
76
|
+
? (layer as TextLayer).text || t("photo_editor.untitled") || "Untitled"
|
|
77
77
|
: "Sticker";
|
|
78
78
|
const isTop = idx === 0;
|
|
79
79
|
const isBottom = idx === sortedLayers.length - 1;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -15,7 +15,7 @@ export const StickerPicker: React.FC<StickerPickerProps> = ({
|
|
|
15
15
|
}) => {
|
|
16
16
|
const tokens = useAppDesignTokens();
|
|
17
17
|
|
|
18
|
-
const styles = StyleSheet.create({
|
|
18
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
19
19
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
20
20
|
grid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
|
|
21
21
|
sticker: {
|
|
@@ -26,7 +26,7 @@ export const StickerPicker: React.FC<StickerPickerProps> = ({
|
|
|
26
26
|
alignItems: "center",
|
|
27
27
|
justifyContent: "center",
|
|
28
28
|
},
|
|
29
|
-
});
|
|
29
|
+
}), [tokens]);
|
|
30
30
|
|
|
31
31
|
return (
|
|
32
32
|
<View style={styles.container}>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
2
|
import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
3
|
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system/atoms";
|
|
4
4
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
@@ -42,7 +42,7 @@ export const TextEditorSheet: React.FC<TextEditorSheetProps> = ({
|
|
|
42
42
|
}) => {
|
|
43
43
|
const tokens = useAppDesignTokens();
|
|
44
44
|
|
|
45
|
-
const styles = StyleSheet.create({
|
|
45
|
+
const styles = useMemo(() => StyleSheet.create({
|
|
46
46
|
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
47
47
|
input: {
|
|
48
48
|
backgroundColor: tokens.colors.surfaceVariant,
|
|
@@ -72,16 +72,16 @@ export const TextEditorSheet: React.FC<TextEditorSheetProps> = ({
|
|
|
72
72
|
borderColor: tokens.colors.primary,
|
|
73
73
|
backgroundColor: tokens.colors.primary + "20",
|
|
74
74
|
},
|
|
75
|
-
});
|
|
75
|
+
}), [tokens]);
|
|
76
76
|
|
|
77
77
|
return (
|
|
78
78
|
<View style={styles.container}>
|
|
79
|
-
<AtomicText type="headlineSmall">{t("
|
|
79
|
+
<AtomicText type="headlineSmall">{t("photo_editor.add_text") || "Edit Text"}</AtomicText>
|
|
80
80
|
|
|
81
81
|
<TextInput
|
|
82
82
|
value={value}
|
|
83
83
|
onChangeText={onChange}
|
|
84
|
-
placeholder={t("
|
|
84
|
+
placeholder={t("photo_editor.tap_to_edit") || "Enter text…"}
|
|
85
85
|
placeholderTextColor={tokens.colors.textSecondary}
|
|
86
86
|
style={styles.input}
|
|
87
87
|
multiline
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
2
|
import * as ImagePicker from "expo-image-picker";
|
|
3
3
|
|
|
4
4
|
export interface ImagePickerResult {
|
|
@@ -16,7 +16,7 @@ export interface UseImagePickerReturn {
|
|
|
16
16
|
export const useImagePicker = (): UseImagePickerReturn => {
|
|
17
17
|
const [loading, setLoading] = useState(false);
|
|
18
18
|
|
|
19
|
-
const pickFromGallery = async (
|
|
19
|
+
const pickFromGallery = useCallback(async (
|
|
20
20
|
options?: ImagePicker.ImagePickerOptions,
|
|
21
21
|
): Promise<ImagePickerResult | null> => {
|
|
22
22
|
setLoading(true);
|
|
@@ -39,9 +39,9 @@ export const useImagePicker = (): UseImagePickerReturn => {
|
|
|
39
39
|
} finally {
|
|
40
40
|
setLoading(false);
|
|
41
41
|
}
|
|
42
|
-
};
|
|
42
|
+
}, []);
|
|
43
43
|
|
|
44
|
-
const takePhoto = async (
|
|
44
|
+
const takePhoto = useCallback(async (
|
|
45
45
|
options?: ImagePicker.ImagePickerOptions,
|
|
46
46
|
): Promise<ImagePickerResult | null> => {
|
|
47
47
|
setLoading(true);
|
|
@@ -63,7 +63,7 @@ export const useImagePicker = (): UseImagePickerReturn => {
|
|
|
63
63
|
} finally {
|
|
64
64
|
setLoading(false);
|
|
65
65
|
}
|
|
66
|
-
};
|
|
66
|
+
}, []);
|
|
67
67
|
|
|
68
68
|
return { pickFromGallery, takePhoto, loading };
|
|
69
69
|
};
|
|
@@ -40,7 +40,6 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
|
|
|
40
40
|
initialCaptionApplied.current = true;
|
|
41
41
|
editor.addTextLayer(tokens.colors.textPrimary, { text: initialCaption });
|
|
42
42
|
}
|
|
43
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
43
|
}, []);
|
|
45
44
|
|
|
46
45
|
const handleTextLayerTap = useCallback(
|
|
@@ -152,12 +151,10 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
|
|
|
152
151
|
fontFamily: selectedFont,
|
|
153
152
|
});
|
|
154
153
|
textEditorSheetRef.current?.present();
|
|
155
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
156
154
|
}, [editor, fontSize, selectedFont, tokens.colors.textPrimary]),
|
|
157
155
|
handleSelectSticker: useCallback((uri: string) => {
|
|
158
156
|
editor.addStickerLayer(uri);
|
|
159
157
|
stickerSheetRef.current?.dismiss();
|
|
160
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
161
158
|
}, [editor]),
|
|
162
159
|
};
|
|
163
160
|
};
|