@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
@@ -56,6 +56,7 @@ function formatDuration(ms) {
56
56
  return `${m}:${s.toString().padStart(2, '0')}`;
57
57
  }
58
58
  function PickScreen({
59
+ isActive = true,
59
60
  items,
60
61
  onPicked,
61
62
  onNext,
@@ -66,10 +67,12 @@ function PickScreen({
66
67
  onCameraModeChange,
67
68
  defaultCameraMode,
68
69
  maxSelection = 1,
69
- aspectRatio = 'free'
70
+ aspectRatio = 'free',
71
+ mediaType = 'any',
72
+ mediaTabs = ['GALLERY', 'PHOTO', 'VIDEO']
70
73
  }) {
71
74
  const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
72
- const [tab, setTab] = (0, _react.useState)('GALLERY');
75
+ const [tab, setTab] = (0, _react.useState)(mediaTabs[0] || 'GALLERY');
73
76
  const [activeAlbum, setActiveAlbum] = (0, _react.useState)({
74
77
  id: 'all',
75
78
  title: 'Recents'
@@ -173,27 +176,32 @@ function PickScreen({
173
176
  };
174
177
  const loadMedia = async albumId => {
175
178
  try {
179
+ console.log('loadMedia called with albumId:', albumId);
176
180
  setLoading(true);
177
181
  const assets = await (0, _MediaLibrary.listMedia)({
178
182
  limit: 200,
179
183
  offset: 0,
180
- type: 'all',
184
+ type: mediaType === 'photo' ? 'image' : mediaType === 'video' ? 'video' : 'all',
181
185
  albumId: albumId === 'all' ? undefined : albumId
182
186
  });
187
+ console.log('loadMedia got assets:', assets.length);
183
188
  setLibrary(assets);
184
189
  if (assets[0] && !multiSelect) {
185
190
  setSelectedMedia(assets[0]);
186
191
  }
187
192
  } catch (err) {
193
+ console.error('loadMedia error:', err);
188
194
  _reactNative.Alert.alert('Library error', err?.message ?? 'Failed to load library.');
189
195
  } finally {
190
196
  setLoading(false);
191
197
  }
192
198
  };
193
199
  (0, _react.useEffect)(() => {
200
+ console.log('PickScreen mounted!');
194
201
  (async () => {
195
202
  try {
196
203
  const ok = await (0, _MediaLibrary.requestMediaAccess)();
204
+ console.log('requestMediaAccess ok?', ok);
197
205
  if (!ok) {
198
206
  _reactNative.Alert.alert('Permission needed', 'Allow photo access to continue.');
199
207
  return;
@@ -219,6 +227,22 @@ function PickScreen({
219
227
  };
220
228
  return [cameraItem, ...list];
221
229
  }, [library, tab]);
230
+ const listData = (0, _react.useMemo)(() => {
231
+ const rows = Array.from({
232
+ length: Math.ceil(filtered.length / 4)
233
+ }).map((_, i) => ({
234
+ id: `row_${i}`,
235
+ type: 'row',
236
+ items: filtered.slice(i * 4, i * 4 + 4)
237
+ }));
238
+ return [{
239
+ id: 'header_preview',
240
+ type: 'preview'
241
+ }, {
242
+ id: 'header_albumRow',
243
+ type: 'albumRow'
244
+ }, ...rows];
245
+ }, [filtered]);
222
246
  (0, _react.useEffect)(() => {
223
247
  let cancelled = false;
224
248
  setVideoPaused(false);
@@ -231,6 +255,7 @@ function PickScreen({
231
255
  setPreviewUri(selectedMedia.uri);
232
256
  return;
233
257
  }
258
+ setPreviewUri(null);
234
259
  try {
235
260
  const fileUri = await (0, _MediaLibrary.exportAsset)(selectedMedia.id);
236
261
  if (!cancelled) setPreviewUri(fileUri);
@@ -244,23 +269,34 @@ function PickScreen({
244
269
  }, [selectedMedia]);
245
270
  const playableUri = (0, _react.useMemo)(() => {
246
271
  if (!selectedMedia) return null;
247
- if (previewUri && !previewUri.startsWith('ph://') && !previewUri.startsWith('content://')) return previewUri;
248
- if (selectedMedia.uri && !selectedMedia.uri.startsWith('ph://') && !selectedMedia.uri.startsWith('content://')) return selectedMedia.uri;
249
- return null;
272
+ if (previewUri) return previewUri;
273
+ return selectedMedia.uri;
250
274
  }, [previewUri, selectedMedia]);
251
275
  const toggleMultiSelect = () => {
252
- setMultiSelect(v => !v);
253
- setSelectedItems([]);
276
+ setMultiSelect(v => {
277
+ if (!v && selectedMedia) {
278
+ setSelectedItems([selectedMedia]);
279
+ } else {
280
+ setSelectedItems([]);
281
+ }
282
+ return !v;
283
+ });
254
284
  };
255
285
  const handleSelectItem = item => {
256
- setSelectedMedia(item);
257
286
  if (multiSelect) {
258
287
  setSelectedItems(prev => {
259
288
  const exists = prev.find(i => i.id === item.id);
260
- if (exists) return prev.filter(i => i.id !== item.id);
289
+ if (exists) {
290
+ const newItems = prev.filter(i => i.id !== item.id);
291
+ setSelectedMedia(newItems.length > 0 ? newItems[newItems.length - 1] : null);
292
+ return newItems;
293
+ }
261
294
  if (prev.length >= maxSelection) return prev;
295
+ setSelectedMedia(item);
262
296
  return [...prev, item];
263
297
  });
298
+ } else {
299
+ setSelectedMedia(item);
264
300
  }
265
301
  _reactNative.Animated.sequence([_reactNative.Animated.timing(scaleAnim, {
266
302
  toValue: 0.97,
@@ -478,129 +514,185 @@ function PickScreen({
478
514
  }, album.id))
479
515
  })]
480
516
  })]
481
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
482
- style: [styles.preview, {
483
- transform: [{
484
- scale: scaleAnim
485
- }]
486
- }, isRatioLocked && previewAspectRatio ? {
487
- height: undefined,
488
- aspectRatio: previewAspectRatio
489
- } : {}],
490
- children: [selectedMedia?.type === 'video' ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
491
- style: styles.previewImage,
492
- onPress: () => setVideoPaused(v => !v),
493
- children: [playableUri ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_VideoPreview.VideoPreview, {
494
- uri: playableUri,
495
- paused: videoPaused,
496
- muted: false,
497
- style: styles.previewImage,
498
- resizeMode: "cover"
499
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
500
- source: {
501
- uri: selectedMedia.thumbnailUri || selectedMedia.uri
502
- },
503
- style: styles.previewImage,
504
- resizeMode: "cover"
505
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
506
- style: styles.previewOverlay,
507
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
508
- style: styles.playPauseCircle,
509
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Ionicons.default, {
510
- name: videoPaused ? 'play' : 'pause',
511
- size: 24,
512
- color: "#fff"
513
- })
514
- })
515
- })]
516
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
517
- source: {
518
- uri: previewUri ?? selectedMedia?.uri
519
- },
520
- style: [styles.previewImage, cropMode === '1:1' ? styles.squareCrop : styles.originalCrop],
521
- resizeMode: isRatioLocked ? 'cover' : cropMode === '1:1' ? 'cover' : 'contain'
522
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
523
- style: styles.previewControls,
524
- children: [isRatioLocked && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
525
- style: styles.ratioBadge,
526
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
527
- style: styles.ratioBadgeText,
528
- children: aspectRatio
529
- })
530
- }), !isRatioLocked && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
531
- style: styles.cropToggle,
532
- onPress: () => setCropMode(v => v === '1:1' ? 'original' : '1:1'),
533
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Ionicons.default, {
534
- name: cropMode === '1:1' ? 'square-outline' : 'expand-outline',
535
- size: 20,
536
- color: "#fff"
537
- })
538
- })]
539
- })]
540
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
541
- style: styles.albumRow,
542
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
543
- style: styles.albumSelector,
544
- onPress: () => setShowAlbumPicker(v => !v),
545
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
546
- style: styles.albumTitle,
547
- children: activeAlbum.title
548
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Ionicons.default, {
549
- name: "chevron-down",
550
- size: 16,
551
- color: "#fff",
552
- style: {
553
- marginLeft: 6
554
- }
555
- })]
556
- }), maxSelection > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
557
- style: {
558
- flexDirection: 'row',
559
- alignItems: 'center',
560
- gap: 8
561
- },
562
- children: [multiSelect && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
563
- style: {
564
- color: '#3b82f6',
565
- fontSize: 12,
566
- fontWeight: '700'
567
- },
568
- children: [selectedItems.length, "/", maxSelection]
569
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
570
- style: [styles.multiSelectBtn, multiSelect && styles.multiSelectBtnActive, multiSelect && selectedItems.length === 0 && {
571
- opacity: 0.35
572
- }],
573
- onPress: () => {
574
- if (multiSelect) {
575
- handleNext();
576
- } else {
577
- toggleMultiSelect();
578
- }
579
- },
580
- disabled: multiSelect && selectedItems.length === 0,
581
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
582
- style: [styles.multiSelectText, multiSelect && styles.multiSelectTextActive],
583
- children: multiSelect ? 'Done' : 'Select'
584
- })
585
- })]
586
- })]
587
517
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
588
- data: filtered,
518
+ data: listData,
519
+ extraData: {
520
+ selectedMedia,
521
+ previewUri,
522
+ playableUri,
523
+ videoPaused,
524
+ cropMode,
525
+ isRatioLocked,
526
+ activeAlbum,
527
+ multiSelect,
528
+ selectedItems,
529
+ showAlbumPicker,
530
+ loading
531
+ },
589
532
  keyExtractor: item => item.id,
