From 06475346dac33c138110395620a6c332f8389f16 Mon Sep 17 00:00:00 2001 From: fightrate-bot Date: Tue, 16 Jun 2026 01:57:06 -0300 Subject: [PATCH] feat(sensor): SensorScreen with DialTile + LiveDot --- src/screens/SensorScreen.tsx | 173 ++++++++++++++++------------------- 1 file changed, 78 insertions(+), 95 deletions(-) diff --git a/src/screens/SensorScreen.tsx b/src/screens/SensorScreen.tsx index afc48dd..bd61fa2 100644 --- a/src/screens/SensorScreen.tsx +++ b/src/screens/SensorScreen.tsx @@ -1,16 +1,14 @@ import React from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { - Activity, - Battery, Bluetooth, RadioReceiver, - ShieldCheck, - Signal, } from 'lucide-react-native'; import { type HeartRateMonitorState } from '../ble/useHeartRateMonitor'; import { formatRssi } from '../ble/rssi'; import { ActionButton } from '../components/ActionButton'; +import { DialTile } from '../components/DialTile'; +import { LiveDot } from '../components/LiveDot'; import { colors, radius, spacing, typography } from '../theme'; type Props = { @@ -22,47 +20,60 @@ export function SensorScreen({ monitor, onMonitorAction }: Props) { const device = monitor.connectedDevice; const services = monitor.services.length ? monitor.services - : ['Heart Rate Service · 0x180D · aguardando conexão']; + : ['Heart Rate Service \· 0x180D \· aguardando conex\ão']; const characteristics = monitor.characteristics.length ? monitor.characteristics - : ['Heart Rate Measurement · 0x2A37 · notify esperado']; + : ['Heart Rate Measurement \· 0x2A37 \· notify esperado']; + + const isReceiving = monitor.phase === 'receiving'; return ( - - SENSOR REAL + + + + {'\•'} + + SENSOR REAL + {friendlyDeviceTitle(device?.name)} - + {phaseLabel(monitor.phase)} + {isReceiving ? : null} - - + - - - @@ -82,8 +93,8 @@ export function SensorScreen({ monitor, onMonitorAction }: Props) { /> ) : ( - - Monitor contínuo ativo + + Monitor cont\ínuo ativo )} {!monitor.batteryOptimizationIgnored ? ( @@ -102,7 +113,7 @@ export function SensorScreen({ monitor, onMonitorAction }: Props) { {monitor.discoveredDevices.length > 0 ? ( ENCONTRADOS AGORA - {monitor.discoveredDevices.map(candidate => ( + {monitor.discoveredDevices.map((candidate) => ( @@ -116,7 +127,7 @@ export function SensorScreen({ monitor, onMonitorAction }: Props) { {friendlyDeviceTitle(candidate.name)} - {candidate.name} · {formatRssi(candidate.rssi)} + {candidate.name} \· {formatRssi(candidate.rssi)} @@ -127,34 +138,8 @@ export function SensorScreen({ monitor, onMonitorAction }: Props) { ) : null} - - - - ); -} - -function SignalDatum({ - icon, - label, - value, -}: { - readonly icon: 'signal' | 'battery' | 'shield' | 'activity'; - readonly label: string; - readonly value: string; -}) { - const Icon = - icon === 'signal' - ? Signal - : icon === 'battery' - ? Battery - : icon === 'activity' - ? Activity - : ShieldCheck; - return ( - - - {value} - {label} + + ); } @@ -169,7 +154,7 @@ function BleInventoryBlock({ return ( {title} - {rows.slice(0, 8).map(row => { + {rows.slice(0, 8).map((row) => { const parsed = parseBleRow(row); return ( @@ -177,7 +162,7 @@ function BleInventoryBlock({ {parsed.name} - {parsed.uuid} · {parsed.detail} + {parsed.uuid} \· {parsed.detail} @@ -189,36 +174,36 @@ function BleInventoryBlock({ function friendlyDeviceTitle(name?: string | null) { if (!name) { - return 'Cinta cardíaca BLE'; + return 'Cinta card\íaca BLE'; } if (/magene|h003/i.test(name)) { return 'Magene H003'; } if (/heart|hrm/i.test(name)) { - return 'Cinta cardíaca BLE'; + return 'Cinta card\íaca BLE'; } return name; } function parseBleRow(row: string) { - const [name = row, uuid = 'BLE', ...rest] = row.split(' · '); + const [name = row, uuid = 'BLE', ...rest] = row.split(' \· '); return { name, uuid, - detail: rest.join(' · ') || 'aguardando leitura', + detail: rest.join(' \· ') || 'aguardando leitura', }; } function phaseLabel(phase: HeartRateMonitorState['phase']) { const labels: Record = { idle: 'parado', - 'requesting-permission': 'permissões', + 'requesting-permission': 'permiss\ões', 'bluetooth-off': 'bluetooth off', scanning: 'buscando', connecting: 'conectando', connected: 'conectado', receiving: 'recebendo', - 'permission-denied': 'sem permissão', + 'permission-denied': 'sem permiss\ão', 'sensor-not-found': 'sem sensor', error: 'erro', }; @@ -227,15 +212,15 @@ function phaseLabel(phase: HeartRateMonitorState['phase']) { function sensorExplanation(monitor: HeartRateMonitorState) { if (monitor.phase === 'receiving' && monitor.samples.length > 0) { - return `Amostras reais chegando: ${monitor.samples.length}. Cada BPM reconhecido está sendo salvo em SQLite.`; + return `Amostras reais chegando: ${monitor.samples.length}. Cada BPM reconhecido est\á sendo salvo em SQLite.`; } if (monitor.phase === 'receiving' || monitor.phase === 'connected') { - return 'Sensor reconhecido. O app está armando a leitura 2A37; se não vier BPM, reativa notify e reconecta sozinho.'; + return 'Sensor reconhecido. O app est\á armando a leitura 2A37; se n\ão vier BPM, reativa notify e reconecta sozinho.'; } if (monitor.phase === 'scanning') { - return 'Busca nativa BLE ativa. Se o app for bloqueado, a notificação mantém o serviço vivo.'; + return 'Busca nativa BLE ativa. Se o app for bloqueado, a notifica\ç\ão mant\ém o servi\ço vivo.'; } - return 'A tela traduz nomes técnicos: 180D é o serviço cardíaco; 2A37 é o dado de BPM.'; + return 'A tela traduz nomes t\écnicos: 180D \é o servi\ço card\íaco; 2A37 \é o dado de BPM.'; } function formatBattery(percent?: number | null) { @@ -259,54 +244,52 @@ const styles = StyleSheet.create({ flexDirection: 'row', justifyContent: 'space-between', gap: spacing.md, + alignItems: 'center', + }, + consoleLeft: { + flex: 1, + }, + eyebrowRow: { + flexDirection: 'row', + alignItems: 'center', + gap: 6, + }, + eyebrowDot: { + fontSize: 18, + lineHeight: 18, + fontWeight: '900', }, eyebrow: { ...typography.label, color: colors.action, }, title: { + ...typography.detailNumeric, color: colors.text, - fontSize: 34, - lineHeight: 36, - fontWeight: '900', - letterSpacing: -2, + fontSize: 22, + lineHeight: 26, + letterSpacing: -0.8, marginTop: spacing.xs, }, phaseBadge: { paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, flexDirection: 'row', alignItems: 'center', - gap: spacing.xs, - height: 36, + gap: 6, + height: 32, + borderRadius: radius.xl, + borderWidth: 1, + borderColor: colors.line, + backgroundColor: colors.surface, }, phaseText: { ...typography.label, color: colors.text, }, - signalGrid: { - flexDirection: 'row', - flexWrap: 'wrap', - gap: spacing.xs, - }, - signalDatum: { - width: '48.5%', - minHeight: 76, - borderWidth: 1, + signalColumn: { + borderTopWidth: 1, borderColor: colors.line, - padding: spacing.sm, - gap: spacing.xxs, - justifyContent: 'center', - }, - signalValue: { - color: colors.text, - fontSize: 18, - lineHeight: 22, - fontWeight: '900', - marginTop: spacing.xs, - }, - signalLabel: { - ...typography.label, - color: colors.textMuted, }, explainer: { ...typography.body, @@ -325,7 +308,7 @@ const styles = StyleSheet.create({ paddingHorizontal: spacing.md, flexDirection: 'row', alignItems: 'center', - gap: spacing.xs, + gap: 6, }, livePillText: { ...typography.label,