@umituz/react-native-photo-editor 1.0.10 → 1.0.12
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/PhotoEditor.tsx +24 -106
- package/src/components/AIMagicSheet.tsx +28 -119
- package/src/components/DraggableSticker.tsx +30 -39
- package/src/components/DraggableText.tsx +43 -77
- package/src/components/EditorToolbar.tsx +52 -26
- package/src/components/FilterPicker.tsx +19 -51
- package/src/components/FontControls.tsx +48 -51
- package/src/components/LayerManager.tsx +29 -61
- package/src/components/StickerPicker.tsx +10 -28
- package/src/components/TextEditorSheet.tsx +12 -23
- package/src/constants.ts +7 -0
- package/src/core/HistoryManager.ts +1 -1
- package/src/hooks/usePhotoEditor.ts +25 -80
- package/src/hooks/usePhotoEditorUI.ts +29 -74
- package/src/styles.ts +26 -106
package/package.json
CHANGED
package/src/PhotoEditor.tsx
CHANGED
|
@@ -13,10 +13,11 @@ import {
|
|
|
13
13
|
import { EditorCanvas } from "./components/EditorCanvas";
|
|
14
14
|
import { EditorToolbar } from "./components/EditorToolbar";
|
|
15
15
|
import { FontControls } from "./components/FontControls";
|
|
16
|
-
import { StickerPicker } from "./components/StickerPicker";
|
|
17
|
-
import { FilterPicker } from "./components/FilterPicker";
|
|
18
16
|
import { LayerManager } from "./components/LayerManager";
|
|
19
17
|
import { TextEditorSheet } from "./components/TextEditorSheet";
|
|
18
|
+
import { StickerPicker } from "./components/StickerPicker";
|
|
19
|
+
import { FilterPicker } from "./components/FilterPicker";
|
|
20
|
+
import { AIMagicSheet } from "./components/AIMagicSheet";
|
|
20
21
|
import { createEditorStyles } from "./styles";
|
|
21
22
|
import { usePhotoEditorUI } from "./hooks/usePhotoEditorUI";
|
|
22
23
|
import { Layer } from "./types";
|
|
@@ -38,7 +39,6 @@ export interface PhotoEditorProps {
|
|
|
38
39
|
initialCaption?: string;
|
|
39
40
|
t: (key: string) => string;
|
|
40
41
|
fonts?: readonly string[];
|
|
41
|
-
stickers?: readonly string[];
|
|
42
42
|
showAI?: boolean;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -51,124 +51,42 @@ export const PhotoEditor: React.FC<PhotoEditorProps> = ({
|
|
|
51
51
|
initialCaption,
|
|
52
52
|
t,
|
|
53
53
|
fonts = DEFAULT_FONTS,
|
|
54
|
-
|
|
55
|
-
showAI = false,
|
|
54
|
+
showAI = true,
|
|
56
55
|
}) => {
|
|
57
56
|
const tokens = useAppDesignTokens();
|
|
58
57
|
const insets = useSafeAreaInsets();
|
|
59
58
|
const styles = useMemo(() => createEditorStyles(tokens, insets), [tokens, insets]);
|
|
59
|
+
const ui = usePhotoEditorUI(initialCaption, tokens);
|
|
60
60
|
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// State
|
|
68
|
-
selectedFont,
|
|
69
|
-
setSelectedFont,
|
|
70
|
-
fontSize,
|
|
71
|
-
setFontSize,
|
|
72
|
-
editingText,
|
|
73
|
-
setEditingText,
|
|
74
|
-
selectedFilter,
|
|
75
|
-
// Domain State
|
|
76
|
-
layers,
|
|
77
|
-
activeLayerId,
|
|
78
|
-
// Actions
|
|
79
|
-
updateLayer,
|
|
80
|
-
deleteLayer,
|
|
81
|
-
selectLayer,
|
|
82
|
-
addTextLayer,
|
|
83
|
-
handleAddText,
|
|
84
|
-
handleTextLayerTap,
|
|
85
|
-
handleSaveText,
|
|
86
|
-
handleSelectFilter,
|
|
87
|
-
handleSelectSticker,
|
|
88
|
-
} = usePhotoEditorUI(initialCaption, tokens);
|
|
61
|
+
const actions: EditorActions = useMemo(() => ({
|
|
62
|
+
addTextLayer: ui.addTextLayer,
|
|
63
|
+
updateLayer: ui.updateLayer,
|
|
64
|
+
getLayers: () => ui.layers,
|
|
65
|
+
getActiveLayerId: () => ui.activeLayerId,
|
|
66
|
+
}), [ui.addTextLayer, ui.updateLayer, ui.layers, ui.activeLayerId]);
|
|
89
67
|
|
|
90
68
|
return (
|
|
91
69
|
<SafeBottomSheetModalProvider>
|
|
92
70
|
<View style={styles.container}>
|
|
93
71
|
<View style={styles.header}>
|
|
94
|
-
<TouchableOpacity
|
|
95
|
-
|
|
96
|
-
</TouchableOpacity>
|
|
97
|
-
<AtomicText style={styles.headerTitle}>{title}</AtomicText>
|
|
98
|
-
<TouchableOpacity style={styles.postButton} onPress={() => onSave?.(imageUri)}>
|
|
99
|
-
<AtomicText style={styles.postButtonText}>{t("preview.share") || "Share"}</AtomicText>
|
|
100
|
-
</TouchableOpacity>
|
|
72
|
+
<TouchableOpacity onPress={onClose}><AtomicIcon name="close" size="md" color="textPrimary" /></TouchableOpacity>
|
|
73
|
+
<AtomicText type="headlineSmall" style={styles.headerTitle}>{title}</AtomicText>
|
|
74
|
+
<TouchableOpacity onPress={() => onSave?.(imageUri)}><AtomicText fontWeight="bold" color="primary">{t("common.save")}</AtomicText></TouchableOpacity>
|
|
101
75
|
</View>
|
|
102
76
|
|
|
103
|
-
<ScrollView
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
<EditorCanvas
|
|
108
|
-
imageUrl={imageUri}
|
|
109
|
-
layers={layers}
|
|
110
|
-
activeLayerId={activeLayerId}
|
|
111
|
-
onLayerTap={handleTextLayerTap}
|
|
112
|
-
onLayerMove={(id, x, y) => updateLayer(id, { x, y })}
|
|
113
|
-
styles={styles}
|
|
114
|
-
/>
|
|
115
|
-
{typeof customTools === "function"
|
|
116
|
-
? customTools({
|
|
117
|
-
addTextLayer,
|
|
118
|
-
updateLayer,
|
|
119
|
-
getLayers: () => layers,
|
|
120
|
-
getActiveLayerId: () => activeLayerId,
|
|
121
|
-
})
|
|
122
|
-
: customTools}
|
|
123
|
-
<FontControls
|
|
124
|
-
fontSize={fontSize}
|
|
125
|
-
selectedFont={selectedFont}
|
|
126
|
-
fonts={fonts}
|
|
127
|
-
onFontSizeChange={(s) => setFontSize(Math.max(12, Math.min(96, s)))}
|
|
128
|
-
onFontSelect={setSelectedFont}
|
|
129
|
-
styles={styles}
|
|
130
|
-
/>
|
|
77
|
+
<ScrollView contentContainerStyle={styles.scrollContent}>
|
|
78
|
+
<EditorCanvas imageUrl={imageUri} layers={ui.layers} activeLayerId={ui.activeLayerId} onLayerTap={ui.handleTextLayerTap} onLayerMove={(id, x, y) => ui.updateLayer(id, { x, y })} styles={styles} />
|
|
79
|
+
{typeof customTools === "function" ? customTools(actions) : customTools}
|
|
80
|
+
<FontControls fontSize={ui.fontSize} selectedFont={ui.selectedFont} fonts={fonts} onFontSizeChange={ui.setFontSize} onFontSelect={ui.setSelectedFont} styles={styles} />
|
|
131
81
|
</ScrollView>
|
|
132
82
|
|
|
133
|
-
<EditorToolbar
|
|
134
|
-
onAddText={handleAddText}
|
|
135
|
-
onAddSticker={() => stickerSheetRef.current?.present()}
|
|
136
|
-
onOpenFilters={() => filterSheetRef.current?.present()}
|
|
137
|
-
onOpenLayers={() => layerSheetRef.current?.present()}
|
|
138
|
-
onAIMagic={showAI ? undefined : undefined}
|
|
139
|
-
styles={styles}
|
|
140
|
-
t={t}
|
|
141
|
-
/>
|
|
142
|
-
|
|
143
|
-
<BottomSheetModal ref={textEditorSheetRef} snapPoints={["40%"]}>
|
|
144
|
-
<TextEditorSheet
|
|
145
|
-
value={editingText}
|
|
146
|
-
onChange={setEditingText}
|
|
147
|
-
onSave={handleSaveText}
|
|
148
|
-
t={t}
|
|
149
|
-
/>
|
|
150
|
-
</BottomSheetModal>
|
|
151
|
-
|
|
152
|
-
<BottomSheetModal ref={stickerSheetRef} snapPoints={["50%"]}>
|
|
153
|
-
<StickerPicker stickers={stickers} onSelectSticker={handleSelectSticker} />
|
|
154
|
-
</BottomSheetModal>
|
|
155
|
-
|
|
156
|
-
<BottomSheetModal ref={filterSheetRef} snapPoints={["40%"]}>
|
|
157
|
-
<FilterPicker selectedFilter={selectedFilter} onSelectFilter={handleSelectFilter} />
|
|
158
|
-
</BottomSheetModal>
|
|
83
|
+
<EditorToolbar onAddText={ui.handleAddText} onAddSticker={() => ui.stickerSheetRef.current?.present()} onOpenFilters={() => ui.filterSheetRef.current?.present()} onOpenLayers={() => ui.layerSheetRef.current?.present()} onAIMagic={showAI ? () => ui.aiSheetRef.current?.present() : undefined} styles={styles} t={t} />
|
|
159
84
|
|
|
160
|
-
<BottomSheetModal ref={
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
selectLayer(id);
|
|
166
|
-
layerSheetRef.current?.dismiss();
|
|
167
|
-
}}
|
|
168
|
-
onDeleteLayer={deleteLayer}
|
|
169
|
-
t={t}
|
|
170
|
-
/>
|
|
171
|
-
</BottomSheetModal>
|
|
85
|
+
<BottomSheetModal ref={ui.textEditorSheetRef} snapPoints={["40%"]}><TextEditorSheet value={ui.editingText} onChange={ui.setEditingText} onSave={ui.handleSaveText} t={t} /></BottomSheetModal>
|
|
86
|
+
<BottomSheetModal ref={ui.stickerSheetRef} snapPoints={["50%"]}><StickerPicker onSelectSticker={ui.handleSelectSticker} /></BottomSheetModal>
|
|
87
|
+
<BottomSheetModal ref={ui.filterSheetRef} snapPoints={["40%"]}><FilterPicker selectedFilter={ui.selectedFilter} onSelectFilter={ui.handleSelectFilter} /></BottomSheetModal>
|
|
88
|
+
<BottomSheetModal ref={ui.layerSheetRef} snapPoints={["50%"]}><LayerManager layers={ui.layers} activeLayerId={ui.activeLayerId} onSelectLayer={ui.selectLayer} onDeleteLayer={ui.deleteLayer} t={t} /></BottomSheetModal>
|
|
89
|
+
<BottomSheetModal ref={ui.aiSheetRef} snapPoints={["60%"]}><AIMagicSheet onGenerateCaption={(_s) => { ui.aiSheetRef.current?.dismiss(); /* AI trigger */ }} /></BottomSheetModal>
|
|
172
90
|
</View>
|
|
173
91
|
</SafeBottomSheetModalProvider>
|
|
174
92
|
);
|
|
@@ -1,21 +1,10 @@
|
|
|
1
1
|
import React, { useState } from "react";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
ScrollView,
|
|
5
|
-
TouchableOpacity,
|
|
6
|
-
StyleSheet,
|
|
7
|
-
ActivityIndicator,
|
|
8
|
-
} from "react-native";
|
|
9
|
-
import {
|
|
10
|
-
AtomicText,
|
|
11
|
-
AtomicIcon,
|
|
12
|
-
useAppDesignTokens,
|
|
13
|
-
} from "@umituz/react-native-design-system";
|
|
2
|
+
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
+
import { AtomicText, AtomicIcon, useAppDesignTokens, AtomicButton } from "@umituz/react-native-design-system";
|
|
14
4
|
|
|
15
5
|
interface AIMagicSheetProps {
|
|
16
6
|
onGenerateCaption: (style: string) => void;
|
|
17
7
|
isLoading?: boolean;
|
|
18
|
-
_t: (key: string) => string;
|
|
19
8
|
}
|
|
20
9
|
|
|
21
10
|
const AI_STYLES = [
|
|
@@ -30,145 +19,65 @@ const AI_STYLES = [
|
|
|
30
19
|
export const AIMagicSheet: React.FC<AIMagicSheetProps> = ({
|
|
31
20
|
onGenerateCaption,
|
|
32
21
|
isLoading = false,
|
|
33
|
-
_t,
|
|
34
22
|
}) => {
|
|
35
23
|
const tokens = useAppDesignTokens();
|
|
36
|
-
const [
|
|
24
|
+
const [selected, setSelected] = useState<string | null>(null);
|
|
37
25
|
|
|
38
26
|
const styles = StyleSheet.create({
|
|
39
|
-
container: { padding:
|
|
40
|
-
header: {
|
|
27
|
+
container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
|
|
28
|
+
header: { flexDirection: "row", alignItems: "center", gap: tokens.spacing.sm },
|
|
29
|
+
grid: { gap: tokens.spacing.sm },
|
|
30
|
+
card: {
|
|
41
31
|
flexDirection: "row",
|
|
42
32
|
alignItems: "center",
|
|
43
|
-
|
|
44
|
-
marginBottom: 16,
|
|
45
|
-
},
|
|
46
|
-
title: {
|
|
47
|
-
fontSize: 18,
|
|
48
|
-
fontWeight: "bold",
|
|
49
|
-
color: tokens.colors.textPrimary,
|
|
50
|
-
},
|
|
51
|
-
subtitle: {
|
|
52
|
-
fontSize: 14,
|
|
53
|
-
color: tokens.colors.textSecondary,
|
|
54
|
-
marginBottom: 16,
|
|
55
|
-
},
|
|
56
|
-
grid: { gap: 12 },
|
|
57
|
-
styleCard: {
|
|
58
|
-
flexDirection: "row",
|
|
59
|
-
alignItems: "center",
|
|
60
|
-
padding: 16,
|
|
33
|
+
padding: tokens.spacing.md,
|
|
61
34
|
backgroundColor: tokens.colors.surfaceVariant,
|
|
62
|
-
borderRadius:
|
|
35
|
+
borderRadius: tokens.borders.radius.md,
|
|
63
36
|
borderWidth: 2,
|
|
64
37
|
borderColor: "transparent",
|
|
65
38
|
},
|
|
66
|
-
|
|
39
|
+
cardActive: {
|
|
67
40
|
borderColor: tokens.colors.primary,
|
|
68
|
-
backgroundColor: tokens.colors.
|
|
69
|
-
},
|
|
70
|
-
styleInfo: { flex: 1, marginLeft: 12 },
|
|
71
|
-
styleLabel: {
|
|
72
|
-
fontSize: 16,
|
|
73
|
-
fontWeight: "bold",
|
|
74
|
-
color: tokens.colors.textPrimary,
|
|
75
|
-
},
|
|
76
|
-
styleLabelActive: { color: tokens.colors.primary },
|
|
77
|
-
styleDesc: {
|
|
78
|
-
fontSize: 12,
|
|
79
|
-
color: tokens.colors.textSecondary,
|
|
80
|
-
marginTop: 2,
|
|
81
|
-
},
|
|
82
|
-
generateButton: {
|
|
83
|
-
backgroundColor: tokens.colors.primary,
|
|
84
|
-
borderRadius: 999,
|
|
85
|
-
padding: 16,
|
|
86
|
-
flexDirection: "row",
|
|
87
|
-
alignItems: "center",
|
|
88
|
-
justifyContent: "center",
|
|
89
|
-
gap: 8,
|
|
90
|
-
marginTop: 16,
|
|
91
|
-
},
|
|
92
|
-
generateButtonDisabled: { opacity: 0.5 },
|
|
93
|
-
generateButtonText: {
|
|
94
|
-
color: tokens.colors.onPrimary,
|
|
95
|
-
fontWeight: "bold",
|
|
96
|
-
fontSize: 16,
|
|
41
|
+
backgroundColor: tokens.colors.primary + "10",
|
|
97
42
|
},
|
|
43
|
+
info: { flex: 1, marginLeft: tokens.spacing.sm },
|
|
98
44
|
});
|
|
99
45
|
|
|
100
|
-
const handleGenerate = () => {
|
|
101
|
-
if (selectedStyle) {
|
|
102
|
-
onGenerateCaption(selectedStyle);
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
|
|
106
46
|
return (
|
|
107
47
|
<View style={styles.container}>
|
|
108
48
|
<View style={styles.header}>
|
|
109
49
|
<AtomicIcon name="sparkles" size="md" color="primary" />
|
|
110
|
-
<AtomicText
|
|
50
|
+
<AtomicText type="headlineSmall">AI Caption Magic</AtomicText>
|
|
111
51
|
</View>
|
|
112
|
-
<AtomicText style={styles.subtitle}>
|
|
113
|
-
Choose a style and let AI create the perfect caption
|
|
114
|
-
</AtomicText>
|
|
115
|
-
|
|
116
52
|
<ScrollView showsVerticalScrollIndicator={false}>
|
|
117
53
|
<View style={styles.grid}>
|
|
118
54
|
{AI_STYLES.map((style) => (
|
|
119
55
|
<TouchableOpacity
|
|
120
56
|
key={style.id}
|
|
121
|
-
style={[
|
|
122
|
-
|
|
123
|
-
selectedStyle === style.id && styles.styleCardActive,
|
|
124
|
-
]}
|
|
125
|
-
onPress={() => setSelectedStyle(style.id)}
|
|
57
|
+
style={[styles.card, selected === style.id && styles.cardActive]}
|
|
58
|
+
onPress={() => setSelected(style.id)}
|
|
126
59
|
>
|
|
127
|
-
<AtomicText style={{ fontSize:
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
<View style={styles.styleInfo}>
|
|
131
|
-
<AtomicText
|
|
132
|
-
style={[
|
|
133
|
-
styles.styleLabel,
|
|
134
|
-
selectedStyle === style.id && styles.styleLabelActive,
|
|
135
|
-
]}
|
|
136
|
-
>
|
|
60
|
+
<AtomicText style={{ fontSize: 24 }}>{style.label.split(" ")[0]}</AtomicText>
|
|
61
|
+
<View style={styles.info}>
|
|
62
|
+
<AtomicText fontWeight="bold" color={selected === style.id ? "primary" : "textPrimary"}>
|
|
137
63
|
{style.label.split(" ").slice(1).join(" ")}
|
|
138
64
|
</AtomicText>
|
|
139
|
-
<AtomicText
|
|
65
|
+
<AtomicText type="labelSmall" color="textSecondary">{style.desc}</AtomicText>
|
|
140
66
|
</View>
|
|
141
|
-
{
|
|
142
|
-
<AtomicIcon name="checkmark-circle" size="md" color="primary" />
|
|
143
|
-
)}
|
|
67
|
+
{selected === style.id && <AtomicIcon name="checkmark-circle" size="md" color="primary" />}
|
|
144
68
|
</TouchableOpacity>
|
|
145
69
|
))}
|
|
146
70
|
</View>
|
|
147
71
|
</ScrollView>
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
onPress={handleGenerate}
|
|
155
|
-
disabled={!selectedStyle || isLoading}
|
|
72
|
+
<AtomicButton
|
|
73
|
+
variant="primary"
|
|
74
|
+
disabled={!selected || isLoading}
|
|
75
|
+
onPress={() => selected && onGenerateCaption(selected)}
|
|
76
|
+
loading={isLoading}
|
|
77
|
+
icon="sparkles"
|
|
156
78
|
>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
) : (
|
|
160
|
-
<>
|
|
161
|
-
<AtomicIcon
|
|
162
|
-
name="sparkles"
|
|
163
|
-
size="sm"
|
|
164
|
-
customColor={tokens.colors.onPrimary}
|
|
165
|
-
/>
|
|
166
|
-
<AtomicText style={styles.generateButtonText}>
|
|
167
|
-
Generate Caption
|
|
168
|
-
</AtomicText>
|
|
169
|
-
</>
|
|
170
|
-
)}
|
|
171
|
-
</TouchableOpacity>
|
|
79
|
+
Generate Caption
|
|
80
|
+
</AtomicButton>
|
|
172
81
|
</View>
|
|
173
82
|
);
|
|
174
83
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { View } from "react-native";
|
|
3
3
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
4
|
import Animated, {
|
|
5
5
|
useAnimatedStyle,
|
|
@@ -15,34 +15,31 @@ interface DraggableStickerProps {
|
|
|
15
15
|
uri: string;
|
|
16
16
|
initialX: number;
|
|
17
17
|
initialY: number;
|
|
18
|
-
rotation?: number;
|
|
19
|
-
scale?: number;
|
|
20
|
-
opacity?: number;
|
|
21
18
|
onDragEnd: (x: number, y: number) => void;
|
|
22
19
|
onPress: () => void;
|
|
23
20
|
isSelected?: boolean;
|
|
21
|
+
rotation?: number;
|
|
22
|
+
scale?: number;
|
|
23
|
+
opacity?: number;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export const DraggableSticker: React.FC<DraggableStickerProps> = ({
|
|
27
27
|
uri,
|
|
28
28
|
initialX,
|
|
29
29
|
initialY,
|
|
30
|
-
rotation = 0,
|
|
31
|
-
scale = 1,
|
|
32
|
-
opacity = 1,
|
|
33
30
|
onDragEnd,
|
|
34
31
|
onPress,
|
|
35
32
|
isSelected,
|
|
33
|
+
rotation = 0,
|
|
34
|
+
scale = 1,
|
|
35
|
+
opacity = 1,
|
|
36
36
|
}) => {
|
|
37
37
|
const tokens = useAppDesignTokens();
|
|
38
38
|
const translateX = useSharedValue(initialX);
|
|
39
39
|
const translateY = useSharedValue(initialY);
|
|
40
|
-
const offset = useSharedValue({ x:
|
|
40
|
+
const offset = useSharedValue({ x: initialX, y: initialY });
|
|
41
41
|
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
const drag = Gesture.Pan()
|
|
45
|
-
.minDistance(5)
|
|
42
|
+
const panGesture = Gesture.Pan()
|
|
46
43
|
.onStart(() => {
|
|
47
44
|
offset.value = { x: translateX.value, y: translateY.value };
|
|
48
45
|
})
|
|
@@ -54,45 +51,39 @@ export const DraggableSticker: React.FC<DraggableStickerProps> = ({
|
|
|
54
51
|
runOnJS(onDragEnd)(translateX.value, translateY.value);
|
|
55
52
|
});
|
|
56
53
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
runOnJS(onPress)();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
const gesture = Gesture.Exclusive(drag, tap);
|
|
54
|
+
const tapGesture = Gesture.Tap().onEnd(() => {
|
|
55
|
+
runOnJS(onPress)();
|
|
56
|
+
});
|
|
64
57
|
|
|
65
58
|
const animatedStyle = useAnimatedStyle(() => ({
|
|
66
59
|
transform: [
|
|
67
60
|
{ translateX: translateX.value },
|
|
68
61
|
{ translateY: translateY.value },
|
|
69
62
|
{ rotate: `${rotation}deg` },
|
|
70
|
-
{ scale
|
|
63
|
+
{ scale },
|
|
71
64
|
],
|
|
72
|
-
opacity
|
|
65
|
+
opacity,
|
|
73
66
|
zIndex: isSelected ? 100 : 50,
|
|
74
67
|
}));
|
|
75
68
|
|
|
76
|
-
const
|
|
77
|
-
container: { position: "absolute", left: 0, top: 0 },
|
|
78
|
-
emojiContainer: {
|
|
79
|
-
padding: 4,
|
|
80
|
-
borderRadius: 8,
|
|
81
|
-
borderWidth: isSelected ? 2 : 0,
|
|
82
|
-
borderColor: tokens.colors.primary,
|
|
83
|
-
borderStyle: "dashed",
|
|
84
|
-
backgroundColor: isSelected
|
|
85
|
-
? tokens.colors.primary + "20"
|
|
86
|
-
: "transparent",
|
|
87
|
-
},
|
|
88
|
-
emoji: { fontSize: 64 },
|
|
89
|
-
});
|
|
69
|
+
const isEmoji = uri.length <= 4 && !uri.startsWith("http");
|
|
90
70
|
|
|
91
71
|
return (
|
|
92
|
-
<GestureDetector gesture={
|
|
93
|
-
<Animated.View style={[
|
|
94
|
-
<View
|
|
95
|
-
|
|
72
|
+
<GestureDetector gesture={Gesture.Exclusive(panGesture, tapGesture)}>
|
|
73
|
+
<Animated.View style={[animatedStyle, { position: "absolute" }]}>
|
|
74
|
+
<View
|
|
75
|
+
style={{
|
|
76
|
+
padding: tokens.spacing.xs,
|
|
77
|
+
borderRadius: tokens.borders.radius.sm,
|
|
78
|
+
borderWidth: isSelected ? 2 : 0,
|
|
79
|
+
borderColor: tokens.colors.primary,
|
|
80
|
+
borderStyle: "dashed",
|
|
81
|
+
backgroundColor: isSelected ? tokens.colors.primary + "10" : "transparent",
|
|
82
|
+
}}
|
|
83
|
+
>
|
|
84
|
+
{isEmoji ? (
|
|
85
|
+
<AtomicText style={{ fontSize: 48 }}>{uri}</AtomicText>
|
|
86
|
+
) : null}
|
|
96
87
|
</View>
|
|
97
88
|
</Animated.View>
|
|
98
89
|
</GestureDetector>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { View } from "react-native";
|
|
3
3
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
4
|
import Animated, {
|
|
5
5
|
useAnimatedStyle,
|
|
@@ -10,25 +10,24 @@ import {
|
|
|
10
10
|
AtomicText,
|
|
11
11
|
useAppDesignTokens,
|
|
12
12
|
} from "@umituz/react-native-design-system";
|
|
13
|
-
import { TextAlign } from "../types";
|
|
14
13
|
|
|
15
14
|
interface DraggableTextProps {
|
|
16
15
|
text: string;
|
|
17
16
|
color: string;
|
|
18
17
|
fontSize?: number;
|
|
19
18
|
fontFamily?: string;
|
|
20
|
-
|
|
19
|
+
initialX: number;
|
|
20
|
+
initialY: number;
|
|
21
|
+
onDragEnd: (x: number, y: number) => void;
|
|
22
|
+
onPress: () => void;
|
|
23
|
+
isSelected?: boolean;
|
|
21
24
|
rotation?: number;
|
|
22
25
|
scale?: number;
|
|
23
26
|
opacity?: number;
|
|
27
|
+
textAlign?: "center" | "left" | "right";
|
|
24
28
|
backgroundColor?: string;
|
|
25
29
|
_strokeColor?: string;
|
|
26
30
|
_strokeWidth?: number;
|
|
27
|
-
initialX: number;
|
|
28
|
-
initialY: number;
|
|
29
|
-
onDragEnd: (x: number, y: number) => void;
|
|
30
|
-
onPress: () => void;
|
|
31
|
-
isSelected?: boolean;
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
export const DraggableText: React.FC<DraggableTextProps> = ({
|
|
@@ -36,26 +35,23 @@ export const DraggableText: React.FC<DraggableTextProps> = ({
|
|
|
36
35
|
color,
|
|
37
36
|
fontSize = 24,
|
|
38
37
|
fontFamily = "System",
|
|
39
|
-
textAlign = "center",
|
|
40
|
-
rotation = 0,
|
|
41
|
-
scale = 1,
|
|
42
|
-
opacity = 1,
|
|
43
|
-
backgroundColor = "transparent",
|
|
44
|
-
_strokeColor,
|
|
45
|
-
_strokeWidth = 2,
|
|
46
38
|
initialX,
|
|
47
39
|
initialY,
|
|
48
40
|
onDragEnd,
|
|
49
41
|
onPress,
|
|
50
42
|
isSelected,
|
|
43
|
+
rotation = 0,
|
|
44
|
+
scale = 1,
|
|
45
|
+
opacity = 1,
|
|
46
|
+
textAlign = "center",
|
|
47
|
+
backgroundColor = "transparent",
|
|
51
48
|
}) => {
|
|
52
49
|
const tokens = useAppDesignTokens();
|
|
53
50
|
const translateX = useSharedValue(initialX);
|
|
54
51
|
const translateY = useSharedValue(initialY);
|
|
55
|
-
const offset = useSharedValue({ x:
|
|
52
|
+
const offset = useSharedValue({ x: initialX, y: initialY });
|
|
56
53
|
|
|
57
|
-
const
|
|
58
|
-
.minDistance(5)
|
|
54
|
+
const panGesture = Gesture.Pan()
|
|
59
55
|
.onStart(() => {
|
|
60
56
|
offset.value = { x: translateX.value, y: translateY.value };
|
|
61
57
|
})
|
|
@@ -67,77 +63,47 @@ export const DraggableText: React.FC<DraggableTextProps> = ({
|
|
|
67
63
|
runOnJS(onDragEnd)(translateX.value, translateY.value);
|
|
68
64
|
});
|
|
69
65
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
.onEnd(() => {
|
|
73
|
-
runOnJS(onPress)();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const gesture = Gesture.Exclusive(drag, tap);
|
|
77
|
-
|
|
78
|
-
const animatedStyle = useAnimatedStyle(() => {
|
|
79
|
-
return {
|
|
80
|
-
transform: [
|
|
81
|
-
{ translateX: translateX.value },
|
|
82
|
-
{ translateY: translateY.value },
|
|
83
|
-
{ rotate: `${rotation}deg` },
|
|
84
|
-
{ scale: scale },
|
|
85
|
-
],
|
|
86
|
-
opacity: opacity,
|
|
87
|
-
zIndex: isSelected ? 100 : 10,
|
|
88
|
-
};
|
|
66
|
+
const tapGesture = Gesture.Tap().onEnd(() => {
|
|
67
|
+
runOnJS(onPress)();
|
|
89
68
|
});
|
|
90
69
|
|
|
70
|
+
const animatedStyle = useAnimatedStyle(() => ({
|
|
71
|
+
transform: [
|
|
72
|
+
{ translateX: translateX.value },
|
|
73
|
+
{ translateY: translateY.value },
|
|
74
|
+
{ rotate: `${rotation}deg` },
|
|
75
|
+
{ scale },
|
|
76
|
+
],
|
|
77
|
+
opacity,
|
|
78
|
+
zIndex: isSelected ? 100 : 10,
|
|
79
|
+
}));
|
|
80
|
+
|
|
91
81
|
return (
|
|
92
|
-
<GestureDetector gesture={
|
|
93
|
-
<Animated.View
|
|
94
|
-
style={[
|
|
95
|
-
styles.container,
|
|
96
|
-
animatedStyle,
|
|
97
|
-
{ position: "absolute", left: 0, top: 0 },
|
|
98
|
-
]}
|
|
99
|
-
>
|
|
82
|
+
<GestureDetector gesture={Gesture.Exclusive(panGesture, tapGesture)}>
|
|
83
|
+
<Animated.View style={[animatedStyle, { position: "absolute" }]}>
|
|
100
84
|
<View
|
|
101
|
-
style={
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
borderWidth: 2,
|
|
110
|
-
borderColor: tokens.colors.primary,
|
|
111
|
-
borderStyle: "dashed",
|
|
112
|
-
backgroundColor:
|
|
113
|
-
backgroundColor === "transparent"
|
|
114
|
-
? `${tokens.colors.primary}10`
|
|
115
|
-
: backgroundColor,
|
|
116
|
-
},
|
|
117
|
-
]}
|
|
85
|
+
style={{
|
|
86
|
+
padding: tokens.spacing.xs,
|
|
87
|
+
borderRadius: tokens.borders.radius.sm,
|
|
88
|
+
borderWidth: isSelected ? 2 : 0,
|
|
89
|
+
borderColor: tokens.colors.primary,
|
|
90
|
+
borderStyle: "dashed",
|
|
91
|
+
backgroundColor: isSelected ? tokens.colors.primary + "10" : backgroundColor,
|
|
92
|
+
}}
|
|
118
93
|
>
|
|
119
94
|
<AtomicText
|
|
95
|
+
fontWeight="900"
|
|
120
96
|
style={{
|
|
121
|
-
fontSize
|
|
122
|
-
fontFamily
|
|
123
|
-
|
|
124
|
-
textAlign
|
|
125
|
-
textTransform: "uppercase",
|
|
126
|
-
color: color,
|
|
127
|
-
minWidth: text ? undefined : 100,
|
|
128
|
-
minHeight: text ? undefined : 40,
|
|
97
|
+
fontSize,
|
|
98
|
+
fontFamily,
|
|
99
|
+
color,
|
|
100
|
+
textAlign,
|
|
129
101
|
}}
|
|
130
102
|
>
|
|
131
|
-
{text}
|
|
103
|
+
{text || "TAP TO EDIT"}
|
|
132
104
|
</AtomicText>
|
|
133
105
|
</View>
|
|
134
106
|
</Animated.View>
|
|
135
107
|
</GestureDetector>
|
|
136
108
|
);
|
|
137
109
|
};
|
|
138
|
-
|
|
139
|
-
const styles = StyleSheet.create({
|
|
140
|
-
container: {
|
|
141
|
-
zIndex: 10,
|
|
142
|
-
},
|
|
143
|
-
});
|