@umituz/react-native-photo-editor 1.0.9 → 1.0.11

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.
@@ -1,6 +1,7 @@
1
1
  import { useState, useCallback, useMemo } from "react";
2
- import { Layer, TextLayer, ImageFilters } from "../types";
3
- import { HistoryManager } from "../core/HistoryManager";
2
+ import { DesignTokens } from "@umituz/react-native-design-system";
3
+ import { Layer, TextLayer, StickerLayer, ImageFilters } from "../types";
4
+ import { HistoryManager, HistoryState } from "../core/HistoryManager";
4
5
 
5
6
  const DEFAULT_FILTERS: ImageFilters = {
6
7
  brightness: 1,
@@ -10,12 +11,9 @@ const DEFAULT_FILTERS: ImageFilters = {
10
11
  grayscale: 0,
11
12
  };
12
13
 
13
- const historyManager = new HistoryManager<Layer[]>();
14
-
15
- import { DesignTokens } from "@umituz/react-native-design-system";
16
-
17
14
  export const usePhotoEditor = (initialLayers: Layer[] = []) => {
18
- const [history, setHistory] = useState(() =>
15
+ const historyManager = useMemo(() => new HistoryManager<Layer[]>(), []);
16
+ const [history, setHistory] = useState<HistoryState<Layer[]>>(() =>
19
17
  historyManager.createInitialState(initialLayers),
20
18
  );
21
19
  const [activeLayerId, setActiveLayerId] = useState<string | null>(
@@ -25,15 +23,12 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
25
23
 
26
24
  const layers = history.present;
27
25
 
28
- const canUndo = historyManager.canUndo(history);
29
- const canRedo = historyManager.canRedo(history);
30
-
31
26
  const pushState = useCallback((newLayers: Layer[]) => {
32
27
  setHistory((prev) => historyManager.push(prev, newLayers));
33
- }, []);
28
+ }, [historyManager]);
34
29
 
35
30
  const addTextLayer = useCallback(
36
- (defaultTokens: DesignTokens) => {
31
+ (tokens: DesignTokens) => {
37
32
  const id = `text-${Date.now()}`;
38
33
  const newLayer: TextLayer = {
39
34
  id,
@@ -47,16 +42,12 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
47
42
  zIndex: layers.length,
48
43
  fontSize: 32,
49
44
  fontFamily: "System",
50
- color: defaultTokens.colors.onPrimary,
45
+ color: tokens.colors.textPrimary,
51
46
  backgroundColor: "transparent",
52
47
  textAlign: "center",
53
- strokeWidth: 2,
54
- strokeColor: defaultTokens.colors.onBackground,
55
48
  };
56
-
57
- const newLayers: Layer[] = [...layers, newLayer];
58
- pushState(newLayers);
59
- setActiveLayerId(newLayer.id);
49
+ pushState([...layers, newLayer]);
50
+ setActiveLayerId(id);
60
51
  return id;
61
52
  },
62
53
  [layers, pushState],
@@ -65,10 +56,10 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
65
56
  const addStickerLayer = useCallback(
66
57
  (uri: string) => {
67
58
  const id = `sticker-${Date.now()}`;
68
- const newLayer: Layer = {
59
+ const newLayer: StickerLayer = {
69
60
  id,
70
61
  type: "sticker",
71
- uri: uri,
62
+ uri,
72
63
  x: 100,
73
64
  y: 100,
74
65
  rotation: 0,
@@ -76,90 +67,44 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
76
67
  opacity: 1,
77
68
  zIndex: layers.length,
78
69
  };
79
-
80
- const newLayers: Layer[] = [...layers, newLayer];
81
- pushState(newLayers);
82
- setActiveLayerId(newLayer.id);
70
+ pushState([...layers, newLayer]);
71
+ setActiveLayerId(id);
83
72
  return id;
84
73
  },
85
74
  [layers, pushState],
86
75
  );
87
76
 
88
77
  const updateLayer = useCallback(
89
- (id: string, updates: Partial<Layer>, silent = false) => {
90
- const newLayers: Layer[] = layers.map((layer) =>
91
- layer.id === id ? ({ ...layer, ...updates } as Layer) : layer,
92
- );
93
- if (silent) {
94
- // Just update state without pushing to history (hacky but works for init)
95
- setHistory((prev) => ({ ...prev, present: newLayers }));
96
- } else {
97
- pushState(newLayers);
98
- }
78
+ (id: string, updates: Partial<Layer>) => {
79
+ const newLayers = layers.map((l) => (l.id === id ? { ...l, ...updates } : l) as Layer);
80
+ pushState(newLayers);
99
81
  },
100
82
  [layers, pushState],
101
83
  );
102
84
 
103
85
  const deleteLayer = useCallback(
104
86
  (id: string) => {
105
- if (layers.length <= 1) return;
106
- const newLayers = layers.filter((layer) => layer.id !== id);
87
+ const newLayers = layers.filter((l) => l.id !== id);
107
88
  pushState(newLayers);
108
- if (activeLayerId === id) {
109
- setActiveLayerId(newLayers[0]?.id || null);
110
- }
89
+ if (activeLayerId === id) setActiveLayerId(newLayers[0]?.id || null);
111
90
  },
112
91
  [layers, activeLayerId, pushState],
113
92
  );
114
93
 
115
- const undo = useCallback(() => {
116
- setHistory((prev) => historyManager.undo(prev));
117
- }, []);
118
-
119
- const redo = useCallback(() => {
120
- setHistory((prev) => historyManager.redo(prev));
121
- }, []);
122
-
123
- const selectLayer = useCallback((id: string) => {
124
- setActiveLayerId(id);
125
- }, []);
126
-
127
- const bringToFront = useCallback(
128
- (id: string) => {
129
- const maxZ = Math.max(...layers.map((l) => l.zIndex), 0);
130
- updateLayer(id, { zIndex: maxZ + 1 });
131
- },
132
- [layers, updateLayer],
133
- );
134
-
135
- const captureImage = useCallback(
136
- async (_viewRef: unknown, backgroundUrl: string) => {
137
- return backgroundUrl;
138
- },
139
- [],
140
- );
141
-
142
- const activeLayer = useMemo(
143
- () => layers.find((l) => l.id === activeLayerId),
144
- [layers, activeLayerId],
145
- );
146
-
147
94
  return {
148
95
  layers: [...layers].sort((a, b) => a.zIndex - b.zIndex),
149
- activeLayer,
150
96
  activeLayerId,
97
+ activeLayer: layers.find((l) => l.id === activeLayerId),
151
98
  addTextLayer,
152
99
  addStickerLayer,
153
100
  updateLayer,
154
101
  deleteLayer,
155
- selectLayer,
156
- undo,
157
- redo,
158
- canUndo,
159
- canRedo,
160
- bringToFront,
102
+ selectLayer: setActiveLayerId,
103
+ undo: useCallback(() => setHistory(historyManager.undo), [historyManager]),
104
+ redo: useCallback(() => setHistory(historyManager.redo), [historyManager]),
105
+ canUndo: historyManager.canUndo(history),
106
+ canRedo: historyManager.canRedo(history),
161
107
  filters,
162
108
  updateFilters: setFilters,
163
- captureImage,
164
109
  };
165
110
  };
@@ -1,7 +1,7 @@
1
1
  import { useRef, useState, useCallback, useEffect } from "react";
2
2
  import { BottomSheetModalRef, DesignTokens } from "@umituz/react-native-design-system";
3
3
  import { usePhotoEditor } from "./usePhotoEditor";
4
- import { Layer, TextLayer } from "../types";
4
+ import { TextLayer } from "../types";
5
5
 
6
6
  export const usePhotoEditorUI = (
7
7
  initialCaption: string | undefined,
@@ -11,45 +11,26 @@ export const usePhotoEditorUI = (
11
11
  const stickerSheetRef = useRef<BottomSheetModalRef>(null);
12
12
  const filterSheetRef = useRef<BottomSheetModalRef>(null);
13
13
  const layerSheetRef = useRef<BottomSheetModalRef>(null);
14
+ const aiSheetRef = useRef<BottomSheetModalRef>(null);
14
15
 
15
- const [selectedFont, setSelectedFont] = useState<string>("Impact");
16
+ const [selectedFont, setSelectedFont] = useState<string>("System");
16
17
  const [fontSize, setFontSize] = useState(48);
17
18
  const [editingText, setEditingText] = useState("");
18
19
  const [selectedFilter, setSelectedFilter] = useState("none");
19
20
 
20
- const {
21
- layers,
22
- activeLayerId,
23
- addTextLayer,
24
- addStickerLayer,
25
- updateLayer,
26
- deleteLayer,
27
- selectLayer,
28
- updateFilters,
29
- filters,
30
- } = usePhotoEditor([]);
21
+ const editor = usePhotoEditor([]);
31
22
 
32
- // Handle initial caption
33
23
  useEffect(() => {
34
24
  if (initialCaption) {
35
- const id = addTextLayer(tokens);
36
- void Promise.resolve().then(() => {
37
- updateLayer(id, { text: initialCaption } as Partial<Layer>, true);
38
- });
25
+ const id = editor.addTextLayer(tokens);
26
+ editor.updateLayer(id, { text: initialCaption });
39
27
  }
40
- }, [initialCaption, addTextLayer, tokens]);
41
-
42
- const handleAddText = useCallback(() => {
43
- addTextLayer(tokens);
44
- void Promise.resolve().then(() => {
45
- textEditorSheetRef.current?.present();
46
- });
47
- }, [addTextLayer, tokens]);
28
+ }, [initialCaption]);
48
29
 
49
30
  const handleTextLayerTap = useCallback(
50
31
  (layerId: string) => {
51
- selectLayer(layerId);
52
- const layer = layers.find((l) => l.id === layerId);
32
+ editor.selectLayer(layerId);
33
+ const layer = editor.layers.find((l) => l.id === layerId);
53
34
  if (layer?.type === "text") {
54
35
  const textLayer = layer as TextLayer;
55
36
  setEditingText(textLayer.text || "");
@@ -57,53 +38,27 @@ export const usePhotoEditorUI = (
57
38
  textEditorSheetRef.current?.present();
58
39
  }
59
40
  },
60
- [selectLayer, layers],
41
+ [editor],
61
42
  );
62
43
 
63
44
  const handleSaveText = useCallback(() => {
64
- if (activeLayerId) {
65
- updateLayer(activeLayerId, {
45
+ if (editor.activeLayerId) {
46
+ editor.updateLayer(editor.activeLayerId, {
66
47
  text: editingText,
67
48
  fontSize,
68
49
  fontFamily: selectedFont,
69
- } as Partial<Layer>);
50
+ });
70
51
  }
71
52
  textEditorSheetRef.current?.dismiss();
72
- }, [activeLayerId, editingText, fontSize, selectedFont, updateLayer, textEditorSheetRef]);
73
-
74
- const handleSelectFilter = useCallback(
75
- (filterId: string, value: number) => {
76
- setSelectedFilter(filterId);
77
- const base = {
78
- brightness: 1,
79
- contrast: 1,
80
- saturation: 1,
81
- sepia: 0,
82
- grayscale: 0,
83
- };
84
- if (filterId === "sepia") updateFilters({ ...base, sepia: value });
85
- else if (filterId === "grayscale")
86
- updateFilters({ ...base, grayscale: value });
87
- else updateFilters(base);
88
- },
89
- [updateFilters],
90
- );
91
-
92
- const handleSelectSticker = useCallback(
93
- (sticker: string) => {
94
- addStickerLayer(sticker);
95
- stickerSheetRef.current?.dismiss();
96
- },
97
- [addStickerLayer],
98
- );
53
+ }, [editor.activeLayerId, editingText, fontSize, selectedFont, editor.updateLayer]);
99
54
 
100
55
  return {
101
- // Refs
56
+ ...editor,
102
57
  textEditorSheetRef,
103
58
  stickerSheetRef,
104
59
  filterSheetRef,
105
60
  layerSheetRef,
106
- // State
61
+ aiSheetRef,
107
62
  selectedFont,
108
63
  setSelectedFont,
109
64
  fontSize,
@@ -111,20 +66,20 @@ export const usePhotoEditorUI = (
111
66
  editingText,
112
67
  setEditingText,
113
68
  selectedFilter,
114
- // Domain State
115
- layers,
116
- activeLayerId,
117
- filters,
118
- // Domain Actions
119
- updateLayer,
120
- deleteLayer,
121
- selectLayer,
122
- addTextLayer,
123
- // UI Actions
124
- handleAddText,
125
69
  handleTextLayerTap,
126
70
  handleSaveText,
127
- handleSelectFilter,
128
- handleSelectSticker,
71
+ handleAddText: () => {
72
+ editor.addTextLayer(tokens);
73
+ textEditorSheetRef.current?.present();
74
+ },
75
+ handleSelectSticker: (s: string) => {
76
+ editor.addStickerLayer(s);
77
+ stickerSheetRef.current?.dismiss();
78
+ },
79
+ handleSelectFilter: (id: string, val: number) => {
80
+ setSelectedFilter(id);
81
+ editor.updateFilters({ ...editor.filters, [id]: val });
82
+ filterSheetRef.current?.dismiss();
83
+ }
129
84
  };
130
85
  };
package/src/styles.ts CHANGED
@@ -12,89 +12,41 @@ export const createEditorStyles = (tokens: DesignTokens, insets: EdgeInsets) =>
12
12
  paddingHorizontal: tokens.spacing.md,
13
13
  paddingTop: insets.top + tokens.spacing.sm,
14
14
  paddingBottom: tokens.spacing.sm,
15
- },
16
- headerButton: {
17
- width: 48,
18
- height: 48,
19
- borderRadius: 24,
20
- alignItems: "center",
21
- justifyContent: "center",
15
+ gap: tokens.spacing.md,
22
16
  },
23
17
  headerTitle: {
24
- fontSize: 18,
25
- fontWeight: "bold",
26
- color: tokens.colors.textPrimary,
27
- },
28
- postButton: {
29
- backgroundColor: tokens.colors.primary,
30
- paddingHorizontal: 20,
31
- paddingVertical: 10,
32
- borderRadius: 999,
18
+ flex: 1,
19
+ textAlign: "center",
33
20
  },
34
- postButtonText: {
35
- color: tokens.colors.onPrimary,
36
- fontWeight: "bold",
37
- fontSize: 14,
21
+ scrollContent: {
22
+ paddingHorizontal: tokens.spacing.md,
23
+ paddingBottom: 120,
24
+ gap: tokens.spacing.lg,
38
25
  },
39
- scrollContent: { paddingHorizontal: tokens.spacing.md, paddingBottom: 120 },
40
26
  canvas: {
41
27
  width: "100%",
42
- aspectRatio: 4 / 5,
43
- borderRadius: 16,
28
+ aspectRatio: 1,
29
+ borderRadius: tokens.borders.radius.lg,
44
30
  overflow: "hidden",
45
31
  backgroundColor: tokens.colors.surfaceVariant,
46
- marginTop: tokens.spacing.sm,
47
32
  },
48
33
  canvasImage: { width: "100%", height: "100%" },
49
- // Slider & Controls
50
34
  controlsPanel: {
51
- marginTop: tokens.spacing.md,
52
- backgroundColor: tokens.colors.surfaceVariant + "80", // Opacity handled by token if possible, else hex
53
- borderRadius: 16,
54
35
  padding: tokens.spacing.md,
55
- borderWidth: 1,
56
- borderColor: tokens.colors.border,
57
- },
58
- sliderRow: {
59
- flexDirection: "row",
60
- alignItems: "center",
61
- justifyContent: "space-between",
62
- marginBottom: tokens.spacing.sm,
63
- },
64
- sliderLabel: {
65
- flexDirection: "row",
66
- alignItems: "center",
67
- gap: tokens.spacing.xs,
68
- },
69
- sliderLabelText: { fontSize: 14, color: tokens.colors.textSecondary },
70
- sliderValue: {
71
- fontSize: 14,
72
- fontWeight: "bold",
73
- color: tokens.colors.primary,
74
- },
75
- sliderTrack: {
76
- height: 6,
77
- backgroundColor: tokens.colors.border,
78
- borderRadius: 3,
79
- marginBottom: tokens.spacing.lg,
80
- },
81
- sliderFill: {
82
- height: "100%",
83
- width: "65%",
84
- backgroundColor: tokens.colors.primary,
85
- borderRadius: 3,
36
+ backgroundColor: tokens.colors.surfaceVariant,
37
+ borderRadius: tokens.borders.radius.md,
38
+ gap: tokens.spacing.md,
86
39
  },
87
- fontLabel: {
88
- fontSize: 14,
89
- color: tokens.colors.textSecondary,
90
- marginBottom: tokens.spacing.sm,
40
+ fontRow: {
41
+ flexDirection: "row",
42
+ gap: tokens.spacing.sm,
43
+ flexWrap: "wrap",
91
44
  },
92
- fontRow: { flexDirection: "row", gap: tokens.spacing.sm },
93
45
  fontChip: {
94
46
  paddingHorizontal: tokens.spacing.md,
95
47
  paddingVertical: tokens.spacing.sm,
96
- backgroundColor: tokens.colors.surfaceVariant,
97
- borderRadius: 8,
48
+ backgroundColor: tokens.colors.surface,
49
+ borderRadius: tokens.borders.radius.sm,
98
50
  borderWidth: 1,
99
51
  borderColor: tokens.colors.border,
100
52
  },
@@ -102,59 +54,27 @@ export const createEditorStyles = (tokens: DesignTokens, insets: EdgeInsets) =>
102
54
  backgroundColor: tokens.colors.primary,
103
55
  borderColor: tokens.colors.primary,
104
56
  },
105
- fontChipText: {
106
- fontWeight: "bold",
107
- fontSize: 14,
108
- color: tokens.colors.textSecondary,
109
- },
110
- fontChipTextActive: { color: tokens.colors.onPrimary },
111
- // Bottom Toolbar
112
57
  bottomToolbar: {
113
58
  position: "absolute",
114
59
  bottom: insets.bottom + tokens.spacing.md,
115
- left: "5%",
116
- right: "5%",
117
- backgroundColor: tokens.colors.surfaceVariant,
60
+ left: tokens.spacing.md,
61
+ right: tokens.spacing.md,
62
+ backgroundColor: tokens.colors.surface,
118
63
  borderRadius: 999,
119
- padding: tokens.spacing.sm,
64
+ padding: tokens.spacing.xs,
120
65
  flexDirection: "row",
121
66
  justifyContent: "space-around",
122
67
  alignItems: "center",
123
68
  borderWidth: 1,
124
69
  borderColor: tokens.colors.border,
125
- shadowColor: "#000",
126
- shadowOffset: {
127
- width: 0,
128
- height: 2,
129
- },
130
- shadowOpacity: 0.1,
131
- shadowRadius: 3.84,
132
- elevation: 5,
133
70
  },
134
71
  toolButton: {
135
72
  alignItems: "center",
136
73
  justifyContent: "center",
137
- width: 56,
138
- height: 56,
139
- borderRadius: 28,
140
- },
141
- toolButtonActive: { backgroundColor: tokens.colors.primary + "20" },
142
- toolLabel: {
143
- fontSize: 10,
144
- fontWeight: "500",
145
- color: tokens.colors.textSecondary,
146
- marginTop: 2,
74
+ padding: tokens.spacing.sm,
75
+ borderRadius: 999,
147
76
  },
148
- toolLabelActive: { color: tokens.colors.primary },
149
- aiMagicButton: {
150
- width: 64,
151
- height: 64,
152
- borderRadius: 32,
153
- backgroundColor: tokens.colors.primary,
154
- alignItems: "center",
155
- justifyContent: "center",
156
- marginTop: -32,
157
- borderWidth: 4,
158
- borderColor: tokens.colors.background,
77
+ toolButtonActive: {
78
+ backgroundColor: tokens.colors.primary + "20",
159
79
  },
160
80
  });