@umituz/react-native-photo-editor 2.0.23 → 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/ARCHITECTURE.md +0 -104
- package/MIGRATION.md +0 -100
- 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
|
@@ -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
|
-
};
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* History Manager Implementation
|
|
3
|
-
* Legacy compatibility wrapper for HistoryService
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { HistoryService } from "../../domain/services/HistoryService";
|
|
7
|
-
|
|
8
|
-
const historyService = new HistoryService(20);
|
|
9
|
-
|
|
10
|
-
export type { HistoryState } from "../../domain/services/HistoryService";
|
|
11
|
-
|
|
12
|
-
export class HistoryManager<T> {
|
|
13
|
-
private readonly maxHistory = 20;
|
|
14
|
-
|
|
15
|
-
createInitialState(initialValue: T) {
|
|
16
|
-
return historyService.createInitialState(initialValue);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
push(history: any, newValue: T) {
|
|
20
|
-
return historyService.push(history, newValue);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
undo(history: any) {
|
|
24
|
-
return historyService.undo(history);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
redo(history: any) {
|
|
28
|
-
return historyService.redo(history);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
canUndo(history: any) {
|
|
32
|
-
return historyService.canUndo(history);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
canRedo(history: any) {
|
|
36
|
-
return historyService.canRedo(history);
|
|
37
|
-
}
|
|
38
|
-
}
|