@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.
- package/README.md +17 -3
- package/android/src/main/java/com/technotoil/image_videoeditor/FrameGrabberModule.kt +2 -6
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaEditorModule.kt +75 -35
- package/android/src/main/java/com/technotoil/image_videoeditor/MediaLibraryModule.kt +51 -35
- package/android/src/main/java/com/technotoil/image_videoeditor/RNVideoPreviewManager.kt +98 -117
- package/ios/RNMediaEditor.m +38 -7
- package/ios/RNMediaLibrary.m +19 -15
- package/ios/RNVideoPreviewManager.m +2 -0
- package/lib/commonjs/components/VideoEditor.js +100 -32
- package/lib/commonjs/components/VideoEditor.js.map +1 -1
- package/lib/commonjs/screens/CropScreen.js +5 -3
- package/lib/commonjs/screens/CropScreen.js.map +1 -1
- package/lib/commonjs/screens/EditorScreen.js +229 -44
- package/lib/commonjs/screens/EditorScreen.js.map +1 -1
- package/lib/commonjs/screens/PickScreen.js +214 -122
- package/lib/commonjs/screens/PickScreen.js.map +1 -1
- package/lib/module/components/VideoEditor.js +100 -33
- package/lib/module/components/VideoEditor.js.map +1 -1
- package/lib/module/screens/CropScreen.js +5 -3
- package/lib/module/screens/CropScreen.js.map +1 -1
- package/lib/module/screens/EditorScreen.js +229 -44
- package/lib/module/screens/EditorScreen.js.map +1 -1
- package/lib/module/screens/PickScreen.js +215 -123
- package/lib/module/screens/PickScreen.js.map +1 -1
- package/lib/typescript/src/components/VideoEditor.d.ts +10 -2
- package/lib/typescript/src/screens/CropScreen.d.ts +2 -1
- package/lib/typescript/src/screens/EditorScreen.d.ts +2 -1
- package/lib/typescript/src/screens/PickScreen.d.ts +4 -1
- package/lib/typescript/src/types.d.ts +1 -0
- package/package.json +4 -1
- package/src/components/VideoEditor.tsx +68 -11
- package/src/screens/CropScreen.tsx +8 -3
- package/src/screens/EditorScreen.tsx +227 -61
- package/src/screens/PickScreen.tsx +197 -119
- 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;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@technotoil/image-video-editor",
|
|
3
|
-
"version": "0.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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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 {
|