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,6 +1,7 @@
1
- import React from 'react';
1
+ import React, {useRef, useState} from 'react';
2
2
  import {
3
- Animated,
3
+ NativeScrollEvent,
4
+ NativeSyntheticEvent,
4
5
  Platform,
5
6
  ScrollView,
6
7
  Text,
@@ -23,11 +24,12 @@ import {
23
24
  SmartphoneIcon,
24
25
  DatabaseIcon,
25
26
  QrCodeIcon,
27
+ ScreencastIcon,
28
+ ChevronIcon,
26
29
  } from '../NetworkIcons';
27
30
 
28
31
  import {isReduxConnected} from '../../customHooks/reduxLogger';
29
32
  import {isAnalyticsConnected} from '../../customHooks/analyticsLogger';
30
- import {triggerNativeHaptic} from '../../native/NativeInspector';
31
33
  import {isLocalDebugEnvironment} from '../../helpers';
32
34
 
33
35
  const TabBar = React.memo(() => {
@@ -42,18 +44,85 @@ const TabBar = React.memo(() => {
42
44
  lastReadApisCount,
43
45
  lastReadLogsCount,
44
46
  lastReadCrashesCount,
45
- unreadPulseAnim,
47
+ mediaCount,
46
48
  } = useInspector();
47
49
 
48
50
  const isReduxAvail = isReduxConnected();
49
51
  const isAnalyticsAvail = isAnalyticsConnected();
50
52
 
53
+ const isMediaActive = activeTab === 'media';
54
+ const showMediaTab = (tabVisibility?.media ?? true) && mediaCount > 0;
55
+
56
+ const scrollViewRef = useRef<ScrollView>(null);
57
+ const [canScrollLeft, setCanScrollLeft] = useState(false);
58
+ const [canScrollRight, setCanScrollRight] = useState(true);
59
+ const scrollOffsetRef = useRef(0);
60
+
61
+ const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
62
+ const {contentOffset, layoutMeasurement, contentSize} = event.nativeEvent;
63
+ const x = contentOffset.x;
64
+ scrollOffsetRef.current = x;
65
+ const hasLeft = x > 8;
66
+ const hasRight = x < contentSize.width - layoutMeasurement.width - 8;
67
+ if (canScrollLeft !== hasLeft) setCanScrollLeft(hasLeft);
68
+ if (canScrollRight !== hasRight) setCanScrollRight(hasRight);
69
+ };
70
+
71
+ const scrollTabs = (offset: number) => {
72
+ scrollViewRef.current?.scrollTo({
73
+ x: Math.max(0, scrollOffsetRef.current + offset),
74
+ animated: true,
75
+ });
76
+ };
77
+
51
78
  return (
52
- <View style={styles.tabBarContainer}>
79
+ <View
80
+ style={[
81
+ styles.tabBarContainer,
82
+ {
83
+ flexDirection: 'row',
84
+ alignItems: 'center',
85
+ paddingHorizontal: 0,
86
+ },
87
+ ]}>
88
+ {/* Scroll Left Arrow Icon — Initially hidden until scrolled */}
89
+ {canScrollLeft && (
90
+ <TouchableOpacity
91
+ onPress={() => scrollTabs(-160)}
92
+ hitSlop={{top: 6, bottom: 6, left: 6, right: 6}}
93
+ activeOpacity={0.7}
94
+ style={{
95
+ paddingLeft: 6,
96
+ paddingRight: 2,
97
+ justifyContent: 'center',
98
+ alignItems: 'center',
99
+ }}>
100
+ <View
101
+ style={{
102
+ width: 22,
103
+ height: 22,
104
+ borderRadius: 11,
105
+ backgroundColor: '#E5E7EB',
106
+ alignItems: 'center',
107
+ justifyContent: 'center',
108
+ }}>
109
+ <ChevronIcon direction="left" size={13} color={AppColors.purple} />
110
+ </View>
111
+ </TouchableOpacity>
112
+ )}
113
+
114
+ {/* Scrollable Main Tabs (#1 - #10) */}
53
115
  <ScrollView
116
+ ref={scrollViewRef}
54
117
  horizontal
55
118
  showsHorizontalScrollIndicator={false}
56
- contentContainerStyle={{paddingRight: 16}}>
119
+ onScroll={handleScroll}
120
+ scrollEventThrottle={16}
121
+ style={{flex: 1}}
122
+ contentContainerStyle={{
123
+ paddingLeft: canScrollLeft ? 4 : 12,
124
+ paddingRight: showMediaTab ? 6 : 12,
125
+ }}>
57
126
  {(
58
127
  [
59
128
  {
@@ -256,6 +325,105 @@ const TabBar = React.memo(() => {
256
325
  );
257
326
  })}
258
327
  </ScrollView>
328
+
329
+ {/* Scroll Right Arrow Icon */}
330
+ {canScrollRight && (
331
+ <TouchableOpacity
332
+ onPress={() => scrollTabs(160)}
333
+ hitSlop={{top: 6, bottom: 6, left: 6, right: 6}}
334
+ activeOpacity={0.7}
335
+ style={{
336
+ paddingLeft: 2,
337
+ paddingRight: showMediaTab ? 4 : 8,
338
+ justifyContent: 'center',
339
+ alignItems: 'center',
340
+ }}>
341
+ <View
342
+ style={{
343
+ width: 22,
344
+ height: 22,
345
+ borderRadius: 11,
346
+ backgroundColor: '#E5E7EB',
347
+ alignItems: 'center',
348
+ justifyContent: 'center',
349
+ }}>
350
+ <ChevronIcon direction="right" size={13} color={AppColors.purple} />
351
+ </View>
352
+ </TouchableOpacity>
353
+ )}
354
+
355
+ {/* Sticky Right Screencast Tab — Visible only when mediaCount > 0 */}
356
+ {showMediaTab && (
357
+ <View
358
+ style={{
359
+ paddingLeft: 6,
360
+ paddingRight: 10,
361
+ borderLeftWidth: 1,
362
+ borderLeftColor: '#E5E7EB',
363
+ backgroundColor: '#ffffff',
364
+ justifyContent: 'center',
365
+ shadowColor: '#000000',
366
+ shadowOffset: {width: -3, height: 0},
367
+ shadowOpacity: 0.12,
368
+ shadowRadius: 4,
369
+ elevation: 4,
370
+ zIndex: 10,
371
+ }}>
372
+ <TouchableScale
373
+ key="media"
374
+ onPress={() => {
375
+ switchActiveTab('media');
376
+ }}
377
+ accessibilityRole="button"
378
+ accessibilityLabel="Media Gallery"
379
+ style={[
380
+ styles.contentTabButton,
381
+ {
382
+ marginRight: 0,
383
+ borderRadius: 8,
384
+ backgroundColor: isMediaActive ? AppColors.purple : '#eeeeee',
385
+ borderColor: isMediaActive ? AppColors.purple : '#E5E7EB',
386
+ borderWidth: 1,
387
+ paddingHorizontal: 10,
388
+ paddingVertical: 7,
389
+ alignItems: 'center',
390
+ justifyContent: 'center',
391
+ },
392
+ isMediaActive && styles.contentTabButtonActive,
393
+ ]}>
394
+ <View
395
+ style={{
396
+ position: 'relative',
397
+ alignItems: 'center',
398
+ justifyContent: 'center',
399
+ }}>
400
+ <ScreencastIcon
401
+ color={isMediaActive ? AppColors.white : AppColors.grayText}
402
+ size={15}
403
+ />
404
+ {mediaCount > 0 && (
405
+ <View
406
+ style={{
407
+ position: 'absolute',
408
+ top: -3,
409
+ right: -4,
410
+ width: 7,
411
+ height: 7,
412
+ borderRadius: 3.5,
413
+ backgroundColor: isMediaActive
414
+ ? AppColors.emerald400
415
+ : AppColors.purple,
416
+ borderWidth: 1.2,
417
+ borderColor: isMediaActive
418
+ ? AppColors.purple
419
+ : AppColors.white,
420
+ }}
421
+ />
422
+ )}
423
+ </View>
424
+ </TouchableScale>
425
+ </View>
426
+ )}
259
427
  </View>
260
428
  );
261
429
  });
@@ -242,6 +242,7 @@ const JsonViewer = React.memo(({
242
242
  {!hideTabs && (
243
243
  <View style={localStyles.toolbar}>
244
244
  <SegmentedTabs
245
+ style={{flex: 1, minWidth: 0, marginRight: 8}}
245
246
  tabs={[
246
247
  {
247
248
  key: 'pretty',
@@ -445,8 +446,9 @@ const localStyles = StyleSheet.create({
445
446
  backgroundColor: AppColors.slate50,
446
447
  borderBottomWidth: 1,
447
448
  borderBottomColor: AppColors.slate200,
448
- paddingHorizontal: 12,
449
- paddingVertical: 8,
449
+ paddingHorizontal: 8,
450
+ paddingVertical: 6,
451
+ gap: 8,
450
452
  },
451
453
  contentWrapper: {
452
454
  backgroundColor: AppColors.white,
@@ -2596,4 +2596,60 @@ export const FilmIcon = ({
2596
2596
  </Svg>
2597
2597
  );
2598
2598
 
2599
+ export const ScreencastIcon = ({
2600
+ color = AppColors.white,
2601
+ size = 18,
2602
+ }: IconProps) => (
2603
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
2604
+ <Path
2605
+ d="M2 16.1A5 5 0 0 1 5.9 20M2 12.05A9 9 0 0 1 9.95 20M2 20h.01"
2606
+ stroke={color}
2607
+ strokeWidth="2"
2608
+ strokeLinecap="round"
2609
+ strokeLinejoin="round"
2610
+ />
2611
+ <Path
2612
+ d="M2 8V6a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2h-6"
2613
+ stroke={color}
2614
+ strokeWidth="2"
2615
+ strokeLinecap="round"
2616
+ strokeLinejoin="round"
2617
+ />
2618
+ </Svg>
2619
+ );
2620
+
2621
+ export const ListIcon = ({color = AppColors.white, size = 14}: IconProps) => (
2622
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
2623
+ <Line x1="8" y1="6" x2="21" y2="6" stroke={color} strokeWidth="2" strokeLinecap="round" />
2624
+ <Line x1="8" y1="12" x2="21" y2="12" stroke={color} strokeWidth="2" strokeLinecap="round" />
2625
+ <Line x1="8" y1="18" x2="21" y2="18" stroke={color} strokeWidth="2" strokeLinecap="round" />
2626
+ <Circle cx="4" cy="6" r="1.5" fill={color} />
2627
+ <Circle cx="4" cy="12" r="1.5" fill={color} />
2628
+ <Circle cx="4" cy="18" r="1.5" fill={color} />
2629
+ </Svg>
2630
+ );
2631
+
2632
+ export const GridIcon = ({color = AppColors.white, size = 14}: IconProps) => (
2633
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
2634
+ <Rect x="3" y="3" width="7" height="7" rx="1.5" stroke={color} strokeWidth="2" />
2635
+ <Rect x="14" y="3" width="7" height="7" rx="1.5" stroke={color} strokeWidth="2" />
2636
+ <Rect x="14" y="14" width="7" height="7" rx="1.5" stroke={color} strokeWidth="2" />
2637
+ <Rect x="3" y="14" width="7" height="7" rx="1.5" stroke={color} strokeWidth="2" />
2638
+ </Svg>
2639
+ );
2640
+
2641
+ export const MinimizeWindowIcon = ({color = AppColors.white, size = 14}: IconProps) => (
2642
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
2643
+ <Path
2644
+ d="M4 14h6v6M20 10h-6V4M14 10l7-7M10 14l-7 7"
2645
+ stroke={color}
2646
+ strokeWidth="2"
2647
+ strokeLinecap="round"
2648
+ strokeLinejoin="round"
2649
+ />
2650
+ </Svg>
2651
+ );
2652
+
2653
+
2654
+
2599
2655
 
@@ -27,6 +27,7 @@ const SegmentedTabs = React.memo(({tabs, activeKey, onChange, style}: SegmentedT
27
27
  borderWidth: 1,
28
28
  borderColor: AppColors.dividerColor,
29
29
  gap: 3,
30
+ minWidth: 0,
30
31
  },
31
32
  style,
32
33
  ]}>
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED FILE — do not edit by hand.
2
2
  // Regenerated from package.json on every build by scripts/gen-version.js.
3
- export const LIB_VERSION = '2.3.18';
3
+ export const LIB_VERSION = '2.3.19';
@@ -16,6 +16,8 @@
16
16
  "viewOnNpm": "View on NPM",
17
17
  "later": "Later",
18
18
  "source": "SOURCE",
19
+ "minimize": "Minimize",
20
+ "tapToExpand": "Tap to expand",
19
21
  "open": "Open",
20
22
  "openInBrowser": "Open in Browser",
21
23
  "openInBrowserPrompt": "Are you sure you want to open this URL in your external browser?"
@@ -54,7 +56,7 @@
54
56
  "crash": "Crash",
55
57
  "device": "Device",
56
58
  "storage": "Storage",
57
- "media": "Media"
59
+ "media": "Screencast"
58
60
  },
59
61
 
60
62
  "settings": {
@@ -203,7 +205,6 @@
203
205
  "purgedSuccess": "Media storage purged"
204
206
  },
205
207
  "general": {
206
-
207
208
  "darkMode": "Dark Mode Theme",
208
209
  "darkModeDescription": "Toggle sleek dark aesthetic or light contrast",
209
210
  "modalHeight": "Inspector Window Height",
@@ -1111,15 +1112,30 @@
1111
1112
  "allPurged": "All captured media purged",
1112
1113
  "converting": "Converting...",
1113
1114
  "convertToGif": "Convert to GIF",
1115
+ "playVideo": "Play Video",
1116
+ "playError": "Unable to play video",
1114
1117
  "convertedSuccess": "Converted to Animated GIF successfully!",
1115
1118
  "convertFailed": "Failed to convert to GIF",
1116
1119
  "share": "Share",
1117
1120
  "copyUri": "Copy URI",
1118
1121
  "uriCopied": "File URI copied to clipboard",
1119
1122
  "delete": "Delete",
1123
+ "deleteSelected": "Delete ({{count}})",
1124
+ "deleteSelectedTitle": "Delete Selected",
1125
+ "deleteSelectedMessage": "Are you sure you want to delete {{count}} selected media items?",
1126
+ "deletedCount": "Deleted {{count}} media items",
1120
1127
  "deleteTitle": "Delete Media",
1121
1128
  "deleteMessage": "Are you sure you want to delete {{filename}}?",
1122
- "shareUnavailable": "Sharing not available on this device"
1129
+ "shareUnavailable": "Sharing not available on this device",
1130
+ "showingResults": "Showing {{count}} results",
1131
+ "showingFilteredResults": "Showing {{count}} of {{total}} results",
1132
+ "selectedOfResults": "Selected {{selected}} of {{total}} results",
1133
+ "minimize": "Minimize",
1134
+ "expand": "Expand Fullscreen",
1135
+ "collapse": "Exit Fullscreen",
1136
+ "speed": "Speed",
1137
+ "loop": "Loop",
1138
+ "pause": "Pause",
1139
+ "play": "Play"
1123
1140
  }
1124
1141
  }
1125
-
package/src/index.tsx CHANGED
@@ -2,6 +2,7 @@ import React, {useEffect, useMemo, useRef, useState, useCallback} from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Animated,
5
+ Easing,
5
6
  PanResponder,
6
7
  Platform,
7
8
  UIManager,
@@ -130,6 +131,7 @@ import {
130
131
  fetchRemoteConfigModuleStatus,
131
132
  isFirebaseRemoteConfigAvailable,
132
133
  } from './helpers/remoteConfig';
134
+ import {ScreenCapture} from './capture';
133
135
 
134
136
  // Constants
135
137
  import {
@@ -184,6 +186,12 @@ const NetworkInspector = ({
184
186
  const [visible, setVisible] = useState<boolean>(
185
187
  controlledVisible ?? initialVisible ?? false,
186
188
  );
189
+ const [isMinimized, setIsMinimized] = useState<boolean>(false);
190
+
191
+ const minimizeInspector = useCallback(() => {
192
+ setIsMinimized(true);
193
+ setVisible(false);
194
+ }, []);
187
195
 
188
196
  useEffect(() => {
189
197
  if (controlledVisible !== undefined) {
@@ -267,6 +275,25 @@ const NetworkInspector = ({
267
275
  const [lastReadLogsCount, setLastReadLogsCount] = useState(0);
268
276
  const [lastReadApisCount, setLastReadApisCount] = useState(0);
269
277
  const [lastReadCrashesCount, setLastReadCrashesCount] = useState(0);
278
+ const [mediaCount, setMediaCount] = useState(0);
279
+
280
+ const refreshMediaCount = useCallback(async () => {
281
+ try {
282
+ const items = await ScreenCapture.getMediaList();
283
+ const count = items ? items.length : 0;
284
+ setMediaCount(count);
285
+ return count;
286
+ } catch {
287
+ setMediaCount(0);
288
+ return 0;
289
+ }
290
+ }, []);
291
+
292
+ useEffect(() => {
293
+ if (visible) {
294
+ refreshMediaCount();
295
+ }
296
+ }, [visible, refreshMediaCount]);
270
297
 
271
298
  // ─── Crash state ───────────────────────────────────────────────────────────
272
299
  const [crashRecords, setCrashRecords] = useState<CrashRecord[]>(() =>
@@ -375,6 +402,7 @@ const NetworkInspector = ({
375
402
  device: false,
376
403
  storage: false,
377
404
  debugging: false,
405
+ media: true,
378
406
  });
379
407
 
380
408
  const [maxNetworkLogs, setMaxNetworkLogs] = useState<number>(250);
@@ -438,6 +466,7 @@ const NetworkInspector = ({
438
466
  device: false,
439
467
  storage: false,
440
468
  debugging: false,
469
+ media: true,
441
470
  });
442
471
 
443
472
  setDefaultTab('apis');
@@ -930,10 +959,11 @@ const NetworkInspector = ({
930
959
  Animated.sequence([
931
960
  Animated.timing(fabShineAnim, {
932
961
  toValue: 1,
933
- duration: 1100,
962
+ duration: 3200,
963
+ easing: Easing.inOut(Easing.ease),
934
964
  useNativeDriver: false,
935
965
  }),
936
- Animated.delay(1600),
966
+ Animated.delay(3500),
937
967
  Animated.timing(fabShineAnim, {
938
968
  toValue: 0,
939
969
  duration: 0,
@@ -989,36 +1019,12 @@ const NetworkInspector = ({
989
1019
 
990
1020
 
991
1021
 
992
- // 100% Native Main-Thread Floating Button Lifecycle
1022
+ // Always hide legacy native circular button in favor of the React Native music-player dock
993
1023
  useEffect(() => {
994
- if (!useNativeFab || !enabled) {
995
- if (useNativeFab) {
996
- hideNativeFloatingButton().catch(() => {});
997
- }
998
- return;
999
- }
1000
-
1001
- if (visible) {
1024
+ if (useNativeFab) {
1002
1025
  hideNativeFloatingButton().catch(() => {});
1003
- } else {
1004
- showNativeFloatingButton().catch(() => {});
1005
- setNativeFloatingButtonBadge(
1006
- logs.length > 0 || analyticsEvents.length > 0,
1007
- ).catch(() => {});
1008
1026
  }
1009
-
1010
- return () => {
1011
- if (useNativeFab) {
1012
- hideNativeFloatingButton().catch(() => {});
1013
- }
1014
- };
1015
- }, [
1016
- useNativeFab,
1017
- enabled,
1018
- visible,
1019
- logs.length,
1020
- analyticsEvents.length,
1021
- ]);
1027
+ }, [useNativeFab, visible]);
1022
1028
 
1023
1029
  // Subscribe to native UI-thread floating button tap events
1024
1030
  useEffect(() => {
@@ -1874,8 +1880,9 @@ const NetworkInspector = ({
1874
1880
  };
1875
1881
  }, [visibleConsoleLogs, logSearch]);
1876
1882
 
1877
- function closeModal() {
1883
+ const closeModal = useCallback(() => {
1878
1884
  setVisible(false);
1885
+ setIsMinimized(false);
1879
1886
  setTimeout(() => {
1880
1887
  setSelected(null);
1881
1888
  setSelectedEvent(null);
@@ -1884,7 +1891,7 @@ const NetworkInspector = ({
1884
1891
  setSelectedReduxAction(null);
1885
1892
  setSelectedCrash(null);
1886
1893
  }, 300);
1887
- }
1894
+ }, []);
1888
1895
 
1889
1896
  function handleClearAll() {
1890
1897
  clearNetworkLogs();
@@ -2049,6 +2056,9 @@ const NetworkInspector = ({
2049
2056
  visible,
2050
2057
  setVisible,
2051
2058
  closeModal,
2059
+ isMinimized,
2060
+ setIsMinimized,
2061
+ minimizeInspector,
2052
2062
  isReady,
2053
2063
  enabled,
2054
2064
  isEnabled: enabled,
@@ -2068,6 +2078,8 @@ const NetworkInspector = ({
2068
2078
  toggleTabVisibility,
2069
2079
  lastReadApisCount,
2070
2080
  lastReadLogsCount,
2081
+ mediaCount,
2082
+ refreshMediaCount,
2071
2083
 
2072
2084
  // ─── Selection / header state ───────────────────────────────────────
2073
2085
  selected,
@@ -2252,6 +2264,8 @@ const NetworkInspector = ({
2252
2264
  toggleTabVisibility,
2253
2265
  lastReadApisCount,
2254
2266
  lastReadLogsCount,
2267
+ mediaCount,
2268
+ refreshMediaCount,
2255
2269
  selected,
2256
2270
  selectedEvent,
2257
2271
  selectedLog,
@@ -2342,6 +2356,7 @@ const NetworkInspector = ({
2342
2356
  deviceFreeRamMb,
2343
2357
  reduxAutoRefresh,
2344
2358
  reduxExpandDepth,
2359
+ isMinimized,
2345
2360
  ],
2346
2361
  );
2347
2362
 
@@ -468,6 +468,7 @@ export interface RecordingOptions {
468
468
 
469
469
  export interface RecordingResult {
470
470
  uri: string;
471
+ thumbnailUri?: string;
471
472
  format: 'mp4' | 'gif';
472
473
  durationMs: number;
473
474
  hasAudio: boolean;
@@ -488,6 +489,7 @@ export interface CapturedMediaItem {
488
489
  type: 'image' | 'video' | 'gif';
489
490
  format: string;
490
491
  uri: string;
492
+ thumbnailUri?: string;
491
493
  filename: string;
492
494
  sizeBytes: number;
493
495
  timestamp: number;
@@ -561,6 +563,21 @@ export const isNativeRecordingActive = async (): Promise<boolean> => {
561
563
  }
562
564
  };
563
565
 
566
+ /**
567
+ * Plays a recorded video file using the native system media player.
568
+ */
569
+ export const playNativeVideo = async (videoUri: string): Promise<boolean> => {
570
+ if (!NativeModule || !NativeModule.playVideo) {
571
+ return false;
572
+ }
573
+ try {
574
+ const result = await NativeModule.playVideo(videoUri);
575
+ return !!result;
576
+ } catch {
577
+ return false;
578
+ }
579
+ };
580
+
564
581
  /**
565
582
  * Converts a recorded MP4 video to an animated GIF.
566
583
  */
@@ -22,6 +22,7 @@ export interface Spec extends TurboModule {
22
22
  stopVideoRecording(): Promise<Object>;
23
23
  isRecording(): Promise<boolean>;
24
24
  convertToGif(videoUri: string, options: Object): Promise<Object>;
25
+ playVideo(videoUri: string): Promise<boolean>;
25
26
  getCapturedMedia(): Promise<string>;
26
27
  deleteCapturedMedia(uri: string): Promise<boolean>;
27
28
  clearAllCapturedMedia(): Promise<boolean>;