From f22bb303a081d6779592b90a68feee2203d7962a Mon Sep 17 00:00:00 2001 From: hc Date: Mon, 13 Apr 2026 16:37:52 +0800 Subject: Dashboard with continuous scan, unified device list, AsyncStorage persistence --- App.tsx | 836 ++++++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 550 insertions(+), 286 deletions(-) (limited to 'App.tsx') diff --git a/App.tsx b/App.tsx index 9dca202..4ce26df 100644 --- a/App.tsx +++ b/App.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect, useRef} from 'react'; +import React, {useState, useEffect, useRef, useCallback} from 'react'; import { StyleSheet, View, @@ -11,9 +11,10 @@ import { DeviceEventEmitter, NativeEventEmitter, NativeModules, - Alert, + ActivityIndicator, } from 'react-native'; import {SafeAreaProvider, SafeAreaView} from 'react-native-safe-area-context'; +import AsyncStorage from '@react-native-async-storage/async-storage'; // ── Native module types ────────────────────────────────────────────── @@ -59,34 +60,44 @@ interface IBP550BTModule { getAllConnectedDevices(): void; } -const iHealthDeviceManagerModule = - NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; -const BP550BTModule = - NativeModules.BP550BTModule as IBP550BTModule; +const mgr = NativeModules.iHealthDeviceManagerModule as IHealthDeviceManager; +const bp550 = NativeModules.BP550BTModule as IBP550BTModule; -type Device = { +// ── Types ──────────────────────────────────────────────────────────── + +type Device = {mac: string; type: string; rssi?: number; timestamp: number}; + +type SavedDevice = {mac: string; type: string; addedAt: string}; + +type Reading = { mac: string; - type: string; - rssi?: number; - timestamp: number; + sys?: number; + dia?: number; + pulse?: number; + date?: string; + battery?: number; + fetchedAt: string; }; +type Screen = 'home' | 'dashboard' | 'debug' | 'debug-device'; + +const STORAGE_KEY_DEVICES = '@ihealth/saved_devices'; +const STORAGE_KEY_READINGS = '@ihealth/readings'; + // ── Helpers ────────────────────────────────────────────────────────── async function requestAndroidPermissions(): Promise { if (Platform.OS !== 'android') return true; - const permissions: string[] = []; + const perms: string[] = []; if (Platform.Version >= 31) { - permissions.push( + perms.push( PermissionsAndroid.PERMISSIONS.BLUETOOTH_SCAN, PermissionsAndroid.PERMISSIONS.BLUETOOTH_CONNECT, ); } - permissions.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); - const results = await PermissionsAndroid.requestMultiple(permissions as any); - return Object.values(results).every( - r => r === PermissionsAndroid.RESULTS.GRANTED, - ); + perms.push(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION); + const r = await PermissionsAndroid.requestMultiple(perms as any); + return Object.values(r).every(v => v === PermissionsAndroid.RESULTS.GRANTED); } function getEmitter() { @@ -101,329 +112,450 @@ function getBPEmitter() { : DeviceEventEmitter; } -// ── Device Detail Screen ───────────────────────────────────────────── +// ── Home Screen ────────────────────────────────────────────────────── -function DeviceScreen({ - device, - onBack, -}: { - device: Device; - onBack: () => void; -}) { - const [connected, setConnected] = useState(false); - const [connecting, setConnecting] = useState(false); - const [log, setLog] = useState([]); +function HomeScreen({onNav}: {onNav: (s: Screen) => void}) { + return ( + + + iHealth + Blood Pressure Monitor - const addLog = (msg: string) => - setLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev]); + onNav('dashboard')}> + Dashboard + View your devices and readings + + + onNav('debug')}> + + Debug Scanner + + + Scan all devices, raw logs + + + + + ); +} + +// ── Dashboard Screen ───────────────────────────────────────────────── +function DashboardScreen({onBack}: {onBack: () => void}) { + const [savedDevices, setSavedDevices] = useState([]); + const [readings, setReadings] = useState([]); + const [foundDevices, setFoundDevices] = useState([]); + const [status, setStatus] = useState('Starting...'); + const foundRef = useRef([]); + const savedRef = useRef([]); + const readingsRef = useRef([]); + const syncingMac = useRef(null); + + // Load saved data on mount + useEffect(() => { + (async () => { + const [devJson, readJson] = await Promise.all([ + AsyncStorage.getItem(STORAGE_KEY_DEVICES), + AsyncStorage.getItem(STORAGE_KEY_READINGS), + ]); + const devs: SavedDevice[] = devJson ? JSON.parse(devJson) : []; + const reads: Reading[] = readJson ? JSON.parse(readJson) : []; + setSavedDevices(devs); + savedRef.current = devs; + setReadings(reads); + readingsRef.current = reads; + await requestAndroidPermissions(); + })(); + }, []); + + // Always-on scan loop: restart scan every time it finishes useEffect(() => { const emitter = getEmitter(); + const bpEmitter = getBPEmitter(); - const connSub = emitter.addListener( - iHealthDeviceManagerModule.Event_Device_Connected ?? 'event_device_connected', - (e: {mac: string; type: string}) => { - if (e.mac === device.mac) { - setConnected(true); - setConnecting(false); - addLog(`Connected to ${e.type}`); + const startScan = () => { + try { mgr.startDiscovery('ALL'); } + catch (_) {} + setStatus('Scanning...'); + }; + + // When a device is found + const scanSub = emitter.addListener( + mgr.Event_Scan_Device ?? 'event_scan_device', + (e: {mac: string; type: string; rssi?: number}) => { + if (e.type === 'AM3') return; + + // Track all found devices + const exists = foundRef.current.find(d => d.mac === e.mac); + if (!exists) { + const updated = [...foundRef.current, {mac: e.mac, type: e.type, rssi: e.rssi, timestamp: Date.now()}]; + foundRef.current = updated; + setFoundDevices(updated); + } + + // Auto-connect if it's a saved device we haven't synced yet + const isSaved = savedRef.current.some(d => d.mac === e.mac); + if (isSaved && !syncingMac.current) { + syncingMac.current = e.mac; + setStatus(`Found ${e.mac.slice(-4)}, connecting...`); + try { mgr.stopDiscovery(); } catch (_) {} + mgr.connectDevice(e.mac, e.type); } }, ); - const failSub = emitter.addListener( - iHealthDeviceManagerModule.Event_Device_Connect_Failed ?? 'event_device_connect_failed', - (e: {mac: string; errorid?: number}) => { - if (e.mac === device.mac) { - setConnecting(false); - addLog(`Connection failed (error: ${e.errorid ?? 'unknown'})`); + // When scan finishes, restart after a short delay + const finSub = emitter.addListener( + mgr.Event_Scan_Finish ?? 'event_scan_finish', + () => { + if (!syncingMac.current) { + setTimeout(startScan, 3000); } }, ); - const dcSub = emitter.addListener( - iHealthDeviceManagerModule.Event_Device_Disconnect ?? 'event_device_disconnect', - (e: {mac: string}) => { - if (e.mac === device.mac) { - setConnected(false); - addLog('Disconnected'); + // When connected, pull data + const connSub = emitter.addListener( + mgr.Event_Device_Connected ?? 'event_device_connected', + async (e: {mac: string; type: string}) => { + if (e.mac !== syncingMac.current) return; + setStatus(`Connected to ${e.mac.slice(-4)}, reading...`); + + // Get battery + const battery = await new Promise(resolve => { + const sub = bpEmitter.addListener( + bp550?.Event_Notify ?? 'event_notify', + (ev: Record) => { + if (ev.action === 'battery_bp' && ev.battery != null) { + sub.remove(); resolve(ev.battery as number); + } + }, + ); + bp550.getBattery(e.mac); + setTimeout(() => { sub.remove(); resolve(undefined); }, 5000); + }); + + // Get offline data + const offlineData = await new Promise[]>(resolve => { + const results: Record[] = []; + const sub = bpEmitter.addListener( + bp550?.Event_Notify ?? 'event_notify', + (ev: Record) => { + if (ev.action === 'historicaldata_bp' && ev.data) { + results.push(...(ev.data as Record[])); + } + if (ev.action === 'get_historical_over_bp') { + sub.remove(); resolve(results); + } + if (ev.action === 'offlinenum' && ev.offlinenum === 0) { + sub.remove(); resolve(results); + } + }, + ); + bp550.getOffLineData(e.mac); + setTimeout(() => { sub.remove(); resolve(results); }, 10000); + }); + + // Save new readings + const newReadings = [...readingsRef.current]; + for (const d of offlineData) { + newReadings.push({ + mac: e.mac, sys: d.sys as number | undefined, + dia: d.dia as number | undefined, + pulse: d.heartRate as number | undefined, + date: d.date as string | undefined, + battery, fetchedAt: new Date().toISOString(), + }); } + if (offlineData.length === 0 && battery != null) { + newReadings.push({mac: e.mac, battery, fetchedAt: new Date().toISOString()}); + } + readingsRef.current = newReadings; + setReadings(newReadings); + await AsyncStorage.setItem(STORAGE_KEY_READINGS, JSON.stringify(newReadings)); + + // Disconnect and resume scanning + try { bp550.disconnect(e.mac); } catch (_) {} + setStatus(`Synced ${e.mac.slice(-4)} (${offlineData.length} readings)`); + syncingMac.current = null; + setTimeout(startScan, 3000); }, ); - // BP550BT event listener - const bpEmitter = getBPEmitter(); - const notifySub = bpEmitter.addListener( - BP550BTModule?.Event_Notify ?? 'event_notify', - (e: Record) => { - addLog(JSON.stringify(e, null, 2)); + const failSub = emitter.addListener( + mgr.Event_Device_Connect_Failed ?? 'event_device_connect_failed', + () => { + setStatus('Connect failed, resuming scan...'); + syncingMac.current = null; + setTimeout(startScan, 3000); }, ); + const dcSub = emitter.addListener( + mgr.Event_Device_Disconnect ?? 'event_device_disconnect', + () => { + if (syncingMac.current) { + syncingMac.current = null; + setTimeout(startScan, 3000); + } + }, + ); + + // Start first scan + startScan(); + return () => { - connSub.remove(); - failSub.remove(); - dcSub.remove(); - notifySub.remove(); + scanSub.remove(); finSub.remove(); connSub.remove(); + failSub.remove(); dcSub.remove(); + try { mgr.stopDiscovery(); } catch (_) {} }; - }, [device.mac]); + }, []); - const connect = () => { - setConnecting(true); - addLog(`Connecting to ${device.mac}...`); - iHealthDeviceManagerModule.connectDevice(device.mac, device.type); + const addDevice = async (dev: Device) => { + const newDev: SavedDevice = {mac: dev.mac, type: dev.type, addedAt: new Date().toISOString()}; + const updated = [...savedRef.current, newDev]; + savedRef.current = updated; + setSavedDevices(updated); + await AsyncStorage.setItem(STORAGE_KEY_DEVICES, JSON.stringify(updated)); + const f = foundRef.current.filter(d => d.mac !== dev.mac); + foundRef.current = f; + setFoundDevices(f); }; - const disconnect = () => { - addLog('Disconnecting...'); - if (BP550BTModule) { - BP550BTModule.disconnect(device.mac); - } else { - iHealthDeviceManagerModule.disconnectDevice(device.mac, device.type); - } + const removeDevice = async (mac: string) => { + const updated = savedRef.current.filter(d => d.mac !== mac); + savedRef.current = updated; + setSavedDevices(updated); + await AsyncStorage.setItem(STORAGE_KEY_DEVICES, JSON.stringify(updated)); }; - const actions: {label: string; fn: () => void; needsConnect: boolean}[] = [ - { - label: 'Get Battery', - needsConnect: true, - fn: () => { - addLog('Requesting battery...'); - BP550BTModule.getBattery(device.mac); - }, - }, - { - label: 'Get Function Info', - needsConnect: true, - fn: () => { - addLog('Requesting function info...'); - BP550BTModule.getFunctionInfo(device.mac); - }, - }, - { - label: 'Get Offline Count', - needsConnect: true, - fn: () => { - addLog('Requesting offline data count...'); - BP550BTModule.getOffLineNum(device.mac); - }, - }, - { - label: 'Get Offline Data', - needsConnect: true, - fn: () => { - addLog('Requesting offline data...'); - BP550BTModule.getOffLineData(device.mac); - }, - }, - { - label: 'Get IDPS', - needsConnect: false, - fn: () => { - addLog('Requesting IDPS...'); - iHealthDeviceManagerModule.getDevicesIDPS(device.mac, (idps) => { - addLog(`IDPS: ${JSON.stringify(idps, null, 2)}`); - }); - }, - }, - ]; + const isDeviceSaved = (mac: string) => savedRef.current.some(d => d.mac === mac); + + const recentReadings = readings + .filter(r => r.sys != null) + .slice(-20) + .reverse(); return ( - - - Back + + + Home - {device.type} - {device.mac} - - {connecting ? 'Connecting...' : connected ? 'Connected' : 'Disconnected'} - + Dashboard 🦄✨ - {!connected && !connecting && ( - - Connect - - )} - {connected && ( - - Disconnect - - )} + {/* Show status only when actively syncing a device */} + {status.includes('connecting') || status.includes('reading') || status.includes('Synced') ? ( + + + {status} + + ) : null} - Actions - - {actions.map(a => ( - - - {a.label} - - - ))} - + {/* All devices — unified list, saved ones marked */} + + Devices ({foundDevices.length} nearby, {savedDevices.length} saved) + + + {/* Merge: all found devices + saved devices not currently found */} + {(() => { + const allMacs = new Set([ + ...foundDevices.map(d => d.mac), + ...savedDevices.map(d => d.mac), + ]); + const merged = Array.from(allMacs).map(mac => { + const found = foundDevices.find(d => d.mac === mac); + const saved = savedDevices.find(d => d.mac === mac); + return { + mac, + type: found?.type ?? saved?.type ?? 'KN550', + rssi: found?.rssi, + nearby: !!found, + saved: !!saved, + }; + }); + // Sort: saved first, then nearby, then rest + merged.sort((a, b) => { + if (a.saved !== b.saved) return a.saved ? -1 : 1; + if (a.nearby !== b.nearby) return a.nearby ? -1 : 1; + return 0; + }); + return merged.map(d => ( + + + BP + + + + {d.type} + {d.saved ? ' ' : ''} + {d.saved && SAVED} + + + {d.mac} + {!d.nearby ? ' (offline)' : d.rssi != null ? ` ${d.rssi} dBm` : ''} + + + {d.saved ? ( + removeDevice(d.mac)}> + Remove + + ) : ( + addDevice({mac: d.mac, type: d.type, timestamp: Date.now()})}> + Save + + )} + + )); + })()} - Log + {/* Readings */} + + Readings ({recentReadings.length}) + String(i)} - renderItem={({item}) => {item}} - style={styles.logList} + style={s.list} + renderItem={({item}) => ( + + + {item.sys ?? '--'} + / + {item.dia ?? '--'} + mmHg + {item.pulse ?? '--'} bpm + + + + {item.date ?? item.fetchedAt.split('T')[0]} + + {item.mac} + + + )} + ListEmptyComponent={ + + {savedDevices.length === 0 + ? 'Add a device to start tracking' + : 'No readings yet. Sync to pull data.'} + + } /> ); } -// ── Scanner Screen ─────────────────────────────────────────────────── +// ── Debug Scanner Screen ───────────────────────────────────────────── -function ScannerScreen({onSelectDevice}: {onSelectDevice: (d: Device) => void}) { +function DebugScannerScreen({ + onBack, + onSelectDevice, +}: { + onBack: () => void; + onSelectDevice: (d: Device) => void; +}) { const [devices, setDevices] = useState([]); const [scanning, setScanning] = useState(false); const [error, setError] = useState(null); const devicesRef = useRef([]); useEffect(() => { - if (!iHealthDeviceManagerModule) { - setError('iHealth native module not found.'); - return; - } + if (!mgr) { setError('iHealth native module not found.'); return; } const emitter = getEmitter(); - - const scanSub = emitter.addListener( - iHealthDeviceManagerModule.Event_Scan_Device ?? 'event_scan_device', - (event: {mac: string; type: string; rssi?: number}) => { - const {mac = '', type = 'Unknown', rssi} = event; - if (type === 'AM3') return; - - const existing = devicesRef.current.findIndex(d => d.mac === mac); + const sub = emitter.addListener( + mgr.Event_Scan_Device ?? 'event_scan_device', + (e: {mac: string; type: string; rssi?: number}) => { + if (e.type === 'AM3') return; + const {mac = '', type = 'Unknown', rssi} = e; + const idx = devicesRef.current.findIndex(d => d.mac === mac); let updated: Device[]; - if (existing >= 0) { + if (idx >= 0) { updated = [...devicesRef.current]; - updated[existing] = {mac, type, rssi, timestamp: Date.now()}; + updated[idx] = {mac, type, rssi, timestamp: Date.now()}; } else { - updated = [ - ...devicesRef.current, - {mac, type, rssi, timestamp: Date.now()}, - ]; + updated = [...devicesRef.current, {mac, type, rssi, timestamp: Date.now()}]; } devicesRef.current = updated; setDevices(updated); }, ); - - const finishSub = emitter.addListener( - iHealthDeviceManagerModule.Event_Scan_Finish ?? 'event_scan_finish', + const finSub = emitter.addListener( + mgr.Event_Scan_Finish ?? 'event_scan_finish', () => setScanning(false), ); - - return () => { - scanSub.remove(); - finishSub.remove(); - }; + return () => { sub.remove(); finSub.remove(); }; }, []); const startScan = async () => { setError(null); - const granted = await requestAndroidPermissions(); - if (!granted) { - setError('Bluetooth permissions denied'); - return; - } + const ok = await requestAndroidPermissions(); + if (!ok) { setError('Permissions denied'); return; } devicesRef.current = []; setDevices([]); setScanning(true); - if (Platform.OS === 'android') { - try { iHealthDeviceManagerModule.startDiscovery('ALL'); } - catch (e) { console.log('Discovery failed:', e); } + try { mgr.startDiscovery('ALL'); } catch (e) { console.log(e); } } else { - const iosTypes = ['KN550', 'BP3L', 'BP5S', 'BP7S', 'AM3S', 'AM4', + const types = ['KN550', 'BP3L', 'BP5S', 'BP7S', 'AM3S', 'AM4', 'PO3', 'HS2', 'HS2S', 'HS4S', 'BG5S', 'BG1S', 'PO1', 'ECG3']; let i = 0; - const scanNext = () => { - if (i < iosTypes.length) { - try { iHealthDeviceManagerModule.startDiscovery(iosTypes[i]); } - catch (e) { console.log(`Failed: ${iosTypes[i]}`, e); } + const next = () => { + if (i < types.length) { + try { mgr.startDiscovery(types[i]); } catch (_) {} i++; - setTimeout(scanNext, 2000); + setTimeout(next, 2000); } }; - setTimeout(scanNext, 2000); + setTimeout(next, 2000); } }; const stopScan = () => { - try { iHealthDeviceManagerModule.stopDiscovery(); } - catch (e) { console.log('Stop failed:', e); } + try { mgr.stopDiscovery(); } catch (_) {} setScanning(false); }; return ( - - iHealth Scanner - - {scanning - ? `Scanning... (${devices.length} found)` - : `${devices.length} device(s) found`} + + + Home + + Debug Scanner + + {scanning ? `Scanning... (${devices.length})` : `${devices.length} device(s)`} - - {error && {error}} - + {error && {error}} - - {scanning ? 'Stop Scan' : 'Start Scan'} - + {scanning ? 'Stop' : 'Start Scan'} - item.mac} renderItem={({item}) => ( - { - stopScan(); - onSelectDevice(item); - }}> - - - {item.type.substring(0, 3)} - + { stopScan(); onSelectDevice(item); }}> + + {item.type.substring(0, 3)} - - {item.type} - {item.mac} + + {item.type} + {item.mac} - {item.rssi != null && ( - {item.rssi} dBm - )} - {'>'} + {item.rssi != null && {item.rssi} dBm} + {'>'} )} - style={styles.list} - contentContainerStyle={devices.length === 0 && styles.emptyList} + style={s.list} + contentContainerStyle={devices.length === 0 && s.emptyList} ListEmptyComponent={ - - {scanning - ? 'Looking for iHealth devices...' - : 'Tap "Start Scan" to find nearby iHealth devices'} + + {scanning ? 'Scanning...' : 'Tap Start Scan'} } /> @@ -431,20 +563,125 @@ function ScannerScreen({onSelectDevice}: {onSelectDevice: (d: Device) => void}) ); } +// ── Debug Device Screen ────────────────────────────────────────────── + +function DebugDeviceScreen({device, onBack}: {device: Device; onBack: () => void}) { + const [connected, setConnected] = useState(false); + const [connecting, setConnecting] = useState(false); + const [log, setLog] = useState([]); + + const addLog = (msg: string) => + setLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev]); + + useEffect(() => { + const emitter = getEmitter(); + const bpEmitter = getBPEmitter(); + const connSub = emitter.addListener( + mgr.Event_Device_Connected ?? 'event_device_connected', + (e: {mac: string}) => { + if (e.mac === device.mac) { setConnected(true); setConnecting(false); addLog('Connected'); } + }, + ); + const failSub = emitter.addListener( + mgr.Event_Device_Connect_Failed ?? 'event_device_connect_failed', + (e: {mac: string; errorid?: number}) => { + if (e.mac === device.mac) { setConnecting(false); addLog(`Connect failed: ${e.errorid}`); } + }, + ); + const dcSub = emitter.addListener( + mgr.Event_Device_Disconnect ?? 'event_device_disconnect', + (e: {mac: string}) => { + if (e.mac === device.mac) { setConnected(false); addLog('Disconnected'); } + }, + ); + const notSub = bpEmitter.addListener( + bp550?.Event_Notify ?? 'event_notify', + (e: Record) => addLog(JSON.stringify(e, null, 2)), + ); + return () => { connSub.remove(); failSub.remove(); dcSub.remove(); notSub.remove(); }; + }, [device.mac]); + + const actions = [ + {label: 'Get Battery', fn: () => { addLog('Battery...'); bp550.getBattery(device.mac); }}, + {label: 'Function Info', fn: () => { addLog('FuncInfo...'); bp550.getFunctionInfo(device.mac); }}, + {label: 'Offline Count', fn: () => { addLog('OfflineNum...'); bp550.getOffLineNum(device.mac); }}, + {label: 'Offline Data', fn: () => { addLog('OfflineData...'); bp550.getOffLineData(device.mac); }}, + {label: 'IDPS', fn: () => { + addLog('IDPS...'); + mgr.getDevicesIDPS(device.mac, idps => addLog(JSON.stringify(idps, null, 2))); + }}, + ]; + + return ( + + + Back + + {device.type} + {device.mac} + + {connecting ? 'Connecting...' : connected ? 'Connected' : 'Disconnected'} + + {!connected && !connecting && ( + { + setConnecting(true); addLog('Connecting...'); + mgr.connectDevice(device.mac, device.type); + }}> + Connect + + )} + {connected && ( + { + addLog('Disconnecting...'); bp550.disconnect(device.mac); + }}> + Disconnect + + )} + + {actions.map(a => ( + + {a.label} + + ))} + + Log + String(i)} + renderItem={({item}) => {item}} + style={s.logList} + /> + + ); +} + // ── App Root ───────────────────────────────────────────────────────── function App() { - const [selectedDevice, setSelectedDevice] = useState(null); + const [screen, setScreen] = useState('home'); + const [debugDevice, setDebugDevice] = useState(null); return ( - {selectedDevice ? ( - setSelectedDevice(null)} + {screen === 'home' && } + {screen === 'dashboard' && ( + setScreen('home')} /> + )} + {screen === 'debug' && ( + setScreen('home')} + onSelectDevice={d => { setDebugDevice(d); setScreen('debug-device'); }} + /> + )} + {screen === 'debug-device' && debugDevice && ( + setScreen('debug')} /> - ) : ( - )} ); @@ -452,28 +689,41 @@ function App() { // ── Styles ─────────────────────────────────────────────────────────── -const styles = StyleSheet.create({ +const s = StyleSheet.create({ container: {flex: 1, backgroundColor: '#f5f5f5', paddingHorizontal: 16}, + // Home + homeCenter: {flex: 1, justifyContent: 'center'}, + homeTitle: {fontSize: 36, fontWeight: '800', color: '#1a1a1a', textAlign: 'center'}, + homeSubtitle: {fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 40}, + homeButton: { + backgroundColor: '#2196F3', padding: 20, borderRadius: 14, + marginBottom: 12, alignItems: 'center', + }, + homeButtonSecondary: {backgroundColor: '#fff', borderWidth: 1, borderColor: '#ddd'}, + homeButtonText: {color: '#fff', fontSize: 18, fontWeight: '700'}, + homeButtonTextSecondary: {color: '#333'}, + homeButtonSub: {color: 'rgba(255,255,255,0.8)', fontSize: 13, marginTop: 4}, + // Common title: {fontSize: 28, fontWeight: '700', color: '#1a1a1a', marginTop: 16}, subtitle: {fontSize: 14, color: '#666', marginTop: 4, marginBottom: 16}, - error: { - color: '#d32f2f', fontSize: 13, marginBottom: 8, - backgroundColor: '#ffebee', padding: 8, borderRadius: 6, - }, - button: { - backgroundColor: '#2196F3', paddingVertical: 14, - borderRadius: 10, alignItems: 'center', marginBottom: 16, - }, + sectionTitle: {fontSize: 16, fontWeight: '600', color: '#333', marginTop: 8, marginBottom: 8}, + error: {color: '#d32f2f', fontSize: 13, marginBottom: 8, backgroundColor: '#ffebee', padding: 8, borderRadius: 6}, + button: {backgroundColor: '#2196F3', paddingVertical: 14, borderRadius: 10, alignItems: 'center', marginBottom: 12}, buttonStop: {backgroundColor: '#f44336'}, buttonText: {color: '#fff', fontSize: 16, fontWeight: '600'}, + buttonOutline: {backgroundColor: 'transparent', borderWidth: 1.5, borderColor: '#2196F3'}, + buttonTextOutline: {color: '#2196F3'}, list: {flex: 1}, emptyList: {flex: 1, justifyContent: 'center', alignItems: 'center'}, emptyText: {color: '#999', fontSize: 15, textAlign: 'center'}, + backButton: {marginTop: 12, marginBottom: 4}, + backText: {color: '#2196F3', fontSize: 16}, + chevron: {fontSize: 18, color: '#ccc', marginLeft: 8}, + // Device rows deviceRow: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', padding: 14, borderRadius: 10, marginBottom: 8, - shadowColor: '#000', shadowOffset: {width: 0, height: 1}, - shadowOpacity: 0.05, shadowRadius: 2, elevation: 1, + shadowColor: '#000', shadowOffset: {width: 0, height: 1}, shadowOpacity: 0.05, shadowRadius: 2, elevation: 1, }, deviceIcon: { width: 44, height: 44, borderRadius: 22, backgroundColor: '#e3f2fd', @@ -482,37 +732,51 @@ const styles = StyleSheet.create({ deviceIconText: {fontSize: 12, fontWeight: '700', color: '#1565c0'}, deviceInfo: {flex: 1}, deviceType: {fontSize: 16, fontWeight: '600', color: '#1a1a1a'}, - deviceMac: { - fontSize: 12, color: '#888', marginTop: 2, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - }, + deviceMac: {fontSize: 12, color: '#888', marginTop: 2, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'}, deviceRssi: {fontSize: 12, color: '#999', marginLeft: 8}, - chevron: {fontSize: 18, color: '#ccc', marginLeft: 8}, - // Device screen - backButton: {marginTop: 12, marginBottom: 4}, - backText: {color: '#2196F3', fontSize: 16}, - statusBadge: { - fontSize: 13, color: '#f44336', marginTop: 8, marginBottom: 16, - fontWeight: '600', + // Dashboard + savedRow: { + flexDirection: 'row', alignItems: 'center', backgroundColor: '#fff', + padding: 14, borderRadius: 10, marginBottom: 8, elevation: 1, }, - statusConnected: {color: '#4caf50'}, - sectionTitle: { - fontSize: 16, fontWeight: '600', color: '#333', - marginTop: 8, marginBottom: 8, + removeText: {color: '#f44336', fontSize: 13, fontWeight: '600'}, + deviceOffline: {opacity: 0.5}, + deviceIconSaved: {backgroundColor: '#c8e6c9'}, + savedBadge: {fontSize: 10, color: '#4caf50', fontWeight: '700'}, + foundRow: { + flexDirection: 'row', alignItems: 'center', backgroundColor: '#e8f5e9', + padding: 14, borderRadius: 10, marginBottom: 8, }, - actionsRow: {flexGrow: 0, marginBottom: 12}, - actionButton: { - backgroundColor: '#e3f2fd', paddingVertical: 10, paddingHorizontal: 16, - borderRadius: 8, marginRight: 8, + addButton: {backgroundColor: '#4caf50', paddingVertical: 8, paddingHorizontal: 16, borderRadius: 8}, + addButtonText: {color: '#fff', fontSize: 14, fontWeight: '600'}, + syncBar: {flexDirection: 'row', alignItems: 'center', padding: 10, backgroundColor: '#e3f2fd', borderRadius: 8, marginBottom: 8}, + syncText: {color: '#1565c0', fontSize: 13, marginLeft: 8}, + syncDone: {color: '#4caf50', fontSize: 13, marginBottom: 8, fontWeight: '600'}, + scanningRow: {flexDirection: 'row', alignItems: 'center', padding: 12}, + scanningText: {color: '#666', fontSize: 13, marginLeft: 8}, + // Readings + readingRow: { + backgroundColor: '#fff', padding: 14, borderRadius: 10, marginBottom: 8, elevation: 1, }, + readingValues: {flexDirection: 'row', alignItems: 'baseline'}, + readingSys: {fontSize: 28, fontWeight: '700', color: '#1a1a1a'}, + readingSlash: {fontSize: 20, color: '#999', marginHorizontal: 4}, + readingDia: {fontSize: 28, fontWeight: '700', color: '#1a1a1a'}, + readingUnit: {fontSize: 12, color: '#999', marginLeft: 6}, + readingPulse: {fontSize: 14, color: '#666', marginLeft: 16}, + readingMeta: {flexDirection: 'row', justifyContent: 'space-between', marginTop: 4}, + readingDate: {fontSize: 12, color: '#999'}, + readingMac: {fontSize: 10, color: '#bbb', fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'}, + // Debug device + statusBadge: {fontSize: 13, color: '#f44336', marginTop: 8, marginBottom: 16, fontWeight: '600'}, + statusConnected: {color: '#4caf50'}, + actionsRow: {flexGrow: 0, marginBottom: 12}, + actionButton: {backgroundColor: '#e3f2fd', paddingVertical: 10, paddingHorizontal: 16, borderRadius: 8, marginRight: 8}, actionDisabled: {backgroundColor: '#eee'}, actionText: {color: '#1565c0', fontSize: 13, fontWeight: '600'}, actionTextDisabled: {color: '#bbb'}, logList: {flex: 1, marginTop: 4}, - logLine: { - fontSize: 11, color: '#555', paddingVertical: 3, - fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', - }, + logLine: {fontSize: 11, color: '#555', paddingVertical: 3, fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace'}, }); export default App; -- cgit