@umituz/react-native-photo-editor 1.1.2 → 2.0.2

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,31 +1,46 @@
1
1
  import { useRef, useState, useCallback, useEffect } from "react";
2
- import { BottomSheetModalRef, DesignTokens } from "@umituz/react-native-design-system";
2
+ import { BottomSheetModalRef } from "@umituz/react-native-design-system/molecules";
3
+ import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
3
4
  import { usePhotoEditor } from "./usePhotoEditor";
4
- import { TextLayer } from "../types";
5
+ import { TextLayer, DEFAULT_IMAGE_FILTERS, ImageFilters, TextAlign } from "../types";
6
+ import type { FilterOption } from "../constants";
7
+ import type { LayerTransform } from "../components/DraggableText";
5
8
 
6
- export const usePhotoEditorUI = (
7
- initialCaption: string | undefined,
8
- tokens: DesignTokens,
9
- ) => {
9
+ export const usePhotoEditorUI = (initialCaption?: string) => {
10
+ const tokens = useAppDesignTokens();
11
+
12
+ // Bottom sheet refs
10
13
  const textEditorSheetRef = useRef<BottomSheetModalRef>(null);
11
14
  const stickerSheetRef = useRef<BottomSheetModalRef>(null);
12
15
  const filterSheetRef = useRef<BottomSheetModalRef>(null);
16
+ const adjustmentsSheetRef = useRef<BottomSheetModalRef>(null);
13
17
  const layerSheetRef = useRef<BottomSheetModalRef>(null);
14
18
  const aiSheetRef = useRef<BottomSheetModalRef>(null);
15
19
 
20
+ // Global text/font state
16
21
  const [selectedFont, setSelectedFont] = useState<string>("System");
17
22
  const [fontSize, setFontSize] = useState(48);
23
+
24
+ // Per-layer text editing state (populated when sheet opens)
18
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
19
32
  const [selectedFilter, setSelectedFilter] = useState("none");
20
33
 
21
34
  const editor = usePhotoEditor([]);
22
35
 
36
+ // Apply initial caption once on mount — single history entry
37
+ const initialCaptionApplied = useRef(false);
23
38
  useEffect(() => {
24
- if (initialCaption) {
25
- const id = editor.addTextLayer(tokens);
26
- editor.updateLayer(id, { text: initialCaption });
39
+ if (initialCaption && !initialCaptionApplied.current) {
40
+ initialCaptionApplied.current = true;
41
+ editor.addTextLayer(tokens.colors.textPrimary, { text: initialCaption });
27
42
  }
28
- }, [initialCaption]);
43
+ }, []);
29
44
 
30
45
  const handleTextLayerTap = useCallback(
31
46
  (layerId: string) => {
@@ -33,12 +48,16 @@ export const usePhotoEditorUI = (
33
48
  const layer = editor.layers.find((l) => l.id === layerId);
34
49
  if (layer?.type === "text") {
35
50
  const textLayer = layer as TextLayer;
36
- setEditingText(textLayer.text || "");
37
- setFontSize(textLayer.fontSize || 48);
51
+ setEditingText(textLayer.text ?? "");
52
+ setFontSize(textLayer.fontSize ?? 48);
53
+ setEditingColor(textLayer.color ?? tokens.colors.textPrimary);
54
+ setEditingAlign(textLayer.textAlign ?? "center");
55
+ setEditingBold(textLayer.isBold ?? false);
56
+ setEditingItalic(textLayer.isItalic ?? false);
38
57
  textEditorSheetRef.current?.present();
39
58
  }
40
59
  },
41
- [editor],
60
+ [editor, tokens.colors.textPrimary],
42
61
  );
43
62
 
44
63
  const handleSaveText = useCallback(() => {
@@ -47,39 +66,95 @@ export const usePhotoEditorUI = (
47
66
  text: editingText,
48
67
  fontSize,
49
68
  fontFamily: selectedFont,
69
+ color: editingColor,
70
+ textAlign: editingAlign,
71
+ isBold: editingBold,
72
+ isItalic: editingItalic,
50
73
  });
51
74
  }
52
75
  textEditorSheetRef.current?.dismiss();
53
- }, [editor.activeLayerId, editingText, fontSize, selectedFont, editor.updateLayer]);
76
+ }, [
77
+ editor,
78
+ editingText,
79
+ fontSize,
80
+ selectedFont,
81
+ editingColor,
82
+ editingAlign,
83
+ editingBold,
84
+ editingItalic,
85
+ ]);
86
+
87
+ const handleSelectFilter = useCallback(
88
+ (option: FilterOption) => {
89
+ setSelectedFilter(option.id);
90
+ const newFilters: ImageFilters = { ...DEFAULT_IMAGE_FILTERS, ...option.filters };
91
+ editor.updateFilters(newFilters);
92
+ filterSheetRef.current?.dismiss();
93
+ },
94
+ [editor],
95
+ );
96
+
97
+ const handleLayerTransform = useCallback(
98
+ (layerId: string, transform: LayerTransform) => {
99
+ editor.updateLayer(layerId, {
100
+ x: transform.x,
101
+ y: transform.y,
102
+ scale: transform.scale,
103
+ rotation: transform.rotation,
104
+ });
105
+ },
106
+ [editor],
107
+ );
54
108
 
