feat(components): ActionButton stopAlert + typography type fixes
This commit is contained in:
1 parent
2c51fd2623
commit
bbe2919c2c
89 files changed
+75993
-9961
No files matched your search
@@ -0,0 +1,423 @@
|
||||
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 { colors, radius, spacing, typography } from '../theme';
|
||||
|
||||
type Props = {
|
||||
readonly monitor: HeartRateMonitorState;
|
||||
readonly onMonitorAction: () => Promise<void>;
|
||||
};
|
||||
|
||||
export function SensorScreen({ monitor, onMonitorAction }: Props) {
|
||||
const device = monitor.connectedDevice;
|
||||
const services = monitor.services.length
|
||||
? monitor.services
|
||||
: ['Heart Rate Service · 0x180D · aguardando conexão'];
|
||||
const characteristics = monitor.characteristics.length
|
||||
? monitor.characteristics
|
||||
: ['Heart Rate Measurement · 0x2A37 · notify esperado'];
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
<View style={styles.sensorConsole}>
|
||||
<View style={styles.consoleTopline}>
|
||||
<View>
|
||||
<Text style={styles.eyebrow}>SENSOR REAL</Text>
|
||||
<Text style={styles.title}>
|
||||
{friendlyDeviceTitle(device?.name)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.phaseBadge}>
|
||||
<Bluetooth color={colors.warning} size={16} strokeWidth={2.4} />
|
||||
<Text style={styles.phaseText}>{phaseLabel(monitor.phase)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.signalGrid}>
|
||||
<SignalDatum
|
||||
icon="signal"
|
||||
label="RSSI"
|
||||
value={formatRssi(device?.rssi)}
|
||||
/>
|
||||
<SignalDatum
|
||||
icon="activity"
|
||||
label="Leituras"
|
||||
value={`${monitor.samples.length}`}
|
||||
/>
|
||||
<SignalDatum
|
||||
icon="battery"
|
||||
label="Bateria"
|
||||
value={formatBattery(device?.batteryPercent)}
|
||||
/>
|
||||
<SignalDatum
|
||||
icon="shield"
|
||||
label="Fundo"
|
||||
value={monitor.batteryOptimizationIgnored ? 'liberado' : 'restrito'}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={styles.explainer}>
|
||||
{monitor.errorMessage ?? sensorExplanation(monitor)}
|
||||
</Text>
|
||||
|
||||
<View style={styles.actionRow}>
|
||||
{!monitor.isMonitoring ? (
|
||||
<ActionButton
|
||||
label="Buscar sensor"
|
||||
onPress={() => {
|
||||
onMonitorAction().catch(() => undefined);
|
||||
}}
|
||||
showIcon={false}
|
||||
variant="primary"
|
||||
/>
|
||||
) : (
|
||||
<View style={styles.livePill}>
|
||||
<Activity color={colors.success} size={15} strokeWidth={2.4} />
|
||||
<Text style={styles.livePillText}>Monitor contínuo ativo</Text>
|
||||
</View>
|
||||
)}
|
||||
{!monitor.batteryOptimizationIgnored ? (
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
monitor.openBatterySettings().catch(() => undefined)
|
||||
}
|
||||
style={styles.secondaryButton}
|
||||
>
|
||||
<Text style={styles.secondaryButtonText}>Bateria</Text>
|
||||
</Pressable>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{monitor.discoveredDevices.length > 0 ? (
|
||||
<View style={styles.discoveryBlock}>
|
||||
<Text style={styles.blockLabel}>ENCONTRADOS AGORA</Text>
|
||||
{monitor.discoveredDevices.map(candidate => (
|
||||
<Pressable
|
||||
key={candidate.id}
|
||||
onPress={() =>
|
||||
monitor.connectToDevice(candidate.id).catch(() => undefined)
|
||||
}
|
||||
style={styles.deviceRow}
|
||||
>
|
||||
<View style={styles.deviceDot} />
|
||||
<View style={styles.deviceInfo}>
|
||||
<Text style={styles.deviceName}>
|
||||
{friendlyDeviceTitle(candidate.name)}
|
||||
</Text>
|
||||
<Text style={styles.deviceMeta}>
|
||||
{candidate.name} · {formatRssi(candidate.rssi)}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.connectPill}>
|
||||
<Text style={styles.connectPillText}>Conectar</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
<BleInventoryBlock title="SERVIÇOS BLE" rows={services} />
|
||||
<BleInventoryBlock title="CARACTERÍSTICAS" rows={characteristics} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<View style={styles.signalDatum}>
|
||||
<Icon color={colors.warning} size={17} strokeWidth={2.3} />
|
||||
<Text style={styles.signalValue}>{value}</Text>
|
||||
<Text style={styles.signalLabel}>{label}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function BleInventoryBlock({
|
||||
title,
|
||||
rows,
|
||||
}: {
|
||||
readonly title: string;
|
||||
readonly rows: readonly string[];
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.diagnosticBlock}>
|
||||
<Text style={styles.blockLabel}>{title}</Text>
|
||||
{rows.slice(0, 8).map(row => {
|
||||
const parsed = parseBleRow(row);
|
||||
return (
|
||||
<View key={row} style={styles.diagnosticRow}>
|
||||
<RadioReceiver color={colors.textDim} size={14} strokeWidth={2.2} />
|
||||
<View style={styles.diagnosticBody}>
|
||||
<Text style={styles.diagnosticTitle}>{parsed.name}</Text>
|
||||
<Text style={styles.diagnosticText}>
|
||||
{parsed.uuid} · {parsed.detail}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
function friendlyDeviceTitle(name?: string | null) {
|
||||
if (!name) {
|
||||
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 name;
|
||||
}
|
||||
|
||||
function parseBleRow(row: string) {
|
||||
const [name = row, uuid = 'BLE', ...rest] = row.split(' · ');
|
||||
return {
|
||||
name,
|
||||
uuid,
|
||||
detail: rest.join(' · ') || 'aguardando leitura',
|
||||
};
|
||||
}
|
||||
|
||||
function phaseLabel(phase: HeartRateMonitorState['phase']) {
|
||||
const labels: Record<HeartRateMonitorState['phase'], string> = {
|
||||
idle: 'parado',
|
||||
'requesting-permission': 'permissões',
|
||||
'bluetooth-off': 'bluetooth off',
|
||||
scanning: 'buscando',
|
||||
connecting: 'conectando',
|
||||
connected: 'conectado',
|
||||
receiving: 'recebendo',
|
||||
'permission-denied': 'sem permissão',
|
||||
'sensor-not-found': 'sem sensor',
|
||||
error: 'erro',
|
||||
};
|
||||
return labels[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.`;
|
||||
}
|
||||
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.';
|
||||
}
|
||||
if (monitor.phase === 'scanning') {
|
||||
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.';
|
||||
}
|
||||
|
||||
function formatBattery(percent?: number | null) {
|
||||
if (typeof percent === 'number') {
|
||||
return `${percent}%`;
|
||||
}
|
||||
return '--';
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
screen: {
|
||||
gap: spacing.sm,
|
||||
},
|
||||
sensorConsole: {
|
||||
borderTopWidth: 1,
|
||||
borderColor: colors.line,
|
||||
paddingVertical: spacing.md,
|
||||
gap: spacing.md,
|
||||
},
|
||||
consoleTopline: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
gap: spacing.md,
|
||||
},
|
||||
eyebrow: {
|
||||
...typography.label,
|
||||
color: colors.action,
|
||||
},
|
||||
title: {
|
||||
color: colors.text,
|
||||
fontSize: 34,
|
||||
lineHeight: 36,
|
||||
fontWeight: '900',
|
||||
letterSpacing: -2,
|
||||
marginTop: spacing.xs,
|
||||
},
|
||||
phaseBadge: {
|
||||
paddingVertical: spacing.xs,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
height: 36,
|
||||
},
|
||||
phaseText: {
|
||||
...typography.label,
|
||||
color: colors.text,
|
||||
},
|
||||
signalGrid: {
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
signalDatum: {
|
||||
width: '48.5%',
|
||||
minHeight: 76,
|
||||
borderWidth: 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,
|
||||
color: colors.textMuted,
|
||||
},
|
||||
actionRow: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
alignItems: 'center',
|
||||
},
|
||||
livePill: {
|
||||
minHeight: 48,
|
||||
flex: 1,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.line,
|
||||
paddingHorizontal: spacing.md,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
},
|
||||
livePillText: {
|
||||
...typography.label,
|
||||
color: colors.success,
|
||||
},
|
||||
secondaryButton: {
|
||||
minHeight: 48,
|
||||
borderWidth: 1,
|
||||
borderColor: colors.line,
|
||||
borderRadius: radius.xl,
|
||||
paddingHorizontal: spacing.md,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
secondaryButtonText: {
|
||||
...typography.label,
|
||||
color: colors.warning,
|
||||
},
|
||||
discoveryBlock: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.line,
|
||||
paddingTop: spacing.md,
|
||||
gap: spacing.sm,
|
||||
},
|
||||
blockLabel: {
|
||||
...typography.label,
|
||||
color: colors.warning,
|
||||
},
|
||||
deviceRow: {
|
||||
flexDirection: 'row',
|
||||
gap: spacing.sm,
|
||||
alignItems: 'center',
|
||||
paddingVertical: spacing.sm,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.line,
|
||||
},
|
||||
deviceDot: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: 5,
|
||||
backgroundColor: colors.success,
|
||||
},
|
||||
deviceInfo: {
|
||||
flex: 1,
|
||||
},
|
||||
deviceName: {
|
||||
color: colors.text,
|
||||
fontSize: 18,
|
||||
lineHeight: 22,
|
||||
fontWeight: '900',
|
||||
},
|
||||
deviceMeta: {
|
||||
...typography.body,
|
||||
color: colors.textMuted,
|
||||
marginTop: spacing.xxs,
|
||||
},
|
||||
connectPill: {
|
||||
borderWidth: 1,
|
||||
borderColor: colors.line,
|
||||
paddingHorizontal: spacing.sm,
|
||||
paddingVertical: spacing.xs,
|
||||
},
|
||||
connectPillText: {
|
||||
...typography.label,
|
||||
color: colors.warning,
|
||||
},
|
||||
diagnosticBlock: {
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: colors.line,
|
||||
paddingTop: spacing.sm,
|
||||
gap: spacing.xs,
|
||||
},
|
||||
diagnosticRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: spacing.xs,
|
||||
paddingVertical: spacing.xs,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: colors.line,
|
||||
},
|
||||
diagnosticBody: {
|
||||
flex: 1,
|
||||
gap: 2,
|
||||
},
|
||||
diagnosticTitle: {
|
||||
...typography.label,
|
||||
color: colors.text,
|
||||
},
|
||||
diagnosticText: {
|
||||
flex: 1,
|
||||
color: colors.textMuted,
|
||||
fontSize: 12,
|
||||
lineHeight: 16,
|
||||
fontWeight: '700',
|
||||
},
|
||||
});
|
||||
Reference in new issue
Block a user