@technotoil/image-video-editor 0.1.0 → 0.1.1
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/lib/commonjs/assets/frames/film_vintage.png +0 -0
- package/lib/commonjs/assets/frames/floral_gold.png +0 -0
- package/lib/commonjs/assets/frames/minimal_double.png +0 -0
- package/lib/commonjs/assets/frames/polaroid_white.png +0 -0
- package/lib/commonjs/assets/frames/watercolor_floral.png +0 -0
- package/lib/commonjs/components/VideoEditor.js +167 -0
- package/lib/commonjs/components/VideoEditor.js.map +1 -0
- package/lib/commonjs/index.js +14 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/CameraView.js +109 -0
- package/lib/commonjs/native/CameraView.js.map +1 -0
- package/lib/commonjs/native/FrameGrabber.js +17 -0
- package/lib/commonjs/native/FrameGrabber.js.map +1 -0
- package/lib/commonjs/native/MediaEditor.js +24 -0
- package/lib/commonjs/native/MediaEditor.js.map +1 -0
- package/lib/commonjs/native/MediaLibrary.js +45 -0
- package/lib/commonjs/native/MediaLibrary.js.map +1 -0
- package/lib/commonjs/native/MediaPicker.js +17 -0
- package/lib/commonjs/native/MediaPicker.js.map +1 -0
- package/lib/commonjs/native/MediaPlayer.js +17 -0
- package/lib/commonjs/native/MediaPlayer.js.map +1 -0
- package/lib/commonjs/native/VideoPreview.js +17 -0
- package/lib/commonjs/native/VideoPreview.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/screens/CropScreen.js +1231 -0
- package/lib/commonjs/screens/CropScreen.js.map +1 -0
- package/lib/commonjs/screens/EditorScreen.js +5858 -0
- package/lib/commonjs/screens/EditorScreen.js.map +1 -0
- package/lib/commonjs/screens/ExportScreen.js +294 -0
- package/lib/commonjs/screens/ExportScreen.js.map +1 -0
- package/lib/commonjs/screens/GalleryScreen.js +510 -0
- package/lib/commonjs/screens/GalleryScreen.js.map +1 -0
- package/lib/commonjs/screens/PickScreen.js +1261 -0
- package/lib/commonjs/screens/PickScreen.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/components/VideoEditor.js +7 -1
- package/lib/module/components/VideoEditor.js.map +1 -1
- package/lib/module/screens/CropScreen.js +22 -7
- package/lib/module/screens/CropScreen.js.map +1 -1
- package/lib/module/screens/EditorScreen.js +142 -42
- package/lib/module/screens/EditorScreen.js.map +1 -1
- package/lib/module/screens/PickScreen.js +76 -16
- package/lib/module/screens/PickScreen.js.map +1 -1
- package/lib/typescript/src/components/VideoEditor.d.ts +9 -1
- package/lib/typescript/src/screens/CropScreen.d.ts +2 -1
- package/lib/typescript/src/screens/PickScreen.d.ts +3 -1
- package/package.json +14 -8
- package/src/components/VideoEditor.tsx +14 -0
- package/src/screens/CropScreen.tsx +47 -31
- package/src/screens/EditorScreen.tsx +139 -45
- package/src/screens/PickScreen.tsx +95 -18
|
@@ -25,6 +25,7 @@ import type { ImageEditOptions, MediaItem, MusicTrack } from '../types';
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
const SCREEN_WIDTH = Dimensions.get('window').width;
|
|
28
|
+
const SCREEN_HEIGHT = Dimensions.get('window').height;
|
|
28
29
|
const TIMELINE_WIDTH = SCREEN_WIDTH - 40;
|
|
29
30
|
const HANDLE_SIZE = 24;
|
|
30
31
|
const CARD_WIDTH = SCREEN_WIDTH * 0.76;
|
|
@@ -472,6 +473,8 @@ export function EditorScreen({
|
|
|
472
473
|
const [newText, setNewText] = useState('');
|
|
473
474
|
const isNewOverlay = React.useRef(false); // track if overlay was just created (not yet saved)
|
|
474
475
|
const originalOverlayBackup = React.useRef<{ id: string; text: string; x: number; y: number; color: string; fontSize: number } | null>(null);
|
|
476
|
+
const [activeDraggingId, setActiveDraggingId] = useState<string | null>(null);
|
|
477
|
+
const [isOverTrash, setIsOverTrash] = useState(false);
|
|
475
478
|
|
|
476
479
|
// ── Stickers ────────────────────────────────────────────────────────────────
|
|
477
480
|
const STICKER_LIST = [
|
|
@@ -826,10 +829,19 @@ export function EditorScreen({
|
|
|
826
829
|
const createTextPan = (id: string) => {
|
|
827
830
|
let startX = 0;
|
|
828
831
|
let startY = 0;
|
|
832
|
+
let pressStartTime = 0;
|
|
833
|
+
let hasMoved = false;
|
|
834
|
+
let longPressTimeout: any = null;
|
|
835
|
+
|
|
829
836
|
return PanResponder.create({
|
|
830
837
|
onStartShouldSetPanResponder: () => true,
|
|
831
838
|
onMoveShouldSetPanResponder: () => true,
|
|
832
839
|
onPanResponderGrant: () => {
|
|
840
|
+
pressStartTime = Date.now();
|
|
841
|
+
hasMoved = false;
|
|
842
|
+
setActiveDraggingId(id);
|
|
843
|
+
setIsOverTrash(false);
|
|
844
|
+
|
|
833
845
|
setOverlays(prev => {
|
|
834
846
|
const item = prev.find(o => o.id === id);
|
|
835
847
|
if (item) {
|
|
@@ -838,13 +850,80 @@ export function EditorScreen({
|
|
|
838
850
|
}
|
|
839
851
|
return prev;
|
|
840
852
|
});
|
|
853
|
+
|
|
854
|
+
// Setup long press timer (600ms)
|
|
855
|
+
if (longPressTimeout) clearTimeout(longPressTimeout);
|
|
856
|
+
longPressTimeout = setTimeout(() => {
|
|
857
|
+
if (!hasMoved) {
|
|
858
|
+
removeTextOverlay(id);
|
|
859
|
+
}
|
|
860
|
+
}, 600);
|
|
841
861
|
},
|
|
842
862
|
onPanResponderMove: (_, gesture) => {
|
|
863
|
+
if (Math.abs(gesture.dx) > 4 || Math.abs(gesture.dy) > 4) {
|
|
864
|
+
hasMoved = true;
|
|
865
|
+
if (longPressTimeout) {
|
|
866
|
+
clearTimeout(longPressTimeout);
|
|
867
|
+
longPressTimeout = null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const newX = startX + gesture.dx;
|
|
872
|
+
const newY = startY + gesture.dy;
|
|
873
|
+
|
|
874
|
+
// Trash zone: bottom 140px of screen, center horizontal
|
|
875
|
+
const isNearTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - (SCREEN_WIDTH / 2)) < 90;
|
|
876
|
+
setIsOverTrash(isNearTrash);
|
|
877
|
+
|
|
843
878
|
setOverlays(prev => prev.map(o => o.id === id ? {
|
|
844
879
|
...o,
|
|
845
|
-
x:
|
|
846
|
-
y:
|
|
880
|
+
x: newX,
|
|
881
|
+
y: newY
|
|
847
882
|
} : o));
|
|
883
|
+
},
|
|
884
|
+
onPanResponderRelease: (_, gesture) => {
|
|
885
|
+
if (longPressTimeout) {
|
|
886
|
+
clearTimeout(longPressTimeout);
|
|
887
|
+
longPressTimeout = null;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
// Check if released over trash zone using final touch screen coordinates
|
|
891
|
+
const releasedOverTrash = gesture.moveY > SCREEN_HEIGHT - 140 && Math.abs(gesture.moveX - (SCREEN_WIDTH / 2)) < 90;
|
|
892
|
+
|
|
893
|
+
// Reset dragging states
|
|
894
|
+
setActiveDraggingId(null);
|
|
895
|
+
setIsOverTrash(false);
|
|
896
|
+
|
|
897
|
+
if (releasedOverTrash) {
|
|
898
|
+
setTimeout(() => {
|
|
899
|
+
removeTextOverlay(id);
|
|
900
|
+
}, 50);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
const pressDuration = Date.now() - pressStartTime;
|
|
905
|
+
if (!hasMoved && pressDuration < 250) {
|
|
906
|
+
pushToHistory();
|
|
907
|
+
setOverlays(prev => {
|
|
908
|
+
const found = prev.find(o => o.id === id);
|
|
909
|
+
if (found) {
|
|
910
|
+
originalOverlayBackup.current = { ...found };
|
|
911
|
+
isNewOverlay.current = false;
|
|
912
|
+
setEditingTextId(id);
|
|
913
|
+
setNewText(found.text);
|
|
914
|
+
setPanel('text');
|
|
915
|
+
}
|
|
916
|
+
return prev;
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
onPanResponderTerminate: () => {
|
|
921
|
+
if (longPressTimeout) {
|
|
922
|
+
clearTimeout(longPressTimeout);
|
|
923
|
+
longPressTimeout = null;
|
|
924
|
+
}
|
|
925
|
+
setActiveDraggingId(null);
|
|
926
|
+
setIsOverTrash(false);
|
|
848
927
|
}
|
|
849
928
|
});
|
|
850
929
|
};
|
|
@@ -1708,24 +1787,11 @@ export function EditorScreen({
|
|
|
1708
1787
|
]}
|
|
1709
1788
|
{...responder.panHandlers}
|
|
1710
1789
|
>
|
|
1711
|
-
<
|
|
1712
|
-
onLongPress={() => removeTextOverlay(overlay.id)}
|
|
1713
|
-
onPress={() => {
|
|
1714
|
-
pushToHistory();
|
|
1715
|
-
// Backup original state (text, color, position, size, etc.)
|
|
1716
|
-
const found = overlays.find(o => o.id === overlay.id);
|
|
1717
|
-
if (found) {
|
|
1718
|
-
originalOverlayBackup.current = { ...found };
|
|
1719
|
-
}
|
|
1720
|
-
isNewOverlay.current = false;
|
|
1721
|
-
setEditingTextId(overlay.id);
|
|
1722
|
-
setNewText(overlay.text);
|
|
1723
|
-
}}
|
|
1724
|
-
>
|
|
1790
|
+
<View style={{ padding: 4 }}>
|
|
1725
1791
|
<Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
|
|
1726
1792
|
{overlay.text}
|
|
1727
1793
|
</Text>
|
|
1728
|
-
</
|
|
1794
|
+
</View>
|
|
1729
1795
|
</View>
|
|
1730
1796
|
);
|
|
1731
1797
|
})}
|
|
@@ -1964,23 +2030,11 @@ export function EditorScreen({
|
|
|
1964
2030
|
]}
|
|
1965
2031
|
{...responder.panHandlers}
|
|
1966
2032
|
>
|
|
1967
|
-
<
|
|
1968
|
-
onLongPress={() => removeTextOverlay(overlay.id)}
|
|
1969
|
-
onPress={() => {
|
|
1970
|
-
pushToHistory();
|
|
1971
|
-
const found = overlays.find(o => o.id === overlay.id);
|
|
1972
|
-
if (found) {
|
|
1973
|
-
originalOverlayBackup.current = { ...found };
|
|
1974
|
-
}
|
|
1975
|
-
isNewOverlay.current = false;
|
|
1976
|
-
setEditingTextId(overlay.id);
|
|
1977
|
-
setNewText(overlay.text);
|
|
1978
|
-
}}
|
|
1979
|
-
>
|
|
2033
|
+
<View style={{ padding: 4 }}>
|
|
1980
2034
|
<Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
|
|
1981
2035
|
{overlay.text}
|
|
1982
2036
|
</Text>
|
|
1983
|
-
</
|
|
2037
|
+
</View>
|
|
1984
2038
|
</View>
|
|
1985
2039
|
);
|
|
1986
2040
|
})}
|
|
@@ -2352,23 +2406,11 @@ export function EditorScreen({
|
|
|
2352
2406
|
]}
|
|
2353
2407
|
{...responder.panHandlers}
|
|
2354
2408
|
>
|
|
2355
|
-
<
|
|
2356
|
-
onLongPress={() => removeTextOverlay(overlay.id)}
|
|
2357
|
-
onPress={() => {
|
|
2358
|
-
pushToHistory();
|
|
2359
|
-
const found = overlays.find(o => o.id === overlay.id);
|
|
2360
|
-
if (found) {
|
|
2361
|
-
originalOverlayBackup.current = { ...found };
|
|
2362
|
-
}
|
|
2363
|
-
isNewOverlay.current = false;
|
|
2364
|
-
setEditingTextId(overlay.id);
|
|
2365
|
-
setNewText(overlay.text);
|
|
2366
|
-
}}
|
|
2367
|
-
>
|
|
2409
|
+
<View style={{ padding: 4 }}>
|
|
2368
2410
|
<Text style={[styles.textOverlay, { color: overlay.color, fontSize: overlay.fontSize }]}>
|
|
2369
2411
|
{overlay.text}
|
|
2370
2412
|
</Text>
|
|
2371
|
-
</
|
|
2413
|
+
</View>
|
|
2372
2414
|
</View>
|
|
2373
2415
|
);
|
|
2374
2416
|
})}
|
|
@@ -3059,6 +3101,23 @@ export function EditorScreen({
|
|
|
3059
3101
|
</View>
|
|
3060
3102
|
</Modal>
|
|
3061
3103
|
|
|
3104
|
+
{/* Global Trash Zone for Drag Delete */}
|
|
3105
|
+
{activeDraggingId !== null && (
|
|
3106
|
+
<View style={styles.globalTrashZone} pointerEvents="none">
|
|
3107
|
+
<View
|
|
3108
|
+
style={[
|
|
3109
|
+
styles.trashZoneContainer,
|
|
3110
|
+
isOverTrash && styles.trashZoneActive,
|
|
3111
|
+
isOverTrash ? { transform: [{ scale: 1.15 }] } : {}
|
|
3112
|
+
]}
|
|
3113
|
+
>
|
|
3114
|
+
<Ionicons name={isOverTrash ? "trash" : "trash-outline"} size={20} color="#fff" />
|
|
3115
|
+
<Text style={styles.trashZoneText}>
|
|
3116
|
+
{isOverTrash ? "Release to Delete" : "Drag here to delete"}
|
|
3117
|
+
</Text>
|
|
3118
|
+
</View>
|
|
3119
|
+
</View>
|
|
3120
|
+
)}
|
|
3062
3121
|
</View>
|
|
3063
3122
|
);
|
|
3064
3123
|
}
|
|
@@ -4163,6 +4222,41 @@ const styles = StyleSheet.create({
|
|
|
4163
4222
|
borderWidth: 1,
|
|
4164
4223
|
borderColor: 'rgba(255, 255, 255, 0.3)',
|
|
4165
4224
|
},
|
|
4225
|
+
globalTrashZone: {
|
|
4226
|
+
position: 'absolute',
|
|
4227
|
+
bottom: Platform.OS === 'ios' ? 48 : 28,
|
|
4228
|
+
left: 0,
|
|
4229
|
+
right: 0,
|
|
4230
|
+
alignItems: 'center',
|
|
4231
|
+
justifyContent: 'center',
|
|
4232
|
+
zIndex: 9999,
|
|
4233
|
+
},
|
|
4234
|
+
trashZoneContainer: {
|
|
4235
|
+
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
4236
|
+
paddingHorizontal: 20,
|
|
4237
|
+
paddingVertical: 12,
|
|
4238
|
+
borderRadius: 24,
|
|
4239
|
+
flexDirection: 'row',
|
|
4240
|
+
alignItems: 'center',
|
|
4241
|
+
justifyContent: 'center',
|
|
4242
|
+
borderWidth: 1.5,
|
|
4243
|
+
borderColor: 'rgba(255, 255, 255, 0.25)',
|
|
4244
|
+
shadowColor: '#000',
|
|
4245
|
+
shadowOffset: { width: 0, height: 4 },
|
|
4246
|
+
shadowOpacity: 0.4,
|
|
4247
|
+
shadowRadius: 6,
|
|
4248
|
+
elevation: 8,
|
|
4249
|
+
},
|
|
4250
|
+
trashZoneActive: {
|
|
4251
|
+
backgroundColor: '#ef4444',
|
|
4252
|
+
borderColor: '#fca5a5',
|
|
4253
|
+
},
|
|
4254
|
+
trashZoneText: {
|
|
4255
|
+
color: '#ffffff',
|
|
4256
|
+
fontSize: 12,
|
|
4257
|
+
fontWeight: '700',
|
|
4258
|
+
marginLeft: 8,
|
|
4259
|
+
},
|
|
4166
4260
|
cropIconText: {
|
|
4167
4261
|
color: '#FFFFFF',
|
|
4168
4262
|
fontSize: 18,
|
|
@@ -50,6 +50,8 @@ export function PickScreen({
|
|
|
50
50
|
cameraModes = ['POST', 'STORY', 'REEL'],
|
|
51
51
|
onCameraModeChange,
|
|
52
52
|
defaultCameraMode,
|
|
53
|
+
maxSelection = 1,
|
|
54
|
+
aspectRatio = 'free',
|
|
53
55
|
}: {
|
|
54
56
|
items: MediaItem[];
|
|
55
57
|
onPicked: (items: MediaItem[]) => void;
|
|
@@ -60,6 +62,8 @@ export function PickScreen({
|
|
|
60
62
|
cameraModes?: string[];
|
|
61
63
|
onCameraModeChange?: (mode: string) => void;
|
|
62
64
|
defaultCameraMode?: string;
|
|
65
|
+
maxSelection?: number;
|
|
66
|
+
aspectRatio?: '1:1' | '4:3' | '4:5' | '16:9' | '9:16' | 'free';
|
|
63
67
|
}) {
|
|
64
68
|
const insets = useSafeAreaInsets();
|
|
65
69
|
const [tab, setTab] = useState<'GALLERY' | 'PHOTO' | 'VIDEO'>('GALLERY');
|
|
@@ -73,7 +77,21 @@ export function PickScreen({
|
|
|
73
77
|
const [albums, setAlbums] = useState<Album[]>([]);
|
|
74
78
|
const [loading, setLoading] = useState(false);
|
|
75
79
|
const [previewUri, setPreviewUri] = useState<string | null>(null);
|
|
76
|
-
|
|
80
|
+
|
|
81
|
+
// Aspect ratio logic
|
|
82
|
+
const isRatioLocked = aspectRatio !== 'free';
|
|
83
|
+
const ratioMap: Record<string, number> = {
|
|
84
|
+
'1:1': 1,
|
|
85
|
+
'4:3': 4 / 3,
|
|
86
|
+
'4:5': 4 / 5,
|
|
87
|
+
'16:9': 16 / 9,
|
|
88
|
+
'9:16': 9 / 16,
|
|
89
|
+
};
|
|
90
|
+
const previewAspectRatio = isRatioLocked ? ratioMap[aspectRatio] ?? 1 : undefined;
|
|
91
|
+
// When ratio is locked, always use 'cover' (fills the fixed frame)
|
|
92
|
+
const [cropMode, setCropMode] = useState<'1:1' | 'original'>(
|
|
93
|
+
isRatioLocked ? '1:1' : '1:1'
|
|
94
|
+
);
|
|
77
95
|
const [videoPaused, setVideoPaused] = useState(false);
|
|
78
96
|
const scaleAnim = useRef(new Animated.Value(1)).current;
|
|
79
97
|
|
|
@@ -248,7 +266,7 @@ export function PickScreen({
|
|
|
248
266
|
setSelectedItems((prev) => {
|
|
249
267
|
const exists = prev.find((i) => i.id === item.id);
|
|
250
268
|
if (exists) return prev.filter((i) => i.id !== item.id);
|
|
251
|
-
if (prev.length >=
|
|
269
|
+
if (prev.length >= maxSelection) return prev;
|
|
252
270
|
return [...prev, item];
|
|
253
271
|
});
|
|
254
272
|
}
|
|
@@ -479,7 +497,15 @@ export function PickScreen({
|
|
|
479
497
|
</View>
|
|
480
498
|
</Modal>
|
|
481
499
|
|
|
482
|
-
<Animated.View
|
|
500
|
+
<Animated.View
|
|
501
|
+
style={[
|
|
502
|
+
styles.preview,
|
|
503
|
+
{ transform: [{ scale: scaleAnim }] },
|
|
504
|
+
isRatioLocked && previewAspectRatio
|
|
505
|
+
? { height: undefined, aspectRatio: previewAspectRatio }
|
|
506
|
+
: {},
|
|
507
|
+
]}
|
|
508
|
+
>
|
|
483
509
|
{selectedMedia?.type === 'video' ? (
|
|
484
510
|
<Pressable style={styles.previewImage} onPress={() => setVideoPaused((v) => !v)}>
|
|
485
511
|
{playableUri ? (
|
|
@@ -488,7 +514,7 @@ export function PickScreen({
|
|
|
488
514
|
paused={videoPaused}
|
|
489
515
|
muted={false}
|
|
490
516
|
style={styles.previewImage}
|
|
491
|
-
resizeMode=
|
|
517
|
+
resizeMode="cover"
|
|
492
518
|
/>
|
|
493
519
|
) : (
|
|
494
520
|
<Image
|
|
@@ -507,21 +533,31 @@ export function PickScreen({
|
|
|
507
533
|
<Image
|
|
508
534
|
source={{ uri: previewUri ?? selectedMedia?.uri }}
|
|
509
535
|
style={[styles.previewImage, cropMode === '1:1' ? styles.squareCrop : styles.originalCrop]}
|
|
510
|
-
resizeMode={cropMode === '1:1' ? 'cover' : 'contain'}
|
|
536
|
+
resizeMode={isRatioLocked ? 'cover' : (cropMode === '1:1' ? 'cover' : 'contain')}
|
|
511
537
|
/>
|
|
512
538
|
)}
|
|
513
539
|
|
|
514
540
|
<View style={styles.previewControls}>
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
541
|
+
{/* Ratio lock badge */}
|
|
542
|
+
{isRatioLocked && (
|
|
543
|
+
<View style={styles.ratioBadge}>
|
|
544
|
+
<Text style={styles.ratioBadgeText}>{aspectRatio}</Text>
|
|
545
|
+
</View>
|
|
546
|
+
)}
|
|
547
|
+
|
|
548
|
+
{/* Crop toggle — hidden when ratio is locked */}
|
|
549
|
+
{!isRatioLocked && (
|
|
550
|
+
<Pressable
|
|
551
|
+
style={styles.cropToggle}
|
|
552
|
+
onPress={() => setCropMode((v) => (v === '1:1' ? 'original' : '1:1'))}
|
|
553
|
+
>
|
|
554
|
+
<Ionicons
|
|
555
|
+
name={cropMode === '1:1' ? 'square-outline' : 'expand-outline'}
|
|
556
|
+
size={20}
|
|
557
|
+
color="#fff"
|
|
558
|
+
/>
|
|
559
|
+
</Pressable>
|
|
560
|
+
)}
|
|
525
561
|
</View>
|
|
526
562
|
</Animated.View>
|
|
527
563
|
|
|
@@ -534,9 +570,36 @@ export function PickScreen({
|
|
|
534
570
|
<Ionicons name="chevron-down" size={16} color="#fff" style={{ marginLeft: 6 }} />
|
|
535
571
|
</Pressable>
|
|
536
572
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
573
|
+
{/* Only show Select/Done button when maxSelection > 1 */}
|
|
574
|
+
{maxSelection > 1 && (
|
|
575
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
576
|
+
{multiSelect && (
|
|
577
|
+
<Text style={{ color: '#3b82f6', fontSize: 12, fontWeight: '700' }}>
|
|
578
|
+
{selectedItems.length}/{maxSelection}
|
|
579
|
+
</Text>
|
|
580
|
+
)}
|
|
581
|
+
<Pressable
|
|
582
|
+
style={[
|
|
583
|
+
styles.multiSelectBtn,
|
|
584
|
+
multiSelect && styles.multiSelectBtnActive,
|
|
585
|
+
multiSelect && selectedItems.length === 0 && { opacity: 0.35 },
|
|
586
|
+
]}
|
|
587
|
+
onPress={() => {
|
|
588
|
+
if (multiSelect) {
|
|
589
|
+
handleNext();
|
|
590
|
+
} else {
|
|
591
|
+
toggleMultiSelect();
|
|
592
|
+
}
|
|
593
|
+
}}
|
|
594
|
+
disabled={multiSelect && selectedItems.length === 0}
|
|
595
|
+
>
|
|
596
|
+
<Text style={[
|
|
597
|
+
styles.multiSelectText,
|
|
598
|
+
multiSelect && styles.multiSelectTextActive,
|
|
599
|
+
]}>{multiSelect ? 'Done' : 'Select'}</Text>
|
|
600
|
+
</Pressable>
|
|
601
|
+
</View>
|
|
602
|
+
)}
|
|
540
603
|
</View>
|
|
541
604
|
|
|
542
605
|
<FlatList
|
|
@@ -794,6 +857,20 @@ const styles = StyleSheet.create({
|
|
|
794
857
|
justifyContent: 'center',
|
|
795
858
|
},
|
|
796
859
|
cropIcon: { color: '#fff', fontSize: 16 },
|
|
860
|
+
ratioBadge: {
|
|
861
|
+
backgroundColor: 'rgba(0,0,0,0.65)',
|
|
862
|
+
paddingHorizontal: 10,
|
|
863
|
+
paddingVertical: 5,
|
|
864
|
+
borderRadius: 12,
|
|
865
|
+
borderWidth: 1,
|
|
866
|
+
borderColor: 'rgba(255,255,255,0.3)',
|
|
867
|
+
},
|
|
868
|
+
ratioBadgeText: {
|
|
869
|
+
color: '#fff',
|
|
870
|
+
fontSize: 12,
|
|
871
|
+
fontWeight: '700',
|
|
872
|
+
letterSpacing: 0.5,
|
|
873
|
+
},
|
|
797
874
|
albumRow: {
|
|
798
875
|
flexDirection: 'row',
|
|
799
876
|
justifyContent: 'space-between',
|