@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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/src/PhotoEditor.tsx +43 -137
  3. package/src/application/hooks/useEditor.ts +4 -6
  4. package/src/application/hooks/useEditorUI.ts +8 -5
  5. package/src/application/stores/EditorStore.ts +17 -6
  6. package/src/domain/entities/Layer.entity.ts +86 -0
  7. package/src/domain/entities/{Layer.ts → Layer.legacy.ts} +3 -3
  8. package/src/domain/entities/StickerLayer.entity.ts +37 -0
  9. package/src/domain/entities/TextLayer.entity.ts +58 -0
  10. package/src/domain/entities/index.ts +9 -0
  11. package/src/domain/services/History.service.ts +69 -0
  12. package/src/domain/services/LayerFactory.service.ts +81 -0
  13. package/src/domain/services/LayerRepository.service.ts +85 -0
  14. package/src/domain/services/LayerService.ts +1 -1
  15. package/src/domain/types.ts +39 -0
  16. package/src/domain/value-objects/FilterSettings.vo.ts +89 -0
  17. package/src/domain/value-objects/LayerDefaults.vo.ts +56 -0
  18. package/src/domain/value-objects/Transform.vo.ts +61 -0
  19. package/src/domain/value-objects/index.ts +13 -0
  20. package/src/index.ts +4 -4
  21. package/src/infrastructure/gesture/createTransformGesture.ts +127 -0
  22. package/src/infrastructure/gesture/useTransformGesture.ts +7 -13
  23. package/src/presentation/components/DraggableLayer.tsx +13 -13
  24. package/src/presentation/components/EditorCanvas.tsx +5 -5
  25. package/src/presentation/components/EditorContent.tsx +72 -0
  26. package/src/presentation/components/EditorHeader.tsx +48 -0
  27. package/src/presentation/components/EditorSheets.tsx +85 -0
  28. package/src/presentation/components/FontControls.tsx +2 -2
  29. package/src/presentation/components/sheets/AdjustmentsSheet.tsx +4 -4
  30. package/src/presentation/components/sheets/FilterSheet.tsx +1 -1
  31. package/src/presentation/components/sheets/LayerManager.tsx +3 -4
  32. package/src/presentation/components/sheets/TextEditorSheet.tsx +1 -1
  33. package/src/types.ts +8 -18
  34. package/src/utils/constants.ts +84 -0
  35. package/src/utils/formatters.ts +29 -0
  36. package/src/utils/helpers.ts +51 -0
  37. package/src/utils/index.ts +9 -0
  38. package/src/utils/validators.ts +38 -0
  39. package/src/components/AIMagicSheet.tsx +0 -107
  40. package/src/components/AdjustmentsSheet.tsx +0 -108
  41. package/src/components/ColorPicker.tsx +0 -77
  42. package/src/components/DraggableSticker.tsx +0 -161
  43. package/src/components/DraggableText.tsx +0 -181
  44. package/src/components/EditorCanvas.tsx +0 -106
  45. package/src/components/EditorToolbar.tsx +0 -155
  46. package/src/components/FilterPicker.tsx +0 -73
  47. package/src/components/FontControls.tsx +0 -132
  48. package/src/components/LayerManager.tsx +0 -164
  49. package/src/components/Slider.tsx +0 -112
  50. package/src/components/StickerPicker.tsx +0 -47
  51. package/src/components/TextEditorSheet.tsx +0 -160
  52. package/src/core/HistoryManager.ts +0 -53
  53. package/src/hooks/usePhotoEditor.ts +0 -172
  54. package/src/hooks/usePhotoEditorUI.ts +0 -162
  55. package/src/infrastructure/history/HistoryManager.ts +0 -38
