@umituz/react-native-photo-editor 2.0.24 → 2.0.25
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 +17 -6
- package/src/domain/entities/Layer.entity.ts +86 -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 +85 -0
- package/src/domain/services/LayerService.ts +1 -1
- 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 +4 -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 +8 -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
|
@@ -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 "../entities/Layer.entity".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 "
|
|
11
|
-
import type {
|
|
10
|
+
import type { Layer } from "../entities/Layer.entity".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 "../entities/Layer.entity".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 "../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 "
|
|
11
|
+
import type { TextAlign } from "../entities/Layer.entity"";
|
|
12
12
|
|
|
13
13
|
interface TextEditorSheetProps {
|
|
14
14
|
value: string;
|
package/src/types.ts
CHANGED
|
@@ -1,25 +1,15 @@
|
|
|
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 "
|
|
8
|
-
export type { Layer, TextLayer, StickerLayer } from "
|
|
6
|
+
export type { TextAlign } from "../entities/Layer.entity"";
|
|
7
|
+
export type { Layer, TextLayer, StickerLayer, TextLayerData, StickerLayerData } from "../entities/Layer.entity"";
|
|
9
8
|
export type { Transform } from "./domain/entities/Transform";
|
|
10
|
-
export type { FilterValues as ImageFilters
|
|
9
|
+
export type { FilterValues as ImageFilters } from "./domain/entities/Filters";
|
|
11
10
|
|
|
12
11
|
// Re-export type guards
|
|
13
|
-
export { isTextLayer, isStickerLayer } from "
|
|
12
|
+
export { isTextLayer, isStickerLayer } from "../entities/Layer.entity"";
|
|
14
13
|
|
|
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
|
-
}
|
|
14
|
+
// Re-export DEFAULT_FILTERS as value (not type)
|
|
15
|
+
export { DEFAULT_FILTERS as DEFAULT_IMAGE_FILTERS } from "./domain/entities/Filters";
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants Utility
|
|
3
|
+
* Shared constants for the editor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_COLORS = [
|
|
7
|
+
"#FFFFFF", "#000000", "#888888", "#CCCCCC",
|
|
8
|
+
"#FF3B30", "#FF9500", "#FFCC00", "#FF2D55",
|
|
9
|
+
"#34C759", "#30B0C7", "#007AFF", "#5AC8FA",
|
|
10
|
+
"#5856D6", "#AF52DE", "#FF6B6B", "#FFD93D",
|
|
11
|
+
"#6BCB77", "#4D96FF", "#C77DFF", "#F72585",
|
|
12
|
+
] as const;
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_FONTS = [
|
|
15
|
+
"System",
|
|
16
|
+
"Impact",
|
|
17
|
+
"Comic",
|
|
18
|
+
"Serif",
|
|
19
|
+
"Retro",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_STICKERS = [
|
|
23
|
+
"😀", "😂", "🤣", "😍", "🥰", "😎", "🤯", "🥳", "😤", "💀",
|
|
24
|
+
"🔥", "❤️", "💯", "✨", "🎉", "🤡", "👀", "🙌", "👏", "💪",
|
|
25
|
+
"🤝", "🙈", "🐶", "🐱", "🦊", "🐸", "🌟", "⭐", "🌈", "☀️",
|
|
26
|
+
"🌙", "💫",
|
|
27
|
+
] as const;
|
|
28
|
+
|
|
29
|
+
export const AI_STYLES = [
|
|
30
|
+
{ id: "viral", label: "✨ Viral", desc: "Catchy & shareable" },
|
|
31
|
+
{ id: "funny", label: "😂 Funny", desc: "Humor that connects" },
|
|
32
|
+
{ id: "savage", label: "🔥 Savage", desc: "Bold & edgy" },
|
|
33
|
+
{ id: "wholesome", label: "💕 Wholesome", desc: "Warm & positive" },
|
|
34
|
+
{ id: "sarcastic", label: "😏 Sarcastic", desc: "Witty & ironic" },
|
|
35
|
+
{ id: "relatable", label: "🎯 Relatable", desc: "Everyone gets it" },
|
|
36
|
+
] as const;
|
|
37
|
+
|
|
38
|
+
export const FILTER_PRESETS = [
|
|
39
|
+
{
|
|
40
|
+
id: "none",
|
|
41
|
+
name: "None",
|
|
42
|
+
icon: "close",
|
|
43
|
+
filters: { brightness: 1, contrast: 1, saturation: 1, sepia: 0, grayscale: 0 },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "sepia",
|
|
47
|
+
name: "Sepia",
|
|
48
|
+
icon: "brush",
|
|
49
|
+
filters: { sepia: 0.7, saturation: 0.8 },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "grayscale",
|
|
53
|
+
name: "B&W",
|
|
54
|
+
icon: "swap-horizontal",
|
|
55
|
+
filters: { grayscale: 1, saturation: 0 },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "vintage",
|
|
59
|
+
name: "Vintage",
|
|
60
|
+
icon: "flash",
|
|
61
|
+
filters: { sepia: 0.3, contrast: 1.1, brightness: 0.9 },
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: "warm",
|
|
65
|
+
name: "Warm",
|
|
66
|
+
icon: "sparkles",
|
|
67
|
+
filters: { brightness: 1.05, saturation: 1.2 },
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "cool",
|
|
71
|
+
name: "Cool",
|
|
72
|
+
icon: "image",
|
|
73
|
+
filters: { contrast: 1.05, brightness: 1.02, saturation: 0.85 },
|
|
74
|
+
},
|
|
75
|
+
] as const;
|
|
76
|
+
|
|
77
|
+
export const SLIDER_CONFIGS = {
|
|
78
|
+
brightness: { min: 0.5, max: 2, step: 0.05, default: 1 },
|
|
79
|
+
contrast: { min: 0.5, max: 2, step: 0.05, default: 1 },
|
|
80
|
+
saturation: { min: 0, max: 2, step: 0.05, default: 1 },
|
|
81
|
+
hueRotate: { min: 0, max: 360, step: 1, default: 0 },
|
|
82
|
+
sepia: { min: 0, max: 1, step: 0.05, default: 0 },
|
|
83
|
+
grayscale: { min: 0, max: 1, step: 0.05, default: 0 },
|
|
84
|
+
} as const;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatters Utility
|
|
3
|
+
* Value formatting for UI display
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function formatPercentage(value: number): string {
|
|
7
|
+
const rounded = (value - 1) * 100;
|
|
8
|
+
return `${rounded >= 0 ? "+" : ""}${Math.round(rounded)}%`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function formatDegrees(value: number): string {
|
|
12
|
+
return `${Math.round(value)}°`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function formatSliderValue(
|
|
16
|
+
value: number,
|
|
17
|
+
type: "percentage" | "degrees" | "integer" = "percentage"
|
|
18
|
+
): string {
|
|
19
|
+
switch (type) {
|
|
20
|
+
case "percentage":
|
|
21
|
+
return formatPercentage(value);
|
|
22
|
+
case "degrees":
|
|
23
|
+
return formatDegrees(value);
|
|
24
|
+
case "integer":
|
|
25
|
+
return Math.round(value).toString();
|
|
26
|
+
default:
|
|
27
|
+
return value.toString();
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers Utility
|
|
3
|
+
* Common helper functions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { FilterData } from "../domain/value-objects/FilterSettings.vo";
|
|
7
|
+
|
|
8
|
+
export function createBrightnessOverlay(brightness: number): {
|
|
9
|
+
color: string;
|
|
10
|
+
opacity: number;
|
|
11
|
+
} | null {
|
|
12
|
+
if (brightness < 1) {
|
|
13
|
+
return { color: "black", opacity: Math.min(0.6, 1 - brightness) };
|
|
14
|
+
}
|
|
15
|
+
if (brightness > 1) {
|
|
16
|
+
return { color: "white", opacity: Math.min(0.4, brightness - 1) };
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function mergeFilters(
|
|
22
|
+
base: FilterData,
|
|
23
|
+
updates: Partial<FilterData>
|
|
24
|
+
): FilterData {
|
|
25
|
+
return {
|
|
26
|
+
...base,
|
|
27
|
+
...updates,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function generateId(prefix: string): string {
|
|
32
|
+
return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function sortByZIndex<T extends { zIndex: number }>(
|
|
36
|
+
items: T[]
|
|
37
|
+
): T[] {
|
|
38
|
+
return [...items].sort((a, b) => a.zIndex - b.zIndex);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function getNextZIndex(items: { zIndex: number }[]): number {
|
|
42
|
+
return items.length > 0 ? Math.max(...items.map((item) => item.zIndex)) + 1 : 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function capitalizeFirst(str: string): string {
|
|
46
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function truncateText(text: string, maxLength: number): string {
|
|
50
|
+
return text.length > maxLength ? text.slice(0, maxLength) + "..." : text;
|
|
51
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validators Utility
|
|
3
|
+
* Input validation for editor operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export function isValidColor(color: string): boolean {
|
|
7
|
+
return /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(color);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isValidUrl(url: string): boolean {
|
|
11
|
+
try {
|
|
12
|
+
new URL(url);
|
|
13
|
+
return true;
|
|
14
|
+
} catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isEmojiString(str: string): boolean {
|
|
20
|
+
return (
|
|
21
|
+
str.length <= 4 &&
|
|
22
|
+
!/^https?:\/\//i.test(str) &&
|
|
23
|
+
!str.startsWith("/") &&
|
|
24
|
+
/^[\p{Emoji}\p{Emoji_Component}]+$/u.test(str)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function clamp(value: number, min: number, max: number): number {
|
|
29
|
+
return Math.min(Math.max(value, min), max);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function validateFontSize(size: number): number {
|
|
33
|
+
return clamp(size, 8, 120);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function validateOpacity(opacity: number): number {
|
|
37
|
+
return clamp(opacity, 0, 1);
|
|
38
|
+
}
|