55
109
  return {
56
110
  ...editor,
111
+ // Sheet refs
57
112
  textEditorSheetRef,
58
113
  stickerSheetRef,
59
114
  filterSheetRef,
115
+ adjustmentsSheetRef,
60
116
  layerSheetRef,
61
117
  aiSheetRef,
118
+ // Font/size
62
119
  selectedFont,
63
120
  setSelectedFont,
64
121
  fontSize,
65
122
  setFontSize,
123
+ // Text editing
66
124
  editingText,
67
125
  setEditingText,
126
+ editingColor,
127
+ setEditingColor,
128
+ editingAlign,
129
+ setEditingAlign,
130
+ editingBold,
131
+ setEditingBold,
132
+ editingItalic,
133
+ setEditingItalic,
134
+ // Filter
68
135
  selectedFilter,
136
+ // Handlers
69
137
  handleTextLayerTap,
70
138
  handleSaveText,
71
- handleAddText: () => {
72
- editor.addTextLayer(tokens);
139
+ handleSelectFilter,
140
+ handleLayerTransform,
141
+ handleAddText: useCallback(() => {
142
+ const color = tokens.colors.textPrimary;
143
+ setEditingText("");
144
+ setEditingColor(color);
145
+ setEditingAlign("center");
146
+ setEditingBold(false);
147
+ setEditingItalic(false);
148
+ // Create layer with the currently active font settings so canvas preview matches sheet
149
+ editor.addTextLayer(color, {
150
+ fontSize,
151
+ fontFamily: selectedFont,
152
+ });
73
153
  textEditorSheetRef.current?.present();
74
- },
75
- handleSelectSticker: (s: string) => {
76
- editor.addStickerLayer(s);
154
+ }, [editor, fontSize, selectedFont, tokens.colors.textPrimary]),
155
+ handleSelectSticker: useCallback((uri: string) => {
156
+ editor.addStickerLayer(uri);
77
157
  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
- }
158
+ }, [editor]),
84
159
  };
85
160
  };
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  export * from "./types";
2
2
  export * from "./constants";
3
3
  export * from "./hooks/usePhotoEditor";
4
+ export * from "./hooks/usePhotoEditorUI";
5
+ export * from "./hooks/useImagePicker";
6
+ export * from "./utils/mediaUtils";
4
7
  export * from "./core/HistoryManager";
5
8
  export * from "./components/EditorCanvas";
6
9
  export * from "./components/LayerManager";
@@ -9,4 +12,9 @@ export * from "./components/FilterPicker";
9
12
  export * from "./components/DraggableText";
10
13
  export * from "./components/DraggableSticker";
11
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";
12
20
  export * from "./PhotoEditor";
package/src/styles.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { StyleSheet } from "react-native";
2
- import { DesignTokens } from "@umituz/react-native-design-system";
2
+ import { DesignTokens } from "@umituz/react-native-design-system/theme";
3
3
  import { EdgeInsets } from "react-native-safe-area-context";
4
4
 
5
5
  export const createEditorStyles = (tokens: DesignTokens, insets: EdgeInsets) =>
@@ -18,8 +18,8 @@ export const createEditorStyles = (tokens: DesignTokens, insets: EdgeInsets) =>
18
18
  flex: 1,
19
19
  textAlign: "center",
20
20
  },
21
- scrollContent: {
22
- paddingHorizontal: tokens.spacing.md,
21
+ scrollContent: {
22
+ paddingHorizontal: tokens.spacing.md,
23
23
  paddingBottom: 120,
24
24
  gap: tokens.spacing.lg,
25
25
  },
@@ -37,8 +37,18 @@ export const createEditorStyles = (tokens: DesignTokens, insets: EdgeInsets) =>
37
37
  borderRadius: tokens.borders.radius.md,
38
38
  gap: tokens.spacing.md,
39
39
  },
40
- fontRow: {
41
- flexDirection: "row",
40
+ sliderRow: {
41
+ flexDirection: "row",
42
+ alignItems: "center",
43
+ justifyContent: "space-between",
44
+ },
45
+ sliderLabel: {
46
+ flexDirection: "row",
47
+ alignItems: "center",
48
+ gap: tokens.spacing.xs,
49
+ },
50
+ fontRow: {
51
+ flexDirection: "row",
42
52
  gap: tokens.spacing.sm,
43
53
  flexWrap: "wrap",
44
54
  },
@@ -74,7 +84,15 @@ export const createEditorStyles = (tokens: DesignTokens, insets: EdgeInsets) =>
74
84
  padding: tokens.spacing.sm,
75
85
  borderRadius: 999,
76
86
  },