590
- numColumns: 4,
533
+ stickyHeaderIndices: [1],
591
534
  style: styles.libraryList,
592
- renderItem: renderThumb,
593
535
  contentContainerStyle: styles.grid,
536
+ getItemLayout: (data, index) => {
537
+ const previewHeight = isRatioLocked && previewAspectRatio ? _reactNative.Dimensions.get('window').width / previewAspectRatio : 420;
538
+ const albumRowHeight = 48;
539
+ const rowHeight = _reactNative.Dimensions.get('window').width / 4;
540
+ if (index === 0) return {
541
+ length: previewHeight,
542
+ offset: 0,
543
+ index
544
+ };
545
+ if (index === 1) return {
546
+ length: albumRowHeight,
547
+ offset: previewHeight,
548
+ index
549
+ };
550
+ const offset = previewHeight + albumRowHeight + (index - 2) * rowHeight;
551
+ return {
552
+ length: rowHeight,
553
+ offset,
554
+ index
555
+ };
556
+ },
557
+ removeClippedSubviews: false,
558
+ windowSize: 11,
594
559
  ListEmptyComponent: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
595
560
  style: styles.empty,
596
561
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
597
562
  style: styles.emptyText,
598
563
  children: loading ? 'Loading…' : 'No media found'
599
564
  })
