@umituz/react-native-photo-editor 2.0.24 → 2.0.26
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 +43 -137
- package/src/application/hooks/useEditor.ts +4 -6
- package/src/application/hooks/useEditorUI.ts +8 -5
- package/src/application/stores/EditorStore.ts +42 -17
- package/src/domain/entities/Layer.entity.ts +138 -0
- package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
- package/src/domain/entities/StickerLayer.entity.ts +37 -0
- package/src/domain/entities/TextLayer.entity.ts +58 -0
- package/src/domain/entities/index.ts +9 -0
- package/src/domain/services/History.service.ts +69 -0
- package/src/domain/services/LayerFactory.service.ts +81 -0
- package/src/domain/services/LayerRepository.service.ts +90 -0
- package/src/domain/services/LayerService.ts +21 -23
- package/src/domain/types.ts +39 -0
- package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
- package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
- package/src/domain/value-objects/Transform.vo.ts +61 -0
- package/src/domain/value-objects/index.ts +13 -0
- package/src/index.ts +6 -4
- package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
- package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
- package/src/presentation/components/DraggableLayer.tsx +13 -13
- package/src/presentation/components/EditorCanvas.tsx +5 -5
- package/src/presentation/components/EditorContent.tsx +72 -0
- package/src/presentation/components/EditorHeader.tsx +48 -0
- package/src/presentation/components/EditorSheets.tsx +85 -0
- package/src/presentation/components/FontControls.tsx +2 -2
- package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
- package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
- package/src/presentation/components/sheets/LayerManager.tsx +3 -4
- package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
- package/src/types.ts +9 -18
- package/src/utils/constants.ts +84 -0
- package/src/utils/formatters.ts +29 -0
- package/src/utils/helpers.ts +51 -0
- package/src/utils/index.ts +9 -0
- package/src/utils/validators.ts +38 -0
- package/src/components/AIMagicSheet.tsx +0 -107
- package/src/components/AdjustmentsSheet.tsx +0 -108
- package/src/components/ColorPicker.tsx +0 -77
- package/src/components/DraggableSticker.tsx +0 -161
- package/src/components/DraggableText.tsx +0 -181
- package/src/components/EditorCanvas.tsx +0 -106
- package/src/components/EditorToolbar.tsx +0 -155
- package/src/components/FilterPicker.tsx +0 -73
- package/src/components/FontControls.tsx +0 -132
- package/src/components/LayerManager.tsx +0 -164
- package/src/components/Slider.tsx +0 -112
- package/src/components/StickerPicker.tsx +0 -47
- package/src/components/TextEditorSheet.tsx +0 -160
- package/src/core/HistoryManager.ts +0 -53
- package/src/hooks/usePhotoEditor.ts +0 -172
- package/src/hooks/usePhotoEditorUI.ts +0 -162
- package/src/infrastructure/history/HistoryManager.ts +0 -38
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transform Gesture Utility
|
|
3
|
+
* Reusable gesture logic for draggable layers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useCallback, useEffect, useRef } from "react";
|
|
7
|
+
import { Gesture } from "react-native-gesture-handler";
|
|
8
|
+
import type { Layer } from "../../domain/entities/Layer.entity";
|
|
9
|
+
|
|
10
|
+
export interface TransformGestureState {
|
|
11
|
+
position: { x: number; y: number };
|
|
12
|
+
scale: number;
|
|
13
|
+
rotation: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface TransformGestureConfig {
|
|
17
|
+
minScale?: number;
|
|
18
|
+
maxScale?: number;
|
|
19
|
+
onTransformEnd: (transform: { x: number; y: number; scale: number; rotation: number }) => void;
|
|
20
|
+
onPress?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useTransformGesture(
|
|
24
|
+
initialTransform: Pick<Layer, "position" | "rotation" | "scale">,
|
|
25
|
+
config: TransformGestureConfig
|
|
26
|
+
) {
|
|
27
|
+
const {
|
|
28
|
+
minScale = 0.2,
|
|
29
|
+
maxScale = 6,
|
|
30
|
+
onTransformEnd,
|
|
31
|
+
onPress,
|
|
32
|
+
} = config;
|
|
33
|
+
|
|
34
|
+
const [state, setState] = useState<TransformGestureState>(() => ({
|
|
35
|
+
position: { x: initialTransform.position.x, y: initialTransform.position.y },
|
|
36
|
+
scale: initialTransform.scale,
|
|
37
|
+
rotation: initialTransform.rotation,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
const stateRef = useRef(state);
|
|
41
|
+
stateRef.current = state;
|
|
42
|
+
|
|
43
|
+
const onTransformEndRef = useRef(onTransformEnd);
|
|
44
|
+
onTransformEndRef.current = onTransformEnd;
|
|
45
|
+
const onPressRef = useRef(onPress);
|
|
46
|
+
onPressRef.current = onPress;
|
|
47
|
+
|
|
48
|
+
const offsetRef = useRef(state.position);
|
|
49
|
+
const scaleStartRef = useRef(state.scale);
|
|
50
|
+
const rotationStartRef = useRef(state.rotation);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setState((prev) => ({
|
|
54
|
+
...prev,
|
|
55
|
+
position: {
|
|
56
|
+
x: initialTransform.position.x ?? prev.position.x,
|
|
57
|
+
y: initialTransform.position.y ?? prev.position.y,
|
|
58
|
+
},
|
|
59
|
+
scale: initialTransform.scale ?? prev.scale,
|
|
60
|
+
rotation: initialTransform.rotation ?? prev.rotation,
|
|
61
|
+
}));
|
|
62
|
+
}, [initialTransform.position.x, initialTransform.position.y, initialTransform.scale, initialTransform.rotation]);
|
|
63
|
+
|
|
64
|
+
const emitTransform = useCallback(() => {
|
|
65
|
+
onTransformEndRef.current({
|
|
66
|
+
x: stateRef.current.position.x,
|
|
67
|
+
y: stateRef.current.position.y,
|
|
68
|
+
scale: stateRef.current.scale,
|
|
69
|
+
rotation: stateRef.current.rotation,
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const panGesture = Gesture.Pan()
|
|
74
|
+
.runOnJS(true)
|
|
75
|
+
.averageTouches(true)
|
|
76
|
+
.onStart(() => {
|
|
77
|
+
offsetRef.current = stateRef.current.position;
|
|
78
|
+
})
|
|
79
|
+
.onUpdate((e: { translationX: number; translationY: number }) => {
|
|
80
|
+
setState({
|
|
81
|
+
...stateRef.current,
|
|
82
|
+
position: {
|
|
83
|
+
x: offsetRef.current.x + e.translationX,
|
|
84
|
+
y: offsetRef.current.y + e.translationY,
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
})
|
|
88
|
+
.onEnd(emitTransform);
|
|
89
|
+
|
|
90
|
+
const pinchGesture = Gesture.Pinch()
|
|
91
|
+
.runOnJS(true)
|
|
92
|
+
.onStart(() => {
|
|
93
|
+
scaleStartRef.current = stateRef.current.scale;
|
|
94
|
+
})
|
|
95
|
+
.onUpdate((e: { scale: number }) => {
|
|
96
|
+
setState({
|
|
97
|
+
...stateRef.current,
|
|
98
|
+
scale: Math.max(minScale, Math.min(maxScale, scaleStartRef.current * e.scale)),
|
|
99
|
+
});
|
|
100
|
+
})
|
|
101
|
+
.onEnd(emitTransform);
|
|
102
|
+
|
|
103
|
+
const rotationGesture = Gesture.Rotation()
|
|
104
|
+
.runOnJS(true)
|
|
105
|
+
.onStart(() => {
|
|
106
|
+
rotationStartRef.current = stateRef.current.rotation;
|
|
107
|
+
})
|
|
108
|
+
.onUpdate((e: { rotation: number }) => {
|
|
109
|
+
setState({
|
|
110
|
+
...stateRef.current,
|
|
111
|
+
rotation: rotationStartRef.current + (e.rotation * 180) / Math.PI,
|
|
112
|
+
});
|
|
113
|
+
})
|
|
114
|
+
.onEnd(emitTransform);
|
|
115
|
+
|
|
116
|
+
const tapGesture = Gesture.Tap()
|
|
117
|
+
.runOnJS(true)
|
|
118
|
+
.onEnd(() => onPressRef.current?.());
|
|
119
|
+
|
|
120
|
+
const composed = Gesture.Exclusive(
|
|
121
|
+
Gesture.Simultaneous(panGesture, pinchGesture, rotationGesture),
|
|
122
|
+
tapGesture
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return { state, gestures: { pan: panGesture, pinch: pinchGesture, rotation: rotationGesture, tap: tapGesture, composed } };
|
|
126
|
+
}
|
|
127
|
+
|
|
@@ -9,12 +9,6 @@ import { Gesture } from "react-native-gesture-handler";
|
|
|
9
9
|
import type { Transform } from "../../domain/entities/Transform";
|
|
10
10
|
import type { TransformGestureConfig, TransformGestureState } from "./types";
|
|
11
11
|
|
|
12
|
-
const DEFAULT_STATE: TransformGestureState = {
|
|
13
|
-
position: { x: 50, y: 50 },
|
|
14
|
-
scale: 1,
|
|
15
|
-
rotation: 0,
|
|
16
|
-
};
|
|
17
|
-
|
|
18
12
|
export function useTransformGesture(
|
|
19
13
|
initialTransform: Partial<Transform>,
|
|
20
14
|
config: TransformGestureConfig
|
|
@@ -35,7 +29,7 @@ export function useTransformGesture(
|
|
|
35
29
|
|
|
36
30
|
// Sync state when props change (undo/redo)
|
|
37
31
|
useEffect(() => {
|
|
38
|
-
setState(prev => ({
|
|
32
|
+
setState((prev: TransformGestureState) => ({
|
|
39
33
|
...prev,
|
|
40
34
|
position: { x: initialTransform.x ?? prev.position.x, y: initialTransform.y ?? prev.position.y },
|
|
41
35
|
scale: initialTransform.scale ?? prev.scale,
|
|
@@ -73,8 +67,8 @@ export function useTransformGesture(
|
|
|
73
67
|
.onStart(() => {
|
|
74
68
|
offsetRef.current = stateRef.current.position;
|
|
75
69
|
})
|
|
76
|
-
.onUpdate((e) => {
|
|
77
|
-
setState(prev => ({
|
|
70
|
+
.onUpdate((e: { translationX: number; translationY: number }) => {
|
|
71
|
+
setState((prev: TransformGestureState) => ({
|
|
78
72
|
...prev,
|
|
79
73
|
position: {
|
|
80
74
|
x: offsetRef.current.x + e.translationX,
|
|
@@ -90,8 +84,8 @@ export function useTransformGesture(
|
|
|
90
84
|
.onStart(() => {
|
|
91
85
|
scaleStartRef.current = stateRef.current.scale;
|
|
92
86
|
})
|
|
93
|
-
.onUpdate((e) => {
|
|
94
|
-
setState(prev => ({
|
|
87
|
+
.onUpdate((e: { scale: number }) => {
|
|
88
|
+
setState((prev: TransformGestureState) => ({
|
|
95
89
|
...prev,
|
|
96
90
|
scale: Math.max(minScale, Math.min(maxScale, scaleStartRef.current * e.scale)),
|
|
97
91
|
}));
|
|
@@ -104,8 +98,8 @@ export function useTransformGesture(
|
|
|
104
98
|
.onStart(() => {
|
|
105
99
|
rotationStartRef.current = stateRef.current.rotation;
|
|
106
100
|
})
|
|
107
|
-
.onUpdate((e) => {
|
|
108
|
-
setState(prev => ({
|
|
101
|
+
.onUpdate((e: { rotation: number }) => {
|
|
102
|
+
setState((prev: TransformGestureState) => ({
|
|
109
103
|
...prev,
|
|
110
104
|
rotation: rotationStartRef.current + (e.rotation * 180) / Math.PI,
|
|
111
105
|
}));
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import React, { memo } from "react";
|
|
8
|
-
import { View, StyleSheet
|
|
8
|
+
import { View, StyleSheet } from "react-native";
|
|
9
9
|
import { GestureDetector } from "react-native-gesture-handler";
|
|
10
10
|
import { Image } from "expo-image";
|
|
11
|
-
import { AtomicText
|
|
12
|
-
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
11
|
+
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
12
|
+
import { useAppDesignTokens, type DesignTokens } from "@umituz/react-native-design-system/theme";
|
|
13
13
|
import { useTransformGesture } from "../../infrastructure/gesture/useTransformGesture";
|
|
14
|
-
import { Layer
|
|
14
|
+
import type { Layer } from "../../domain/entities/Layer.entity";
|
|
15
15
|
|
|
16
16
|
interface DraggableLayerProps {
|
|
17
17
|
layer: Layer;
|
|
@@ -30,15 +30,15 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
|
|
|
30
30
|
onTransformEnd,
|
|
31
31
|
}) => {
|
|
32
32
|
const tokens = useAppDesignTokens();
|
|
33
|
-
const { state, gestures } = useTransformGesture(
|
|
34
|
-
|
|
35
|
-
onPress
|
|
36
|
-
|
|
33
|
+
const { state, gestures } = useTransformGesture(
|
|
34
|
+
{ x: layer.x, y: layer.y, scale: layer.scale, rotation: layer.rotation },
|
|
35
|
+
{ onTransformEnd, onPress }
|
|
36
|
+
);
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
39
|
<GestureDetector gesture={gestures.composed}>
|
|
40
40
|
<View
|
|
41
|
-
accessibilityLabel={
|
|
41
|
+
accessibilityLabel={layer.isText() ? layer.text || "Text layer" : "Sticker layer"}
|
|
42
42
|
accessibilityRole="button"
|
|
43
43
|
style={[
|
|
44
44
|
styles.container,
|
|
@@ -63,12 +63,12 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
|
|
|
63
63
|
borderStyle: "dashed",
|
|
64
64
|
backgroundColor: isSelected
|
|
65
65
|
? tokens.colors.primary + "10"
|
|
66
|
-
:
|
|
66
|
+
: layer.isText()
|
|
67
67
|
? layer.backgroundColor
|
|
68
68
|
: "transparent",
|
|
69
69
|
}}
|
|
70
70
|
>
|
|
71
|
-
{
|
|
71
|
+
{layer.isText() ? (
|
|
72
72
|
<AtomicText
|
|
73
73
|
style={{
|
|
74
74
|
fontSize: layer.fontSize,
|
|
@@ -81,7 +81,7 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
|
|
|
81
81
|
>
|
|
82
82
|
{layer.text || "TAP TO EDIT"}
|
|
83
83
|
</AtomicText>
|
|
84
|
-
) :
|
|
84
|
+
) : layer.isSticker() ? (
|
|
85
85
|
renderStickerContent(layer.uri, tokens)
|
|
86
86
|
) : null}
|
|
87
87
|
</View>
|
|
@@ -92,7 +92,7 @@ export const DraggableLayer = memo<DraggableLayerProps>(({
|
|
|
92
92
|
|
|
93
93
|
DraggableLayer.displayName = "DraggableLayer";
|
|
94
94
|
|
|
95
|
-
function renderStickerContent(uri: string,
|
|
95
|
+
function renderStickerContent(uri: string, _tokens: DesignTokens) {
|
|
96
96
|
const isEmoji = isEmojiString(uri);
|
|
97
97
|
|
|
98
98
|
if (isEmoji) {
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { memo } from "react";
|
|
7
|
-
import { View, StyleSheet } from "react-native";
|
|
7
|
+
import { View, StyleSheet, type ViewStyle } from "react-native";
|
|
8
8
|
import { Image } from "expo-image";
|
|
9
9
|
import { DraggableLayer } from "./DraggableLayer";
|
|
10
|
-
import { Layer } from "../../domain/entities/Layer";
|
|
11
|
-
import type {
|
|
10
|
+
import type { Layer } from "../../domain/entities/Layer.entity";
|
|
11
|
+
import type { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
|
|
12
12
|
|
|
13
13
|
interface EditorCanvasProps {
|
|
14
14
|
imageUrl: string;
|
|
15
15
|
layers: Layer[];
|
|
16
16
|
activeLayerId: string | null;
|
|
17
|
-
filters:
|
|
17
|
+
filters: FilterSettings;
|
|
18
18
|
onLayerTap: (layerId: string) => void;
|
|
19
19
|
onLayerTransform: (layerId: string, transform: { x: number; y: number; scale: number; rotation: number }) => void;
|
|
20
|
-
style?:
|
|
20
|
+
style?: ViewStyle;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export const EditorCanvas = memo<EditorCanvasProps>(({
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Content Component
|
|
3
|
+
* Scrollable content area with canvas and tools
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { ScrollView } from "react-native";
|
|
8
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
9
|
+
import { EditorCanvas } from "./EditorCanvas";
|
|
10
|
+
import { FontControls } from "./FontControls";
|
|
11
|
+
import type { Layer } from "../../domain/entities/Layer.entity";
|
|
12
|
+
import type { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
|
|
13
|
+
import type { EditorUIState } from "../../application/hooks/useEditorUI";
|
|
14
|
+
|
|
15
|
+
interface EditorContentProps {
|
|
16
|
+
imageUrl: string;
|
|
17
|
+
layers: Layer[];
|
|
18
|
+
filters: FilterSettings;
|
|
19
|
+
activeLayerId: string | null;
|
|
20
|
+
selectedFont: string;
|
|
21
|
+
fontSize: number;
|
|
22
|
+
fonts: readonly string[];
|
|
23
|
+
customTools?: React.ReactNode | ((ui: EditorUIState) => React.ReactNode);
|
|
24
|
+
ui: EditorUIState;
|
|
25
|
+
onFontSizeChange: (size: number) => void;
|
|
26
|
+
onFontSelect: (font: string) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function EditorContent({
|
|
30
|
+
imageUrl,
|
|
31
|
+
layers,
|
|
32
|
+
filters,
|
|
33
|
+
activeLayerId,
|
|
34
|
+
selectedFont,
|
|
35
|
+
fontSize,
|
|
36
|
+
fonts,
|
|
37
|
+
customTools,
|
|
38
|
+
ui,
|
|
39
|
+
onFontSizeChange,
|
|
40
|
+
onFontSelect,
|
|
41
|
+
}: EditorContentProps) {
|
|
42
|
+
const tokens = useAppDesignTokens();
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<ScrollView
|
|
46
|
+
contentContainerStyle={{
|
|
47
|
+
padding: tokens.spacing.md,
|
|
48
|
+
gap: tokens.spacing.md,
|
|
49
|
+
}}
|
|
50
|
+
>
|
|
51
|
+
<EditorCanvas
|
|
52
|
+
imageUrl={imageUrl}
|
|
53
|
+
layers={layers}
|
|
54
|
+
activeLayerId={activeLayerId}
|
|
55
|
+
filters={filters}
|
|
56
|
+
onLayerTap={ui.handleTextLayerTap}
|
|
57
|
+
onLayerTransform={ui.handleLayerTransform}
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
{typeof customTools === "function" ? customTools(ui) : customTools}
|
|
61
|
+
|
|
62
|
+
<FontControls
|
|
63
|
+
fontSize={fontSize}
|
|
64
|
+
selectedFont={selectedFont}
|
|
65
|
+
fonts={fonts}
|
|
66
|
+
onFontSizeChange={onFontSizeChange}
|
|
67
|
+
onFontSelect={onFontSelect}
|
|
68
|
+
/>
|
|
69
|
+
</ScrollView>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Header Component
|
|
3
|
+
* Header with close, title, and save buttons
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { View, TouchableOpacity } from "react-native";
|
|
8
|
+
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
|
+
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
+
|
|
11
|
+
interface EditorHeaderProps {
|
|
12
|
+
title: string;
|
|
13
|
+
saveLabel: string;
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
onSave: () => void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function EditorHeader({ title, saveLabel, onClose, onSave }: EditorHeaderProps) {
|
|
19
|
+
const tokens = useAppDesignTokens();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<View
|
|
23
|
+
style={{
|
|
24
|
+
flexDirection: "row",
|
|
25
|
+
alignItems: "center",
|
|
26
|
+
justifyContent: "space-between",
|
|
27
|
+
paddingHorizontal: tokens.spacing.md,
|
|
28
|
+
paddingVertical: tokens.spacing.sm,
|
|
29
|
+
borderBottomWidth: 1,
|
|
30
|
+
borderBottomColor: tokens.colors.border,
|
|
31
|
+
}}
|
|
32
|
+
>
|
|
33
|
+
<TouchableOpacity onPress={onClose} accessibilityLabel="Close editor" accessibilityRole="button">
|
|
34
|
+
<AtomicIcon name="close" size="md" color="textPrimary" />
|
|
35
|
+
</TouchableOpacity>
|
|
36
|
+
|
|
37
|
+
<AtomicText type="headlineSmall" style={{ flex: 1, textAlign: "center" as const }}>
|
|
38
|
+
{title}
|
|
39
|
+
</AtomicText>
|
|
40
|
+
|
|
41
|
+
<TouchableOpacity onPress={onSave} accessibilityLabel="Save" accessibilityRole="button">
|
|
42
|
+
<AtomicText fontWeight="bold" color="primary">
|
|
43
|
+
{saveLabel}
|
|
44
|
+
</AtomicText>
|
|
45
|
+
</TouchableOpacity>
|
|
46
|
+
</View>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Editor Sheets Component
|
|
3
|
+
* All bottom sheets for the editor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React from "react";
|
|
7
|
+
import { BottomSheetModal } from "@umituz/react-native-design-system/molecules";
|
|
8
|
+
import { TextEditorSheet } from "./sheets/TextEditorSheet";
|
|
9
|
+
import { StickerPicker } from "./sheets/StickerPicker";
|
|
10
|
+
import { FilterSheet } from "./sheets/FilterSheet";
|
|
11
|
+
import { AdjustmentsSheet } from "./sheets/AdjustmentsSheet";
|
|
12
|
+
import { LayerManager } from "./sheets/LayerManager";
|
|
13
|
+
import { AIMagicSheet } from "./sheets/AIMagicSheet";
|
|
14
|
+
import type { FilterOption } from "./sheets/FilterSheet";
|
|
15
|
+
import type { FilterSettings } from "../../domain/value-objects/FilterSettings.vo";
|
|
16
|
+
import type { EditorUIState } from "../../application/hooks/useEditorUI";
|
|
17
|
+
|
|
18
|
+
interface EditorSheetsProps {
|
|
19
|
+
ui: EditorUIState;
|
|
20
|
+
filters: FilterSettings;
|
|
21
|
+
t: (key: string) => string;
|
|
22
|
+
onAICaption?: (style: string) => Promise<string> | void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function EditorSheets({ ui, filters, t, onAICaption }: EditorSheetsProps) {
|
|
26
|
+
return (
|
|
27
|
+
<>
|
|
28
|
+
<BottomSheetModal ref={ui.textEditorSheetRef} snapPoints={["55%"]}>
|
|
29
|
+
<TextEditorSheet
|
|
30
|
+
value={ui.editingText}
|
|
31
|
+
onChange={ui.setEditingText}
|
|
32
|
+
onSave={ui.handleSaveText}
|
|
33
|
+
t={t}
|
|
34
|
+
color={ui.editingColor}
|
|
35
|
+
onColorChange={ui.setEditingColor}
|
|
36
|
+
textAlign={ui.editingAlign}
|
|
37
|
+
onTextAlignChange={ui.setEditingAlign}
|
|
38
|
+
isBold={ui.editingBold}
|
|
39
|
+
onBoldChange={ui.setEditingBold}
|
|
40
|
+
isItalic={ui.editingItalic}
|
|
41
|
+
onItalicChange={ui.setEditingItalic}
|
|
42
|
+
/>
|
|
43
|
+
</BottomSheetModal>
|
|
44
|
+
|
|
45
|
+
<BottomSheetModal ref={ui.stickerSheetRef} snapPoints={["50%"]}>
|
|
46
|
+
<StickerPicker onSelectSticker={ui.handleSelectSticker} />
|
|
47
|
+
</BottomSheetModal>
|
|
48
|
+
|
|
49
|
+
<BottomSheetModal ref={ui.filterSheetRef} snapPoints={["40%"]}>
|
|
50
|
+
<FilterSheet
|
|
51
|
+
selectedFilter={ui.selectedFilter}
|
|
52
|
+
onSelectFilter={(option: FilterOption) => {
|
|
53
|
+
ui.setSelectedFilter(option.id);
|
|
54
|
+
ui.updateFilters(option.filters);
|
|
55
|
+
ui.filterSheetRef.current?.dismiss();
|
|
56
|
+
}}
|
|
57
|
+
/>
|
|
58
|
+
</BottomSheetModal>
|
|
59
|
+
|
|
60
|
+
<BottomSheetModal ref={ui.adjustmentsSheetRef} snapPoints={["55%"]}>
|
|
61
|
+
<AdjustmentsSheet filters={filters} onFiltersChange={ui.updateFilters} />
|
|
62
|
+
</BottomSheetModal>
|
|
63
|
+
|
|
64
|
+
<BottomSheetModal ref={ui.layerSheetRef} snapPoints={["55%"]}>
|
|
65
|
+
<LayerManager
|
|
66
|
+
layers={ui.layers}
|
|
67
|
+
activeLayerId={ui.activeLayerId}
|
|
68
|
+
onSelectLayer={ui.selectLayer}
|
|
69
|
+
onDeleteLayer={ui.deleteLayer}
|
|
70
|
+
onDuplicateLayer={ui.duplicateLayer}
|
|
71
|
+
onMoveLayerUp={ui.moveLayerUp}
|
|
72
|
+
onMoveLayerDown={ui.moveLayerDown}
|
|
73
|
+
t={t}
|
|
74
|
+
/>
|
|
75
|
+
</BottomSheetModal>
|
|
76
|
+
|
|
77
|
+
{onAICaption && (
|
|
78
|
+
<BottomSheetModal ref={ui.aiSheetRef} snapPoints={["60%"]}>
|
|
79
|
+
<AIMagicSheet onGenerateCaption={onAICaption} />
|
|
80
|
+
</BottomSheetModal>
|
|
81
|
+
)}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import React, { memo } from "react";
|
|
7
|
-
import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
|
|
7
|
+
import { View, ScrollView, TouchableOpacity, StyleSheet, type ViewStyle } from "react-native";
|
|
8
8
|
import { AtomicText } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
10
|
import { Slider } from "./ui/Slider";
|
|
@@ -23,7 +23,7 @@ interface FontControlsProps {
|
|
|
23
23
|
fonts?: readonly string[];
|
|
24
24
|
onFontSizeChange: (size: number) => void;
|
|
25
25
|
onFontSelect: (font: string) => void;
|
|
26
|
-
style?:
|
|
26
|
+
style?: ViewStyle;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export const FontControls = memo<FontControlsProps>(({
|
|
@@ -8,11 +8,11 @@ import { View, TouchableOpacity } from "react-native";
|
|
|
8
8
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
10
|
import { Slider } from "../ui/Slider";
|
|
11
|
-
import { DEFAULT_FILTERS } from "../../../domain/entities/Filters";
|
|
11
|
+
import { DEFAULT_FILTERS, type FilterValues } from "../../../domain/entities/Filters";
|
|
12
12
|
|
|
13
13
|
interface AdjustmentsSheetProps {
|
|
14
|
-
filters:
|
|
15
|
-
onFiltersChange: (filters:
|
|
14
|
+
filters: FilterValues;
|
|
15
|
+
onFiltersChange: (filters: FilterValues) => void;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const AdjustmentsSheet = memo<AdjustmentsSheetProps>(({
|
|
@@ -21,7 +21,7 @@ export const AdjustmentsSheet = memo<AdjustmentsSheetProps>(({
|
|
|
21
21
|
}) => {
|
|
22
22
|
const tokens = useAppDesignTokens();
|
|
23
23
|
|
|
24
|
-
const update = (key:
|
|
24
|
+
const update = (key: keyof FilterValues, val: number) => {
|
|
25
25
|
onFiltersChange({ ...filters, [key]: val });
|
|
26
26
|
};
|
|
27
27
|
|
|
@@ -7,7 +7,7 @@ import React, { memo } from "react";
|
|
|
7
7
|
import { View, ScrollView, TouchableOpacity } from "react-native";
|
|
8
8
|
import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
|
-
import { Layer
|
|
10
|
+
import { Layer } from "../../../domain/entities/Layer.entity";
|
|
11
11
|
|
|
12
12
|
interface LayerManagerProps {
|
|
13
13
|
layers: Layer[];
|
|
@@ -47,7 +47,7 @@ export const LayerManager = memo<LayerManagerProps>(({
|
|
|
47
47
|
) : (
|
|
48
48
|
sortedLayers.map((layer, idx) => {
|
|
49
49
|
const isActive = activeLayerId === layer.id;
|
|
50
|
-
const label =
|
|
50
|
+
const label = layer.isText()
|
|
51
51
|
? layer.text || t("photo_editor.untitled") || "Untitled"
|
|
52
52
|
: "Sticker";
|
|
53
53
|
const isTop = idx === 0;
|
|
@@ -60,12 +60,11 @@ export const LayerManager = memo<LayerManagerProps>(({
|
|
|
60
60
|
flexDirection: "row",
|
|
61
61
|
alignItems: "center",
|
|
62
62
|
padding: tokens.spacing.sm,
|
|
63
|
-
backgroundColor: tokens.colors.surfaceVariant,
|
|
63
|
+
backgroundColor: isActive ? tokens.colors.primary + "10" : tokens.colors.surfaceVariant,
|
|
64
64
|
borderRadius: tokens.borders.radius.md,
|
|
65
65
|
marginBottom: tokens.spacing.xs,
|
|
66
66
|
borderWidth: 2,
|
|
67
67
|
borderColor: isActive ? tokens.colors.primary : "transparent",
|
|
68
|
-
backgroundColor: isActive ? tokens.colors.primary + "10" : tokens.colors.surfaceVariant,
|
|
69
68
|
}}
|
|
70
69
|
onPress={() => onSelectLayer(layer.id)}
|
|
71
70
|
accessibilityLabel={`${layer.type} layer: ${label}`}
|
|
@@ -8,7 +8,7 @@ import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
|
|
|
8
8
|
import { AtomicText, AtomicButton } from "@umituz/react-native-design-system/atoms";
|
|
9
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
10
10
|
import { ColorPicker } from "../ui/ColorPicker";
|
|
11
|
-
import type { TextAlign } from "../../../domain/
|
|
11
|
+
import type { TextAlign } from "../../../domain/types";
|
|
12
12
|
|
|
13
13
|
interface TextEditorSheetProps {
|
|
14
14
|
value: string;
|
package/src/types.ts
CHANGED
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* @deprecated Use types from src/domain/entities/ instead
|
|
2
|
+
* Central Type Exports
|
|
3
|
+
* Re-exports domain types for convenience
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
|
-
export type { TextAlign } from "./domain/
|
|
8
|
-
export type { Layer, TextLayer, StickerLayer } from "./domain/entities/Layer";
|
|
6
|
+
export type { TextAlign } from "./domain/types";
|
|
7
|
+
export type { Layer, TextLayer, StickerLayer } from "./domain/entities/Layer.entity";
|
|
8
|
+
export type { TextContent as TextLayerData, StickerContent as StickerLayerData } from "./domain/types";
|
|
9
9
|
export type { Transform } from "./domain/entities/Transform";
|
|
10
|
-
export type { FilterValues as ImageFilters
|
|
10
|
+
export type { FilterValues as ImageFilters } from "./domain/entities/Filters";
|
|
11
11
|
|
|
12
12
|
// Re-export type guards
|
|
13
|
-
export { isTextLayer, isStickerLayer } from "./domain/entities/Layer";
|
|
13
|
+
export { isTextLayer, isStickerLayer } from "./domain/entities/Layer.entity";
|
|
14
14
|
|
|
15
|
-
//
|
|
16
|
-
export
|
|
17
|
-
export interface StickerLayerData extends StickerLayer {}
|
|
18
|
-
|
|
19
|
-
// Legacy EditorState (kept for compatibility)
|
|
20
|
-
export interface EditorState {
|
|
21
|
-
layers: Layer[];
|
|
22
|
-
activeLayerId: string | null;
|
|
23
|
-
canvasSize: { width: number; height: number };
|
|
24
|
-
filters: ImageFilters;
|
|
25
|
-
}
|
|
15
|
+
// Re-export DEFAULT_FILTERS as value (not type)
|
|
16
|
+
export { DEFAULT_FILTERS as DEFAULT_IMAGE_FILTERS } from "./domain/entities/Filters";
|