77
- toolButtonActive: {
87
+ toolButtonActive: {
78
88
  backgroundColor: tokens.colors.primary + "20",
79
89
  },
90
+ aiMagicButton: {
91
+ width: 48,
92
+ height: 48,
93
+ borderRadius: 24,
94
+ backgroundColor: tokens.colors.primary,
95
+ alignItems: "center",
96
+ justifyContent: "center",
97
+ },
80
98
  });
package/src/types.ts CHANGED
@@ -10,8 +10,17 @@ export interface ImageFilters {
10
10
  saturation: number;
11
11
  sepia: number;
12
12
  grayscale: number;
13
+ hueRotate?: number; // 0-360 degrees
13
14
  }
14
15
 
16
+ export const DEFAULT_IMAGE_FILTERS: ImageFilters = {
17
+ brightness: 1,
18
+ contrast: 1,
19
+ saturation: 1,
20
+ sepia: 0,
21
+ grayscale: 0,
22
+ };
23
+
15
24
  export interface BaseLayer {
16
25
  id: string;
17
26
  x: number;
@@ -20,7 +29,7 @@ export interface BaseLayer {
20
29
  scale: number;
21
30
  opacity: number;
22
31
  zIndex: number;
23
- type: "text" | "sticker" | "image";
32
+ type: "text" | "sticker";
24
33
  }
25
34
 
26
35
  export interface TextLayer extends BaseLayer {
@@ -31,6 +40,8 @@ export interface TextLayer extends BaseLayer {
31
40
  color: string;
32
41
  backgroundColor: string;
33
42
  textAlign: TextAlign;
43
+ isBold?: boolean;
44
+ isItalic?: boolean;
34
45
  strokeColor?: string;
35
46
  strokeWidth?: number;
36
47
  }
@@ -0,0 +1,69 @@
1
+ import * as MediaLibrary from "expo-media-library";
2
+ import { File, Paths } from "expo-file-system";
3
+ import { Share } from "react-native";
4
+
5
+ export interface SaveResult {
6
+ success: boolean;
7
+ uri?: string;
8
+ error?: string;
9
+ }
10
+
11
+ /**
12
+ * Save an image URI to the device's media library (Camera Roll / Photos).
13
+ * Requests permission if not already granted.
14
+ */
15
+ export const saveToMediaLibrary = async (uri: string): Promise<SaveResult> => {
16
+ try {
17
+ const { status } = await MediaLibrary.requestPermissionsAsync();
18
+ if (status !== "granted") {
19
+ return { success: false, error: "Media library permission denied" };
20
+ }
21
+ const asset = await MediaLibrary.createAssetAsync(uri);
22
+ return { success: true, uri: asset.uri };
23
+ } catch (error) {
24
+ return { success: false, error: String(error) };
25
+ }
26
+ };
27
+
28
+ /**
29
+ * Copy an image to the app's cache directory and return the local URI.
30
+ * Useful for temporary storage before sharing.
31
+ */
32
+ export const copyToCacheDirectory = async (uri: string): Promise<string> => {
33
+ const fileName = `photo_editor_${Date.now()}.jpg`;
34
+ const destFile = new File(Paths.cache, fileName);
35
+ const srcFile = new File(uri);
36
+ srcFile.copy(destFile);
37
+ return destFile.uri;
38
+ };
39
+
40
+ /**
41
+ * Share an image using the native share sheet.
42
+ * @param uri - local file URI of the image
43
+ * @param message - optional text message to share alongside the image
44
+ */
45
+ export const shareImage = async (
46
+ uri: string,
47
+ message?: string,
48
+ ): Promise<void> => {
49
+ await Share.share({
50
+ url: uri,
51
+ message: message ?? "",
52
+ title: message,
53
+ });
54
+ };
55
+
56
+ /**
57
+ * Delete a local file in the app's cache directory.
58
+ * Safe to call — will not throw even if the file doesn't exist.
59
+ */
60
+ export const deleteLocalFile = async (uri: string): Promise<void> => {
61
+ try {
62
+ const file = new File(uri);
63
+ if (file.exists) {
64
+ file.delete();
65
+ }
66
+ } catch {
67
+ // silent — best-effort cleanup
68
+ }
69
+ };
package/tsconfig.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "allowSyntheticDefaultImports": true,
9
9
  "strict": true,
10
10
  "forceConsistentCasingInFileNames": true,
11
- "moduleResolution": "node",
11
+ "moduleResolution": "bundler",
12
12
  "resolveJsonModule": true,
13
13
  "isolatedModules": true,
14
14
  "noEmit": true,