react-native-inapp-inspector 2.3.18 → 2.3.19

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 (79) hide show
  1. package/android/src/main/java/com/inappinspector/NetworkInspectorModule.kt +47 -0
  2. package/dist/commonjs/capture.d.ts +2 -0
  3. package/dist/commonjs/capture.js +3 -0
  4. package/dist/commonjs/components/EmptyState.d.ts +1 -0
  5. package/dist/commonjs/components/EmptyState.js +2 -2
  6. package/dist/commonjs/components/Inspector/FabLauncher.js +494 -39
  7. package/dist/commonjs/components/Inspector/InspectorHeader.js +45 -26
  8. package/dist/commonjs/components/Inspector/MainScreen.js +4 -3
  9. package/dist/commonjs/components/Inspector/MediaGalleryTab.js +418 -13
  10. package/dist/commonjs/components/Inspector/MediaPreviewModal.js +376 -88
  11. package/dist/commonjs/components/Inspector/NetworkDetail.js +49 -63
  12. package/dist/commonjs/components/Inspector/SettingsPanel.js +499 -119
  13. package/dist/commonjs/components/Inspector/TabBar.js +166 -4
  14. package/dist/commonjs/components/JsonViewer.js +4 -3
  15. package/dist/commonjs/components/NetworkIcons.d.ts +4 -0
  16. package/dist/commonjs/components/NetworkIcons.js +26 -1
  17. package/dist/commonjs/components/SegmentedTabs.js +1 -0
  18. package/dist/commonjs/constants/version.d.ts +1 -1
  19. package/dist/commonjs/constants/version.js +1 -1
  20. package/dist/commonjs/i18n/locales/en.json +20 -2
  21. package/dist/commonjs/index.js +45 -30
  22. package/dist/commonjs/native/NativeInspector.d.ts +3 -0
  23. package/dist/commonjs/native/NativeInspector.js +14 -1
  24. package/dist/commonjs/native/NativeNetworkInspector.d.ts +1 -0
  25. package/dist/commonjs/styles/AppColors.js +78 -78
  26. package/dist/commonjs/types/enums.d.ts +2 -0
  27. package/dist/commonjs/types/enums.js +2 -0
  28. package/dist/commonjs/types/interfaces.d.ts +5 -0
  29. package/dist/esm/capture.d.ts +2 -0
  30. package/dist/esm/capture.js +4 -1
  31. package/dist/esm/components/EmptyState.d.ts +1 -0
  32. package/dist/esm/components/EmptyState.js +2 -2
  33. package/dist/esm/components/Inspector/FabLauncher.js +462 -40
  34. package/dist/esm/components/Inspector/InspectorHeader.js +46 -27
  35. package/dist/esm/components/Inspector/MainScreen.js +4 -3
  36. package/dist/esm/components/Inspector/MediaGalleryTab.js +419 -14
  37. package/dist/esm/components/Inspector/MediaPreviewModal.js +379 -91
  38. package/dist/esm/components/Inspector/NetworkDetail.js +49 -63
  39. package/dist/esm/components/Inspector/SettingsPanel.js +500 -120
  40. package/dist/esm/components/Inspector/TabBar.js +135 -6
  41. package/dist/esm/components/JsonViewer.js +4 -3
  42. package/dist/esm/components/NetworkIcons.d.ts +4 -0
  43. package/dist/esm/components/NetworkIcons.js +21 -0
  44. package/dist/esm/components/SegmentedTabs.js +1 -0
  45. package/dist/esm/constants/version.d.ts +1 -1
  46. package/dist/esm/constants/version.js +1 -1
  47. package/dist/esm/i18n/locales/en.json +20 -2
  48. package/dist/esm/index.js +44 -29
  49. package/dist/esm/native/NativeInspector.d.ts +3 -0
  50. package/dist/esm/native/NativeInspector.js +12 -0
  51. package/dist/esm/native/NativeNetworkInspector.d.ts +1 -0
  52. package/dist/esm/styles/AppColors.js +78 -78
  53. package/dist/esm/types/enums.d.ts +2 -0
  54. package/dist/esm/types/enums.js +2 -0
  55. package/dist/esm/types/interfaces.d.ts +5 -0
  56. package/ios/NetworkInspectorModule.mm +241 -136
  57. package/package.json +1 -1
  58. package/react-native-inapp-inspector.podspec +1 -1
  59. package/src/capture.ts +8 -0
  60. package/src/components/EmptyState.tsx +3 -1
  61. package/src/components/Inspector/FabLauncher.tsx +562 -52
  62. package/src/components/Inspector/InspectorHeader.tsx +60 -22
  63. package/src/components/Inspector/MainScreen.tsx +3 -2
  64. package/src/components/Inspector/MediaGalleryTab.tsx +502 -28
  65. package/src/components/Inspector/MediaPreviewModal.tsx +465 -117
  66. package/src/components/Inspector/NetworkDetail.tsx +80 -97
  67. package/src/components/Inspector/SettingsPanel.tsx +708 -152
  68. package/src/components/Inspector/TabBar.tsx +174 -6
  69. package/src/components/JsonViewer.tsx +4 -2
  70. package/src/components/NetworkIcons.tsx +56 -0
  71. package/src/components/SegmentedTabs.tsx +1 -0
  72. package/src/constants/version.ts +1 -1
  73. package/src/i18n/locales/en.json +20 -4
  74. package/src/index.tsx +46 -31
  75. package/src/native/NativeInspector.ts +17 -0
  76. package/src/native/NativeNetworkInspector.ts +1 -0
  77. package/src/styles/AppColors.ts +80 -80
  78. package/src/types/enums.ts +2 -0
  79. package/src/types/interfaces.ts +5 -0
