@umituz/react-native-photo-editor 1.0.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 +32 -0
- package/src/PhotoEditor.tsx +153 -0
- package/src/components/AIMagicSheet.tsx +174 -0
- package/src/components/DraggableSticker.tsx +100 -0
- package/src/components/DraggableText.tsx +143 -0
- package/src/components/EditorCanvas.tsx +80 -0
- package/src/components/EditorToolbar.tsx +54 -0
- package/src/components/FilterPicker.tsx +102 -0
- package/src/components/FontControls.tsx +82 -0
- package/src/components/LayerManager.tsx +111 -0
- package/src/components/StickerPicker.tsx +94 -0
- package/src/components/TextEditorSheet.tsx +63 -0
- package/src/core/HistoryManager.ts +75 -0
- package/src/hooks/usePhotoEditor.ts +165 -0
- package/src/hooks/usePhotoEditorUI.ts +126 -0
- package/src/index.ts +11 -0
- package/src/styles.ts +160 -0
- package/src/types.ts +50 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, TouchableOpacity } from "react-native";
|
|
3
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
4
|
+
|
|
5
|
+
interface EditorToolbarProps {
|
|
6
|
+
onAddText: () => void;
|
|
7
|
+
onAddSticker: () => void;
|
|
8
|
+
onAIMagic?: () => void; // Optional AI integration
|
|
9
|
+
onOpenFilters: () => void;
|
|
10
|
+
onOpenLayers: () => void;
|
|
11
|
+
styles: Record<string, object>;
|
|
12
|
+
t: (key: string) => string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const EditorToolbar: React.FC<EditorToolbarProps> = ({
|
|
16
|
+
onAddText,
|
|
17
|
+
onAddSticker,
|
|
18
|
+
onAIMagic,
|
|
19
|
+
onOpenFilters,
|
|
20
|
+
onOpenLayers,
|
|
21
|
+
styles,
|
|
22
|
+
t,
|
|
23
|
+
}) => {
|
|
24
|
+
return (
|
|
25
|
+
<View style={styles.bottomToolbar}>
|
|
26
|
+
<TouchableOpacity
|
|
27
|
+
style={[styles.toolButton, styles.toolButtonActive]}
|
|
28
|
+
onPress={onAddText}
|
|
29
|
+
>
|
|
30
|
+
<AtomicIcon name="text" size="md" color="primary" />
|
|
31
|
+
<AtomicText style={[styles.toolLabel, styles.toolLabelActive]}>
|
|
32
|
+
Text
|
|
33
|
+
</AtomicText>
|
|
34
|
+
</TouchableOpacity>
|
|
35
|
+
<TouchableOpacity style={styles.toolButton} onPress={onAddSticker}>
|
|
36
|
+
<AtomicIcon name="happy" size="md" color="textSecondary" />
|
|
37
|
+
<AtomicText style={styles.toolLabel}>{t("editor.sticker")}</AtomicText>
|
|
38
|
+
</TouchableOpacity>
|
|
39
|
+
{onAIMagic && (
|
|
40
|
+
<TouchableOpacity style={styles.aiMagicButton} onPress={onAIMagic}>
|
|
41
|
+
<AtomicIcon name="sparkles" size="lg" customColor="#fff" />
|
|
42
|
+
</TouchableOpacity>
|
|
43
|
+
)}
|
|
44
|
+
<TouchableOpacity style={styles.toolButton} onPress={onOpenFilters}>
|
|
45
|
+
<AtomicIcon name="color-filter" size="md" color="textSecondary" />
|
|
46
|
+
<AtomicText style={styles.toolLabel}>{t("editor.filters")}</AtomicText>
|
|
47
|
+
</TouchableOpacity>
|
|
48
|
+
<TouchableOpacity style={styles.toolButton} onPress={onOpenLayers}>
|
|
49
|
+
<AtomicIcon name="layers" size="md" color="textSecondary" />
|
|
50
|
+
<AtomicText style={styles.toolLabel}>Layer</AtomicText>
|
|
51
|
+
</TouchableOpacity>
|
|
52
|
+
</View>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
AtomicText,
|
|
5
|
+
AtomicIcon,
|
|
6
|
+
useAppDesignTokens,
|
|
7
|
+
} from "@umituz/react-native-design-system";
|
|
8
|
+
|
|
9
|
+
interface FilterOption {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
icon: string;
|
|
13
|
+
value: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const FILTERS: FilterOption[] = [
|
|
17
|
+
{ id: "none", name: "None", icon: "close-circle", value: 0 },
|
|
18
|
+
{ id: "sepia", name: "Sepia", icon: "color-palette", value: 0.5 },
|
|
19
|
+
{ id: "grayscale", name: "B&W", icon: "contrast", value: 1 },
|
|
20
|
+
{ id: "vintage", name: "Vintage", icon: "time", value: 0.7 },
|
|
21
|
+
{ id: "warm", name: "Warm", icon: "sunny", value: 0.3 },
|
|
22
|
+
{ id: "cool", name: "Cool", icon: "snow", value: 0.3 },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
interface FilterPickerProps {
|
|
26
|
+
selectedFilter: string;
|
|
27
|
+
onSelectFilter: (filterId: string, value: number) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const FilterPicker: React.FC<FilterPickerProps> = ({
|
|
31
|
+
selectedFilter,
|
|
32
|
+
onSelectFilter,
|
|
33
|
+
}) => {
|
|
34
|
+
const tokens = useAppDesignTokens();
|
|
35
|
+
|
|
36
|
+
const styles = StyleSheet.create({
|
|
37
|
+
container: { padding: 16 },
|
|
38
|
+
title: {
|
|
39
|
+
fontSize: 18,
|
|
40
|
+
fontWeight: "bold",
|
|
41
|
+
color: tokens.colors.textPrimary,
|
|
42
|
+
marginBottom: 16,
|
|
43
|
+
},
|
|
44
|
+
grid: {
|
|
45
|
+
flexDirection: "row",
|
|
46
|
+
flexWrap: "wrap",
|
|
47
|
+
gap: 12,
|
|
48
|
+
},
|
|
49
|
+
filter: {
|
|
50
|
+
width: 80,
|
|
51
|
+
height: 80,
|
|
52
|
+
borderRadius: 12,
|
|
53
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
justifyContent: "center",
|
|
56
|
+
borderWidth: 2,
|
|
57
|
+
borderColor: "transparent",
|
|
58
|
+
},
|
|
59
|
+
filterActive: {
|
|
60
|
+
borderColor: tokens.colors.primary,
|
|
61
|
+
backgroundColor: tokens.colors.primaryContainer,
|
|
62
|
+
},
|
|
63
|
+
filterName: {
|
|
64
|
+
marginTop: 4,
|
|
65
|
+
fontSize: 12,
|
|
66
|
+
color: tokens.colors.textSecondary,
|
|
67
|
+
},
|
|
68
|
+
filterNameActive: { color: tokens.colors.primary },
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<View style={styles.container}>
|
|
73
|
+
<AtomicText style={styles.title}>Filters</AtomicText>
|
|
74
|
+
<View style={styles.grid}>
|
|
75
|
+
{FILTERS.map((filter) => (
|
|
76
|
+
<TouchableOpacity
|
|
77
|
+
key={filter.id}
|
|
78
|
+
style={[
|
|
79
|
+
styles.filter,
|
|
80
|
+
selectedFilter === filter.id && styles.filterActive,
|
|
81
|
+
]}
|
|
82
|
+
onPress={() => onSelectFilter(filter.id, filter.value)}
|
|
83
|
+
>
|
|
84
|
+
<AtomicIcon
|
|
85
|
+
name={filter.icon as any}
|
|
86
|
+
size="lg"
|
|
87
|
+
color={selectedFilter === filter.id ? "primary" : "textSecondary"}
|
|
88
|
+
/>
|
|
89
|
+
<AtomicText
|
|
90
|
+
style={[
|
|
91
|
+
styles.filterName,
|
|
92
|
+
selectedFilter === filter.id && styles.filterNameActive,
|
|
93
|
+
]}
|
|
94
|
+
>
|
|
95
|
+
{filter.name}
|
|
96
|
+
</AtomicText>
|
|
97
|
+
</TouchableOpacity>
|
|
98
|
+
))}
|
|
99
|
+
</View>
|
|
100
|
+
</View>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, ScrollView, TouchableOpacity } from "react-native";
|
|
3
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system";
|
|
4
|
+
|
|
5
|
+
interface FontControlsProps {
|
|
6
|
+
fontSize: number;
|
|
7
|
+
selectedFont: string;
|
|
8
|
+
fonts: readonly string[];
|
|
9
|
+
onFontSizeChange: (size: number) => void;
|
|
10
|
+
onFontSelect: (font: string) => void;
|
|
11
|
+
styles: Record<string, object>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const FontControls: React.FC<FontControlsProps> = ({
|
|
15
|
+
fontSize,
|
|
16
|
+
selectedFont,
|
|
17
|
+
fonts,
|
|
18
|
+
onFontSizeChange,
|
|
19
|
+
onFontSelect,
|
|
20
|
+
styles,
|
|
21
|
+
}) => {
|
|
22
|
+
return (
|
|
23
|
+
<View style={styles.controlsPanel}>
|
|
24
|
+
<View style={styles.sliderRow}>
|
|
25
|
+
<View style={styles.sliderLabel}>
|
|
26
|
+
<AtomicIcon name="text" size="sm" color="textSecondary" />
|
|
27
|
+
<AtomicText style={styles.sliderLabelText}>Text Size</AtomicText>
|
|
28
|
+
</View>
|
|
29
|
+
<AtomicText style={styles.sliderValue}>{fontSize}px</AtomicText>
|
|
30
|
+
</View>
|
|
31
|
+
<View style={styles.sliderTrack}>
|
|
32
|
+
<View
|
|
33
|
+
style={[
|
|
34
|
+
styles.sliderFill,
|
|
35
|
+
{ width: `${((fontSize - 12) / 84) * 100}%` },
|
|
36
|
+
]}
|
|
37
|
+
/>
|
|
38
|
+
</View>
|
|
39
|
+
<View style={{ flexDirection: "row", gap: 8, marginBottom: 16 }}>
|
|
40
|
+
<TouchableOpacity
|
|
41
|
+
onPress={() => onFontSizeChange(fontSize - 4)}
|
|
42
|
+
style={styles.fontChip}
|
|
43
|
+
>
|
|
44
|
+
<AtomicText style={styles.fontChipText}>-</AtomicText>
|
|
45
|
+
</TouchableOpacity>
|
|
46
|
+
<TouchableOpacity
|
|
47
|
+
onPress={() => onFontSizeChange(fontSize + 4)}
|
|
48
|
+
style={styles.fontChip}
|
|
49
|
+
>
|
|
50
|
+
<AtomicText style={styles.fontChipText}>+</AtomicText>
|
|
51
|
+
</TouchableOpacity>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
<AtomicText style={styles.fontLabel}>Font Style</AtomicText>
|
|
55
|
+
<ScrollView
|
|
56
|
+
horizontal
|
|
57
|
+
showsHorizontalScrollIndicator={false}
|
|
58
|
+
contentContainerStyle={styles.fontRow}
|
|
59
|
+
>
|
|
60
|
+
{fonts.map((font) => (
|
|
61
|
+
<TouchableOpacity
|
|
62
|
+
key={font}
|
|
63
|
+
style={[
|
|
64
|
+
styles.fontChip,
|
|
65
|
+
selectedFont === font && styles.fontChipActive,
|
|
66
|
+
]}
|
|
67
|
+
onPress={() => onFontSelect(font)}
|
|
68
|
+
>
|
|
69
|
+
<AtomicText
|
|
70
|
+
style={[
|
|
71
|
+
styles.fontChipText,
|
|
72
|
+
selectedFont === font && styles.fontChipTextActive,
|
|
73
|
+
]}
|
|
74
|
+
>
|
|
75
|
+
{font}
|
|
76
|
+
</AtomicText>
|
|
77
|
+
</TouchableOpacity>
|
|
78
|
+
))}
|
|
79
|
+
</ScrollView>
|
|
80
|
+
</View>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
AtomicText,
|
|
5
|
+
AtomicIcon,
|
|
6
|
+
useAppDesignTokens,
|
|
7
|
+
} from "@umituz/react-native-design-system";
|
|
8
|
+
import { Layer, TextLayer } from "../types";
|
|
9
|
+
|
|
10
|
+
interface LayerManagerProps {
|
|
11
|
+
layers: Layer[];
|
|
12
|
+
activeLayerId: string | null;
|
|
13
|
+
onSelectLayer: (id: string) => void;
|
|
14
|
+
onDeleteLayer: (id: string) => void;
|
|
15
|
+
t: (key: string) => string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const LayerManager: React.FC<LayerManagerProps> = ({
|
|
19
|
+
layers,
|
|
20
|
+
activeLayerId,
|
|
21
|
+
onSelectLayer,
|
|
22
|
+
onDeleteLayer,
|
|
23
|
+
t,
|
|
24
|
+
}) => {
|
|
25
|
+
const tokens = useAppDesignTokens();
|
|
26
|
+
|
|
27
|
+
const styles = StyleSheet.create({
|
|
28
|
+
container: { padding: 16 },
|
|
29
|
+
title: {
|
|
30
|
+
fontSize: 18,
|
|
31
|
+
fontWeight: "bold",
|
|
32
|
+
color: tokens.colors.textPrimary,
|
|
33
|
+
marginBottom: 16,
|
|
34
|
+
},
|
|
35
|
+
emptyText: {
|
|
36
|
+
color: tokens.colors.textSecondary,
|
|
37
|
+
textAlign: "center",
|
|
38
|
+
padding: 24,
|
|
39
|
+
},
|
|
40
|
+
layerItem: {
|
|
41
|
+
flexDirection: "row",
|
|
42
|
+
alignItems: "center",
|
|
43
|
+
padding: 12,
|
|
44
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
45
|
+
borderRadius: 12,
|
|
46
|
+
marginBottom: 8,
|
|
47
|
+
borderWidth: 2,
|
|
48
|
+
borderColor: "transparent",
|
|
49
|
+
},
|
|
50
|
+
layerItemActive: {
|
|
51
|
+
borderColor: tokens.colors.primary,
|
|
52
|
+
backgroundColor: tokens.colors.primaryContainer,
|
|
53
|
+
},
|
|
54
|
+
layerInfo: { flex: 1, marginLeft: 12 },
|
|
55
|
+
layerType: { fontSize: 12, color: tokens.colors.textSecondary },
|
|
56
|
+
layerText: {
|
|
57
|
+
fontSize: 14,
|
|
58
|
+
color: tokens.colors.textPrimary,
|
|
59
|
+
fontWeight: "500",
|
|
60
|
+
},
|
|
61
|
+
deleteButton: { padding: 8 },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View style={styles.container}>
|
|
66
|
+
<AtomicText style={styles.title}>Layers</AtomicText>
|
|
67
|
+
{layers.length === 0 ? (
|
|
68
|
+
<AtomicText style={styles.emptyText}>
|
|
69
|
+
No layers yet. Add text or stickers!
|
|
70
|
+
</AtomicText>
|
|
71
|
+
) : (
|
|
72
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
73
|
+
{layers.map((layer) => (
|
|
74
|
+
<TouchableOpacity
|
|
75
|
+
key={layer.id}
|
|
76
|
+
style={[
|
|
77
|
+
styles.layerItem,
|
|
78
|
+
activeLayerId === layer.id && styles.layerItemActive,
|
|
79
|
+
]}
|
|
80
|
+
onPress={() => onSelectLayer(layer.id)}
|
|
81
|
+
>
|
|
82
|
+
<AtomicIcon
|
|
83
|
+
name={layer.type === "text" ? "text" : "happy"}
|
|
84
|
+
size="md"
|
|
85
|
+
color={activeLayerId === layer.id ? "primary" : "textSecondary"}
|
|
86
|
+
/>
|
|
87
|
+
<View style={styles.layerInfo}>
|
|
88
|
+
<AtomicText style={styles.layerType}>
|
|
89
|
+
{layer.type === "text" ? "Text" : "Sticker"}
|
|
90
|
+
</AtomicText>
|
|
91
|
+
<AtomicText style={styles.layerText} numberOfLines={1}>
|
|
92
|
+
{layer.type === "text"
|
|
93
|
+
? (layer as TextLayer).text || t("editor.untitled")
|
|
94
|
+
: "Emoji"}
|
|
95
|
+
</AtomicText>
|
|
96
|
+
</View>
|
|
97
|
+
{layers.length > 1 && (
|
|
98
|
+
<TouchableOpacity
|
|
99
|
+
style={styles.deleteButton}
|
|
100
|
+
onPress={() => onDeleteLayer(layer.id)}
|
|
101
|
+
>
|
|
102
|
+
<AtomicIcon name="trash" size="sm" color="error" />
|
|
103
|
+
</TouchableOpacity>
|
|
104
|
+
)}
|
|
105
|
+
</TouchableOpacity>
|
|
106
|
+
))}
|
|
107
|
+
</ScrollView>
|
|
108
|
+
)}
|
|
109
|
+
</View>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
AtomicText,
|
|
5
|
+
useAppDesignTokens,
|
|
6
|
+
} from "@umituz/react-native-design-system";
|
|
7
|
+
|
|
8
|
+
const STICKERS = [
|
|
9
|
+
"😀",
|
|
10
|
+
"😂",
|
|
11
|
+
"🤣",
|
|
12
|
+
"😍",
|
|
13
|
+
"🥰",
|
|
14
|
+
"😎",
|
|
15
|
+
"🤯",
|
|
16
|
+
"🥳",
|
|
17
|
+
"😤",
|
|
18
|
+
"💀",
|
|
19
|
+
"🔥",
|
|
20
|
+
"❤️",
|
|
21
|
+
"💯",
|
|
22
|
+
"✨",
|
|
23
|
+
"🎉",
|
|
24
|
+
"🤡",
|
|
25
|
+
"👀",
|
|
26
|
+
"🙌",
|
|
27
|
+
"👏",
|
|
28
|
+
"💪",
|
|
29
|
+
"🤝",
|
|
30
|
+
"🙈",
|
|
31
|
+
"🐶",
|
|
32
|
+
"🐱",
|
|
33
|
+
"🦊",
|
|
34
|
+
"🐸",
|
|
35
|
+
"🌟",
|
|
36
|
+
"⭐",
|
|
37
|
+
"🌈",
|
|
38
|
+
"☀️",
|
|
39
|
+
"🌙",
|
|
40
|
+
"💫",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
interface StickerPickerProps {
|
|
44
|
+
onSelectSticker: (sticker: string) => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const StickerPicker: React.FC<StickerPickerProps> = ({
|
|
48
|
+
onSelectSticker,
|
|
49
|
+
}) => {
|
|
50
|
+
const tokens = useAppDesignTokens();
|
|
51
|
+
|
|
52
|
+
const styles = StyleSheet.create({
|
|
53
|
+
container: { padding: 16 },
|
|
54
|
+
title: {
|
|
55
|
+
fontSize: 18,
|
|
56
|
+
fontWeight: "bold",
|
|
57
|
+
color: tokens.colors.textPrimary,
|
|
58
|
+
marginBottom: 16,
|
|
59
|
+
},
|
|
60
|
+
grid: {
|
|
61
|
+
flexDirection: "row",
|
|
62
|
+
flexWrap: "wrap",
|
|
63
|
+
gap: 12,
|
|
64
|
+
},
|
|
65
|
+
sticker: {
|
|
66
|
+
width: 56,
|
|
67
|
+
height: 56,
|
|
68
|
+
borderRadius: 12,
|
|
69
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
justifyContent: "center",
|
|
72
|
+
},
|
|
73
|
+
stickerText: { fontSize: 32 },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<View style={styles.container}>
|
|
78
|
+
<AtomicText style={styles.title}>Emoji</AtomicText>
|
|
79
|
+
<ScrollView showsVerticalScrollIndicator={false}>
|
|
80
|
+
<View style={styles.grid}>
|
|
81
|
+
{STICKERS.map((sticker, index) => (
|
|
82
|
+
<TouchableOpacity
|
|
83
|
+
key={index}
|
|
84
|
+
style={styles.sticker}
|
|
85
|
+
onPress={() => onSelectSticker(sticker)}
|
|
86
|
+
>
|
|
87
|
+
<AtomicText style={styles.stickerText}>{sticker}</AtomicText>
|
|
88
|
+
</TouchableOpacity>
|
|
89
|
+
))}
|
|
90
|
+
</View>
|
|
91
|
+
</ScrollView>
|
|
92
|
+
</View>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
|
|
3
|
+
import {
|
|
4
|
+
AtomicText,
|
|
5
|
+
useAppDesignTokens,
|
|
6
|
+
} from "@umituz/react-native-design-system";
|
|
7
|
+
|
|
8
|
+
interface TextEditorSheetProps {
|
|
9
|
+
value: string;
|
|
10
|
+
onChange: (text: string) => void;
|
|
11
|
+
onSave: () => void;
|
|
12
|
+
t: (key: string) => string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const TextEditorSheet: React.FC<TextEditorSheetProps> = ({
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
onSave,
|
|
19
|
+
t,
|
|
20
|
+
}) => {
|
|
21
|
+
const tokens = useAppDesignTokens();
|
|
22
|
+
|
|
23
|
+
const styles = StyleSheet.create({
|
|
24
|
+
container: { padding: 16 },
|
|
25
|
+
title: { fontSize: 18, fontWeight: "bold", marginBottom: 16 },
|
|
26
|
+
input: {
|
|
27
|
+
backgroundColor: tokens.colors.surfaceVariant,
|
|
28
|
+
borderRadius: 12,
|
|
29
|
+
padding: 16,
|
|
30
|
+
fontSize: 18,
|
|
31
|
+
color: tokens.colors.textPrimary,
|
|
32
|
+
marginBottom: 16,
|
|
33
|
+
textAlign: "center",
|
|
34
|
+
},
|
|
35
|
+
saveButton: {
|
|
36
|
+
backgroundColor: tokens.colors.primary,
|
|
37
|
+
borderRadius: 999,
|
|
38
|
+
padding: 16,
|
|
39
|
+
alignItems: "center",
|
|
40
|
+
},
|
|
41
|
+
saveButtonText: { color: tokens.colors.onPrimary, fontWeight: "bold" },
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<View style={styles.container}>
|
|
46
|
+
<AtomicText style={styles.title}>{t("editor.add_text")}</AtomicText>
|
|
47
|
+
<TextInput
|
|
48
|
+
value={value}
|
|
49
|
+
onChangeText={onChange}
|
|
50
|
+
placeholder={t("editor.tap_to_edit")}
|
|
51
|
+
placeholderTextColor={tokens.colors.textSecondary}
|
|
52
|
+
style={styles.input}
|
|
53
|
+
multiline
|
|
54
|
+
autoFocus
|
|
55
|
+
/>
|
|
56
|
+
<TouchableOpacity onPress={onSave} style={styles.saveButton}>
|
|
57
|
+
<AtomicText style={styles.saveButtonText}>
|
|
58
|
+
{t("common.save")}
|
|
59
|
+
</AtomicText>
|
|
60
|
+
</TouchableOpacity>
|
|
61
|
+
</View>
|
|
62
|
+
);
|
|
63
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History Manager for Undo/Redo functionality
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface HistoryState<T> {
|
|
6
|
+
past: T[];
|
|
7
|
+
present: T;
|
|
8
|
+
future: T[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class HistoryManager<T> {
|
|
12
|
+
private maxHistory = 20;
|
|
13
|
+
|
|
14
|
+
createInitialState(initialValue: T): HistoryState<T> {
|
|
15
|
+
return {
|
|
16
|
+
past: [],
|
|
17
|
+
present: initialValue,
|
|
18
|
+
future: [],
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
push(history: HistoryState<T>, newValue: T): HistoryState<T> {
|
|
23
|
+
const { past, present } = history;
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
past: [...past.slice(-this.maxHistory + 1), present],
|
|
27
|
+
present: newValue,
|
|
28
|
+
future: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
undo(history: HistoryState<T>): HistoryState<T> {
|
|
33
|
+
const { past, present, future } = history;
|
|
34
|
+
|
|
35
|
+
if (past.length === 0) {
|
|
36
|
+
return history;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const previous = past[past.length - 1];
|
|
40
|
+
const newPast = past.slice(0, past.length - 1);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
past: newPast,
|
|
44
|
+
present: previous,
|
|
45
|
+
future: [present, ...future],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
redo(history: HistoryState<T>): HistoryState<T> {
|
|
50
|
+
const { past, present, future } = history;
|
|
51
|
+
|
|
52
|
+
if (future.length === 0) {
|
|
53
|
+
return history;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const next = future[0];
|
|
57
|
+
const newFuture = future.slice(1);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
past: [...past, present],
|
|
61
|
+
present: next,
|
|
62
|
+
future: newFuture,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
canUndo(history: HistoryState<T>): boolean {
|
|
67
|
+
return history.past.length > 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
canRedo(history: HistoryState<T>): boolean {
|
|
71
|
+
return history.future.length > 0;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const historyManager = new HistoryManager();
|