@@ -1,112 +0,0 @@
1
- import React, { useRef, useState } from "react";
2
- import { View } from "react-native";
3
- import { Gesture, GestureDetector } from "react-native-gesture-handler";
4
- import { AtomicText } from "@umituz/react-native-design-system/atoms";
5
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
6
-
7
- interface SliderProps {
8
- label: string;
9
- value: number;
10
- min: number;
11
- max: number;
12
- step?: number;
13
- onValueChange: (val: number) => void;
14
- formatValue?: (val: number) => string;
15
- }
16
-
17
- export const Slider: React.FC<SliderProps> = ({
18
- label,
19
- value,
20
- min,
21
- max,
22
- step = 0.05,
23
- onValueChange,
24
- formatValue,
25
- }) => {
26
- const tokens = useAppDesignTokens();
27
- const [trackWidth, setTrackWidth] = useState(200);
28
- const trackWidthRef = useRef(200);
29
- const startValueRef = useRef(value);
30
- const valueRef = useRef(value);
31
- valueRef.current = value;
32
-
33
- const clamp = (v: number) => Math.max(min, Math.min(max, v));
34
- const snap = (v: number) => parseFloat((Math.round(v / step) * step).toFixed(4));
35
-
36
- const panGesture = Gesture.Pan()
37
- .runOnJS(true)
38
- .onStart(() => {
39
- startValueRef.current = valueRef.current;
40
- })
41
- .onUpdate((e) => {
42
- const ratio = e.translationX / trackWidthRef.current;
43
- const delta = ratio * (max - min);
44
- onValueChange(snap(clamp(startValueRef.current + delta)));
45
- });
46
-
47
- const percent = Math.max(0, Math.min(1, (value - min) / (max - min)));
48
- const thumbOffset = percent * trackWidth - 12;
49
- const displayValue = formatValue ? formatValue(value) : value.toFixed(1);
50
-
51
- return (
52
- <View style={{ gap: tokens.spacing.xs }}>
53
- <View
54
- style={{
55
- flexDirection: "row",
56
- justifyContent: "space-between",
57
- alignItems: "center",
58
- }}
59
- >
60
- <AtomicText type="labelMedium" color="textSecondary">
61
- {label}
62
- </AtomicText>
63
- <AtomicText type="labelMedium" color="primary" fontWeight="bold">
64
- {displayValue}
65
- </AtomicText>
66
- </View>
67
- <GestureDetector gesture={panGesture}>
68
- <View
69
- style={{ height: 44, justifyContent: "center", paddingHorizontal: 12 }}
70
- onLayout={(e) => {
71
- const w = Math.max(1, e.nativeEvent.layout.width - 24);
72
- setTrackWidth(w);
73
- trackWidthRef.current = w;
74
- }}
75
- >
76
- {/* Track background */}
77
- <View
78
- style={{
79
- height: 4,
80
- backgroundColor: tokens.colors.surfaceVariant,
81
- borderRadius: 2,
82
- }}
83
- >
84
- {/* Filled portion */}
85
- <View
86
- style={{
87
- width: `${percent * 100}%`,
88
- height: "100%",
89
- backgroundColor: tokens.colors.primary,
90
- borderRadius: 2,
91
- }}
92
- />
93
- </View>
94
- {/* Thumb */}
95
- <View
96
- style={{
97
- position: "absolute",
98
- left: thumbOffset + 12,
99
- width: 24,
100
- height: 24,
101
- borderRadius: 12,
102
- backgroundColor: tokens.colors.primary,
103
- borderWidth: 2.5,
104
- borderColor: tokens.colors.surface,
105
- top: 10,
106
- }}
107
- />
108
- </View>
109
- </GestureDetector>
110
- </View>
111
- );
112
- };
@@ -1,47 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { View, ScrollView, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicText } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { DEFAULT_STICKERS } from "../constants";
6
-
7
- interface StickerPickerProps {
8
- onSelectSticker: (sticker: string) => void;
9
- stickers?: readonly string[];
10
- }
11
-
12
- export const StickerPicker: React.FC<StickerPickerProps> = ({
13
- onSelectSticker,
14
- stickers = DEFAULT_STICKERS,
15
- }) => {
16
- const tokens = useAppDesignTokens();
17
-
18
- const styles = useMemo(() => StyleSheet.create({
19
- container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
20
- grid: { flexDirection: "row", flexWrap: "wrap", gap: tokens.spacing.sm },
21
- sticker: {
22
- width: 50,
23
- height: 50,
24
- borderRadius: tokens.borders.radius.sm,
25
- backgroundColor: tokens.colors.surfaceVariant,
26
- alignItems: "center",
27
- justifyContent: "center",
28
- },
29
- }), [tokens]);
30
-
31
- return (
32
- <View style={styles.container}>
33
- <AtomicText type="headlineSmall">Stickers</AtomicText>
34
- <ScrollView showsVerticalScrollIndicator={false}>
35
- <View style={styles.grid}>
36
- {stickers.map((s, i) => (
37
- <TouchableOpacity key={`${s}-${i}`} style={styles.sticker} onPress={() => onSelectSticker(s)}>
38
- <AtomicText style={{ fontSize: 32 }}>{s}</AtomicText>
39
- </TouchableOpacity>
40
- ))}
41
- </View>
42
- </ScrollView>
43
- </View>
44
- );
45
- };
46
-
47
- export default React.memo(StickerPicker);
@@ -1,160 +0,0 @@
1
- import React, { useMemo } from "react";
2
- import { View, TextInput, TouchableOpacity, StyleSheet } from "react-native";
3
- import { AtomicText, AtomicButton } from "@umituz/react-native-design-system/atoms";
4
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
5
- import { ColorPicker } from "./ColorPicker";
6
- import type { TextAlign } from "../types";
7
-
8
- interface TextEditorSheetProps {
9
- value: string;
10
- onChange: (text: string) => void;
11
- onSave: () => void;
12
- t: (key: string) => string;
13
- color?: string;
14
- onColorChange?: (color: string) => void;
15
- textAlign?: TextAlign;
16
- onTextAlignChange?: (align: TextAlign) => void;
17
- isBold?: boolean;
18
- onBoldChange?: (bold: boolean) => void;
19
- isItalic?: boolean;
20
- onItalicChange?: (italic: boolean) => void;
21
- }
22
-
23
- const ALIGN_OPTIONS: { value: TextAlign; icon: string }[] = [
24
- { value: "left", icon: "«" },
25
- { value: "center", icon: "≡" },
26
- { value: "right", icon: "»" },
27
- ];
28
-
29
- export const TextEditorSheet: React.FC<TextEditorSheetProps> = ({
30
- value,
31
- onChange,
32
- onSave,
33
- t,
34
- color = "#FFFFFF",
35
- onColorChange,
36
- textAlign = "center",
37
- onTextAlignChange,
38
- isBold = false,
39
- onBoldChange,
40
- isItalic = false,
41
- onItalicChange,
42
- }) => {
43
- const tokens = useAppDesignTokens();
44
-
45
- const styles = useMemo(() => StyleSheet.create({
46
- container: { padding: tokens.spacing.md, gap: tokens.spacing.md },
47
- input: {
48
- backgroundColor: tokens.colors.surfaceVariant,
49
- borderRadius: tokens.borders.radius.md,
50
- padding: tokens.spacing.md,
51
- fontSize: 18,
52
- color: tokens.colors.textPrimary,
53
- textAlign: "center",
54
- minHeight: 90,
55
- },
56
- row: {
57
- flexDirection: "row",
58
- alignItems: "center",
59
- gap: tokens.spacing.sm,
60
- },
61
- styleBtn: {
62
- width: 44,
63
- height: 44,
64
- borderRadius: tokens.borders.radius.sm,
65
- borderWidth: 1.5,
66
- borderColor: tokens.colors.border,
67
- alignItems: "center",
68
- justifyContent: "center",
69
- backgroundColor: tokens.colors.surfaceVariant,
70
- },
71
- styleBtnActive: {
72
- borderColor: tokens.colors.primary,
73
- backgroundColor: tokens.colors.primary + "20",
74
- },
75
- }), [tokens]);
76
-
77
- return (
78
- <View style={styles.container}>
79
- <AtomicText type="headlineSmall">{t("photo_editor.add_text") || "Edit Text"}</AtomicText>
80
-
81
- <TextInput
82
- value={value}
83
- onChangeText={onChange}
84
- placeholder={t("photo_editor.tap_to_edit") || "Enter text…"}
85
- placeholderTextColor={tokens.colors.textSecondary}
86
- style={styles.input}
87
- multiline
88
- autoFocus
89
- />
90
-
91
- {/* Style row: Bold, Italic, Alignment */}
92
- <View style={styles.row}>
93
- {onBoldChange && (
94
- <TouchableOpacity
95
- style={[styles.styleBtn, isBold && styles.styleBtnActive]}
96
- onPress={() => onBoldChange(!isBold)}
97
- accessibilityLabel="Bold"
98
- accessibilityRole="button"
99
- accessibilityState={{ selected: isBold }}
100
- >
101
- <AtomicText fontWeight="bold" color={isBold ? "primary" : "textSecondary"}>
102
- B
103
- </AtomicText>
104
- </TouchableOpacity>
105
- )}
106
-
107
- {onItalicChange && (
108
- <TouchableOpacity
109
- style={[styles.styleBtn, isItalic && styles.styleBtnActive]}
110
- onPress={() => onItalicChange(!isItalic)}
111
- accessibilityLabel="Italic"
112
- accessibilityRole="button"
113
- accessibilityState={{ selected: isItalic }}
114
- >
115
- <AtomicText
116
- color={isItalic ? "primary" : "textSecondary"}
117
- style={{ fontStyle: "italic" }}
118
- >
119
- I
120
- </AtomicText>
121
- </TouchableOpacity>
122
- )}
123
-
124
- {onTextAlignChange && (
125
- <View style={[styles.row, { marginLeft: tokens.spacing.sm }]}>
126
- {ALIGN_OPTIONS.map(({ value: align, icon }) => (
127
- <TouchableOpacity
128
- key={align}
129
- style={[styles.styleBtn, textAlign === align && styles.styleBtnActive]}
130
- onPress={() => onTextAlignChange(align)}
131
- accessibilityLabel={`Align ${align}`}
132
- accessibilityRole="button"
133
- accessibilityState={{ selected: textAlign === align }}
134
- >
135
- <AtomicText color={textAlign === align ? "primary" : "textSecondary"}>
136
- {icon}
137
- </AtomicText>
138
- </TouchableOpacity>
139
- ))}
140
- </View>
141
- )}
142
- </View>
143
-
144
- {/* Color picker */}
145
- {onColorChange && (
146
- <ColorPicker
147
- label="Text Color"
148
- selectedColor={color}
149
- onSelectColor={onColorChange}
150
- />
151
- )}
152
-
153
- <AtomicButton variant="primary" onPress={onSave}>
154
- {t("common.save") || "Save"}
155
- </AtomicButton>
156
- </View>
157
- );
158
- };
159
-
160
- export default React.memo(TextEditorSheet);
@@ -1,53 +0,0 @@
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 readonly maxHistory = 20;
13
-
14
- createInitialState(initialValue: T): HistoryState<T> {
15
- return { past: [], present: initialValue, future: [] };
16
- }
17
-
18
- push(history: HistoryState<T>, newValue: T): HistoryState<T> {
19
- return {
20
- past: [...history.past.slice(-this.maxHistory + 1), history.present],
21
- present: newValue,
22
- future: [],
23
- };
24
- }
25
-
26
- undo(history: HistoryState<T>): HistoryState<T> {
27
- if (history.past.length === 0) return history;
28
- const previous = history.past[history.past.length - 1];
29
- return {
30
- past: history.past.slice(0, -1),
31
- present: previous,
32
- future: [history.present, ...history.future],
33
- };
34
- }
35
-
36
- redo(history: HistoryState<T>): HistoryState<T> {
37
- if (history.future.length === 0) return history;
38
- const next = history.future[0];
39
- return {
40
- past: [...history.past, history.present],
41
- present: next,
42
- future: history.future.slice(1),
43
- };
44
- }
45
-
46
- canUndo(history: HistoryState<T>): boolean {
47
- return history.past.length > 0;
48
- }
49
-
50
- canRedo(history: HistoryState<T>): boolean {
51
- return history.future.length > 0;
52
- }
53
- }
@@ -1,172 +0,0 @@
1
- import { useState, useCallback, useMemo } from "react";
2
- import { Layer, TextLayer, StickerLayer, ImageFilters, DEFAULT_IMAGE_FILTERS } from "../types";
3
- import { HistoryManager, HistoryState } from "../core/HistoryManager";
4
-
5
- export const usePhotoEditor = (initialLayers: Layer[] = []) => {
6
- const historyManager = useMemo(() => new HistoryManager<Layer[]>(), []);
7
- const [history, setHistory] = useState<HistoryState<Layer[]>>(() =>
8
- historyManager.createInitialState(initialLayers),
9
- );
10
- const [activeLayerId, setActiveLayerId] = useState<string | null>(
11
- initialLayers[0]?.id ?? null,
12
- );
13
- const [filters, setFilters] = useState<ImageFilters>(DEFAULT_IMAGE_FILTERS);
14
-
15
- const layers = history.present;
16
-
17
- const pushState = useCallback(
18
- (newLayers: Layer[]): void => {
19
- setHistory((prev) => historyManager.push(prev, newLayers));
20
- },
21
- [historyManager],
22
- );
23
-
24
- const addTextLayer = useCallback(
25
- (
26
- defaultColor = "#FFFFFF",
27
- overrides: Partial<Omit<TextLayer, "id" | "type">> = {},
28
- ) => {
29
- const id = `text-${Date.now()}`;
30
- const newLayer: TextLayer = {
31
- id,
32
- type: "text",
33
- text: "",
34
- x: 50,
35
- y: 50,
36
- rotation: 0,
37
- scale: 1,
38
- opacity: 1,
39
- zIndex: layers.length,
40
- fontSize: 32,
41
- fontFamily: "System",
42
- color: defaultColor,
43
- backgroundColor: "transparent",
44
- textAlign: "center",
45
- ...overrides,
46
- };
47
- pushState([...layers, newLayer]);
48
- setActiveLayerId(id);
49
- return id;
50
- },
51
- [layers, pushState],
52
- );
53
-
54
- const addStickerLayer = useCallback(
55
- (uri: string) => {
56
- const id = `sticker-${Date.now()}`;
57
- const newLayer: StickerLayer = {
58
- id,
59
- type: "sticker",
60
- uri,
61
- x: 100,
62
- y: 100,
63
- rotation: 0,
64
- scale: 1,
65
- opacity: 1,
66
- zIndex: layers.length,
67
- };
68
- pushState([...layers, newLayer]);
69
- setActiveLayerId(id);
70
- return id;
71
- },
72
- [layers, pushState],
73
- );
74
-
75
- const updateLayer = useCallback(
76
- (id: string, updates: Partial<Layer>): void => {
77
- const newLayers = layers.map((l) => {
78
- if (l.id !== id) return l;
79
- // Type-safe merge: cast to Layer since we're merging valid Partial<Layer> with existing Layer
80
- return { ...l, ...updates } as Layer;
81
- });
82
- pushState(newLayers);
83
- },
84
- [layers, pushState],
85
- );
86
-
87
- const deleteLayer = useCallback(
88
- (id: string): void => {
89
- const newLayers = layers.filter((l) => l.id !== id);
90
- pushState(newLayers);
91
- if (activeLayerId === id) {
92
- setActiveLayerId(newLayers[0]?.id ?? null);
93
- }
94
- },
95
- [layers, activeLayerId, pushState],
96
- );
97
-
98
- const duplicateLayer = useCallback(
99
- (id: string): string | null => {
100
- const layer = layers.find((l) => l.id === id);
101
- if (!layer) return null;
102
- const newId = `${layer.type}-${Date.now()}`;
103
- // Calculate the next zIndex to avoid conflicts
104
- const maxZIndex = layers.length > 0 ? Math.max(...layers.map((l) => l.zIndex)) : -1;
105
- const newLayer = { ...layer, id: newId, x: layer.x + 20, y: layer.y + 20, zIndex: maxZIndex + 1 };
106
- pushState([...layers, newLayer]);
107
- setActiveLayerId(newId);
108
- return newId;
109
- },
110
- [layers, pushState],
111
- );
112
-
113
- const moveLayerUp = useCallback(
114
- (id: string): void => {
115
- const sorted = [...layers].sort((a, b) => a.zIndex - b.zIndex);
116
- const idx = sorted.findIndex((l) => l.id === id);
117
- if (idx >= sorted.length - 1) return;
118
- const reordered = [...sorted];
119
- [reordered[idx], reordered[idx + 1]] = [reordered[idx + 1], reordered[idx]];
120
- pushState(reordered.map((l, i) => ({ ...l, zIndex: i })));
121
- },
122
- [layers, pushState],
123
- );
124
-
125
- const moveLayerDown = useCallback(
126
- (id: string) => {
127
- const sorted = [...layers].sort((a, b) => a.zIndex - b.zIndex);
128
- const idx = sorted.findIndex((l) => l.id === id);
129
- if (idx <= 0) return;
130
- const reordered = [...sorted];
131
- [reordered[idx], reordered[idx - 1]] = [reordered[idx - 1], reordered[idx]];
132
- pushState(reordered.map((l, i) => ({ ...l, zIndex: i })));
133
- },
134
- [layers, pushState],
135
- );
136
-
137
- const undo = useCallback(
138
- (): void => setHistory((prev) => historyManager.undo(prev)),
139
- [historyManager],
140
- );
141
-
142
- const redo = useCallback(
143
- (): void => setHistory((prev) => historyManager.redo(prev)),
144
- [historyManager],
145
- );
146
-
147
- // Memoize return object to prevent infinite re-renders
148
- const sortedLayers = useMemo(() => [...layers].sort((a, b) => a.zIndex - b.zIndex), [layers]);
149
- const activeLayer = useMemo(() => layers.find((l) => l.id === activeLayerId), [layers, activeLayerId]);
150
- const canUndo = useMemo(() => historyManager.canUndo(history), [history]);
151
- const canRedo = useMemo(() => historyManager.canRedo(history), [history]);
152
-
153
- return {
154
- layers: sortedLayers,
155
- activeLayerId,
156
- activeLayer,
157
- addTextLayer,
158
- addStickerLayer,
159
- updateLayer,
160
- deleteLayer,
161
- duplicateLayer,
162
- moveLayerUp,
163
- moveLayerDown,
164
- selectLayer: setActiveLayerId,
165
- undo,
166
- redo,
167
- canUndo,
168
- canRedo,
169
- filters,
170
- updateFilters: setFilters,
171
- };
172
- };
@@ -1,162 +0,0 @@
1
- import { useRef, useState, useCallback, useEffect } from "react";
2
- import { BottomSheetModalRef } from "@umituz/react-native-design-system/molecules";
3
- import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
4
- import { usePhotoEditor } from "./usePhotoEditor";
5
- import { TextLayer, DEFAULT_IMAGE_FILTERS, ImageFilters, TextAlign } from "../types";
6
- import type { FilterOption } from "../constants";
7
- import type { LayerTransform } from "../components/DraggableText";
8
-
9
- export const usePhotoEditorUI = (initialCaption?: string) => {
10
- const tokens = useAppDesignTokens();
11
-
12
- // Bottom sheet refs
13
- const textEditorSheetRef = useRef<BottomSheetModalRef>(null);
14
- const stickerSheetRef = useRef<BottomSheetModalRef>(null);
15
- const filterSheetRef = useRef<BottomSheetModalRef>(null);
16
- const adjustmentsSheetRef = useRef<BottomSheetModalRef>(null);
17
- const layerSheetRef = useRef<BottomSheetModalRef>(null);
18
- const aiSheetRef = useRef<BottomSheetModalRef>(null);
19
-
20
- // Global text/font state
21
- const [selectedFont, setSelectedFont] = useState<string>("System");
22
- const [fontSize, setFontSize] = useState(48);
23
-
24
- // Per-layer text editing state (populated when sheet opens)
25
- const [editingText, setEditingText] = useState("");
26
- const [editingColor, setEditingColor] = useState<string>(tokens.colors.textPrimary);
27
- const [editingAlign, setEditingAlign] = useState<TextAlign>("center");
28
- const [editingBold, setEditingBold] = useState(false);
29
- const [editingItalic, setEditingItalic] = useState(false);
30
-
31
- // Filter state
32
- const [selectedFilter, setSelectedFilter] = useState("none");
33
-
34
- const editor = usePhotoEditor([]);
35
-
36
- // Apply initial caption once on mount — single history entry
37
- const prevInitialCaptionRef = useRef<string | undefined>(undefined);
38
-
39
- useEffect(() => {
40
- // Only apply if initialCaption changed and is different from previous value
41
- if (initialCaption && initialCaption !== prevInitialCaptionRef.current) {
42
- prevInitialCaptionRef.current = initialCaption;
43
- editor.addTextLayer(tokens.colors.textPrimary, { text: initialCaption });
44
- }
45
- }, [initialCaption, editor]);
46
-
47
- const handleTextLayerTap = useCallback(
48
- (layerId: string): void => {
49
- editor.selectLayer(layerId);
50
- const layer = editor.layers.find((l) => l.id === layerId);
51
- if (layer?.type === "text") {
52
- const textLayer = layer as TextLayer;
53
- setEditingText(textLayer.text ?? "");
54
- setFontSize(textLayer.fontSize ?? 48);
55
- setEditingColor(textLayer.color ?? tokens.colors.textPrimary);
56
- setEditingAlign(textLayer.textAlign ?? "center");
57
- setEditingBold(textLayer.isBold ?? false);
58
- setEditingItalic(textLayer.isItalic ?? false);
59
- textEditorSheetRef.current?.present();
60
- }
61
- },
62
- [editor, tokens.colors.textPrimary],
63
- );
64
-
65
- const handleSaveText = useCallback((): void => {
66
- if (editor.activeLayerId) {
67
- editor.updateLayer(editor.activeLayerId, {
68
- text: editingText,
69
- fontSize,
70
- fontFamily: selectedFont,
71
- color: editingColor,
72
- textAlign: editingAlign,
73
- isBold: editingBold,
74
- isItalic: editingItalic,
75
- });
76
- }
77
- textEditorSheetRef.current?.dismiss();
78
- }, [
79
- editor,
80
- editingText,
81
- fontSize,
82
- selectedFont,
83
- editingColor,
84
- editingAlign,
85
- editingBold,
86
- editingItalic,
87
- ]);
88
-
89
- const handleSelectFilter = useCallback(
90
- (option: FilterOption): void => {
91
- setSelectedFilter(option.id);
92
- const newFilters: ImageFilters = { ...DEFAULT_IMAGE_FILTERS, ...option.filters };
93
- editor.updateFilters(newFilters);
94
- filterSheetRef.current?.dismiss();
95
- },
96
- [editor],
97
- );
98
-
99
- const handleLayerTransform = useCallback(
100
- (layerId: string, transform: LayerTransform): void => {
101
- editor.updateLayer(layerId, {
102
- x: transform.x,
103
- y: transform.y,
104
- scale: transform.scale,
105
- rotation: transform.rotation,
106
- });
107
- },
108
- [editor],
109
- );
110
-
111
- return {
112
- ...editor,
113
- // Sheet refs
114
- textEditorSheetRef,
115
- stickerSheetRef,
116
- filterSheetRef,
117
- adjustmentsSheetRef,
118
- layerSheetRef,
119
- aiSheetRef,
120
- // Font/size
121
- selectedFont,
122
- setSelectedFont,
123
- fontSize,
124
- setFontSize,
125
- // Text editing
126
- editingText,
127
- setEditingText,
128
- editingColor,
129
- setEditingColor,
130
- editingAlign,
131
- setEditingAlign,
132
- editingBold,
133
- setEditingBold,
134
- editingItalic,
135
- setEditingItalic,
136
- // Filter
137
- selectedFilter,
138
- // Handlers
139
- handleTextLayerTap,
140
- handleSaveText,
141
- handleSelectFilter,
142
- handleLayerTransform,
143
- handleAddText: useCallback((): void => {
144
- const color = tokens.colors.textPrimary;
145
- setEditingText("");
146
- setEditingColor(color);
147
- setEditingAlign("center");
148
- setEditingBold(false);
149
- setEditingItalic(false);
150
- // Create layer with the currently active font settings so canvas preview matches sheet
151
- editor.addTextLayer(color, {
152
- fontSize,
153
- fontFamily: selectedFont,
154
- });
155
- textEditorSheetRef.current?.present();
156
- }, [editor, fontSize, selectedFont, tokens.colors.textPrimary]),
157
- handleSelectSticker: useCallback((uri: string): void => {
158
- editor.addStickerLayer(uri);
159
- stickerSheetRef.current?.dismiss();
160
- }, [editor]),
161
- };
162
- };