@umituz/react-native-photo-editor 2.0.10 → 2.0.12

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/eslint.config.js CHANGED
@@ -13,6 +13,9 @@ module.exports = [
13
13
  sourceType: "module",
14
14
  project: "./tsconfig.json",
15
15
  },
16
+ globals: {
17
+ console: "readonly",
18
+ },
16
19
  },
17
20
  plugins: {
18
21
  "@typescript-eslint": tseslint,
@@ -23,6 +26,7 @@ module.exports = [
23
26
  "@typescript-eslint/explicit-function-return-type": "off",
24
27
  "@typescript-eslint/no-explicit-any": "warn",
25
28
  "no-console": "off",
29
+ "no-undef": "off",
26
30
  },
27
31
  },
28
32
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-photo-editor",
3
- "version": "2.0.10",
3
+ "version": "2.0.12",
4
4
  "description": "A powerful, generic photo editor for React Native",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -36,12 +36,23 @@
36
36
  "react-native-safe-area-context": ">=4.0.0"
37
37
  },
38
38
  "devDependencies": {
39
+ "@react-native-async-storage/async-storage": "^3.0.1",
40
+ "@react-navigation/bottom-tabs": "^7.15.5",
41
+ "@react-navigation/elements": "^2.9.10",
42
+ "@react-navigation/native": "^7.1.33",
43
+ "@react-navigation/stack": "^7.8.5",
44
+ "@tanstack/react-query": "^5.90.21",
39
45
  "@types/react": "^19.1.0",
46
+ "@types/uuid": "^10.0.0",
40
47
  "@typescript-eslint/eslint-plugin": "^7.0.0",
41
48
  "@typescript-eslint/parser": "^7.0.0",
42
49
  "@umituz/react-native-design-system": "^4.25.90",
43
50
  "eslint": "^8.57.0",
51
+ "expo-clipboard": "^55.0.8",
52
+ "expo-crypto": "^55.0.9",
44
53
  "expo-file-system": "~19.0.21",
54
+ "expo-font": "^55.0.4",
55
+ "expo-haptics": "^55.0.8",
45
56
  "expo-image": "~3.0.11",
46
57
  "expo-image-picker": "~17.0.10",
47
58
  "expo-media-library": "~18.2.1",
@@ -49,6 +60,8 @@
49
60
  "react-native": "0.81.4",
50
61
  "react-native-gesture-handler": "^2.30.0",
51
62
  "react-native-safe-area-context": "^5.6.2",
52
- "typescript": "^5.3.0"
63
+ "react-native-svg": "^15.15.3",
64
+ "typescript": "^5.3.0",
65
+ "uuid": "^13.0.0"
53
66
  }
54
67
  }
@@ -1,11 +1,11 @@
1
- import React, { useMemo } from "react";
1
+ import React, { useMemo, useCallback } from "react";
2
2
  import { View, ScrollView, TouchableOpacity } from "react-native";
3
3
  import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
4
4
  import { BottomSheetModal } from "@umituz/react-native-design-system/molecules";
5
5
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
6
6
  import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
7
7
 
8
- import { EditorCanvas } from "./components/EditorCanvas";
8
+ import EditorCanvas from "./components/EditorCanvas";
9
9
  import { EditorToolbar } from "./components/EditorToolbar";
10
10
  import { FontControls } from "./components/FontControls";
11
11
  import { LayerManager } from "./components/LayerManager";
@@ -19,19 +19,6 @@ import { usePhotoEditorUI } from "./hooks/usePhotoEditorUI";
19
19
  import { Layer, ImageFilters } from "./types";
20
20
  import { DEFAULT_FONTS } from "./constants";
21
21
 
