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
package/dist/commonjs/index.js
CHANGED
|
@@ -68,7 +68,7 @@ const NativeInspector_2 = require("./native/NativeInspector");
|
|
|
68
68
|
const constants_1 = require("./constants");
|
|
69
69
|
// Stylesheet
|
|
70
70
|
const styles_1 = require("./styles");
|
|
71
|
-
const NetworkInspector = ({ enabled = true,
|
|
71
|
+
const NetworkInspector = ({ enabled = true, storage, navigationRef, appIcon, environment, initialVisible = false, visible: controlledVisible, }) => {
|
|
72
72
|
// Set custom storage synchronously during render phase
|
|
73
73
|
(0, settingsStore_1.setCustomStorage)(storage || null);
|
|
74
74
|
const [isDark, setIsDark] = (0, react_1.useState)(false);
|
|
@@ -80,7 +80,7 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
80
80
|
const [modalHeightPercent, setModalHeightPercent] = (0, react_1.useState)(90);
|
|
81
81
|
const [modalAnimationType, setModalAnimationType] = (0, react_1.useState)('slide');
|
|
82
82
|
const [logs, setLogs] = (0, react_1.useState)([]);
|
|
83
|
-
const [visible, setVisible] = (0, react_1.useState)(
|
|
83
|
+
const [visible, setVisible] = (0, react_1.useState)(controlledVisible ?? initialVisible ?? false);
|
|
84
84
|
(0, react_1.useEffect)(() => {
|
|
85
85
|
if (controlledVisible !== undefined) {
|
|
86
86
|
setVisible(controlledVisible);
|
|
@@ -784,22 +784,26 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
784
784
|
const useNativeFab = (react_native_1.Platform.OS === 'ios' || react_native_1.Platform.OS === 'android') && isNativeModule;
|
|
785
785
|
// 100% Native Main-Thread Floating Button Lifecycle
|
|
786
786
|
(0, react_1.useEffect)(() => {
|
|
787
|
-
if (!useNativeFab || !
|
|
787
|
+
if (!useNativeFab || !enabled) {
|
|
788
788
|
if (useNativeFab) {
|
|
789
|
-
(0, NativeInspector_2.hideNativeFloatingButton)();
|
|
789
|
+
(0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
|
|
790
790
|
}
|
|
791
791
|
return;
|
|
792
792
|
}
|
|
793
793
|
if (visible) {
|
|
794
|
-
(0, NativeInspector_2.hideNativeFloatingButton)();
|
|
794
|
+
(0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
|
|
795
795
|
}
|
|
796
796
|
else {
|
|
797
|
-
(0, NativeInspector_2.showNativeFloatingButton)();
|
|
798
|
-
(0, NativeInspector_2.setNativeFloatingButtonBadge)(logs.length > 0 || analyticsEvents.length > 0);
|
|
797
|
+
(0, NativeInspector_2.showNativeFloatingButton)().catch(() => { });
|
|
798
|
+
(0, NativeInspector_2.setNativeFloatingButtonBadge)(logs.length > 0 || analyticsEvents.length > 0).catch(() => { });
|
|
799
799
|
}
|
|
800
|
+
return () => {
|
|
801
|
+
if (useNativeFab) {
|
|
802
|
+
(0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
|
|
803
|
+
}
|
|
804
|
+
};
|
|
800
805
|
}, [
|
|
801
806
|
useNativeFab,
|
|
802
|
-
isEnabled,
|
|
803
807
|
enabled,
|
|
804
808
|
visible,
|
|
805
809
|
logs.length,
|
|
@@ -858,20 +862,11 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
858
862
|
if (freshCrashes.length > 0) {
|
|
859
863
|
setCrashRecords(freshCrashes);
|
|
860
864
|
}
|
|
861
|
-
|
|
862
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
863
|
-
}, [visible]);
|
|
864
|
-
(0, react_1.useEffect)(() => {
|
|
865
|
-
if (visible) {
|
|
866
|
-
const task = react_native_1.InteractionManager.runAfterInteractions(() => {
|
|
865
|
+
const frame = requestAnimationFrame(() => {
|
|
867
866
|
setIsReady(true);
|
|
868
867
|
});
|
|
869
|
-
const fallbackTimer = setTimeout(() => {
|
|
870
|
-
setIsReady(true);
|
|
871
|
-
}, 350);
|
|
872
868
|
return () => {
|
|
873
|
-
|
|
874
|
-
clearTimeout(fallbackTimer);
|
|
869
|
+
cancelAnimationFrame(frame);
|
|
875
870
|
};
|
|
876
871
|
}
|
|
877
872
|
else {
|
|
@@ -1707,7 +1702,8 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
1707
1702
|
setVisible,
|
|
1708
1703
|
closeModal,
|
|
1709
1704
|
isReady,
|
|
1710
|
-
|
|
1705
|
+
enabled,
|
|
1706
|
+
isEnabled: enabled,
|
|
1711
1707
|
appIcon,
|
|
1712
1708
|
environment,
|
|
1713
1709
|
modalHeightPercent,
|
|
@@ -1884,14 +1880,22 @@ const NetworkInspector = ({ enabled = true, isEnabled = true, storage, navigatio
|
|
|
1884
1880
|
</InspectorContext_1.InspectorContext.Provider>);
|
|
1885
1881
|
};
|
|
1886
1882
|
const NetworkInspectorWrapper = (props) => {
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1883
|
+
const enabled = props?.enabled ?? true;
|
|
1884
|
+
(0, react_1.useEffect)(() => {
|
|
1885
|
+
if (!enabled) {
|
|
1886
|
+
(0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
|
|
1887
|
+
}
|
|
1888
|
+
return () => {
|
|
1889
|
+
(0, NativeInspector_2.hideNativeFloatingButton)().catch(() => { });
|
|
1890
|
+
};
|
|
1891
|
+
}, [enabled]);
|
|
1892
|
+
// If enabled is false, return null immediately with 0 overhead
|
|
1893
|
+
if (!enabled) {
|
|
1890
1894
|
return null;
|
|
1891
1895
|
}
|
|
1892
1896
|
return (<i18n_1.I18nextProvider i18n={i18n_1.i18n}>
|
|
1893
1897
|
<ErrorBoundary_1.default fallbackType="inline">
|
|
1894
|
-
<NetworkInspector {...props}/>
|
|
1898
|
+
<NetworkInspector {...props} enabled={enabled}/>
|
|
1895
1899
|
</ErrorBoundary_1.default>
|
|
1896
1900
|
</i18n_1.I18nextProvider>);
|
|
1897
1901
|
};
|
|
@@ -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;
|
|
@@ -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={[
|
|
@@ -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();
|
|
@@ -295,81 +372,19 @@ export const StorageTab = React.memo(() => {
|
|
|
295
372
|
{isLoading ? (<View style={styles.loadingContainer}>
|
|
296
373
|
<ActivityIndicator size="small" color={AppColors.purple}/>
|
|
297
374
|
<Text style={styles.loadingText}>Loading storage entries...</Text>
|
|
298
|
-
</View>) : filteredEntries.
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
{filteredEntries.map(entry => {
|
|
312
|
-
const isExpanded = Boolean(expandedKeys[entry.key]);
|
|
313
|
-
const badge = getTypeBadge(entry.type);
|
|
314
|
-
const formattedBytes = entry.byteSize < 1024
|
|
315
|
-
? `${entry.byteSize} B`
|
|
316
|
-
: `${(entry.byteSize / 1024).toFixed(1)} KB`;
|
|
317
|
-
return (<View key={entry.key} style={styles.entryCard}>
|
|
318
|
-
{/* Entry Header: Key name, Type Badge, Size, Actions */}
|
|
319
|
-
<View style={styles.entryHeader}>
|
|
320
|
-
<View style={{ flex: 1, marginRight: 8 }}>
|
|
321
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6, flexWrap: 'wrap' }}>
|
|
322
|
-
<Text style={styles.entryKey} numberOfLines={1} selectable>
|
|
323
|
-
{entry.key}
|
|
324
|
-
</Text>
|
|
325
|
-
<View style={[
|
|
326
|
-
styles.typeBadge,
|
|
327
|
-
{ backgroundColor: badge.bg, borderColor: `${badge.color}33` },
|
|
328
|
-
]}>
|
|
329
|
-
<Text style={[styles.typeBadgeText, { color: badge.color }]}>
|
|
330
|
-
{badge.label}
|
|
331
|
-
</Text>
|
|
332
|
-
</View>
|
|
333
|
-
<Text style={styles.sizeText}>{formattedBytes}</Text>
|
|
334
|
-
</View>
|
|
335
|
-
</View>
|
|
336
|
-
|
|
337
|
-
{/* Actions: Copy, Edit, Delete */}
|
|
338
|
-
<View style={styles.entryActions}>
|
|
339
|
-
<TouchableScale hitSlop={6} onPress={() => {
|
|
340
|
-
copyToClipboard(entry.value, entry.key);
|
|
341
|
-
showToast(`Copied value of "${entry.key}"`);
|
|
342
|
-
}} style={styles.entryActionBtn}>
|
|
343
|
-
<CopyIcon size={12} color={AppColors.grayText}/>
|
|
344
|
-
</TouchableScale>
|
|
345
|
-
|
|
346
|
-
<TouchableScale hitSlop={6} onPress={() => handleOpenEdit(entry)} style={styles.entryActionBtn}>
|
|
347
|
-
<PencilIcon size={12} color={AppColors.purple}/>
|
|
348
|
-
</TouchableScale>
|
|
349
|
-
|
|
350
|
-
<TouchableScale hitSlop={6} onPress={() => handleDeleteKey(entry.key)} style={[styles.entryActionBtn, { backgroundColor: `${AppColors.errorColor}12` }]}>
|
|
351
|
-
<TrashIcon size={12} color={AppColors.errorColor}/>
|
|
352
|
-
</TouchableScale>
|
|
353
|
-
</View>
|
|
354
|
-
</View>
|
|
355
|
-
|
|
356
|
-
{/* Entry Value Preview / Viewer */}
|
|
357
|
-
<TouchableScale onPress={() => toggleExpand(entry.key)} style={styles.valuePreviewBox}>
|
|
358
|
-
<Text style={styles.valuePreviewText} numberOfLines={isExpanded ? undefined : 3} selectable>
|
|
359
|
-
{entry.type === 'json' && entry.parsedValue
|
|
360
|
-
? isExpanded
|
|
361
|
-
? JSON.stringify(entry.parsedValue, null, 2)
|
|
362
|
-
: entry.value
|
|
363
|
-
: entry.value}
|
|
364
|
-
</Text>
|
|
365
|
-
{entry.value.length > 80 && (<Text style={styles.expandHint}>
|
|
366
|
-
{isExpanded ? '▲ Collapse' : '▼ Expand'}
|
|
367
|
-
</Text>)}
|
|
368
|
-
</TouchableScale>
|
|
369
|
-
</View>);
|
|
370
|
-
})}
|
|
371
|
-
<View style={{ height: 60 }}/>
|
|
372
|
-
</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 }}/>}/>)}
|
|
373
388
|
|
|
374
389
|
{/* ── Create / Edit Key Modal ── */}
|
|
375
390
|
<Modal visible={modalVisible} transparent animationType="fade" onRequestClose={() => setModalVisible(false)}>
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from 'react';
|
|
2
|
+
import { Animated, StyleSheet, View } from 'react-native';
|
|
3
|
+
import { AppColors } from '../styles/AppColors';
|
|
4
|
+
const SkeletonPlaceholder = React.memo(function SkeletonPlaceholder({ cardCount = 4, }) {
|
|
5
|
+
const shimmerAnim = useRef(new Animated.Value(0.35)).current;
|
|
6
|
+
useEffect(() => {
|
|
7
|
+
const animation = Animated.loop(Animated.sequence([
|
|
8
|
+
Animated.timing(shimmerAnim, {
|
|
9
|
+
toValue: 0.85,
|
|
10
|
+
duration: 750,
|
|
11
|
+
useNativeDriver: true,
|
|
12
|
+
}),
|
|
13
|
+
Animated.timing(shimmerAnim, {
|
|
14
|
+
toValue: 0.35,
|
|
15
|
+
duration: 750,
|
|
16
|
+
useNativeDriver: true,
|
|
17
|
+
}),
|
|
18
|
+
]));
|
|
19
|
+
animation.start();
|
|
20
|
+
return () => animation.stop();
|
|
21
|
+
}, [shimmerAnim]);
|
|
22
|
+
return (<View style={skeletonStyles.container}>
|
|
23
|
+
{/* ─── Search & Scope Toolbar Skeleton ─── */}
|
|
24
|
+
<View style={skeletonStyles.toolbarSkeleton}>
|
|
25
|
+
<Animated.View style={[
|
|
26
|
+
skeletonStyles.searchBarSkeleton,
|
|
27
|
+
{ opacity: shimmerAnim },
|
|
28
|
+
]}/>
|
|
29
|
+
<View style={skeletonStyles.actionButtonsRow}>
|
|
30
|
+
<Animated.View style={[skeletonStyles.iconButtonSkeleton, { opacity: shimmerAnim }]}/>
|
|
31
|
+
<Animated.View style={[skeletonStyles.iconButtonSkeleton, { opacity: shimmerAnim }]}/>
|
|
32
|
+
</View>
|
|
33
|
+
</View>
|
|
34
|
+
|
|
35
|
+
{/* ─── Quick Filter Chips Skeleton Strip ─── */}
|
|
36
|
+
<View style={skeletonStyles.chipStripSkeleton}>
|
|
37
|
+
<Animated.View style={[skeletonStyles.chipSkeleton, { width: 48, opacity: shimmerAnim }]}/>
|
|
38
|
+
<Animated.View style={[skeletonStyles.chipSkeleton, { width: 68, opacity: shimmerAnim }]}/>
|
|
39
|
+
<Animated.View style={[skeletonStyles.chipSkeleton, { width: 76, opacity: shimmerAnim }]}/>
|
|
40
|
+
<Animated.View style={[skeletonStyles.chipSkeleton, { width: 58, opacity: shimmerAnim }]}/>
|
|
41
|
+
</View>
|
|
42
|
+
|
|
43
|
+
{/* ─── List Cards Skeleton ─── */}
|
|
44
|
+
{Array.from({ length: cardCount }).map((_, i) => (<Animated.View key={`skeleton_card_${i}`} style={[skeletonStyles.cardSkeleton, { opacity: shimmerAnim }]}>
|
|
45
|
+
{/* Top row: Status pill + Method + Time */}
|
|
46
|
+
<View style={skeletonStyles.cardTopRow}>
|
|
47
|
+
<View style={skeletonStyles.badgeGroup}>
|
|
48
|
+
<View style={skeletonStyles.statusBadgeSkeleton}/>
|
|
49
|
+
<View style={skeletonStyles.methodBadgeSkeleton}/>
|
|
50
|
+
</View>
|
|
51
|
+
<View style={skeletonStyles.timeSkeleton}/>
|
|
52
|
+
</View>
|
|
53
|
+
|
|
54
|
+
{/* Middle row: URL lines */}
|
|
55
|
+
<View style={skeletonStyles.urlLineLong}/>
|
|
56
|
+
<View style={skeletonStyles.urlLineShort}/>
|
|
57
|
+
|
|
58
|
+
{/* Bottom row: Latency & Size */}
|
|
59
|
+
<View style={skeletonStyles.cardBottomRow}>
|
|
60
|
+
<View style={skeletonStyles.metaPillSkeleton}/>
|
|
61
|
+
<View style={skeletonStyles.metaPillSkeleton}/>
|
|
62
|
+
</View>
|
|
63
|
+
</Animated.View>))}
|
|
64
|
+
</View>);
|
|
65
|
+
});
|
|
66
|
+
const skeletonStyles = StyleSheet.create({
|
|
67
|
+
container: {
|
|
68
|
+
flex: 1,
|
|
69
|
+
paddingHorizontal: 12,
|
|
70
|
+
paddingTop: 8,
|
|
71
|
+
},
|
|
72
|
+
toolbarSkeleton: {
|
|
73
|
+
flexDirection: 'row',
|
|
74
|
+
alignItems: 'center',
|
|
75
|
+
gap: 8,
|
|
76
|
+
marginBottom: 8,
|
|
77
|
+
},
|
|
78
|
+
searchBarSkeleton: {
|
|
79
|
+
flex: 1,
|
|
80
|
+
height: 36,
|
|
81
|
+
borderRadius: 8,
|
|
82
|
+
backgroundColor: AppColors.graySurface,
|
|
83
|
+
borderWidth: 1,
|
|
84
|
+
borderColor: AppColors.dividerColor,
|
|
85
|
+
},
|
|
86
|
+
actionButtonsRow: {
|
|
87
|
+
flexDirection: 'row',
|
|
88
|
+
gap: 6,
|
|
89
|
+
},
|
|
90
|
+
iconButtonSkeleton: {
|
|
91
|
+
width: 36,
|
|
92
|
+
height: 36,
|
|
93
|
+
borderRadius: 8,
|
|
94
|
+
backgroundColor: AppColors.graySurface,
|
|
95
|
+
borderWidth: 1,
|
|
96
|
+
borderColor: AppColors.dividerColor,
|
|
97
|
+
},
|
|
98
|
+
chipStripSkeleton: {
|
|
99
|
+
flexDirection: 'row',
|
|
100
|
+
gap: 6,
|
|
101
|
+
marginBottom: 10,
|
|
102
|
+
},
|
|
103
|
+
chipSkeleton: {
|
|
104
|
+
height: 24,
|
|
105
|
+
borderRadius: 6,
|
|
106
|
+
backgroundColor: AppColors.graySurface,
|
|
107
|
+
borderWidth: 1,
|
|
108
|
+
borderColor: AppColors.dividerColor,
|
|
109
|
+
},
|
|
110
|
+
cardSkeleton: {
|
|
111
|
+
backgroundColor: AppColors.primaryLight,
|
|
112
|
+
borderRadius: 10,
|
|
113
|
+
padding: 12,
|
|
114
|
+
marginBottom: 8,
|
|
115
|
+
borderWidth: 1,
|
|
116
|
+
borderColor: AppColors.dividerColor,
|
|
117
|
+
},
|
|
118
|
+
cardTopRow: {
|
|
119
|
+
flexDirection: 'row',
|
|
120
|
+
justifyContent: 'space-between',
|
|
121
|
+
alignItems: 'center',
|
|
122
|
+
marginBottom: 8,
|
|
123
|
+
},
|
|
124
|
+
badgeGroup: {
|
|
125
|
+
flexDirection: 'row',
|
|
126
|
+
alignItems: 'center',
|
|
127
|
+
gap: 6,
|
|
128
|
+
},
|
|
129
|
+
statusBadgeSkeleton: {
|
|
130
|
+
width: 38,
|
|
131
|
+
height: 18,
|
|
132
|
+
borderRadius: 4,
|
|
133
|
+
backgroundColor: AppColors.graySurface,
|
|
134
|
+
},
|
|
135
|
+
methodBadgeSkeleton: {
|
|
136
|
+
width: 44,
|
|
137
|
+
height: 18,
|
|
138
|
+
borderRadius: 4,
|
|
139
|
+
backgroundColor: AppColors.graySurface,
|
|
140
|
+
},
|
|
141
|
+
timeSkeleton: {
|
|
142
|
+
width: 48,
|
|
143
|
+
height: 12,
|
|
144
|
+
borderRadius: 4,
|
|
145
|
+
backgroundColor: AppColors.graySurface,
|
|
146
|
+
},
|
|
147
|
+
urlLineLong: {
|
|
148
|
+
height: 13,
|
|
149
|
+
borderRadius: 4,
|
|
150
|
+
backgroundColor: AppColors.graySurface,
|
|
151
|
+
marginBottom: 5,
|
|
152
|
+
width: '90%',
|
|
153
|
+
},
|
|
154
|
+
urlLineShort: {
|
|
155
|
+
height: 11,
|
|
156
|
+
borderRadius: 4,
|
|
157
|
+
backgroundColor: AppColors.graySurface,
|
|
158
|
+
marginBottom: 8,
|
|
159
|
+
width: '55%',
|
|
160
|
+
},
|
|
161
|
+
cardBottomRow: {
|
|
162
|
+
flexDirection: 'row',
|
|
163
|
+
gap: 8,
|
|
164
|
+
marginTop: 2,
|
|
165
|
+
},
|
|
166
|
+
metaPillSkeleton: {
|
|
167
|
+
width: 52,
|
|
168
|
+
height: 14,
|
|
169
|
+
borderRadius: 4,
|
|
170
|
+
backgroundColor: AppColors.graySurface,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
export default SkeletonPlaceholder;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const LIB_VERSION = "2.3.
|
|
1
|
+
export declare const LIB_VERSION = "2.3.2";
|
|
@@ -37,11 +37,11 @@ export declare const getRegisteredMMKVInstanceIds: () => string[];
|
|
|
37
37
|
*/
|
|
38
38
|
export declare const calculateByteSize: (str: string) => number;
|
|
39
39
|
/**
|
|
40
|
-
* Determine type and format value safely
|
|
40
|
+
* Determine type and format value safely without blocking JS thread
|
|
41
41
|
*/
|
|
42
42
|
export declare const analyzeStorageValue: (rawVal: any) => {
|
|
43
43
|
strVal: string;
|
|
44
|
-
|
|
44
|
+
parsedValue?: any;
|
|
45
45
|
type: "json" | "string" | "number" | "boolean" | "null";
|
|
46
46
|
byteSize: number;
|
|
47
47
|
};
|