import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { View, Text, FlatList, TouchableOpacity, TextInput, StyleSheet, ScrollView, Alert, } from 'react-native'; import type { PluginComponentProps, StorageAdapter, DebuggerPlugin } from '../../core/types'; import { storageAdapterRegistry } from './storageAdapterRegistry'; import { copyToClipboard, safeStringify, safeParse } from '../../core/utils'; const StorageBrowserPanel: React.FC = ({ theme }) => { const [adapters, setAdapters] = useState>(new Map()); const [activeAdapterId, setActiveAdapterId] = useState(null); const [keys, setKeys] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [selectedKey, setSelectedKey] = useState(null); const [selectedValue, setSelectedValue] = useState(''); const [isEditing, setIsEditing] = useState(false); const [editValue, setEditValue] = useState(''); const [loading, setLoading] = useState(false); useEffect(() => { const unsubscribe = storageAdapterRegistry.subscribe(() => { const all = storageAdapterRegistry.getAll(); setAdapters(all); if (!activeAdapterId && all.size > 0) { setActiveAdapterId(Array.from(all.keys())[0]); } }); const all = storageAdapterRegistry.getAll(); setAdapters(all); if (all.size > 0) setActiveAdapterId(Array.from(all.keys())[0]); return unsubscribe; }, []); const activeAdapter = activeAdapterId ? adapters.get(activeAdapterId) : null; const loadKeys = useCallback(async () => { if (!activeAdapter) return; setLoading(true); try { const allKeys = await activeAdapter.getAllKeys(); setKeys(allKeys.sort()); } catch { setKeys([]); } setLoading(false); }, [activeAdapter]); useEffect(() => { loadKeys(); }, [loadKeys]); const handleSelectKey = useCallback( async (key: string) => { if (!activeAdapter) return; setSelectedKey(key); try { const value = await activeAdapter.getItem(key); setSelectedValue(value ?? ''); } catch { setSelectedValue('[Error reading value]'); } }, [activeAdapter], ); const handleSave = useCallback(async () => { if (!activeAdapter || !selectedKey) return; try { await activeAdapter.setItem(selectedKey, editValue); setSelectedValue(editValue); setIsEditing(false); } catch { /* ignore */ } }, [activeAdapter, selectedKey, editValue]); const handleDelete = useCallback( async (key: string) => { if (!activeAdapter) return; Alert.alert('Delete', `Delete "${key}"?`, [ { text: 'Cancel', style: 'cancel' }, { text: 'Delete', style: 'destructive', onPress: async () => { try { await activeAdapter.removeItem(key); if (selectedKey === key) { setSelectedKey(null); setSelectedValue(''); } loadKeys(); } catch { /* ignore */ } }, }, ]); }, [activeAdapter, selectedKey, loadKeys], ); const handleExportAll = useCallback(async () => { if (!activeAdapter) return; const data: Record = {}; for (const key of keys) { try { data[key] = await activeAdapter.getItem(key); } catch { data[key] = null; } } copyToClipboard(safeStringify(data)); }, [activeAdapter, keys]); const filteredKeys = useMemo(() => { if (!searchQuery) return keys; const q = searchQuery.toLowerCase(); return keys.filter((k) => k.toLowerCase().includes(q)); }, [keys, searchQuery]); const formattedValue = useMemo(() => { const parsed = safeParse(selectedValue); if (parsed !== null && typeof parsed === 'object') return safeStringify(parsed); return selectedValue; }, [selectedValue]); if (adapters.size === 0) { return ( 💾 No Storage Adapters Register an adapter:{'\n\n'} { 'setStorageAdapter("async", {\n name: "AsyncStorage",\n getAllKeys: () => AsyncStorage.getAllKeys(),\n getItem: (k) => AsyncStorage.getItem(k),\n setItem: (k,v) => AsyncStorage.setItem(k,v),\n removeItem: (k) => AsyncStorage.removeItem(k),\n})' } ); } if (selectedKey) { return ( { setSelectedKey(null); setIsEditing(false); }} activeOpacity={0.7} > ← Back {!isEditing && ( { setEditValue(selectedValue); setIsEditing(true); }} activeOpacity={0.7} > Edit )} copyToClipboard(selectedValue)} activeOpacity={0.7}> Copy handleDelete(selectedKey)} activeOpacity={0.7}> Delete {selectedKey} {isEditing ? ( Save setIsEditing(false)} activeOpacity={0.7}> Cancel ) : ( {formattedValue} )} ); } return ( {/* Adapter Selector */} {Array.from(adapters.keys()).length > 1 && ( {Array.from(adapters.entries()).map(([id, adapter]) => ( setActiveAdapterId(id)} activeOpacity={0.7} > {adapter.name} ))} )} {keys.length} keys ↻ Refresh Export item} renderItem={({ item }) => ( handleSelectKey(item)} onLongPress={() => handleDelete(item)} activeOpacity={0.7} > {item} )} ListEmptyComponent={ {loading ? 'Loading...' : 'No storage keys'} } /> ); }; const styles = StyleSheet.create({ container: { flex: 1 }, emptyContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, }, emptyIcon: { fontSize: 48, marginBottom: 16 }, emptyTitle: { fontSize: 16, fontWeight: '700', marginBottom: 8 }, emptyDesc: { fontSize: 13, textAlign: 'center', lineHeight: 20 }, adapterBar: { maxHeight: 44 }, adapterBarContent: { paddingHorizontal: 12, paddingVertical: 8, gap: 6 }, adapterChip: { paddingHorizontal: 12, paddingVertical: 4, borderRadius: 12, borderWidth: 1 }, adapterChipText: { fontSize: 12, fontWeight: '600' }, actionBar: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, }, keyCount: { fontSize: 13, fontWeight: '600' }, actionBarBtns: { flexDirection: 'row', gap: 12 }, actionText: { fontSize: 12, fontWeight: '600' }, searchContainer: { paddingHorizontal: 12, paddingVertical: 8 }, searchInput: { height: 36, borderRadius: 8, paddingHorizontal: 12, fontSize: 13, borderWidth: 1 }, keyRow: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 12, borderBottomWidth: StyleSheet.hairlineWidth, }, keyName: { fontSize: 13, fontFamily: 'monospace', flex: 1 }, keyArrow: { fontSize: 18 }, emptyListContainer: { alignItems: 'center', paddingVertical: 40 }, emptyText: { fontSize: 13 }, detailHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 12, paddingVertical: 10, borderBottomWidth: StyleSheet.hairlineWidth, }, backBtn: { fontSize: 14, fontWeight: '600' }, detailActions: { flexDirection: 'row', gap: 12 }, keyDisplay: { padding: 12 }, keyText: { fontSize: 12, fontFamily: 'monospace' }, valueScrollView: { flex: 1 }, valueBlock: { padding: 12, margin: 12, borderRadius: 8 }, valueCode: { fontSize: 12, fontFamily: 'monospace', lineHeight: 18 }, editSection: { flex: 1, padding: 12 }, editArea: { flex: 1, borderWidth: 1, borderRadius: 8, padding: 12, fontSize: 12, fontFamily: 'monospace', }, editBtns: { flexDirection: 'row', alignItems: 'center', gap: 12, marginTop: 12, justifyContent: 'flex-end', }, saveBtn: { paddingHorizontal: 20, paddingVertical: 8, borderRadius: 8 }, saveBtnText: { color: '#FFF', fontWeight: '700', fontSize: 13 }, cancelBtnText: { fontSize: 13 }, }); export function createStorageBrowserPlugin(): DebuggerPlugin { return { id: 'storage-browser', name: 'Storage', icon: '💾', component: StorageBrowserPanel, order: 60, }; }