react-native-inapp-inspector 2.2.9 → 2.3.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/dist/commonjs/components/AppHeaderLogo.js +45 -12
- package/dist/commonjs/components/BrandSquareIcon.js +92 -49
- package/dist/commonjs/components/EmptyState.d.ts +7 -2
- package/dist/commonjs/components/EmptyState.js +11 -5
- package/dist/commonjs/components/Inspector/AnalyticsTab.js +29 -18
- package/dist/commonjs/components/Inspector/ConsoleTab.js +6 -1
- package/dist/commonjs/components/Inspector/CrashDetail.js +43 -9
- package/dist/commonjs/components/Inspector/CrashTab.js +34 -3
- package/dist/commonjs/components/Inspector/DeviceInfoTab.js +80 -16
- package/dist/commonjs/components/Inspector/InspectorHeader.js +16 -3
- package/dist/commonjs/components/Inspector/MainScreen.js +7 -11
- package/dist/commonjs/components/Inspector/NetworkDetail.js +3 -2
- package/dist/commonjs/components/Inspector/NetworkTab.js +103 -4
- package/dist/commonjs/components/Inspector/ReduxTab.js +19 -3
- package/dist/commonjs/components/Inspector/SettingsPanel.js +4 -4
- package/dist/commonjs/components/Inspector/StorageTab.js +123 -92
- package/dist/commonjs/components/JsonViewer.js +2 -0
- package/dist/commonjs/components/MetaAccordion.js +15 -3
- package/dist/commonjs/components/SkeletonPlaceholder.d.ts +6 -0
- package/dist/commonjs/components/SkeletonPlaceholder.js +208 -0
- package/dist/commonjs/constants/version.d.ts +1 -1
- package/dist/commonjs/constants/version.js +1 -1
- package/dist/commonjs/customHooks/networkLogger.d.ts +3 -0
- package/dist/commonjs/customHooks/networkLogger.js +10 -1
- package/dist/commonjs/customHooks/storageInspector.d.ts +2 -2
- package/dist/commonjs/customHooks/storageInspector.js +17 -13
- package/dist/commonjs/helpers/index.d.ts +6 -0
- package/dist/commonjs/helpers/index.js +196 -6
- package/dist/commonjs/helpers/searchQueryParser.js +62 -20
- package/dist/commonjs/i18n/index.d.ts +13 -0
- package/dist/commonjs/i18n/index.js +35 -2
- package/dist/commonjs/index.d.ts +3 -0
- package/dist/commonjs/index.js +108 -35
- package/dist/commonjs/styles/AppColors.d.ts +4 -0
- package/dist/commonjs/styles/AppColors.js +11 -1
- package/dist/commonjs/styles/AppFonts.d.ts +11 -0
- package/dist/commonjs/styles/AppFonts.js +11 -1
- package/dist/commonjs/types/interfaces.d.ts +2 -2
- package/dist/esm/components/AppHeaderLogo.js +45 -12
- package/dist/esm/components/BrandSquareIcon.js +92 -49
- package/dist/esm/components/EmptyState.d.ts +7 -2
- package/dist/esm/components/EmptyState.js +11 -5
- package/dist/esm/components/Inspector/AnalyticsTab.js +30 -19
- package/dist/esm/components/Inspector/ConsoleTab.js +6 -1
- package/dist/esm/components/Inspector/CrashDetail.js +43 -9
- package/dist/esm/components/Inspector/CrashTab.js +34 -3
- package/dist/esm/components/Inspector/DeviceInfoTab.js +80 -16
- package/dist/esm/components/Inspector/InspectorHeader.js +17 -4
- package/dist/esm/components/Inspector/MainScreen.js +8 -12
- package/dist/esm/components/Inspector/NetworkDetail.js +3 -2
- package/dist/esm/components/Inspector/NetworkTab.js +103 -4
- package/dist/esm/components/Inspector/ReduxTab.js +19 -3
- package/dist/esm/components/Inspector/SettingsPanel.js +4 -4
- package/dist/esm/components/Inspector/StorageTab.js +124 -93
- package/dist/esm/components/JsonViewer.js +2 -0
- package/dist/esm/components/MetaAccordion.js +15 -3
- package/dist/esm/components/SkeletonPlaceholder.d.ts +6 -0
- package/dist/esm/components/SkeletonPlaceholder.js +173 -0
- package/dist/esm/constants/version.d.ts +1 -1
- package/dist/esm/constants/version.js +1 -1
- package/dist/esm/customHooks/networkLogger.d.ts +3 -0
- package/dist/esm/customHooks/networkLogger.js +8 -0
- package/dist/esm/customHooks/storageInspector.d.ts +2 -2
- package/dist/esm/customHooks/storageInspector.js +17 -13
- package/dist/esm/helpers/index.d.ts +6 -0
- package/dist/esm/helpers/index.js +193 -5
- package/dist/esm/helpers/searchQueryParser.js +62 -20
- package/dist/esm/i18n/index.d.ts +13 -0
- package/dist/esm/i18n/index.js +33 -3
- package/dist/esm/index.d.ts +3 -0
- package/dist/esm/index.js +97 -37
- package/dist/esm/styles/AppColors.d.ts +4 -0
- package/dist/esm/styles/AppColors.js +9 -0
- package/dist/esm/styles/AppFonts.d.ts +11 -0
- package/dist/esm/styles/AppFonts.js +9 -0
- package/dist/esm/types/interfaces.d.ts +2 -2
- package/package.json +1 -1
- package/src/components/AppHeaderLogo.tsx +46 -12
- package/src/components/BrandSquareIcon.tsx +127 -78
- package/src/components/EmptyState.tsx +27 -5
- package/src/components/Inspector/AnalyticsTab.tsx +48 -27
- package/src/components/Inspector/ConsoleTab.tsx +10 -0
- package/src/components/Inspector/CrashDetail.tsx +65 -9
- package/src/components/Inspector/CrashTab.tsx +41 -3
- package/src/components/Inspector/DeviceInfoTab.tsx +87 -16
- package/src/components/Inspector/InspectorHeader.tsx +24 -4
- package/src/components/Inspector/MainScreen.tsx +7 -11
- package/src/components/Inspector/NetworkDetail.tsx +4 -4
- package/src/components/Inspector/NetworkTab.tsx +129 -4
- package/src/components/Inspector/ReduxTab.tsx +26 -8
- package/src/components/Inspector/SettingsPanel.tsx +4 -4
- package/src/components/Inspector/StorageTab.tsx +191 -117
- package/src/components/JsonViewer.tsx +2 -0
- package/src/components/MetaAccordion.tsx +18 -3
- package/src/components/SkeletonPlaceholder.tsx +206 -0
- package/src/constants/version.ts +1 -1
- package/src/customHooks/networkLogger.ts +15 -0
- package/src/customHooks/storageInspector.ts +18 -14
- package/src/helpers/index.ts +213 -5
- package/src/helpers/searchQueryParser.ts +63 -20
- package/src/i18n/index.ts +38 -3
- package/src/index.tsx +124 -37
- package/src/styles/AppColors.ts +9 -0
- package/src/styles/AppFonts.ts +16 -0
- package/src/types/interfaces.ts +2 -2
|
@@ -9,12 +9,27 @@ import { AppColors } from '../../styles/AppColors';
|
|
|
9
9
|
import { AppFonts } from '../../styles/AppFonts';
|
|
10
10
|
import { METHOD_COLORS } from '../../constants';
|
|
11
11
|
import { LIB_VERSION } from '../../constants';
|
|
12
|
-
import { getStatusColor, getAppName, formatTime, getSize } from '../../helpers';
|
|
12
|
+
import { getStatusColor, getAppName, formatTime, getSize, getAppVersionAndBuild, } from '../../helpers';
|
|
13
|
+
import { getNativeDeviceMetrics } from '../../native/NativeInspector';
|
|
13
14
|
import { UpdateAvailableModal } from './UpdateAvailableModal';
|
|
14
15
|
import { WhiteBackNavigation, TrashIcon, SettingsIcon, CloseWhite, ClockIcon, SizeIcon, AppleIcon, AndroidIcon, NpmIcon, ResetIcon, BoltIcon, } from '../NetworkIcons';
|
|
15
16
|
const InspectorHeader = React.memo(() => {
|
|
16
17
|
const { modalHeightPercent, appIcon, selected, setSelected, selectedEvent, setSelectedEvent, selectedLog, setSelectedLog, selectedReduxSlice, setSelectedReduxSlice, selectedReduxAction, setSelectedReduxAction, reduxState, reduxLastActionMap, showHeaderInfo, setShowHeaderInfo, updateAvailable, latestNpmVersion, clearAnim, activePulseAnim, unreadPulseAnim, runClearAllWithAnimation, settingsPage, setSettingsPage, resetToDefaults, closeModal, detailTitle, activeTab, environment, visible, selectedCrash, setSelectedCrash, } = useInspector();
|
|
17
18
|
const [showUpdateModal, setShowUpdateModal] = React.useState(false);
|
|
19
|
+
const [appVersionString, setAppVersionString] = React.useState(() => {
|
|
20
|
+
return getAppVersionAndBuild().formatted;
|
|
21
|
+
});
|
|
22
|
+
React.useEffect(() => {
|
|
23
|
+
getNativeDeviceMetrics()
|
|
24
|
+
.then(metrics => {
|
|
25
|
+
if (metrics?.appVersion) {
|
|
26
|
+
const v = metrics.appVersion;
|
|
27
|
+
const b = metrics.appBuild || '1';
|
|
28
|
+
setAppVersionString(`${v} (${b})`);
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
.catch(() => { });
|
|
32
|
+
}, []);
|
|
18
33
|
const envConfig = useMemo(() => {
|
|
19
34
|
const rawEnv = (environment || (__DEV__ ? 'DEV' : 'PROD')).trim();
|
|
20
35
|
const clean = rawEnv.toUpperCase();
|
|
@@ -265,9 +280,7 @@ const InspectorHeader = React.memo(() => {
|
|
|
265
280
|
color: `${AppColors.white}EB`,
|
|
266
281
|
letterSpacing: 0.1,
|
|
267
282
|
}} numberOfLines={1}>
|
|
268
|
-
{
|
|
269
|
-
? `iOS ${Platform.Version}`
|
|
270
|
-
: `Android ${Platform.Version}`}
|
|
283
|
+
{appVersionString}
|
|
271
284
|
</Text>
|
|
272
285
|
</View>
|
|
273
286
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Animated, Modal, Platform, Pressable, StatusBar, StyleSheet, View, } from 'react-native';
|
|
3
3
|
import { useInspector } from './InspectorContext';
|
|
4
4
|
import ErrorBoundary from '../ErrorBoundary';
|
|
5
5
|
import FabLauncher from './FabLauncher';
|
|
@@ -11,6 +11,7 @@ import LogDetail from './LogDetail';
|
|
|
11
11
|
import ConsoleTab from './ConsoleTab';
|
|
12
12
|
import AnalyticsTab from './AnalyticsTab';
|
|
13
13
|
import AnalyticsDetail from '../AnalyticsDetail';
|
|
14
|
+
import SkeletonPlaceholder from '../SkeletonPlaceholder';
|
|
14
15
|
import ReduxTab from './ReduxTab';
|
|
15
16
|
import ReduxDetail from './ReduxDetail';
|
|
16
17
|
import BundleTab from './BundleTab';
|
|
@@ -26,7 +27,7 @@ import styles from '../../styles';
|
|
|
26
27
|
import { AppColors } from '../../styles/AppColors';
|
|
27
28
|
import NavigationTracker from './NavigationTracker';
|
|
28
29
|
const MainScreen = () => {
|
|
29
|
-
const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady,
|
|
30
|
+
const { visible, modalAnimationType, closeModal, modalHeightPercent, selected, selectedEvent, selectedLog, selectedReduxSlice, selectedReduxAction, selectedCrash, settingsPage, activeTab, isReady, enabled, useNativeFab, hasNavigationContext, setNavState, } = useInspector();
|
|
30
31
|
const isDetailActive = (activeTab === 'apis' && selected != null) ||
|
|
31
32
|
(activeTab === 'analytics' && selectedEvent != null) ||
|
|
32
33
|
(activeTab === 'logs' && selectedLog != null) ||
|
|
@@ -68,7 +69,7 @@ const MainScreen = () => {
|
|
|
68
69
|
}, [activeTab]);
|
|
69
70
|
return (<>
|
|
70
71
|
{(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
71
|
-
|
|
72
|
+
enabled &&
|
|
72
73
|
!visible &&
|
|
73
74
|
!useNativeFab && <FabLauncher />}
|
|
74
75
|
<Modal visible={visible} animationType={modalAnimationType} transparent statusBarTranslucent={true}>
|
|
@@ -88,10 +89,10 @@ const MainScreen = () => {
|
|
|
88
89
|
<InspectorHeader />
|
|
89
90
|
|
|
90
91
|
<View style={{ flex: 1 }}>
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
{!isDetailActive && <TabBar />}
|
|
92
|
+
{/* ─── Horizontal Scrollable Tab Bar inside Content (Always visible) ─── */}
|
|
93
|
+
{!isDetailActive && <TabBar />}
|
|
94
94
|
|
|
95
|
+
{isReady ? (<View style={{ flex: 1 }}>
|
|
95
96
|
{/* Persistent List Layer - Never unmounted, preserves 100% native scroll with smooth tab transition */}
|
|
96
97
|
<Animated.View style={[
|
|
97
98
|
{
|
|
@@ -143,12 +144,7 @@ const MainScreen = () => {
|
|
|
143
144
|
{activeTab === 'redux' && <ReduxDetail />}
|
|
144
145
|
{activeTab === 'crash' && selectedCrash != null && (<CrashDetail />)}
|
|
145
146
|
</Animated.View>)}
|
|
146
|
-
</View>) : (<
|
|
147
|
-
<ActivityIndicator size="large" color={AppColors.purple}/>
|
|
148
|
-
<Text style={[styles.emptySub, { marginTop: 12 }]}>
|
|
149
|
-
Loading logs...
|
|
150
|
-
</Text>
|
|
151
|
-
</View>)}
|
|
147
|
+
</View>) : (<SkeletonPlaceholder />)}
|
|
152
148
|
|
|
153
149
|
{/* Settings Panel Layer - Rendered on top with smooth slide & spring transition */}
|
|
154
150
|
{settingsPage !== null && (<Animated.View style={[
|
|
@@ -396,8 +396,9 @@ const NetworkDetail = React.memo(() => {
|
|
|
396
396
|
selected.responseHeaders?.['Content-Type']} url={selected.url}/>
|
|
397
397
|
|
|
398
398
|
{(() => {
|
|
399
|
-
const routeInfo = logRouteMapRef.current.get(selected.id)
|
|
400
|
-
|
|
399
|
+
const routeInfo = logRouteMapRef.current.get(selected.id) ||
|
|
400
|
+
selected?.routeInfo;
|
|
401
|
+
if (!routeInfo || !routeInfo.path || routeInfo.path === 'Navigators')
|
|
401
402
|
return null;
|
|
402
403
|
return <SourcePageCard routeInfo={routeInfo}/>;
|
|
403
404
|
})()}
|
|
@@ -469,7 +469,7 @@ const NetworkTab = React.memo(() => {
|
|
|
469
469
|
<View style={{
|
|
470
470
|
backgroundColor: `${AppColors.purple}20`,
|
|
471
471
|
borderRadius: 10,
|
|
472
|
-
paddingHorizontal:
|
|
472
|
+
paddingHorizontal: 6,
|
|
473
473
|
paddingVertical: 1.5,
|
|
474
474
|
}}>
|
|
475
475
|
<Text style={{
|
|
@@ -522,14 +522,104 @@ const NetworkTab = React.memo(() => {
|
|
|
522
522
|
styles.toolbarBtn,
|
|
523
523
|
filtersAccordion.isOpen &&
|
|
524
524
|
styles.toolbarBtnActive,
|
|
525
|
-
]} onPress={filtersAccordion.toggleOpen} hitSlop={
|
|
525
|
+
]} onPress={filtersAccordion.toggleOpen} hitSlop={6}>
|
|
526
526
|
<FilterIcon color={filtersAccordion.isOpen
|
|
527
527
|
? AppColors.purple
|
|
528
|
-
: AppColors.grayTextStrong} size={
|
|
528
|
+
: AppColors.grayTextStrong} size={16}/>
|
|
529
|
+
{(statusFilters.size > 0 ||
|
|
530
|
+
methodFilters.size > 0) && (<View style={styles.activeFilterDot}/>)}
|
|
529
531
|
</TouchableScale>
|
|
530
532
|
</View>
|
|
531
533
|
</View>
|
|
532
534
|
|
|
535
|
+
{/* Quick Search Scope & Filter Tags Row */}
|
|
536
|
+
<View style={{ paddingBottom: 4 }}>
|
|
537
|
+
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
|
|
538
|
+
paddingHorizontal: 12,
|
|
539
|
+
flexDirection: 'row',
|
|
540
|
+
alignItems: 'center',
|
|
541
|
+
gap: 5,
|
|
542
|
+
paddingVertical: 2,
|
|
543
|
+
}}>
|
|
544
|
+
{/* Search Scopes */}
|
|
545
|
+
{SEARCH_SCOPES.map(s => {
|
|
546
|
+
const isActive = searchScope === s.id;
|
|
547
|
+
return (<TouchableScale key={`scope_${s.id}`} onPress={() => setSearchScope(s.id)} hitSlop={6} style={{
|
|
548
|
+
flexDirection: 'row',
|
|
549
|
+
alignItems: 'center',
|
|
550
|
+
gap: 3.5,
|
|
551
|
+
paddingHorizontal: 7.5,
|
|
552
|
+
paddingVertical: 3.5,
|
|
553
|
+
borderRadius: 6,
|
|
554
|
+
backgroundColor: isActive
|
|
555
|
+
? AppColors.purple
|
|
556
|
+
: AppColors.grayBackground,
|
|
557
|
+
borderWidth: 1,
|
|
558
|
+
borderColor: isActive
|
|
559
|
+
? AppColors.purple
|
|
560
|
+
: AppColors.grayBorderSecondary,
|
|
561
|
+
}}>
|
|
562
|
+
{s.icon(isActive ? AppColors.white : AppColors.grayTextWeak)}
|
|
563
|
+
<Text style={{
|
|
564
|
+
fontFamily: AppFonts.interBold,
|
|
565
|
+
fontSize: 10,
|
|
566
|
+
color: isActive ? AppColors.white : AppColors.grayText,
|
|
567
|
+
}}>
|
|
568
|
+
{s.label}
|
|
569
|
+
</Text>
|
|
570
|
+
</TouchableScale>);
|
|
571
|
+
})}
|
|
572
|
+
|
|
573
|
+
<View style={{
|
|
574
|
+
width: 1,
|
|
575
|
+
height: 14,
|
|
576
|
+
backgroundColor: AppColors.dividerColor,
|
|
577
|
+
marginHorizontal: 3,
|
|
578
|
+
}}/>
|
|
579
|
+
|
|
580
|
+
{/* Quick Query Suggestions */}
|
|
581
|
+
{QUICK_API_QUERY_TAGS.map(q => {
|
|
582
|
+
const isActive = search.includes(q.query);
|
|
583
|
+
return (<TouchableScale key={`quick_${q.query}`} onPress={() => {
|
|
584
|
+
setSearch(prev => {
|
|
585
|
+
if (prev.includes(q.query)) {
|
|
586
|
+
return prev
|
|
587
|
+
.replace(q.query, '')
|
|
588
|
+
.replace(/\s+/g, ' ')
|
|
589
|
+
.trim();
|
|
590
|
+
}
|
|
591
|
+
return `${prev} ${q.query}`.trim();
|
|
592
|
+
});
|
|
593
|
+
}} hitSlop={6} style={{
|
|
594
|
+
flexDirection: 'row',
|
|
595
|
+
alignItems: 'center',
|
|
596
|
+
gap: 3.5,
|
|
597
|
+
paddingHorizontal: 7.5,
|
|
598
|
+
paddingVertical: 3.5,
|
|
599
|
+
borderRadius: 6,
|
|
600
|
+
backgroundColor: isActive
|
|
601
|
+
? `${AppColors.purple}20`
|
|
602
|
+
: AppColors.grayBackground,
|
|
603
|
+
borderWidth: 1,
|
|
604
|
+
borderColor: isActive
|
|
605
|
+
? AppColors.purple
|
|
606
|
+
: AppColors.grayBorderSecondary,
|
|
607
|
+
}}>
|
|
608
|
+
{q.icon(isActive ? AppColors.purple : AppColors.grayTextWeak)}
|
|
609
|
+
<Text style={{
|
|
610
|
+
fontFamily: AppFonts.interMedium,
|
|
611
|
+
fontSize: 10,
|
|
612
|
+
color: isActive
|
|
613
|
+
? AppColors.purple
|
|
614
|
+
: AppColors.grayText,
|
|
615
|
+
}}>
|
|
616
|
+
{q.label}
|
|
617
|
+
</Text>
|
|
618
|
+
</TouchableScale>);
|
|
619
|
+
})}
|
|
620
|
+
</ScrollView>
|
|
621
|
+
</View>
|
|
622
|
+
|
|
533
623
|
{/* Quick Filter Horizontal Chips Bar with Live Counts */}
|
|
534
624
|
<View style={{ marginBottom: 8 }}>
|
|
535
625
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{
|
|
@@ -815,7 +905,16 @@ const NetworkTab = React.memo(() => {
|
|
|
815
905
|
? `${logs.length} requests`
|
|
816
906
|
: `${filteredLogs.length} of ${logs.length} filtered requests`}
|
|
817
907
|
</Text>)}
|
|
818
|
-
</View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 ||
|
|
908
|
+
</View>} ListEmptyComponent={<EmptyState isSearch={search.length > 0 ||
|
|
909
|
+
statusFilters.size > 0 ||
|
|
910
|
+
methodFilters.size > 0} searchQuery={search} customTitle={search.length > 0
|
|
911
|
+
? 'No matching API requests'
|
|
912
|
+
: 'No network activity'} onClearSearch={() => {
|
|
913
|
+
setSearch('');
|
|
914
|
+
setStatusFilters(new Set());
|
|
915
|
+
setMethodFilters(new Set());
|
|
916
|
+
setSearchScope('all');
|
|
917
|
+
}}/>} ListFooterComponent={groupedData.length > 0 ? (<EndOfListFooter count={filteredLogs.length} label="requests"/>) : null} contentContainerStyle={[
|
|
819
918
|
styles.listContent,
|
|
820
919
|
filteredLogs.length === 0 && { flexGrow: 1 },
|
|
821
920
|
]} keyboardShouldPersistTaps="handled"/>
|
|
@@ -396,9 +396,25 @@ const ReduxTab = React.memo(() => {
|
|
|
396
396
|
<View style={styles.searchContainer}>
|
|
397
397
|
<SearchIcon color={AppColors.grayTextWeak} size={15}/>
|
|
398
398
|
<TextInput accessible={true} accessibilityRole="search" accessibilityLabel="Search Redux slices or actions" accessibilityHint="Type to filter state slices in real time" placeholder="Search Redux slices or actions..." placeholderTextColor={AppColors.grayTextWeak} value={reduxSearch} onChangeText={setReduxSearch} style={styles.searchInput} autoCorrect={false} autoCapitalize="none"/>
|
|
399
|
-
{reduxSearch.length > 0 && (<
|
|
400
|
-
<
|
|
401
|
-
|
|
399
|
+
{reduxSearch.length > 0 && (<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
400
|
+
<View style={{
|
|
401
|
+
backgroundColor: `${AppColors.purple}20`,
|
|
402
|
+
borderRadius: 10,
|
|
403
|
+
paddingHorizontal: 6,
|
|
404
|
+
paddingVertical: 1.5,
|
|
405
|
+
}}>
|
|
406
|
+
<Text style={{
|
|
407
|
+
color: AppColors.purple,
|
|
408
|
+
fontSize: 9.5,
|
|
409
|
+
fontFamily: AppFonts.interBold,
|
|
410
|
+
}}>
|
|
411
|
+
{filteredSlices.length}
|
|
412
|
+
</Text>
|
|
413
|
+
</View>
|
|
414
|
+
<Pressable accessible={true} accessibilityRole="button" accessibilityLabel="Clear search input" onPress={() => setReduxSearch('')} hitSlop={10}>
|
|
415
|
+
<ClearIcon color={AppColors.grayTextWeak} size={14}/>
|
|
416
|
+
</Pressable>
|
|
417
|
+
</View>)}
|
|
402
418
|
</View>
|
|
403
419
|
|
|
404
420
|
{/* Pause / Resume Live Updates Button */}
|
|
@@ -108,8 +108,8 @@ const SettingsPanel = () => {
|
|
|
108
108
|
bundle: Boolean(tabVisibility?.bundle),
|
|
109
109
|
performance: Boolean(tabVisibility?.performance),
|
|
110
110
|
crash: Boolean(tabVisibility?.crash),
|
|
111
|
-
device: Boolean(tabVisibility?.device
|
|
112
|
-
storage: Boolean(tabVisibility?.storage
|
|
111
|
+
device: Boolean(tabVisibility?.device),
|
|
112
|
+
storage: Boolean(tabVisibility?.storage),
|
|
113
113
|
}));
|
|
114
114
|
// Synchronize staged state with tabVisibility when tabVisibility updates
|
|
115
115
|
useEffect(() => {
|
|
@@ -121,8 +121,8 @@ const SettingsPanel = () => {
|
|
|
121
121
|
bundle: Boolean(tabVisibility?.bundle),
|
|
122
122
|
performance: Boolean(tabVisibility?.performance),
|
|
123
123
|
crash: Boolean(tabVisibility?.crash),
|
|
124
|
-
device: Boolean(tabVisibility?.device
|
|
125
|
-
storage: Boolean(tabVisibility?.storage
|
|
124
|
+
device: Boolean(tabVisibility?.device),
|
|
125
|
+
storage: Boolean(tabVisibility?.storage),
|
|
126
126
|
});
|
|
127
127
|
}, [tabVisibility]);
|
|
128
128
|
const hasUnsavedChanges = useMemo(() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useEffect, useMemo, useCallback } from 'react';
|
|
2
|
-
import { View, Text, ScrollView, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
|
|
2
|
+
import { View, Text, ScrollView, FlatList, TextInput, Modal, Alert, StyleSheet, ActivityIndicator, Platform, } from 'react-native';
|
|
3
3
|
import TouchableScale from '../TouchableScale';
|
|
4
4
|
import { AppColors } from '../../styles/AppColors';
|
|
5
5
|
import { AppFonts } from '../../styles/AppFonts';
|
|
@@ -8,6 +8,92 @@ import { DatabaseIcon, SearchIcon, ClearIcon, PlusIcon, PencilIcon, TrashIcon, C
|
|
|
8
8
|
import { fetchStorageEntries, setStorageEntry, removeStorageEntry, clearStorageDriver, isAsyncStorageConnected, isMMKVConnected, getRegisteredMMKVInstanceIds, subscribeToStorageChanges, } from '../../customHooks/storageInspector';
|
|
9
9
|
import { copyToClipboard } from '../../helpers';
|
|
10
10
|
import { showToast } from '../../helpers/toast';
|
|
11
|
+
const getTypeBadge = (type) => {
|
|
12
|
+
switch (type) {
|
|
13
|
+
case 'json':
|
|
14
|
+
return { label: 'JSON', color: AppColors.purple, bg: `${AppColors.purple}16` };
|
|
15
|
+
case 'number':
|
|
16
|
+
return { label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16` };
|
|
17
|
+
case 'boolean':
|
|
18
|
+
return { label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16` };
|
|
19
|
+
case 'null':
|
|
20
|
+
return { label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16` };
|
|
21
|
+
default:
|
|
22
|
+
return { label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16` };
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
const StorageEntryCard = React.memo(function StorageEntryCard({ entry, isExpanded, onToggleExpand, onCopy, onEdit, onDelete, badge, }) {
|
|
26
|
+
const formattedBytes = useMemo(() => {
|
|
27
|
+
return entry.byteSize < 1024
|
|
28
|
+
? `${entry.byteSize} B`
|
|
29
|
+
: `${(entry.byteSize / 1024).toFixed(1)} KB`;
|
|
30
|
+
}, [entry.byteSize]);
|
|
31
|
+
// Lazy compute displayed value to avoid layout thrashing on large JSON strings
|
|
32
|
+
const displayValue = useMemo(() => {
|
|
33
|
+
if (!isExpanded) {
|
|
34
|
+
if (entry.value.length > 250) {
|
|
35
|
+
return entry.value.slice(0, 250) + '...';
|
|
36
|
+
}
|
|
37
|
+
return entry.value;
|
|
38
|
+
}
|
|
39
|
+
if (entry.type === 'json') {
|
|
40
|
+
try {
|
|
41
|
+
const parsed = entry.parsedValue ?? JSON.parse(entry.value);
|
|
42
|
+
return JSON.stringify(parsed, null, 2);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return entry.value;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return entry.value;
|
|
49
|
+
}, [isExpanded, entry.value, entry.type, entry.parsedValue]);
|
|
50
|
+
return (<View style={styles.entryCard}>
|
|
51
|
+
{/* Entry Header: Key name, Type Badge, Size, Actions */}
|
|
52
|
+
<View style={styles.entryHeader}>
|
|
53
|
+
<View style={{ flex: 1, marginRight: 8 }}>
|
|
54
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
|
55
|
+
<Text style={styles.entryKey} numberOfLines={1} selectable>
|
|
56
|
+
{entry.key}
|
|
57
|
+
</Text>
|
|
58
|
+
<View style={[
|
|
59
|
+
styles.typeBadge,
|
|
60
|
+
{ backgroundColor: badge.bg, borderColor: `${badge.color}33` },
|
|
61
|
+
]}>
|
|
62
|
+
<Text style={[styles.typeBadgeText, { color: badge.color }]}>
|
|
63
|
+
{badge.label}
|
|
64
|
+
</Text>
|
|
65
|
+
</View>
|
|
66
|
+
<Text style={styles.sizeText}>{formattedBytes}</Text>
|
|
67
|
+
</View>
|
|
68
|
+
</View>
|
|
69
|
+
|
|
70
|
+
{/* Actions: Copy, Edit, Delete */}
|
|
71
|
+
<View style={styles.entryActions}>
|
|
72
|
+
<TouchableScale hitSlop={6} onPress={() => onCopy(entry)} style={styles.entryActionBtn}>
|
|
73
|
+
<CopyIcon size={12} color={AppColors.grayText}/>
|
|
74
|
+
</TouchableScale>
|
|
75
|
+
|
|
76
|
+
<TouchableScale hitSlop={6} onPress={() => onEdit(entry)} style={styles.entryActionBtn}>
|
|
77
|
+
<PencilIcon size={12} color={AppColors.purple}/>
|
|
78
|
+
</TouchableScale>
|
|
79
|
+
|
|
80
|
+
<TouchableScale hitSlop={6} onPress={() => onDelete(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
|
|
81
|
+
<TrashIcon size={12} color={AppColors.errorColor}/>
|
|
82
|
+
</TouchableScale>
|
|
83
|
+
</View>
|
|
84
|
+
</View>
|
|
85
|
+
|
|
86
|
+
{/* Entry Value Preview / Viewer */}
|
|
87
|
+
<TouchableScale onPress={() => onToggleExpand(entry.key)} style={styles.valuePreviewBox}>
|
|
88
|
+
<Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable={isExpanded}>
|
|
89
|
+
{displayValue}
|
|
90
|
+
</Text>
|
|
91
|
+
{entry.value.length > 80 && (<Text style={styles.expandHint}>
|
|
92
|
+
{isExpanded ? '▲ Collapse' : '▼ Expand'}
|
|
93
|
+
</Text>)}
|
|
94
|
+
</TouchableScale>
|
|
95
|
+
</View>);
|
|
96
|
+
});
|
|
11
97
|
export const StorageTab = React.memo(() => {
|
|
12
98
|
const { t } = useTranslation();
|
|
13
99
|
const [activeDriver, setActiveDriver] = useState('asyncStorage');
|
|
@@ -166,20 +252,11 @@ export const StorageTab = React.memo(() => {
|
|
|
166
252
|
return `${(totalBytes / 1024).toFixed(1)} KB`;
|
|
167
253
|
return `${(totalBytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
168
254
|
}, [totalBytes]);
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return { label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16` };
|
|
175
|
-
case 'boolean':
|
|
176
|
-
return { label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16` };
|
|
177
|
-
case 'null':
|
|
178
|
-
return { label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16` };
|
|
179
|
-
default:
|
|
180
|
-
return { label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16` };
|
|
181
|
-
}
|
|
182
|
-
};
|
|
255
|
+
const handleCopyEntry = useCallback((entry) => {
|
|
256
|
+
copyToClipboard(entry.value, entry.key);
|
|
257
|
+
showToast(`Copied value of "${entry.key}"`);
|
|
258
|
+
}, []);
|
|
259
|
+
const renderItem = useCallback(({ item }) => (<StorageEntryCard entry={item} isExpanded={Boolean(expandedKeys[item.key])} onToggleExpand={toggleExpand} onCopy={handleCopyEntry} onEdit={handleOpenEdit} onDelete={handleDeleteKey} badge={getTypeBadge(item.type)}/>), [expandedKeys, handleCopyEntry]);
|
|
183
260
|
const isConnected = activeDriver === 'asyncStorage'
|
|
184
261
|
? isAsyncStorageConnected()
|
|
185
262
|
: isMMKVConnected();
|
|
@@ -245,9 +322,25 @@ export const StorageTab = React.memo(() => {
|
|
|
245
322
|
<View style={styles.searchBar}>
|
|
246
323
|
<SearchIcon size={14} color={AppColors.grayTextWeak}/>
|
|
247
324
|
<TextInput value={search} onChangeText={setSearch} placeholder={`Search ${entries.length} keys & values...`} placeholderTextColor={AppColors.grayTextWeak} style={styles.searchInput} clearButtonMode="while-editing"/>
|
|
248
|
-
{search.length > 0 && (<
|
|
249
|
-
<
|
|
250
|
-
|
|
325
|
+
{search.length > 0 && (<View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
|
|
326
|
+
<View style={{
|
|
327
|
+
backgroundColor: `${AppColors.purple}20`,
|
|
328
|
+
borderRadius: 10,
|
|
329
|
+
paddingHorizontal: 6,
|
|
330
|
+
paddingVertical: 1.5,
|
|
331
|
+
}}>
|
|
332
|
+
<Text style={{
|
|
333
|
+
color: AppColors.purple,
|
|
334
|
+
fontSize: 9.5,
|
|
335
|
+
fontFamily: AppFonts.interBold,
|
|
336
|
+
}}>
|
|
337
|
+
{filteredEntries.length}
|
|
338
|
+
</Text>
|
|
339
|
+
</View>
|
|
340
|
+
<TouchableScale onPress={() => setSearch('')} hitSlop={8}>
|
|
341
|
+
<ClearIcon size={13} color={AppColors.grayTextWeak}/>
|
|
342
|
+
</TouchableScale>
|
|
343
|
+
</View>)}
|
|
251
344
|
</View>
|
|
252
345
|
|
|
253
346
|
{/* Action Buttons: Add Key & Refresh */}
|
|
@@ -279,81 +372,19 @@ export const StorageTab = React.memo(() => {
|
|
|
279
372
|
{isLoading ? (<View style={styles.loadingContainer}>
|
|
280
373
|
<ActivityIndicator size="small" color={AppColors.purple}/>
|
|
281
374
|
<Text style={styles.loadingText}>Loading storage entries...</Text>
|
|
282
|
-
</View>) : filteredEntries.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
{filteredEntries.map(entry => {
|
|
296
|
-
const isExpanded = Boolean(expandedKeys[entry.key]);
|
|
297
|
-
const badge = getTypeBadge(entry.type);
|
|
298
|
-
const formattedBytes = entry.byteSize < 1024
|
|
299
|
-
? `${entry.byteSize} B`
|
|
300
|
-
: `${(entry.byteSize / 1024).toFixed(1)} KB`;
|
|
301
|
-
return (<View key={entry.key} style={styles.entryCard}>
|
|
302
|
-
{/* Entry Header: Key name, Type Badge, Size, Actions */}
|
|
303
|
-
<View style={styles.entryHeader}>
|
|
304
|
-
<View style={{ flex: 1, marginRight: 8 }}>
|
|
305
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
|
306
|
-
<Text style={styles.entryKey} numberOfLines={1} selectable>
|
|
307
|
-
{entry.key}
|
|
308
|
-
</Text>
|
|
309
|
-
<View style={[
|
|
310
|
-
styles.typeBadge,
|
|
311
|
-
{ backgroundColor: badge.bg, borderColor: `${badge.color}33` },
|
|
312
|
-
]}>
|
|
313
|
-
<Text style={[styles.typeBadgeText, { color: badge.color }]}>
|
|
314
|
-
{badge.label}
|
|
315
|
-
</Text>
|
|
316
|
-
</View>
|
|
317
|
-
<Text style={styles.sizeText}>{formattedBytes}</Text>
|
|
318
|
-
</View>
|
|
319
|
-
</View>
|
|
320
|
-
|
|
321
|
-
{/* Actions: Copy, Edit, Delete */}
|
|
322
|
-
<View style={styles.entryActions}>
|
|
323
|
-
<TouchableScale hitSlop={6} onPress={() => {
|
|
324
|
-
copyToClipboard(entry.value, entry.key);
|
|
325
|
-
showToast(`Copied value of "${entry.key}"`);
|
|
326
|
-
}} style={styles.entryActionBtn}>
|
|
327
|
-
<CopyIcon size={12} color={AppColors.grayText}/>
|
|
328
|
-
</TouchableScale>
|
|
329
|
-
|
|
330
|
-
<TouchableScale hitSlop={6} onPress={() => handleOpenEdit(entry)} style={styles.entryActionBtn}>
|
|
331
|
-
<PencilIcon size={12} color={AppColors.purple}/>
|
|
332
|
-
</TouchableScale>
|
|
333
|
-
|
|
334
|
-
<TouchableScale hitSlop={6} onPress={() => handleDeleteKey(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
|
|
335
|
-
<TrashIcon size={12} color={AppColors.errorColor}/>
|
|
336
|
-
</TouchableScale>
|
|
337
|
-
</View>
|
|
338
|
-
</View>
|
|
339
|
-
|
|
340
|
-
{/* Entry Value Preview / Viewer */}
|
|
341
|
-
<TouchableScale onPress={() => toggleExpand(entry.key)} style={styles.valuePreviewBox}>
|
|
342
|
-
<Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable>
|
|
343
|
-
{entry.type === 'json' && entry.parsedValue
|
|
344
|
-
? isExpanded
|
|
345
|
-
? JSON.stringify(entry.parsedValue, null, 2)
|
|
346
|
-
: entry.value
|
|
347
|
-
: entry.value}
|
|
348
|
-
</Text>
|
|
349
|
-
{entry.value.length > 80 && (<Text style={styles.expandHint}>
|
|
350
|
-
{isExpanded ? '▲ Collapse' : '▼ Expand'}
|
|
351
|
-
</Text>)}
|
|
352
|
-
</TouchableScale>
|
|
353
|
-
</View>);
|
|
354
|
-
})}
|
|
355
|
-
<View style={{ height: 60 }}/>
|
|
356
|
-
</ScrollView>)}
|
|
375
|
+
</View>) : (<FlatList data={filteredEntries} keyExtractor={item => item.key} renderItem={renderItem} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={5} removeClippedSubviews={Platform.OS === 'android'} style={styles.scrollArea} contentContainerStyle={styles.scrollContent} showsVerticalScrollIndicator={false} ListEmptyComponent={<View style={styles.emptyContainer}>
|
|
376
|
+
<View style={styles.emptyIconWrap}>
|
|
377
|
+
<DatabaseIcon size={28} color={AppColors.grayTextWeak}/>
|
|
378
|
+
</View>
|
|
379
|
+
<Text style={styles.emptyTitle}>
|
|
380
|
+
{search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
|
|
381
|
+
</Text>
|
|
382
|
+
<Text style={styles.emptySubtitle}>
|
|
383
|
+
{search
|
|
384
|
+
? 'Try modifying your search query'
|
|
385
|
+
: `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
|
|
386
|
+
</Text>
|
|
387
|
+
</View>} ListFooterComponent={<View style={{ height: 60 }}/>}/>)}
|
|
357
388
|
|
|
358
389
|
{/* ── Create / Edit Key Modal ── */}
|
|
359
390
|
<Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
|
|
@@ -5,6 +5,7 @@ import { useTranslation } from '../i18n';
|
|
|
5
5
|
import TreeNode from './TreeNode';
|
|
6
6
|
import SegmentedTabs from './SegmentedTabs';
|
|
7
7
|
import HighlightText from './HighlightText';
|
|
8
|
+
import CopyButton from './CopyButton';
|
|
8
9
|
import { PrettyIcon, RawIcon, TableIcon } from './NetworkIcons';
|
|
9
10
|
// Styles
|
|
10
11
|
import { AppColors } from '../styles/AppColors';
|
|
@@ -151,6 +152,7 @@ const JsonViewer = React.memo(({ data, search, forceOpen, defaultExpandDepth = 1
|
|
|
151
152
|
icon: (isActive) => (<TableIcon color={isActive ? AppColors.white : AppColors.slate400} size={12}/>),
|
|
152
153
|
},
|
|
153
154
|
]} activeKey={mode} onChange={key => setMode(key)}/>
|
|
155
|
+
<CopyButton value={data} label="JSON"/>
|
|
154
156
|
</View>)}
|
|
155
157
|
|
|
156
158
|
{/* ── Main View Content Area (Royal Monospace Theme) ── */}
|
|
@@ -11,6 +11,7 @@ import { getDurationColor, handleOpenExternalLink } from '../helpers';
|
|
|
11
11
|
import { ChevronIcon, CalendarIcon, StatusIcon, ClockIcon, SizeIcon, TerminalIcon, GlobeIcon, } from './NetworkIcons';
|
|
12
12
|
// Components
|
|
13
13
|
import TouchableScale from './TouchableScale';
|
|
14
|
+
import CopyButton from './CopyButton';
|
|
14
15
|
// Stylesheet
|
|
15
16
|
import { AppColors } from '../styles/AppColors';
|
|
16
17
|
import { AppFonts } from '../styles/AppFonts';
|
|
@@ -60,9 +61,20 @@ const MetaAccordion = ({ status, statusColor, duration, size, triggeredAt, metho
|
|
|
60
61
|
<Pressable onPress={toggleOpen} hitSlop={12}>
|
|
61
62
|
<View style={styles.metaHeader}>
|
|
62
63
|
<Text style={styles.metaTitle}>Metadata</Text>
|
|
63
|
-
<
|
|
64
|
-
<
|
|
65
|
-
|
|
64
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
|
65
|
+
<CopyButton value={{
|
|
66
|
+
status: status ?? 0,
|
|
67
|
+
duration: duration != null ? `${duration} ms` : '—',
|
|
68
|
+
size: size || '—',
|
|
69
|
+
triggeredAt: triggeredAt || '',
|
|
70
|
+
method: method || 'GET',
|
|
71
|
+
contentType: contentType || 'application/json',
|
|
72
|
+
url: url || '',
|
|
73
|
+
}} label="Metadata"/>
|
|
74
|
+
<Animated.View style={chevronStyle}>
|
|
75
|
+
<ChevronIcon color={AppColors.grayTextWeak} size={14}/>
|
|
76
|
+
</Animated.View>
|
|
77
|
+
</View>
|
|
66
78
|
</View>
|
|
67
79
|
</Pressable>
|
|
68
80
|
|