@technotoil/image-video-editor 0.1.1 → 0.1.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.
Files changed (35) hide show
  1. package/README.md +17 -3
  2. package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +2 -6
  3. package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +75 -35
  4. package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +51 -35
  5. package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +98 -117
  6. package/ios/RNMediaEditor.m +38 -7
  7. package/ios/RNMediaLibrary.m +19 -15
  8. package/ios/RNVideoPreviewManager.m +2 -0
  9. package/lib/commonjs/components/VideoEditor.js +100 -32
  10. package/lib/commonjs/components/VideoEditor.js.map +1 -1
  11. package/lib/commonjs/screens/CropScreen.js +5 -3
  12. package/lib/commonjs/screens/CropScreen.js.map +1 -1
  13. package/lib/commonjs/screens/EditorScreen.js +229 -44
  14. package/lib/commonjs/screens/EditorScreen.js.map +1 -1
  15. package/lib/commonjs/screens/PickScreen.js +214 -122
  16. package/lib/commonjs/screens/PickScreen.js.map +1 -1
  17. package/lib/module/components/VideoEditor.js +100 -33
  18. package/lib/module/components/VideoEditor.js.map +1 -1
  19. package/lib/module/screens/CropScreen.js +5 -3
  20. package/lib/module/screens/CropScreen.js.map +1 -1
  21. package/lib/module/screens/EditorScreen.js +229 -44
  22. package/lib/module/screens/EditorScreen.js.map +1 -1
  23. package/lib/module/screens/PickScreen.js +215 -123
  24. package/lib/module/screens/PickScreen.js.map +1 -1
  25. package/lib/typescript/src/components/VideoEditor.d.ts +10 -2
  26. package/lib/typescript/src/screens/CropScreen.d.ts +2 -1
  27. package/lib/typescript/src/screens/EditorScreen.d.ts +2 -1
  28. package/lib/typescript/src/screens/PickScreen.d.ts +4 -1
  29. package/lib/typescript/src/types.d.ts +1 -0
  30. package/package.json +4 -1
  31. package/src/components/VideoEditor.tsx +68 -11
  32. package/src/screens/CropScreen.tsx +8 -3
  33. package/src/screens/EditorScreen.tsx +227 -61
  34. package/src/screens/PickScreen.tsx +197 -119
  35. package/src/types.ts +1 -0
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  import type { MediaItem, MusicTrack } from '../types';
3
3
  export interface VideoEditorProps {
4
4
  onClose?: () => void;
5
- onFinishExport?: (editedMedia: Record<string, MediaItem>, paths: string[], editedArray: MediaItem[], cameraMode?: string) => void;
5
+ onFinishExport?: (editedMedia: Record<string, MediaItem>, paths: string[], editedArray: MediaItem[], cameraMode?: string, globalMusic?: MusicTrack) => void;
6
6
  headerTitle?: string;
7
7
  customCancelIcon?: React.ReactNode;
8
8
  onCancelPress?: () => void;
@@ -17,5 +17,13 @@ export interface VideoEditorProps {
17
17
  * '16:9' = Landscape, '9:16' = Portrait, 'free' = No restriction (default)
18
18
  */
19
19
  aspectRatio?: '1:1' | '4:3' | '4:5' | '16:9' | '9:16' | 'free';
20
+ /**
21
+ * Maximum video duration allowed (in milliseconds).
22
+ */
23
+ maxVideoDurationMs?: number;
24
+ /** Filter the media type that can be picked. Default: 'any' */
25
+ mediaType?: 'photo' | 'video' | 'any';
26
+ /** Control which tabs are shown in the picker. Default: ['GALLERY', 'PHOTO', 'VIDEO'] */
27
+ mediaTabs?: ('GALLERY' | 'PHOTO' | 'VIDEO')[];
20
28
  }
21
- export default function VideoEditor({ onClose, onFinishExport, headerTitle, customCancelIcon, onCancelPress, cameraModes, defaultCameraMode, musicList, maxSelection, aspectRatio, }: VideoEditorProps): React.JSX.Element;
29
+ export default function VideoEditor({ onClose, onFinishExport, headerTitle, customCancelIcon, onCancelPress, cameraModes, defaultCameraMode, musicList, maxSelection, aspectRatio, maxVideoDurationMs, mediaType, mediaTabs, }: VideoEditorProps): React.JSX.Element;
@@ -5,6 +5,7 @@ interface CropScreenProps {
5
5
  onBack: () => void;
6
6
  onSave: (uri: string, thumb?: string, duration?: number) => void;
7
7
  aspectRatio?: '1:1' | '4:3' | '4:5' | '16:9' | '9:16' | 'free';
8
+ maxVideoDurationMs?: number;
8
9
  }
9
- export declare function CropScreen({ item, onBack, onSave, aspectRatio }: CropScreenProps): React.JSX.Element;
10
+ export declare function CropScreen({ item, onBack, onSave, aspectRatio, maxVideoDurationMs }: CropScreenProps): React.JSX.Element;
10
11
  export {};
@@ -1,10 +1,11 @@
1
1
  import React from 'react';
2
2
  import type { MediaItem, MusicTrack } from '../types';
3
- export declare function EditorScreen({ items, initialIndex, onBack, onSaved, onOpenCrop, musicList, }: {
3
+ export declare function EditorScreen({ items, initialIndex, onBack, onSaved, onOpenCrop, musicList, maxVideoDurationMs, }: {
4
4
  items: MediaItem[];
5
5
  initialIndex?: number;
6
6
  onBack: () => void;
7
7
  onSaved: (updatedItems: MediaItem[]) => void;
8
8
  onOpenCrop: (item: MediaItem) => void;
9
9
  musicList?: MusicTrack[];
10
+ maxVideoDurationMs?: number;
10
11
  }): React.JSX.Element;
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import type { MediaItem } from '../types';
3
- export declare function PickScreen({ items, onPicked, onNext, headerTitle, customCancelIcon, onCancelPress, cameraModes, onCameraModeChange, defaultCameraMode, maxSelection, aspectRatio, }: {
3
+ export declare function PickScreen({ isActive, items, onPicked, onNext, headerTitle, customCancelIcon, onCancelPress, cameraModes, onCameraModeChange, defaultCameraMode, maxSelection, aspectRatio, mediaType, mediaTabs, }: {
4
4
  items: MediaItem[];
5
5
  onPicked: (items: MediaItem[]) => void;
6
6
  onNext: (picked: MediaItem[]) => void;
@@ -12,4 +12,7 @@ export declare function PickScreen({ items, onPicked, onNext, headerTitle, custo
12
12
  defaultCameraMode?: string;
13
13
  maxSelection?: number;
14
14
  aspectRatio?: '1:1' | '4:3' | '4:5' | '16:9' | '9:16' | 'free';
15
+ mediaType?: 'photo' | 'video' | 'any';
16
+ mediaTabs?: ('GALLERY' | 'PHOTO' | 'VIDEO')[];
17
+ isActive?: boolean;
15
18
  }): React.JSX.Element;
@@ -52,6 +52,7 @@ export type VideoTrimOptions = ImageEditOptions & {
52
52
  mute?: boolean;
53
53
  isImage?: boolean;
54
54
  musicUri?: string;
55
+ musicOffsetMs?: number;
55
56
  };
56
57
  export type FrameCaptureOptions = {
57
58
  timeMs: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@technotoil/image-video-editor",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A high-performance React Native image and video editor featuring video trimming, filters, photo overlay frames, and camera/gallery integration.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -103,5 +103,8 @@
103
103
  }
104
104
  ]
105
105
  ]
106
+ },
107
+ "dependencies": {
108
+ "react-native-fs": "^2.20.0"
106
109
  }
107
110
  }
@@ -1,5 +1,5 @@
1
1
  import React, { useState } from 'react';
2
- import { Alert, StatusBar, useColorScheme } from 'react-native';
2
+ import { Alert, StatusBar, useColorScheme, View, Text, ActivityIndicator } from 'react-native';
3
3
  import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
4
4
  import { PickScreen } from '../screens/PickScreen';
5
5
  import { CropScreen } from '../screens/CropScreen';
@@ -7,6 +7,9 @@ import { EditorScreen } from '../screens/EditorScreen';
7
7
  import { ExportScreen } from '../screens/ExportScreen';
8
8
  import { exportAsset } from '../native/MediaLibrary';
9
9
  import type { MediaItem, MusicTrack } from '../types';
10
+ import Ionicons from 'react-native-vector-icons/Ionicons';
11
+
12
+ Ionicons.loadFont().catch(() => {});
10
13
 
11
14
  export interface VideoEditorProps {
12
15
  onClose?: () => void;
@@ -14,7 +17,8 @@ export interface VideoEditorProps {
14
17
  editedMedia: Record<string, MediaItem>,
15
18
  paths: string[],
16
19
  editedArray: MediaItem[],
17
- cameraMode?: string
20
+ cameraMode?: string,
21
+ globalMusic?: MusicTrack
18
22
  ) => void;
19
23
  headerTitle?: string;
20
24
  customCancelIcon?: React.ReactNode;
@@ -30,6 +34,14 @@ export interface VideoEditorProps {
30
34
  * '16:9' = Landscape, '9:16' = Portrait, 'free' = No restriction (default)
31
35
  */
32
36
  aspectRatio?: '1:1' | '4:3' | '4:5' | '16:9' | '9:16' | 'free';
37
+ /**
38
+ * Maximum video duration allowed (in milliseconds).
39
+ */
40
+ maxVideoDurationMs?: number;
41
+ /** Filter the media type that can be picked. Default: 'any' */
42
+ mediaType?: 'photo' | 'video' | 'any';
43
+ /** Control which tabs are shown in the picker. Default: ['GALLERY', 'PHOTO', 'VIDEO'] */
44
+ mediaTabs?: ('GALLERY' | 'PHOTO' | 'VIDEO')[];
33
45
  }
34
46
 
35
47
  export default function VideoEditor({
@@ -43,6 +55,9 @@ export default function VideoEditor({
43
55
  musicList,
44
56
  maxSelection = 1,
45
57
  aspectRatio = 'free',
58
+ maxVideoDurationMs,
59
+ mediaType = 'any',
60
+ mediaTabs = ['GALLERY', 'PHOTO', 'VIDEO'],
46
61
  }: VideoEditorProps) {
47
62
  const clampedMax = Math.min(5, Math.max(1, maxSelection));
48
63
  const isDarkMode = useColorScheme() === 'dark';
@@ -52,19 +67,31 @@ export default function VideoEditor({
52
67
  const [editedMedia, setEditedMedia] = useState<Record<string, MediaItem>>({});
53
68
  const [originals, setOriginals] = useState<Record<string, MediaItem>>({});
54
69
  const [selectedCameraMode, setSelectedCameraMode] = useState<string>(defaultCameraMode || 'STORY');
70
+ const [processing, setProcessing] = useState(false);
71
+ const [exportCache, setExportCache] = useState<Record<string, string>>({});
55
72
 
56
73
  const ensureExported = async (item: MediaItem, ignoreEdits = false): Promise<MediaItem> => {
74
+ console.log(`[ensureExported] Start for item: ${item.id}, uri: ${item.uri}`);
57
75
  if (!ignoreEdits && editedMedia[item.id]) {
76
+ console.log(`[ensureExported] Found in editedMedia! Returning early.`);
58
77
  return editedMedia[item.id];
59
78
  }
79
+ if (exportCache[item.id]) {
80
+ console.log(`[ensureExported] Found in exportCache! Returning cached URI: ${exportCache[item.id]}`);
81
+ return { ...item, uri: exportCache[item.id] };
82
+ }
60
83
  if (!item.uri.startsWith('ph://') && !item.uri.startsWith('content://')) {
84
+ console.log(`[ensureExported] URI is already local file, skipping native export.`);
61
85
  return item;
62
86
  }
63
87
  try {
88
+ console.log(`[ensureExported] Calling native exportAsset...`);
64
89
  const fileUri = await exportAsset(item.id);
90
+ console.log(`[ensureExported] Native export success! New URI: ${fileUri}`);
91
+ setExportCache(prev => ({ ...prev, [item.id]: fileUri }));
65
92
  return { ...item, uri: fileUri };
66
93
  } catch (err: any) {
67
- console.error('ASSET EXPORT FROM LIBRARY FAILED:', err?.message ?? err);
94
+ console.error('[ensureExported] ASSET EXPORT FROM LIBRARY FAILED:', err?.message ?? err);
68
95
  return item;
69
96
  }
70
97
  };
@@ -77,8 +104,9 @@ export default function VideoEditor({
77
104
  backgroundColor="transparent"
78
105
  translucent={true}
79
106
  />
80
- {screen === 'pick' && (
107
+ <View style={{ flex: 1, display: screen === 'pick' ? 'flex' : 'none' }}>
81
108
  <PickScreen
109
+ isActive={screen === 'pick'}
82
110
  items={items}
83
111
  headerTitle={headerTitle}
84
112
  customCancelIcon={customCancelIcon}
@@ -90,6 +118,8 @@ export default function VideoEditor({
90
118
  onCameraModeChange={(mode) => {
91
119
  setSelectedCameraMode(mode);
92
120
  }}
121
+ mediaType={mediaType}
122
+ mediaTabs={mediaTabs}
93
123
  onPicked={(picked: MediaItem[]) => {
94
124
  // Save originals for "Fresh Start" editing
95
125
  const newOriginals = { ...originals };
@@ -101,22 +131,48 @@ export default function VideoEditor({
101
131
  setItems(picked);
102
132
  }}
103
133
  onNext={async (picked) => {
134
+ console.log(`[onNext] Triggered with ${picked?.length} items`);
135
+ if (processing) {
136
+ console.log(`[onNext] Aborting, already processing!`);
137
+ return;
138
+ }
104
139
  if (!picked || picked.length === 0) {
140
+ console.log(`[onNext] Aborting, picked is empty!`);
105
141
  return;
106
142
  }
107
- const resolvedItems = await Promise.all(
108
- picked.map(item => ensureExported(item, false))
109
- );
110
- setItems(resolvedItems);
111
- setCurrent(resolvedItems[0]);
112
- setScreen('editor');
143
+ console.log(`[onNext] Setting processing=true`);
144
+ setProcessing(true);
145
+
146
+ try {
147
+ console.log(`[onNext] Starting Promise.all for ${picked.length} items`);
148
+ const resolvedItems = await Promise.all(
149
+ picked.map(item => ensureExported(item, false))
150
+ );
151
+
152
+ console.log(`[onNext] Promise.all completed! Updating state...`);
153
+ setItems(resolvedItems);
154
+ setCurrent(resolvedItems[0]);
155
+ setScreen('editor');
156
+ console.log(`[onNext] Screen set to editor`);
157
+ } catch (e) {
158
+ console.error(`[onNext] Promise.all threw an error!`, e);
159
+ } finally {
160
+ console.log(`[onNext] Finally block - setting processing=false`);
161
+ setProcessing(false);
162
+ }
113
163
  }}
114
164
  />
115
- )}
165
+ {processing && (
166
+ <View style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', alignItems: 'center' }}>
167
+ <ActivityIndicator size="large" color="#ffffff" />
168
+ </View>
169
+ )}
170
+ </View>
116
171
  {screen === 'editor' && current && (
117
172
  <EditorScreen
118
173
  items={items}
119
174
  initialIndex={Math.max(0, items.findIndex(it => it.id === current.id))}
175
+ maxVideoDurationMs={maxVideoDurationMs}
120
176
  onBack={() => {
121
177
  setEditedMedia({});
122
178
  const restoredItems = items.map(item => originals[item.id] || item);
@@ -157,6 +213,7 @@ export default function VideoEditor({
157
213
  <CropScreen
158
214
  item={current}
159
215
  aspectRatio={aspectRatio}
216
+ maxVideoDurationMs={maxVideoDurationMs}
160
217
  onBack={() => setScreen('editor')}
161
218
  onSave={(uri, thumbnailUri, durationMs) => {
162
219
  const updated = {
@@ -100,8 +100,9 @@ interface CropScreenProps {
100
100
  onBack: () => void;
101
101
  onSave: (uri: string, thumb?: string, duration?: number) => void;
102
102
  aspectRatio?: '1:1' | '4:3' | '4:5' | '16:9' | '9:16' | 'free';
103
+ maxVideoDurationMs?: number;
103
104
  }
104
- export function CropScreen({ item, onBack, onSave, aspectRatio = 'free' }: CropScreenProps) {
105
+ export function CropScreen({ item, onBack, onSave, aspectRatio = 'free', maxVideoDurationMs }: CropScreenProps) {
105
106
  const isRatioLocked = aspectRatio !== 'free';
106
107
  const getInitialRatioLabel = () => {
107
108
  if (!aspectRatio || aspectRatio === 'free') return 'Free';
@@ -402,9 +403,13 @@ export function CropScreen({ item, onBack, onSave, aspectRatio = 'free' }: CropS
402
403
 
403
404
  const outUri = item.type === 'image'
404
405
  ? await editImage(item.uri, options)
405
- : await trimVideo(item.uri, { startMs: 0, endMs: item.durationMs || 10000, ...options });
406
+ : await trimVideo(item.uri, { startMs: 0, endMs: maxVideoDurationMs ? Math.min(item.durationMs || 10000, maxVideoDurationMs) : (item.durationMs || 10000), ...options });
406
407
 
407
- onSave(outUri, item.type === 'image' ? outUri : undefined, item.durationMs);
408
+ const clampedDuration = item.type === 'video' && maxVideoDurationMs
409
+ ? Math.min(item.durationMs || 10000, maxVideoDurationMs)
410
+ : item.durationMs;
411
+
412
+ onSave(outUri, item.type === 'image' ? outUri : undefined, clampedDuration);
408
413
  } catch (err: any) {
409
414
  Alert.alert('Apply failed', err?.message ?? 'Could not process crop.');
410
415
  } finally {