@@ -1,15 +1,43 @@
1
- import React from 'react';
2
- import {Animated, View} from 'react-native';
1
+ import React, {useCallback, useEffect, useRef, useState} from 'react';
2
+ import {
3
+ Animated,
4
+ PanResponder,
5
+ Platform,
6
+ StyleSheet,
7
+ Text,
8
+ TouchableOpacity,
9
+ useWindowDimensions,
10
+ View,
11
+ } from 'react-native';
3
12
  import LinearGradient from 'react-native-linear-gradient';
4
13
  import {useInspector} from './InspectorContext';
5
14
  import TouchableScale from '../TouchableScale';
15
+ import AppHeaderLogo from '../AppHeaderLogo';
6
16
  import BrandCircleIcon from '../BrandCircleIcon';
17
+ import {
18
+ AndroidIcon,
19
+ AppleIcon,
20
+ CameraIcon,
21
+ NpmIcon,
22
+ VideoCameraIcon,
23
+ } from '../NetworkIcons';
24
+ import {CapturedMediaItem, ScreenCapture} from '../../capture';
25
+ import {triggerNativeHaptic} from '../../native/NativeInspector';
26
+ import {showToast} from '../../helpers/toast';
27
+ import {getAppName, getAppVersionAndBuild} from '../../helpers';
28
+ import {LIB_VERSION} from '../../constants';
7
29
  import {AppColors} from '../../styles/AppColors';
30
+ import {AppFonts} from '../../styles/AppFonts';
8
31
  import styles from '../../styles';
32
+ import {useTranslation} from '../../i18n';
33
+ import {MediaPreviewModal} from './MediaPreviewModal';
9
34
 