22
- export interface EditorActions {
23
- addTextLayer: (defaultColor?: string) => string;
24
- updateLayer: (id: string, updates: Partial<Layer>) => void;
25
- duplicateLayer: (id: string) => string | null;
26
- deleteLayer: (id: string) => void;
27
- moveLayerUp: (id: string) => void;
28
- moveLayerDown: (id: string) => void;
29
- getLayers: () => Layer[];
30
- getActiveLayerId: () => string | null;
31
- undo: () => void;
32
- redo: () => void;
33
- }
34
-
35
22
  export interface PhotoEditorProps {
36
23
  imageUri: string;
37
24
  /**
@@ -42,8 +29,8 @@ export interface PhotoEditorProps {
42
29
  onSave?: (uri: string, layers: Layer[], filters: ImageFilters) => void;
43
30
  onClose: () => void;
44
31
  title?: string;
45
- /** Render extra tools below the canvas. Receives editor action helpers. */
46
- customTools?: React.ReactNode | ((actions: EditorActions) => React.ReactNode);
32
+ /** Render extra tools below the canvas. Receives editor state helpers. */
33
+ customTools?: React.ReactNode | ((ui: ReturnType<typeof usePhotoEditorUI>) => React.ReactNode);
47
34
  initialCaption?: string;
48
35
  t: (key: string) => string;
49
36
  fonts?: readonly string[];
@@ -51,7 +38,7 @@ export interface PhotoEditorProps {
51
38
  onAICaption?: (style: string) => Promise<string> | void;
52
39
  }
53
40
 
54
- export const PhotoEditor: React.FC<PhotoEditorProps> = ({
41
+ export function PhotoEditor({
55
42
  imageUri,
56
43
  onSave,
57
44
  onClose,
@@ -61,7 +48,7 @@ export const PhotoEditor: React.FC<PhotoEditorProps> = ({
61
48
  t,
62
49
  fonts = DEFAULT_FONTS,
63
50
  onAICaption,
64
- }) => {
51
+ }: PhotoEditorProps) {
65
52
  const tokens = useAppDesignTokens();
66
53
  const insets = useSafeAreaInsets();
67
54
  const styles = useMemo(
@@ -70,25 +57,11 @@ export const PhotoEditor: React.FC<PhotoEditorProps> = ({
70
57
  );
71
58
  const ui = usePhotoEditorUI(initialCaption);
72
59
 
73
- const actions: EditorActions = useMemo(
74
- () => ({
75
- addTextLayer: (color?: string) =>
76
- ui.addTextLayer(color ?? tokens.colors.textPrimary),
77
- updateLayer: ui.updateLayer,
78
- duplicateLayer: ui.duplicateLayer,
79
- deleteLayer: ui.deleteLayer,
80
- moveLayerUp: ui.moveLayerUp,
81
- moveLayerDown: ui.moveLayerDown,
82
- getLayers: () => ui.layers,
83
- getActiveLayerId: () => ui.activeLayerId,
84
- undo: ui.undo,
85
- redo: ui.redo,
86
- }),
87
- [ui, tokens.colors.textPrimary],
60
+ const handleSave = useCallback(
61
+ () => onSave?.(imageUri, ui.layers, ui.filters),
62
+ [onSave, imageUri, ui.layers, ui.filters],
88
63
  );
89
64
 
90
- const handleSave = () => onSave?.(imageUri, ui.layers, ui.filters);
91
-
92
65
  return (
93
66
  <View style={styles.container}>
94
67
  {/* Header */}
@@ -125,7 +98,7 @@ export const PhotoEditor: React.FC<PhotoEditorProps> = ({
125
98
  styles={styles}
126
99
  />
127
100
 
128
- {typeof customTools === "function" ? customTools(actions) : customTools}
101
+ {typeof customTools === "function" ? customTools(ui) : customTools}
129
102
 
130
103
  <FontControls
131
104
  fontSize={ui.fontSize}
@@ -208,4 +181,4 @@ export const PhotoEditor: React.FC<PhotoEditorProps> = ({
208
181
  )}
209
182
  </View>
210
183
  );
211
- };
184
+ }
@@ -34,7 +34,8 @@ export const useImagePicker = (): UseImagePickerReturn => {
34
34
  if (result.canceled || result.assets.length === 0) return null;
35
35
  const asset = result.assets[0];
36
36
  return { uri: asset.uri, width: asset.width, height: asset.height };
37
- } catch {
37
+ } catch (error) {
38
+ console.error("[useImagePicker] Error picking from gallery:", error);
38
39
  return null;
39
40
  } finally {
40
41
  setLoading(false);
@@ -58,7 +59,8 @@ export const useImagePicker = (): UseImagePickerReturn => {
58
59
  if (result.canceled || result.assets.length === 0) return null;
59
60
  const asset = result.assets[0];
60
61
  return { uri: asset.uri, width: asset.width, height: asset.height };
61
- } catch {
62
+ } catch (error) {
63
+ console.error("[useImagePicker] Error taking photo:", error);
62
64
  return null;
63
65
  } finally {
64
66
  setLoading(false);
@@ -15,7 +15,7 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
15
15
  const layers = history.present;
16
16
 
17
17
  const pushState = useCallback(
18
- (newLayers: Layer[]) => {
18
+ (newLayers: Layer[]): void => {
19
19
  setHistory((prev) => historyManager.push(prev, newLayers));
20
20
  },
21
21
  [historyManager],
@@ -73,17 +73,19 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
73
73
  );
74
74
 
75
75
  const updateLayer = useCallback(
76
- (id: string, updates: Partial<Layer>) => {
77
- const newLayers = layers.map(
78
- (l) => (l.id === id ? ({ ...l, ...updates } as Layer) : l),
79
- );
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
+ });
80
82
  pushState(newLayers);
81
83
  },
82
84
  [layers, pushState],
83
85
  );
84
86
 
85
87
  const deleteLayer = useCallback(
86
- (id: string) => {
88
+ (id: string): void => {
87
89
  const newLayers = layers.filter((l) => l.id !== id);
88
90
  pushState(newLayers);
89
91
  if (activeLayerId === id) {
@@ -94,11 +96,13 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
94
96
  );
95
97
 
96
98
  const duplicateLayer = useCallback(
97
- (id: string) => {
99
+ (id: string): string | null => {
98
100
  const layer = layers.find((l) => l.id === id);
99
101
  if (!layer) return null;
100
102
  const newId = `${layer.type}-${Date.now()}`;
101
- const newLayer = { ...layer, id: newId, x: layer.x + 20, y: layer.y + 20, zIndex: layers.length };
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 };
102
106
  pushState([...layers, newLayer]);
103
107
  setActiveLayerId(newId);
104
108
  return newId;
@@ -107,7 +111,7 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
107
111
  );
108
112
 
109
113
  const moveLayerUp = useCallback(
110
- (id: string) => {
114
+ (id: string): void => {
111
115
  const sorted = [...layers].sort((a, b) => a.zIndex - b.zIndex);
112
116
  const idx = sorted.findIndex((l) => l.id === id);
113
117
  if (idx >= sorted.length - 1) return;
@@ -131,19 +135,25 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
131
135
  );
132
136
 
133
137
  const undo = useCallback(
134
- () => setHistory((prev) => historyManager.undo(prev)),
138
+ (): void => setHistory((prev) => historyManager.undo(prev)),
135
139
  [historyManager],
136
140
  );
137
141
 
138
142
  const redo = useCallback(
139
- () => setHistory((prev) => historyManager.redo(prev)),
143
+ (): void => setHistory((prev) => historyManager.redo(prev)),
140
144
  [historyManager],
141
145
  );
142
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
+
143
153
  return {
144
- layers: [...layers].sort((a, b) => a.zIndex - b.zIndex),
154
+ layers: sortedLayers,
145
155
  activeLayerId,
146
- activeLayer: layers.find((l) => l.id === activeLayerId),
156
+ activeLayer,
147
157
  addTextLayer,
148
158
  addStickerLayer,
149
159
  updateLayer,
@@ -154,8 +164,8 @@ export const usePhotoEditor = (initialLayers: Layer[] = []) => {
154
164
  selectLayer: setActiveLayerId,
155
165
  undo,
156
166
  redo,
157
- canUndo: historyManager.canUndo(history),
158
- canRedo: historyManager.canRedo(history),
167
+ canUndo,
168
+ canRedo,
159
169
  filters,
160
170
  updateFilters: setFilters,
161
171
  };
@@ -34,16 +34,18 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
34
34
  const editor = usePhotoEditor([]);
35
35
 
36
36
  // Apply initial caption once on mount — single history entry
37
- const initialCaptionApplied = useRef(false);
37
+ const prevInitialCaptionRef = useRef<string | undefined>(undefined);
38
+
38
39
  useEffect(() => {
39
- if (initialCaption && !initialCaptionApplied.current) {
40
- initialCaptionApplied.current = true;
40
+ // Only apply if initialCaption changed and is different from previous value
41
+ if (initialCaption && initialCaption !== prevInitialCaptionRef.current) {
42
+ prevInitialCaptionRef.current = initialCaption;
41
43
  editor.addTextLayer(tokens.colors.textPrimary, { text: initialCaption });
42
44
  }
43
- }, []);
45
+ }, [initialCaption, editor]);
44
46
 
45
47
  const handleTextLayerTap = useCallback(
46
- (layerId: string) => {
48
+ (layerId: string): void => {
47
49
  editor.selectLayer(layerId);
48
50
  const layer = editor.layers.find((l) => l.id === layerId);
49
51
  if (layer?.type === "text") {
@@ -60,7 +62,7 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
60
62
  [editor, tokens.colors.textPrimary],
61
63
  );
62
64
 
63
- const handleSaveText = useCallback(() => {
65
+ const handleSaveText = useCallback((): void => {
64
66
  if (editor.activeLayerId) {
65
67
  editor.updateLayer(editor.activeLayerId, {
66
68
  text: editingText,
@@ -85,7 +87,7 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
85
87
  ]);
86
88
 
87
89
  const handleSelectFilter = useCallback(
88
- (option: FilterOption) => {
90
+ (option: FilterOption): void => {
89
91
  setSelectedFilter(option.id);
90
92
  const newFilters: ImageFilters = { ...DEFAULT_IMAGE_FILTERS, ...option.filters };
91
93
  editor.updateFilters(newFilters);
@@ -95,7 +97,7 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
95
97
  );
96
98
 
97
99
  const handleLayerTransform = useCallback(
98
- (layerId: string, transform: LayerTransform) => {
100
+ (layerId: string, transform: LayerTransform): void => {
99
101
  editor.updateLayer(layerId, {
100
102
  x: transform.x,
101
103
  y: transform.y,
@@ -138,7 +140,7 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
138
140
  handleSaveText,
139
141
  handleSelectFilter,
140
142
  handleLayerTransform,
141
- handleAddText: useCallback(() => {
143
+ handleAddText: useCallback((): void => {
142
144
  const color = tokens.colors.textPrimary;
143
145
  setEditingText("");
144
146
  setEditingColor(color);
@@ -152,7 +154,7 @@ export const usePhotoEditorUI = (initialCaption?: string) => {
152
154
  });
153
155
  textEditorSheetRef.current?.present();
154
156
  }, [editor, fontSize, selectedFont, tokens.colors.textPrimary]),
155
- handleSelectSticker: useCallback((uri: string) => {
157
+ handleSelectSticker: useCallback((uri: string): void => {
156
158
  editor.addStickerLayer(uri);
157
159
  stickerSheetRef.current?.dismiss();
158
160
  }, [editor]),
package/src/index.ts CHANGED
@@ -1,20 +1,9 @@
1
+ // Public API exports
2
+ export { PhotoEditor } from "./PhotoEditor";
3
+ export type { PhotoEditorProps } from "./PhotoEditor";
4
+
5
+ // Type exports for consumer usage
1
6
  export * from "./types";
7
+
8
+ // Constant exports for consumer customization
2
9
  export * from "./constants";
3
- export * from "./hooks/usePhotoEditor";
4
- export * from "./hooks/usePhotoEditorUI";
5
- export * from "./hooks/useImagePicker";
6
- export * from "./utils/mediaUtils";
7
- export * from "./core/HistoryManager";
8
- export * from "./components/EditorCanvas";
9
- export * from "./components/LayerManager";
10
- export * from "./components/FontControls";
11
- export * from "./components/FilterPicker";
12
- export * from "./components/DraggableText";
13
- export * from "./components/DraggableSticker";
14
- export * from "./components/AIMagicSheet";
15
- export * from "./components/TextEditorSheet";
16
- export * from "./components/StickerPicker";
17
- export * from "./components/EditorToolbar";
18
- export * from "./components/ColorPicker";
19
- export * from "./components/AdjustmentsSheet";
20
- export * from "./PhotoEditor";
package/src/types.ts CHANGED
@@ -59,3 +59,12 @@ export interface EditorState {
59
59
  canvasSize: { width: number; height: number };
60
60
  filters: ImageFilters;
61
61
  }
62
+
63
+ // Type guards for discriminating union types
64
+ export function isTextLayer(layer: Layer): layer is TextLayer {
65
+ return layer.type === "text";
66
+ }
67
+
68
+ export function isStickerLayer(layer: Layer): layer is StickerLayer {
69
+ return layer.type === "sticker";
70
+ }
@@ -63,7 +63,8 @@ export const deleteLocalFile = async (uri: string): Promise<void> => {
63
63
  if (file.exists) {
64
64
  file.delete();
65
65
  }
66
- } catch {
67
- // silentbest-effort cleanup
66
+ } catch (error) {
67
+ // Best-effort cleanup log but don't throw
68
+ console.warn("[mediaUtils] Failed to delete local file:", uri, error);
68
69
  }
69
70
  };
package/tsconfig.json CHANGED
@@ -2,6 +2,7 @@
2
2
  "compilerOptions": {
3
3
  "target": "ES2020",
4
4
  "lib": ["ES2020"],
5
+ "module": "ESNext",
5
6
  "allowJs": false,
6
7
  "skipLibCheck": true,
7
8
  "esModuleInterop": true,