600
- })
565
+ }),
566
+ renderItem: ({
567
+ item
568
+ }) => {
569
+ if (item.type === 'preview') {
570
+ const previewStyleHeight = isRatioLocked && previewAspectRatio ? _reactNative.Dimensions.get('window').width / previewAspectRatio : 420;
571
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Animated.View, {
572
+ style: [styles.preview, {
573
+ transform: [{
574
+ scale: scaleAnim
575
+ }],
576
+ height: previewStyleHeight
577
+ }],
578
+ children: [selectedMedia?.type === 'video' ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
579
+ style: styles.previewImage,
580
+ onPress: () => setVideoPaused(v => !v),
581
+ children: [playableUri && isActive ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_VideoPreview.VideoPreview, {
582
+ uri: playableUri,
583
+ paused: videoPaused || !isActive,
584
+ muted: false,
585
+ style: styles.previewImage,
586
+ resizeMode: "cover"
587
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
588
+ source: {
589
+ uri: selectedMedia.thumbnailUri || selectedMedia.uri
590
+ },
591
+ style: styles.previewImage,
592
+ resizeMode: "cover"
593
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
594
+ style: styles.previewOverlay,
595
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
596
+ style: styles.playPauseCircle,
597
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Ionicons.default, {
598
+ name: videoPaused ? 'play' : 'pause',
599
+ size: 24,
600
+ color: "#fff"
601
+ })
602
+ })
603
+ })]
604
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Image, {
605
+ source: {
606
+ uri: previewUri ?? selectedMedia?.uri
607
+ },
608
+ style: [styles.previewImage, cropMode === '1:1' ? styles.squareCrop : styles.originalCrop],
609
+ resizeMode: isRatioLocked ? 'cover' : cropMode === '1:1' ? 'cover' : 'contain'
610
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
611
+ style: styles.previewControls,
612
+ children: [isRatioLocked && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
613
+ style: styles.ratioBadge,
614
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
615
+ style: styles.ratioBadgeText,
616
+ children: aspectRatio
617
+ })
618
+ }), !isRatioLocked && /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
619
+ style: styles.cropToggle,
620
+ onPress: () => setCropMode(v => v === '1:1' ? 'original' : '1:1'),
621
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_Ionicons.default, {
622
+ name: cropMode === '1:1' ? 'square-outline' : 'expand-outline',
623
+ size: 20,
624
+ color: "#fff"
625
+ })
626
+ })]
627
+ })]
628
+ });
629
+ }
630
+ if (item.type === 'albumRow') {
631
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
632
+ style: [styles.albumRow, {
633
+ backgroundColor: '#0b0f1a',
634
+ zIndex: 999,
635
+ elevation: 5
636
+ }],
637
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
638
+ style: styles.albumSelector,
639
+ onPress: () => setShowAlbumPicker(v => !v),
640
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
641
+ style: styles.albumTitle,
642
+ children: activeAlbum.title
643
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_Ionicons.default, {
644
+ name: "chevron-down",
645
+ size: 16,
646
+ color: "#fff",
647
+ style: {
648
+ marginLeft: 6
649
+ }
650
+ })]
651
+ }), maxSelection > 1 && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
652
+ style: {
653
+ flexDirection: 'row',
654
+ alignItems: 'center',
655
+ gap: 8
656
+ },
657
+ children: [multiSelect && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
658
+ style: {
659
+ color: '#3b82f6',
660
+ fontSize: 12,
661
+ fontWeight: '700'
662
+ },
663
+ children: [selectedItems.length, "/", maxSelection]
664
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
665
+ style: [styles.multiSelectBtn, multiSelect && styles.multiSelectBtnActive, multiSelect && selectedItems.length === 0 && {
666
+ opacity: 0.35
667
+ }],
668
+ onPress: () => {
669
+ if (multiSelect) {
670
+ handleNext();
671
+ } else {
672
+ toggleMultiSelect();
673
+ }
674
+ },
675
+ disabled: multiSelect && selectedItems.length === 0,
676
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
677
+ style: [styles.multiSelectText, multiSelect && styles.multiSelectTextActive],
678
+ children: multiSelect ? 'Done' : 'Select multiple'
679
+ })
680
+ })]
681
+ })]
682
+ });
683
+ }
684
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
685
+ style: {
686
+ flexDirection: 'row'
687
+ },
688
+ children: item.items.map(mediaItem => renderThumb({
689
+ item: mediaItem
690
+ }))
691
+ });
692
+ }
601
693
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
602
694
  style: styles.tabBar,
603
- children: TABS.map(t => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
695
+ children: mediaTabs.filter(t => mediaType === 'any' || mediaType === 'photo' && t !== 'VIDEO' || mediaType === 'video' && t !== 'PHOTO').map(t => /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
604
696
  onPress: () => setTab(t),
605
697
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
606
698
  style: [styles.tab, tab === t && styles.tabActive],
@@ -900,7 +992,7 @@ const styles = _reactNative.StyleSheet.create({
900
992
  justifyContent: 'space-between',
901
993
  alignItems: 'center',
902
994
  paddingHorizontal: 16,
903
- paddingVertical: 10
995
+ height: 48
904
996
  },
905
997
  albumSelector: {
906
998
  flexDirection: 'row',