react-native-inapp-inspector 2.3.0 → 2.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/components/Inspector/MainScreen.js +7 -11
- package/dist/commonjs/components/Inspector/StorageTab.js +104 -89
- 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/storageInspector.d.ts +2 -2
- package/dist/commonjs/customHooks/storageInspector.js +17 -13
- package/dist/commonjs/index.js +28 -24
- package/dist/commonjs/types/interfaces.d.ts +1 -2
- package/dist/esm/components/Inspector/MainScreen.js +8 -12
- package/dist/esm/components/Inspector/StorageTab.js +105 -90
- 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/storageInspector.d.ts +2 -2
- package/dist/esm/customHooks/storageInspector.js +17 -13
- package/dist/esm/index.js +29 -25
- package/dist/esm/types/interfaces.d.ts +1 -2
- package/package.json +1 -1
- package/src/components/Inspector/MainScreen.tsx +7 -11
- package/src/components/Inspector/StorageTab.tsx +170 -114
- package/src/components/SkeletonPlaceholder.tsx +206 -0
- package/src/constants/version.ts +1 -1
- package/src/customHooks/storageInspector.ts +18 -14
- package/src/index.tsx +35 -26
- package/src/types/interfaces.ts +1 -2
|
@@ -255,34 +255,38 @@ export const calculateByteSize = (str) => {
|
|
|
255
255
|
return str.length * 2;
|
|
256
256
|
};
|
|
257
257
|
/**
|
|
258
|
-
* Determine type and format value safely
|
|
258
|
+
* Determine type and format value safely without blocking JS thread
|
|
259
259
|
*/
|
|
260
260
|
export const analyzeStorageValue = (rawVal) => {
|
|
261
261
|
if (rawVal === null || rawVal === undefined) {
|
|
262
262
|
return { strVal: 'null', type: 'null', byteSize: 4 };
|
|
263
263
|
}
|
|
264
264
|
const strVal = typeof rawVal === 'string' ? rawVal : String(rawVal);
|
|
265
|
-
const byteSize =
|
|
265
|
+
const byteSize = strVal.length * 2;
|
|
266
266
|
if (typeof rawVal === 'number') {
|
|
267
|
-
return { strVal,
|
|
267
|
+
return { strVal, parsedValue: rawVal, type: 'number', byteSize };
|
|
268
268
|
}
|
|
269
269
|
if (typeof rawVal === 'boolean') {
|
|
270
|
-
return { strVal,
|
|
270
|
+
return { strVal, parsedValue: rawVal, type: 'boolean', byteSize };
|
|
271
271
|
}
|
|
272
272
|
const trimmed = strVal.trim();
|
|
273
273
|
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
274
274
|
(trimmed.startsWith('[') && trimmed.endsWith(']'))) {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
275
|
+
// For small/medium JSON, parse eagerly; for huge payloads (>10KB), parse lazily on expand
|
|
276
|
+
if (trimmed.length < 10000) {
|
|
277
|
+
try {
|
|
278
|
+
const parsed = JSON.parse(trimmed);
|
|
279
|
+
return { strVal, parsedValue: parsed, type: 'json', byteSize };
|
|
280
|
+
}
|
|
281
|
+
catch { }
|
|
278
282
|
}
|
|
279
|
-
|
|
283
|
+
return { strVal, type: 'json', byteSize };
|
|
280
284
|
}
|
|
281
285
|
if (trimmed === 'true' || trimmed === 'false') {
|
|
282
|
-
return { strVal,
|
|
286
|
+
return { strVal, parsedValue: trimmed === 'true', type: 'boolean', byteSize };
|
|
283
287
|
}
|
|
284
288
|
if (!isNaN(Number(trimmed)) && trimmed !== '') {
|
|
285
|
-
return { strVal,
|
|
289
|
+
return { strVal, parsedValue: Number(trimmed), type: 'number', byteSize };
|
|
286
290
|
}
|
|
287
291
|
return { strVal, type: 'string', byteSize };
|
|
288
292
|
};
|
|
@@ -307,7 +311,7 @@ export const fetchStorageEntries = async (driver, instanceId) => {
|
|
|
307
311
|
entries.push({
|
|
308
312
|
key: k,
|
|
309
313
|
value: analysis.strVal,
|
|
310
|
-
parsedValue: analysis.
|
|
314
|
+
parsedValue: analysis.parsedValue,
|
|
311
315
|
type: analysis.type,
|
|
312
316
|
byteSize: analysis.byteSize,
|
|
313
317
|
});
|
|
@@ -320,7 +324,7 @@ export const fetchStorageEntries = async (driver, instanceId) => {
|
|
|
320
324
|
entries.push({
|
|
321
325
|
key: k,
|
|
322
326
|
value: analysis.strVal,
|
|
323
|
-
parsedValue: analysis.
|
|
327
|
+
parsedValue: analysis.parsedValue,
|
|
324
328
|
type: analysis.type,
|
|
325
329
|
byteSize: analysis.byteSize,
|
|
326
330
|
});
|
|
@@ -361,7 +365,7 @@ export const fetchStorageEntries = async (driver, instanceId) => {
|
|
|
361
365
|
entries.push({
|
|
362
366
|
key: k,
|
|
363
367
|
value: analysis.strVal,
|
|
364
|
-
parsedValue: analysis.
|
|
368
|
+
parsedValue: analysis.parsedValue,
|
|
365
369
|
type: analysis.type,
|
|
366
370
|
byteSize: analysis.byteSize,
|
|
367
371
|
});
|
package/dist/esm/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|
2
|
-
import { Alert, Animated, PanResponder, Platform, UIManager, LogBox,
|
|
2
|
+
import { Alert, Animated, PanResponder, Platform, UIManager, LogBox, } from 'react-native';
|
|
3
3
|
import { NavigationContext } from '@react-navigation/native';
|
|
4
4
|
// i18n
|
|
5
5
|
import { I18nextProvider, i18n } from './i18n';
|
|
@@ -27,7 +27,7 @@ import { showNativeFloatingButton, hideNativeFloatingButton, setNativeFloatingBu
|
|
|
27
27
|
import { LIB_VERSION } from './constants';
|
|
28
28
|
// Stylesheet
|
|
29
29
|
import { toggleGlobalTheme } from './styles';
|
|
30
|
-
const NetworkInspector = ({ enabled = true,
|
|
30
|
+
const NetworkInspector = ({ enabled = true, storage, navigationRef, appIcon, environment, initialVisible = false, visible: controlledVisible, }) => {
|
|
31
31
|
// Set custom storage synchronously during render phase
|
|
32
32
|
setCustomStorage(storage || null);
|
|
33
33
|
const [isDark, setIsDark] = useState(false);
|
|
@@ -39,7 +39,7 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
39
39
|
const [modalHeightPercent, setModalHeightPercent] = useState(90);
|
|
40
40
|
const [modalAnimationType, setModalAnimationType] = useState('slide');
|
|
41
41
|
const [logs, setLogs] = useState([]);
|
|
42
|
-
const [visible, setVisible] = useState(
|
|
42
|
+
const [visible, setVisible] = useState(controlledVisible ?? initialVisible ?? false);
|
|
43
43
|
useEffect(() => {
|
|
44
44
|
if (controlledVisible !== undefined) {
|
|
45
45
|
setVisible(controlledVisible);
|
|
@@ -743,22 +743,26 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
743
743
|
const useNativeFab = (Platform.OS === 'ios' || Platform.OS === 'android') && isNativeModule;
|
|
744
744
|
// 100% Native Main-Thread Floating Button Lifecycle
|
|
745
745
|
useEffect(() => {
|
|
746
|
-
if (!useNativeFab || !
|
|
746
|
+
if (!useNativeFab || !enabled) {
|
|
747
747
|
if (useNativeFab) {
|
|
748
|
-
hideNativeFloatingButton();
|
|
748
|
+
hideNativeFloatingButton().catch(() => { });
|
|
749
749
|
}
|
|
750
750
|
return;
|
|
751
751
|
}
|
|
752
752
|
if (visible) {
|
|
753
|
-
hideNativeFloatingButton();
|
|
753
|
+
hideNativeFloatingButton().catch(() => { });
|
|
754
754
|
}
|
|
755
755
|
else {
|
|
756
|
-
showNativeFloatingButton();
|
|
757
|
-
setNativeFloatingButtonBadge(logs.length > 0 || analyticsEvents.length > 0);
|
|
756
|
+
showNativeFloatingButton().catch(() => { });
|
|
757
|
+
setNativeFloatingButtonBadge(logs.length > 0 || analyticsEvents.length > 0).catch(() => { });
|
|
758
758
|
}
|
|
759
|
+
return () => {
|
|
760
|
+
if (useNativeFab) {
|
|
761
|
+
hideNativeFloatingButton().catch(() => { });
|
|
762
|
+
}
|
|
763
|
+
};
|
|
759
764
|
}, [
|
|
760
765
|
useNativeFab,
|
|
761
|
-
isEnabled,
|
|
762
766
|
enabled,
|
|
763
767
|
visible,
|
|
764
768
|
logs.length,
|
|
@@ -817,20 +821,11 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
817
821
|
if (freshCrashes.length > 0) {
|
|
818
822
|
setCrashRecords(freshCrashes);
|
|
819
823
|
}
|
|
820
|
-
|
|
821
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
822
|
-
}, [visible]);
|
|
823
|
-
useEffect(() => {
|
|
824
|
-
if (visible) {
|
|
825
|
-
const task = InteractionManager.runAfterInteractions(() => {
|
|
824
|
+
const frame = requestAnimationFrame(() => {
|
|
826
825
|
setIsReady(true);
|
|
827
826
|
});
|
|
828
|
-
const fallbackTimer = setTimeout(() => {
|
|
829
|
-
setIsReady(true);
|
|
830
|
-
}, 350);
|
|
831
827
|
return () => {
|
|
832
|
-
|
|
833
|
-
clearTimeout(fallbackTimer);
|
|
828
|
+
cancelAnimationFrame(frame);
|
|
834
829
|
};
|
|
835
830
|
}
|
|
836
831
|
else {
|
|
@@ -1666,7 +1661,8 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
1666
1661
|
setVisible,
|
|
1667
1662
|
closeModal,
|
|
1668
1663
|
isReady,
|
|
1669
|
-
|
|
1664
|
+
enabled,
|
|
1665
|
+
isEnabled: enabled,
|
|
1670
1666
|
appIcon,
|
|
1671
1667
|
environment,
|
|
1672
1668
|
modalHeightPercent,
|
|
@@ -1843,14 +1839,22 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
1843
1839
|
</InspectorContext.Provider>);
|
|
1844
1840
|
};
|
|
1845
1841
|
const NetworkInspectorWrapper = (props) => {
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1842
|
+
const enabled = props?.enabled ?? true;
|
|
1843
|
+
useEffect(() => {
|
|
1844
|
+
if (!enabled) {
|
|
1845
|
+
hideNativeFloatingButton().catch(() => { });
|
|
1846
|
+
}
|
|
1847
|
+
return () => {
|
|
1848
|
+
hideNativeFloatingButton().catch(() => { });
|
|
1849
|
+
};
|
|
1850
|
+
}, [enabled]);
|
|
1851
|
+
// If enabled is false, return null immediately with 0 overhead
|
|
1852
|
+
if (!enabled) {
|
|
1849
1853
|
return null;
|
|
1850
1854
|
}
|
|
1851
1855
|
return (<I18nextProvider i18n={i18n}>
|
|
1852
1856
|
<ErrorBoundary fallbackType="inline">
|
|
1853
|
-
<NetworkInspector {...props}/>
|
|
1857
|
+
<NetworkInspector {...props} enabled={enabled}/>
|
|
1854
1858
|
</ErrorBoundary>
|
|
1855
1859
|
</I18nextProvider>);
|
|
1856
1860
|
};
|
|
@@ -157,8 +157,6 @@ export interface PersistedSettings {
|
|
|
157
157
|
}
|
|
158
158
|
export interface NetworkInspectorProps {
|
|
159
159
|
enabled?: boolean;
|
|
160
|
-
isEnabled?: boolean;
|
|
161
|
-
forceEnable?: boolean;
|
|
162
160
|
storage?: InspectorStorage;
|
|
163
161
|
navigationRef?: any;
|
|
164
162
|
appIcon?: any;
|
|
@@ -174,6 +172,7 @@ export interface InspectorContextValue {
|
|
|
174
172
|
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
|
|
175
173
|
closeModal: () => void;
|
|
176
174
|
isReady: boolean;
|
|
175
|
+
enabled: boolean;
|
|
177
176
|
isEnabled: boolean;
|
|
178
177
|
appIcon?: any;
|
|
179
178
|
environment?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-inapp-inspector",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "The zero-config, all-in-one in-app debugger for React Native & Expo. Inspect Network (fetch/axios), Console logs, Stack Traces, Redux State, Firebase Analytics, and JS Bundle size directly on device.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -21,6 +21,7 @@ import LogDetail from './LogDetail';
|
|
|
21
21
|
import ConsoleTab from './ConsoleTab';
|
|
22
22
|
import AnalyticsTab from './AnalyticsTab';
|
|
23
23
|
import AnalyticsDetail from '../AnalyticsDetail';
|
|
24
|
+
import SkeletonPlaceholder from '../SkeletonPlaceholder';
|
|
24
25
|
import ReduxTab from './ReduxTab';
|
|
25
26
|
import ReduxDetail from './ReduxDetail';
|
|
26
27
|
import BundleTab from './BundleTab';
|
|
@@ -51,7 +52,7 @@ const MainScreen = () => {
|
|
|
51
52
|
settingsPage,
|
|
52
53
|
activeTab,
|
|
53
54
|
isReady,
|
|
54
|
-
|
|
55
|
+
enabled,
|
|
55
56
|
useNativeFab,
|
|
56
57
|
hasNavigationContext,
|
|
57
58
|
setNavState,
|
|
@@ -104,7 +105,7 @@ const MainScreen = () => {
|
|
|
104
105
|
return (
|
|
105
106
|
<>
|
|
106
107
|
{(Platform.OS === 'ios' || Platform.OS === 'android') &&
|
|
107
|
-
|
|
108
|
+
enabled &&
|
|
108
109
|
!visible &&
|
|
109
110
|
!useNativeFab && <FabLauncher />}
|
|
110
111
|
<Modal
|
|
@@ -137,11 +138,11 @@ const MainScreen = () => {
|
|
|
137
138
|
<InspectorHeader />
|
|
138
139
|
|
|
139
140
|
<View style={{flex: 1}}>
|
|
141
|
+
{/* ─── Horizontal Scrollable Tab Bar inside Content (Always visible) ─── */}
|
|
142
|
+
{!isDetailActive && <TabBar />}
|
|
143
|
+
|
|
140
144
|
{isReady ? (
|
|
141
145
|
<View style={{flex: 1}}>
|
|
142
|
-
{/* ─── Horizontal Scrollable Tab Bar inside Content ─── */}
|
|
143
|
-
{!isDetailActive && <TabBar />}
|
|
144
|
-
|
|
145
146
|
{/* Persistent List Layer - Never unmounted, preserves 100% native scroll with smooth tab transition */}
|
|
146
147
|
<Animated.View
|
|
147
148
|
style={[
|
|
@@ -207,12 +208,7 @@ const MainScreen = () => {
|
|
|
207
208
|
)}
|
|
208
209
|
</View>
|
|
209
210
|
) : (
|
|
210
|
-
<
|
|
211
|
-
<ActivityIndicator size="large" color={AppColors.purple} />
|
|
212
|
-
<Text style={[styles.emptySub, {marginTop: 12}]}>
|
|
213
|
-
Loading logs...
|
|
214
|
-
</Text>
|
|
215
|
-
</View>
|
|
211
|
+
<SkeletonPlaceholder />
|
|
216
212
|
)}
|
|
217
213
|
|
|
218
214
|
{/* Settings Panel Layer - Rendered on top with smooth slide & spring transition */}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
View,
|
|
4
4
|
Text,
|
|
5
5
|
ScrollView,
|
|
6
|
+
FlatList,
|
|
6
7
|
TextInput,
|
|
7
8
|
Modal,
|
|
8
9
|
Alert,
|
|
@@ -43,6 +44,131 @@ import {
|
|
|
43
44
|
import {copyToClipboard} from '../../helpers';
|
|
44
45
|
import {showToast} from '../../helpers/toast';
|
|
45
46
|
|
|
47
|
+
const getTypeBadge = (type: StorageEntry['type']) => {
|
|
48
|
+
switch (type) {
|
|
49
|
+
case 'json':
|
|
50
|
+
return {label: 'JSON', color: AppColors.purple, bg: `${AppColors.purple}16`};
|
|
51
|
+
case 'number':
|
|
52
|
+
return {label: 'NUM', color: AppColors.warningIconGold, bg: `${AppColors.warningIconGold}16`};
|
|
53
|
+
case 'boolean':
|
|
54
|
+
return {label: 'BOOL', color: AppColors.emerald500, bg: `${AppColors.emerald500}16`};
|
|
55
|
+
case 'null':
|
|
56
|
+
return {label: 'NULL', color: AppColors.grayTextWeak, bg: `${AppColors.grayTextWeak}16`};
|
|
57
|
+
default:
|
|
58
|
+
return {label: 'STR', color: AppColors.blue500, bg: `${AppColors.blue500}16`};
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const StorageEntryCard = React.memo(function StorageEntryCard({
|
|
63
|
+
entry,
|
|
64
|
+
isExpanded,
|
|
65
|
+
onToggleExpand,
|
|
66
|
+
onCopy,
|
|
67
|
+
onEdit,
|
|
68
|
+
onDelete,
|
|
69
|
+
badge,
|
|
70
|
+
}: {
|
|
71
|
+
entry: StorageEntry;
|
|
72
|
+
isExpanded: boolean;
|
|
73
|
+
onToggleExpand: (key: string) => void;
|
|
74
|
+
onCopy: (entry: StorageEntry) => void;
|
|
75
|
+
onEdit: (entry: StorageEntry) => void;
|
|
76
|
+
onDelete: (key: string) => void;
|
|
77
|
+
badge: {label: string; color: string; bg: string};
|
|
78
|
+
}) {
|
|
79
|
+
const formattedBytes = useMemo(() => {
|
|
80
|
+
return entry.byteSize < 1024
|
|
81
|
+
? `${entry.byteSize} B`
|
|
82
|
+
: `${(entry.byteSize / 1024).toFixed(1)} KB`;
|
|
83
|
+
}, [entry.byteSize]);
|
|
84
|
+
|
|
85
|
+
// Lazy compute displayed value to avoid layout thrashing on large JSON strings
|
|
86
|
+
const displayValue = useMemo(() => {
|
|
87
|
+
if (!isExpanded) {
|
|
88
|
+
if (entry.value.length > 250) {
|
|
89
|
+
return entry.value.slice(0, 250) + '...';
|
|
90
|
+
}
|
|
91
|
+
return entry.value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (entry.type === 'json') {
|
|
95
|
+
try {
|
|
96
|
+
const parsed = entry.parsedValue ?? JSON.parse(entry.value);
|
|
97
|
+
return JSON.stringify(parsed, null, 2);
|
|
98
|
+
} catch {
|
|
99
|
+
return entry.value;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return entry.value;
|
|
103
|
+
}, [isExpanded, entry.value, entry.type, entry.parsedValue]);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<View style={styles.entryCard}>
|
|
107
|
+
{/* Entry Header: Key name, Type Badge, Size, Actions */}
|
|
108
|
+
<View style={styles.entryHeader}>
|
|
109
|
+
<View style={{flex: 1, marginRight: 8}}>
|
|
110
|
+
<View style={{flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap'}}>
|
|
111
|
+
<Text style={styles.entryKey} numberOfLines={1} selectable>
|
|
112
|
+
{entry.key}
|
|
113
|
+
</Text>
|
|
114
|
+
<View
|
|
115
|
+
style={[
|
|
116
|
+
styles.typeBadge,
|
|
117
|
+
{backgroundColor: badge.bg, borderColor: `${badge.color}33`},
|
|
118
|
+
]}>
|
|
119
|
+
<Text style={[styles.typeBadgeText, {color: badge.color}]}>
|
|
120
|
+
{badge.label}
|
|
121
|
+
</Text>
|
|
122
|
+
</View>
|
|
123
|
+
<Text style={styles.sizeText}>{formattedBytes}</Text>
|
|
124
|
+
</View>
|
|
125
|
+
</View>
|
|
126
|
+
|
|
127
|
+
{/* Actions: Copy, Edit, Delete */}
|
|
128
|
+
<View style={styles.entryActions}>
|
|
129
|
+
<TouchableScale
|
|
130
|
+
hitSlop={6}
|
|
131
|
+
onPress={() => onCopy(entry)}
|
|
132
|
+
style={styles.entryActionBtn}>
|
|
133
|
+
<CopyIcon size={12} color={AppColors.grayText} />
|
|
134
|
+
</TouchableScale>
|
|
135
|
+
|
|
136
|
+
<TouchableScale
|
|
137
|
+
hitSlop={6}
|
|
138
|
+
onPress={() => onEdit(entry)}
|
|
139
|
+
style={styles.entryActionBtn}>
|
|
140
|
+
<PencilIcon size={12} color={AppColors.purple} />
|
|
141
|
+
</TouchableScale>
|
|
142
|
+
|
|
143
|
+
<TouchableScale
|
|
144
|
+
hitSlop={6}
|
|
145
|
+
onPress={() => onDelete(entry.key)}
|
|
146
|
+
style={[styles.entryActionBtn, {backgroundColor: `${AppColors.errorColor}12`}]}>
|
|
147
|
+
<TrashIcon size={12} color={AppColors.errorColor} />
|
|
148
|
+
</TouchableScale>
|
|
149
|
+
</View>
|
|
150
|
+
</View>
|
|
151
|
+
|
|
152
|
+
{/* Entry Value Preview / Viewer */}
|
|
153
|
+
<TouchableScale
|
|
154
|
+
onPress={() => onToggleExpand(entry.key)}
|
|
155
|
+
style={styles.valuePreviewBox}>
|
|
156
|
+
<Text
|
|
157
|
+
style={styles.valuePreviewText}
|
|
158
|
+
numberOfLines={isExpanded ? undefined : 3}
|
|
159
|
+
selectable={isExpanded}>
|
|
160
|
+
{displayValue}
|
|
161
|
+
</Text>
|
|
162
|
+
{entry.value.length > 80 && (
|
|
163
|
+
<Text style={styles.expandHint}>
|
|
164
|
+
{isExpanded ? '▲ Collapse' : '▼ Expand'}
|
|
165
|
+
</Text>
|
|
166
|
+
)}
|
|
167
|
+
</TouchableScale>
|
|
168
|
+
</View>
|
|
169
|
+
);
|
|
170
|
+
});
|
|
171
|
+
|
|
46
172
|
export const StorageTab = React.memo(() => {
|
|
47
173
|
const {t} = useTranslation();
|
|
48
174
|
const [activeDriver, setActiveDriver] = useState<StorageDriver>('asyncStorage');
|
|
@@ -235,20 +361,25 @@ export const StorageTab = React.memo(() => {
|
|
|
235
361
|
return `${(totalBytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
236
362
|
}, [totalBytes]);
|
|
237
363
|
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
364
|
+
const handleCopyEntry = useCallback((entry: StorageEntry) => {
|
|
365
|
+
copyToClipboard(entry.value, entry.key);
|
|
366
|
+
showToast(`Copied value of "${entry.key}"`);
|
|
367
|
+
}, []);
|
|
368
|
+
|
|
369
|
+
const renderItem = useCallback(
|
|
370
|
+
({item}: {item: StorageEntry}) => (
|
|
371
|
+
<StorageEntryCard
|
|
372
|
+
entry={item}
|
|
373
|
+
isExpanded={Boolean(expandedKeys[item.key])}
|
|
374
|
+
onToggleExpand={toggleExpand}
|
|
375
|
+
onCopy={handleCopyEntry}
|
|
376
|
+
onEdit={handleOpenEdit}
|
|
377
|
+
onDelete={handleDeleteKey}
|
|
378
|
+
badge={getTypeBadge(item.type)}
|
|
379
|
+
/>
|
|
380
|
+
),
|
|
381
|
+
[expandedKeys, handleCopyEntry],
|
|
382
|
+
);
|
|
252
383
|
|
|
253
384
|
const isConnected =
|
|
254
385
|
activeDriver === 'asyncStorage'
|
|
@@ -423,110 +554,35 @@ export const StorageTab = React.memo(() => {
|
|
|
423
554
|
<ActivityIndicator size="small" color={AppColors.purple} />
|
|
424
555
|
<Text style={styles.loadingText}>Loading storage entries...</Text>
|
|
425
556
|
</View>
|
|
426
|
-
) : filteredEntries.length === 0 ? (
|
|
427
|
-
<ScrollView
|
|
428
|
-
contentContainerStyle={styles.emptyContainer}
|
|
429
|
-
showsVerticalScrollIndicator={false}>
|
|
430
|
-
<View style={styles.emptyIconWrap}>
|
|
431
|
-
<DatabaseIcon size={28} color={AppColors.grayTextWeak} />
|
|
432
|
-
</View>
|
|
433
|
-
<Text style={styles.emptyTitle}>
|
|
434
|
-
{search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
|
|
435
|
-
</Text>
|
|
436
|
-
<Text style={styles.emptySubtitle}>
|
|
437
|
-
{search
|
|
438
|
-
? 'Try modifying your search query'
|
|
439
|
-
: `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
|
|
440
|
-
</Text>
|
|
441
|
-
</ScrollView>
|
|
442
557
|
) : (
|
|
443
|
-
<
|
|
558
|
+
<FlatList
|
|
559
|
+
data={filteredEntries}
|
|
560
|
+
keyExtractor={item => item.key}
|
|
561
|
+
renderItem={renderItem}
|
|
562
|
+
initialNumToRender={10}
|
|
563
|
+
maxToRenderPerBatch={10}
|
|
564
|
+
windowSize={5}
|
|
565
|
+
removeClippedSubviews={Platform.OS === 'android'}
|
|
444
566
|
style={styles.scrollArea}
|
|
445
567
|
contentContainerStyle={styles.scrollContent}
|
|
446
|
-
showsVerticalScrollIndicator={false}
|
|
447
|
-
{
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
entry.byteSize < 1024
|
|
452
|
-
? `${entry.byteSize} B`
|
|
453
|
-
: `${(entry.byteSize / 1024).toFixed(1)} KB`;
|
|
454
|
-
|
|
455
|
-
return (
|
|
456
|
-
<View key={entry.key} style={styles.entryCard}>
|
|
457
|
-
{/* Entry Header: Key name, Type Badge, Size, Actions */}
|
|
458
|
-
<View style={styles.entryHeader}>
|
|
459
|
-
<View style={{flex: 1, marginRight: 8}}>
|
|
460
|
-
<View style={{flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap'}}>
|
|
461
|
-
<Text style={styles.entryKey} numberOfLines={1} selectable>
|
|
462
|
-
{entry.key}
|
|
463
|
-
</Text>
|
|
464
|
-
<View
|
|
465
|
-
style={[
|
|
466
|
-
styles.typeBadge,
|
|
467
|
-
{backgroundColor: badge.bg, borderColor: `${badge.color}33`},
|
|
468
|
-
]}>
|
|
469
|
-
<Text style={[styles.typeBadgeText, {color: badge.color}]}>
|
|
470
|
-
{badge.label}
|
|
471
|
-
</Text>
|
|
472
|
-
</View>
|
|
473
|
-
<Text style={styles.sizeText}>{formattedBytes}</Text>
|
|
474
|
-
</View>
|
|
475
|
-
</View>
|
|
476
|
-
|
|
477
|
-
{/* Actions: Copy, Edit, Delete */}
|
|
478
|
-
<View style={styles.entryActions}>
|
|
479
|
-
<TouchableScale
|
|
480
|
-
hitSlop={6}
|
|
481
|
-
onPress={() => {
|
|
482
|
-
copyToClipboard(entry.value, entry.key);
|
|
483
|
-
showToast(`Copied value of "${entry.key}"`);
|
|
484
|
-
}}
|
|
485
|
-
style={styles.entryActionBtn}>
|
|
486
|
-
<CopyIcon size={12} color={AppColors.grayText} />
|
|
487
|
-
</TouchableScale>
|
|
488
|
-
|
|
489
|
-
<TouchableScale
|
|
490
|
-
hitSlop={6}
|
|
491
|
-
onPress={() => handleOpenEdit(entry)}
|
|
492
|
-
style={styles.entryActionBtn}>
|
|
493
|
-
<PencilIcon size={12} color={AppColors.purple} />
|
|
494
|
-
</TouchableScale>
|
|
495
|
-
|
|
496
|
-
<TouchableScale
|
|
497
|
-
hitSlop={6}
|
|
498
|
-
onPress={() => handleDeleteKey(entry.key)}
|
|
499
|
-
style={[styles.entryActionBtn, {backgroundColor: `${AppColors.errorColor}12`}]}>
|
|
500
|
-
<TrashIcon size={12} color={AppColors.errorColor} />
|
|
501
|
-
</TouchableScale>
|
|
502
|
-
</View>
|
|
503
|
-
</View>
|
|
504
|
-
|
|
505
|
-
{/* Entry Value Preview / Viewer */}
|
|
506
|
-
<TouchableScale
|
|
507
|
-
onPress={() => toggleExpand(entry.key)}
|
|
508
|
-
style={styles.valuePreviewBox}>
|
|
509
|
-
<Text
|
|
510
|
-
style={styles.valuePreviewText}
|
|
511
|
-
numberOfLines={isExpanded ? undefined : 3}
|
|
512
|
-
selectable>
|
|
513
|
-
{entry.type === 'json' && entry.parsedValue
|
|
514
|
-
? isExpanded
|
|
515
|
-
? JSON.stringify(entry.parsedValue, null, 2)
|
|
516
|
-
: entry.value
|
|
517
|
-
: entry.value}
|
|
518
|
-
</Text>
|
|
519
|
-
{entry.value.length > 80 && (
|
|
520
|
-
<Text style={styles.expandHint}>
|
|
521
|
-
{isExpanded ? '▲ Collapse' : '▼ Expand'}
|
|
522
|
-
</Text>
|
|
523
|
-
)}
|
|
524
|
-
</TouchableScale>
|
|
568
|
+
showsVerticalScrollIndicator={false}
|
|
569
|
+
ListEmptyComponent={
|
|
570
|
+
<View style={styles.emptyContainer}>
|
|
571
|
+
<View style={styles.emptyIconWrap}>
|
|
572
|
+
<DatabaseIcon size={28} color={AppColors.grayTextWeak} />
|
|
525
573
|
</View>
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
574
|
+
<Text style={styles.emptyTitle}>
|
|
575
|
+
{search ? 'No matching keys found' : `No ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} Keys`}
|
|
576
|
+
</Text>
|
|
577
|
+
<Text style={styles.emptySubtitle}>
|
|
578
|
+
{search
|
|
579
|
+
? 'Try modifying your search query'
|
|
580
|
+
: `Storage is auto-detected. Tap "+ Add" above to create your first ${activeDriver === 'asyncStorage' ? 'AsyncStorage' : 'MMKV'} key!`}
|
|
581
|
+
</Text>
|
|
582
|
+
</View>
|
|
583
|
+
}
|
|
584
|
+
ListFooterComponent={<View style={{height: 60}} />}
|
|
585
|
+
/>
|
|
530
586
|
)}
|
|
531
587
|
|
|
532
588
|
{/* ── Create / Edit Key Modal ── */}
|