10
35
  const FabLauncher = () => {
11
36
  const {
12
37
  setVisible,
38
+ isMinimized,
39
+ setIsMinimized,
40
+ appIcon,
13
41
  fabPan,
14
42
  fabPanResponder,
15
43
  fabDraggedRef,
@@ -18,63 +46,545 @@ const FabLauncher = () => {
18
46
  unreadPulseAnim,
19
47
  logs,
20
48
  analyticsEvents,
49
+ refreshMediaCount,
21
50
  } = useInspector();
51
+ const {width: screenWidth} = useWindowDimensions();
52
+ const {t} = useTranslation();
53
+
54
+ const [appVersionString] = useState<string>(() => {
55
+ return getAppVersionAndBuild().formatted;
56
+ });
57
+
58
+ const [isRecording, setIsRecording] = useState(false);
59
+ const [recordingSeconds, setRecordingSeconds] = useState(0);
60
+ const recordingTimerRef = useRef<NodeJS.Timeout | null>(null);
61
+ const [previewMediaItem, setPreviewMediaItem] =
62
+ useState<CapturedMediaItem | null>(null);
63
+
64
+ // Draggable pan responder for the minimized floating player bar
65
+ const playerPan = useRef(new Animated.ValueXY({x: 0, y: 0})).current;
66
+ const playerPanRef = useRef({x: 0, y: 0});
67
+ useEffect(() => {
68
+ const idX = playerPan.x.addListener(v => (playerPanRef.current.x = v.value));
69
+ const idY = playerPan.y.addListener(v => (playerPanRef.current.y = v.value));
70
+ return () => {
71
+ playerPan.x.removeListener(idX);
72
+ playerPan.y.removeListener(idY);
73
+ };
74
+ }, [playerPan]);
75
+ const playerDraggedRef = useRef(false);
76
+ const playerPanResponder = useRef(
77
+ PanResponder.create({
78
+ onStartShouldSetPanResponder: () => false,
79
+ onMoveShouldSetPanResponder: (_e, g) =>
80
+ Math.abs(g.dx) > 4 || Math.abs(g.dy) > 4,
81
+ onPanResponderGrant: () => {
82
+ playerDraggedRef.current = true;
83
+ playerPan.setOffset({
84
+ x: playerPanRef.current.x,
85
+ y: playerPanRef.current.y,
86
+ });
87
+ playerPan.setValue({x: 0, y: 0});
88
+ },
89
+ onPanResponderMove: Animated.event(
90
+ [null, {dx: playerPan.x, dy: playerPan.y}],
91
+ {useNativeDriver: false},
92
+ ),
93
+ onPanResponderRelease: () => {
94
+ playerPan.flattenOffset();
95
+ setTimeout(() => {
96
+ playerDraggedRef.current = false;
97
+ }, 50);
98
+ },
99
+ onPanResponderTerminate: () => {
100
+ playerPan.flattenOffset();
101
+ playerDraggedRef.current = false;
102
+ },
103
+ }),
104
+ ).current;
105
+
106
+ // Sync initial recording state
107
+ useEffect(() => {
108
+ ScreenCapture.isRecording()
109
+ .then(active => {
110
+ setIsRecording(Boolean(active));
111
+ })
112
+ .catch(() => {});
113
+ }, []);
114
+
115
+ // Timer tick for video recording
116
+ useEffect(() => {
117
+ if (isRecording) {
118
+ setRecordingSeconds(0);
119
+ recordingTimerRef.current = setInterval(() => {
120
+ setRecordingSeconds(prev => prev + 1);
121
+ }, 1000);
122
+ } else {
123
+ if (recordingTimerRef.current) {
124
+ clearInterval(recordingTimerRef.current);
125
+ recordingTimerRef.current = null;
126
+ }
127
+ setRecordingSeconds(0);
128
+ }
129
+ return () => {
130
+ if (recordingTimerRef.current) {
131
+ clearInterval(recordingTimerRef.current);
132
+ }
133
+ };
134
+ }, [isRecording]);
135
+
136
+ const handleTakeScreenshot = useCallback(async () => {
137
+ if (playerDraggedRef.current) return;
138
+ try {
139
+ triggerNativeHaptic('light');
140
+ const result = await ScreenCapture.takeScreenshot({
141
+ format: 'png',
142
+ quality: 0.9,
143
+ hideInspector: true,
144
+ });
145
+ if (result) {
146
+ triggerNativeHaptic('success');
147
+ const newItem: CapturedMediaItem = {
148
+ id: `screenshot_${result.timestamp}.png`,
149
+ type: 'image',
150
+ format: result.format,
151
+ uri: result.uri,
152
+ filename:
153
+ result.uri.split('/').pop() || `screenshot_${result.timestamp}.png`,
154
+ sizeBytes: result.sizeBytes,
155
+ timestamp: result.timestamp,
156
+ width: result.width,
157
+ height: result.height,
158
+ };
159
+ setPreviewMediaItem(newItem);
160
+ refreshMediaCount?.().catch(() => {});
161
+ showToast(t('header.screenshotCaptured', 'Screenshot captured'));
162
+ } else {
163
+ showToast(t('header.screenshotFailed', 'Failed to capture screenshot'));
164
+ }
165
+ } catch {
166
+ showToast(t('header.screenshotError', 'Error capturing screenshot'));
167
+ }
168
+ }, [refreshMediaCount, t]);
169
+
170
+ const handleToggleVideoRecording = useCallback(async () => {
171
+ if (playerDraggedRef.current) return;
172
+ try {
173
+ if (isRecording) {
174
+ triggerNativeHaptic('medium');
175
+ const result = await ScreenCapture.stopRecording();
176
+ setIsRecording(false);
177
+ if (result) {
178
+ triggerNativeHaptic('success');
179
+ const newItem: CapturedMediaItem = {
180
+ id: `video_${result.timestamp}.${result.format}`,
181
+ type: result.format === 'gif' ? 'gif' : 'video',
182
+ format: result.format,
183
+ uri: result.uri,
184
+ filename:
185
+ result.uri.split('/').pop() ||
186
+ `video_${result.timestamp}.${result.format}`,
187
+ sizeBytes: result.sizeBytes,
188
+ timestamp: result.timestamp,
189
+ durationMs: result.durationMs,
190
+ width: result.width,
191
+ height: result.height,
192
+ hasAudio: result.hasAudio,
193
+ };
194
+ setPreviewMediaItem(newItem);
195
+ refreshMediaCount?.().catch(() => {});
196
+ showToast(
197
+ t('header.recordingSaved', {
198
+ duration: (result.durationMs / 1000).toFixed(1),
199
+ defaultValue: `Recording saved (${(result.durationMs / 1000).toFixed(1)}s)`,
200
+ }),
201
+ );
202
+ }
203
+ } else {
204
+ triggerNativeHaptic('medium');
205
+ const started = await ScreenCapture.startRecording({
206
+ fps: 24,
207
+ audioSource: 'none',
208
+ });
209
+ if (started) {
210
+ setIsRecording(true);
211
+ showToast(t('header.recordingStarted', 'Recording started'));
212
+ } else {
213
+ showToast(t('header.recordingStartFailed', 'Failed to start recording'));
214
+ }
215
+ }
216
+ } catch {
217
+ showToast(t('header.recordingError', 'Error during screen recording'));
218
+ setIsRecording(false);
219
+ }
220
+ }, [isRecording, refreshMediaCount, t]);
221
+
222
+ const handleExpandInspector = useCallback(() => {
223
+ if (playerDraggedRef.current) return;
224
+ triggerNativeHaptic('light');
225
+ setIsMinimized?.(false);
226
+ setVisible(true);
227
+ }, [setIsMinimized, setVisible]);
228
+
229
+ const formatTime = (secs: number) => {
230
+ const mins = Math.floor(secs / 60);
231
+ const remainingSecs = secs % 60;
232
+ return `${mins}:${remainingSecs < 10 ? '0' : ''}${remainingSecs}`;
233
+ };
22
234
 
23
235
  return (
24
- <Animated.View
25
- style={[styles.fabWrapper, {transform: fabPan.getTranslateTransform()}]}
26
- {...fabPanResponder.panHandlers}>
27
- <TouchableScale
28
- style={{alignItems: 'center', justifyContent: 'center'}}
29
- onPress={() => {
30
- if (fabDraggedRef.current) return;
31
- setVisible(true);
32
- }}
33
- hitSlop={10}>
236
+ <>
237
+ {/* ─── Case 1: Minimized Music Player-Style Bottom Bar (Draggable) ─── */}
238
+ {isMinimized ? (
239
+ <Animated.View
240
+ style={[
241
+ fabStyles.playerContainer,
242
+ {transform: playerPan.getTranslateTransform()},
243
+ ]}
244
+ {...playerPanResponder.panHandlers}>
245
+ {/* Header-matched Gradient Background */}
246
+ <LinearGradient
247
+ colors={[AppColors.indigo600, AppColors.violet600]}
248
+ start={{x: 0, y: 0}}
249
+ end={{x: 1, y: 1}}
250
+ style={fabStyles.gradientBg}
251
+ pointerEvents="none"
252
+ />
253
+
254
+ {/* Enhanced Shining sweep streak across the player bar */}
255
+ <View pointerEvents="none" style={fabStyles.playerShineClip}>
256
+ <Animated.View
257
+ style={[
258
+ fabStyles.playerShineStreak,
259
+ {
260
+ transform: [
261
+ {
262
+ translateX: fabShineAnim.interpolate({
263
+ inputRange: [0, 1],
264
+ outputRange: [-80, screenWidth + 80],
265
+ }),
266
+ },
267
+ {rotate: '28deg'},
268
+ ],
269
+ },
270
+ ]}>
271
+ <LinearGradient
272
+ colors={[
273
+ 'rgba(255, 255, 255, 0)',
274
+ 'rgba(255, 255, 255, 0.18)',
275
+ 'rgba(255, 255, 255, 0.70)',
276
+ 'rgba(255, 255, 255, 1)',
277
+ 'rgba(255, 255, 255, 0.70)',
278
+ 'rgba(255, 255, 255, 0.18)',
279
+ 'rgba(255, 255, 255, 0)',
280
+ ]}
281
+ locations={[0, 0.22, 0.44, 0.5, 0.56, 0.78, 1]}
282
+ start={{x: 0, y: 0.5}}
283
+ end={{x: 1, y: 0.5}}
284
+ style={{flex: 1}}
285
+ />
286
+ </Animated.View>
287
+ </View>
288
+
289
+ {/* App Logo & Expand Info (Music Player Left Section) */}
290
+ <TouchableOpacity
291
+ activeOpacity={0.75}
292
+ onPress={handleExpandInspector}
293
+ style={fabStyles.brandSection}
294
+ accessibilityRole="button"
295
+ accessibilityLabel={t('mediaGallery.expand', 'Expand Inspector')}>
296
+ <AppHeaderLogo size={36} customIcon={appIcon} />
297
+ <View style={fabStyles.titleWrapper}>
298
+ <Text style={fabStyles.appTitle} numberOfLines={1}>
299
+ {getAppName()}
300
+ </Text>
301
+ <View style={fabStyles.versionRow}>
302
+ {/* OS Version Chip */}
303
+ <View style={fabStyles.versionChip}>
304
+ {Platform.OS === 'ios' ? (
305
+ <AppleIcon
306
+ color={`${AppColors.white}E6`}
307
+ size={9}
308
+ />
309
+ ) : (
310
+ <AndroidIcon
311
+ color={`${AppColors.white}E6`}
312
+ size={9}
313
+ />
314
+ )}
315
+ <Text style={fabStyles.versionChipText} numberOfLines={1}>
316
+ {appVersionString}
317
+ </Text>
318
+ </View>
319
+
320
+ {/* NPM Version Chip */}
321
+ <View style={fabStyles.versionChip}>
322
+ <NpmIcon size={9} color={AppColors.npmRedLight} />
323
+ <Text style={fabStyles.versionChipText} numberOfLines={1}>
324
+ v{LIB_VERSION}
325
+ </Text>
326
+ </View>
327
+ </View>
328
+ </View>
329
+ </TouchableOpacity>
330
+
331
+ {/* Action Buttons: Photo & Record (Matching Header Toolbar Style) */}
332
+ <View style={fabStyles.actionsRow}>
333
+ {/* Photo Action */}
334
+ <TouchableScale
335
+ onPress={handleTakeScreenshot}
336
+ hitSlop={{top: 6, bottom: 6, left: 6, right: 6}}
337
+ style={fabStyles.actionBtn}>
338
+ <CameraIcon size={13} color={AppColors.white} />
339
+ <Text style={fabStyles.actionText}>
340
+ {t('header.photo', 'Photo')}
341
+ </Text>
342
+ </TouchableScale>
343
+
344
+ {/* Record Action */}
345
+ <TouchableScale
346
+ onPress={handleToggleVideoRecording}
347
+ hitSlop={{top: 6, bottom: 6, left: 6, right: 6}}
348
+ style={[
349
+ fabStyles.actionBtn,
350
+ isRecording && fabStyles.recordingBtnActive,
351
+ ]}>
352
+ {isRecording ? (
353
+ <View style={fabStyles.recordingDot} />
354
+ ) : (
355
+ <VideoCameraIcon size={13} color={AppColors.white} />
356
+ )}
357
+ <Text
358
+ style={[
359
+ fabStyles.actionText,
360
+ isRecording && {color: AppColors.red300},
361
+ ]}>
362
+ {isRecording
363
+ ? `${t('header.rec', 'REC')} ${formatTime(recordingSeconds)}`
364
+ : t('header.record', 'Record')}
365
+ </Text>
366
+ </TouchableScale>
367
+ </View>
368
+ </Animated.View>
369
+ ) : (
370
+ /* ─── Case 2: Full Original Circular Draggable FAB Icon (when Inspector is closed) ── */
34
371
  <Animated.View
35
- style={[styles.fabPulseRing, {transform: [{scale: pulseAnim}]}]}
36
- />
37
- <BrandCircleIcon size={62} />
38
- {/* #4 — shining sweep, clipped inside the circular launcher */}
39
- <View pointerEvents="none" style={styles.fabShineClip}>
40
- <Animated.View
41
- style={[
42
- styles.fabShineStreak,
43
- {
44
- transform: [
372
+ style={[styles.fabWrapper, {transform: fabPan.getTranslateTransform()}]}
373
+ {...fabPanResponder.panHandlers}>
374
+ <TouchableScale
375
+ style={{alignItems: 'center', justifyContent: 'center'}}
376
+ onPress={() => {
377
+ if (fabDraggedRef.current) return;
378
+ triggerNativeHaptic('light');
379
+ setVisible(true);
380
+ }}
381
+ hitSlop={10}>
382
+ <Animated.View
383
+ style={[styles.fabPulseRing, {transform: [{scale: pulseAnim}]}]}
384
+ />
385
+ <BrandCircleIcon size={62} />
386
+ {/* Shining sweep, clipped inside the circular launcher */}
387
+ <View pointerEvents="none" style={styles.fabShineClip}>
388
+ <Animated.View
389
+ style={[
390
+ styles.fabShineStreak,
45
391
  {
46
- translateX: fabShineAnim.interpolate({
47
- inputRange: [0, 1],
48
- outputRange: [-48, 96],
49
- }),
392
+ transform: [
393
+ {
394
+ translateX: fabShineAnim.interpolate({
395
+ inputRange: [0, 1],
396
+ outputRange: [-48, 96],
397
+ }),
398
+ },
399
+ {rotate: '25deg'},
400
+ ],
50
401
  },
51
- {rotate: '25deg'},
52
- ],
53
- },
54
- ]}>
55
- <LinearGradient
56
- colors={[
57
- `${AppColors.white}00`,
58
- `${AppColors.white}8C`,
59
- `${AppColors.white}00`,
60
- ]}
61
- start={{x: 0, y: 0.5}}
62
- end={{x: 1, y: 0.5}}
63
- style={{flex: 1}}
64
- />
65
- </Animated.View>
66
- </View>
67
- {(logs.length > 0 || analyticsEvents.length > 0) && (
68
- <Animated.View
69
- style={[
70
- styles.fabGreenDot,
71
- {transform: [{scale: unreadPulseAnim}]},
72
- ]}
73
- />
74
- )}
75
- </TouchableScale>
76
- </Animated.View>
402
+ ]}>
403
+ <LinearGradient
404
+ colors={[
405
+ 'rgba(255, 255, 255, 0)',
406
+ 'rgba(255, 255, 255, 0.25)',
407
+ 'rgba(255, 255, 255, 0.95)',
408
+ 'rgba(255, 255, 255, 0.25)',
409
+ 'rgba(255, 255, 255, 0)',
410
+ ]}
411
+ locations={[0, 0.25, 0.5, 0.75, 1]}
412
+ start={{x: 0, y: 0.5}}
413
+ end={{x: 1, y: 0.5}}
414
+ style={{flex: 1}}
415
+ />
416
+ </Animated.View>
417
+ </View>
418
+ {(logs.length > 0 || analyticsEvents.length > 0) && (
419
+ <Animated.View
420
+ style={[
421
+ styles.fabGreenDot,
422
+ {transform: [{scale: unreadPulseAnim}]},
423
+ ]}
424
+ />
425
+ )}
426
+ </TouchableScale>
427
+ </Animated.View>
428
+ )}
429
+
430
+ {/* Instant Media Preview Modal for Screenshots and Screen Recordings */}
431
+ <MediaPreviewModal
432
+ item={previewMediaItem}
433
+ visible={!!previewMediaItem}
434
+ onClose={() => setPreviewMediaItem(null)}
435
+ onDelete={async item => {
436
+ await ScreenCapture.deleteMedia(item.uri);
437
+ refreshMediaCount?.().catch(() => {});
438
+ setPreviewMediaItem(null);
439
+ }}
440
+ onConvertToGif={async item => {
441
+ const gif = await ScreenCapture.convertToGif(item.uri, {
442
+ fps: 12,
443
+ width: 480,
444
+ });
445
+ if (gif) {
446
+ refreshMediaCount?.().catch(() => {});
447
+ setPreviewMediaItem({
448
+ id: `anim_${gif.timestamp}.gif`,
449
+ type: 'gif',
450
+ format: 'gif',
451
+ uri: gif.uri,
452
+ filename: gif.uri.split('/').pop() || `anim_${gif.timestamp}.gif`,
453
+ sizeBytes: gif.sizeBytes,
454
+ timestamp: gif.timestamp,
455
+ durationMs: gif.durationMs,
456
+ });
457
+ }
458
+ }}
459
+ />
460
+ </>
77
461
  );
78
462
  };
79
463
 
464
+ const fabStyles = StyleSheet.create({
465
+ // ─── Music Player Bar Styles (Matching Header Gradient & Aesthetic) ───
466
+ playerContainer: {
467
+ position: 'absolute',
468
+ bottom: Platform.OS === 'ios' ? 34 : 18,
469
+ left: 12,
470
+ right: 12,
471
+ minHeight: 58,
472
+ flexDirection: 'row',
473
+ alignItems: 'center',
474
+ justifyContent: 'space-between',
475
+ borderRadius: 16,
476
+ borderWidth: 1,
477
+ borderColor: 'rgba(255, 255, 255, 0.28)',
478
+ paddingHorizontal: 12,
479
+ paddingVertical: 7,
480
+ gap: 8,
481
+ zIndex: 99999,
482
+ elevation: 25,
483
+ shadowColor: AppColors.indigo600,
484
+ shadowOffset: {width: 0, height: 8},
485
+ shadowOpacity: 0.45,
486
+ shadowRadius: 14,
487
+ overflow: 'hidden',
488
+ },
489
+ gradientBg: {
490
+ position: 'absolute',
491
+ top: 0,
492
+ left: 0,
493
+ right: 0,
494
+ bottom: 0,
495
+ borderRadius: 16,
496
+ },
497
+ playerShineClip: {
498
+ position: 'absolute',
499
+ top: 0,
500
+ left: 0,
501
+ right: 0,
502
+ bottom: 0,
503
+ borderRadius: 16,
504
+ overflow: 'hidden',
505
+ },
506
+ playerShineStreak: {
507
+ position: 'absolute',
508
+ top: -60,
509
+ width: 75,
510
+ height: 200,
511
+ },
512
+ brandSection: {
513
+ flexDirection: 'row',
514
+ alignItems: 'center',
515
+ gap: 9,
516
+ flex: 1,
517
+ minWidth: 0,
518
+ paddingVertical: 2,
519
+ },
520
+ titleWrapper: {
521
+ flex: 1,
522
+ minWidth: 0,
523
+ justifyContent: 'center',
524
+ gap: 2,
525
+ },
526
+ appTitle: {
527
+ fontFamily: AppFonts.interBold,
528
+ fontSize: 13.5,
529
+ color: AppColors.white,
530
+ letterSpacing: -0.2,
531
+ },
532
+ versionRow: {
533
+ flexDirection: 'row',
534
+ alignItems: 'center',
535
+ gap: 4,
536
+ minWidth: 0,
537
+ },
538
+ versionChip: {
539
+ flexDirection: 'row',
540
+ alignItems: 'center',
541
+ backgroundColor: `${AppColors.white}1F`,
542
+ borderRadius: 5,
543
+ paddingHorizontal: 5,
544
+ paddingVertical: 1.5,
545
+ gap: 3,
546
+ borderWidth: 1,
547
+ borderColor: `${AppColors.white}2E`,
548
+ flexShrink: 1,
549
+ minWidth: 0,
550
+ },
551
+ versionChipText: {
552
+ fontFamily: AppFonts.interMedium,
553
+ fontSize: 8.5,
554
+ color: `${AppColors.white}EB`,
555
+ letterSpacing: 0.1,
556
+ },
557
+ actionsRow: {
558
+ flexDirection: 'row',
559
+ alignItems: 'center',
560
+ gap: 6,
561
+ },
562
+ actionBtn: {
563
+ flexDirection: 'row',
564
+ alignItems: 'center',
565
+ gap: 4.5,
566
+ backgroundColor: 'rgba(255, 255, 255, 0.22)',
567
+ paddingHorizontal: 10,
568
+ paddingVertical: 6,
569
+ borderRadius: 7,
570
+ borderWidth: 1,
571
+ borderColor: 'rgba(255, 255, 255, 0.32)',
572
+ },
573
+ recordingBtnActive: {
574
+ backgroundColor: 'rgba(239, 68, 68, 0.32)',
575
+ borderColor: AppColors.red500,
576
+ },
577
+ recordingDot: {
578
+ width: 6,
579
+ height: 6,
580
+ borderRadius: 3,
581
+ backgroundColor: AppColors.red500,
582
+ },
583
+ actionText: {
584
+ fontFamily: AppFonts.interSemiBold,
585
+ fontSize: 11,
586
+ color: AppColors.white,
587
+ },
588
+ });
589
+
80
590
  export default FabLauncher;