|
1 | 1 | import React from 'react'; |
2 | | -import { View, Text, FlatList, TouchableOpacity, StyleSheet, SafeAreaView, Share, Platform, Alert } from 'react-native'; |
3 | | -import { useTheme, ThemeColors } from '@/theme'; |
| 2 | +import { View, Text, FlatList, StyleSheet, SafeAreaView, Share, Platform, Alert } from 'react-native'; |
| 3 | +import { useTheme, type Theme } from '@/theme'; |
| 4 | +import { Badge, Button, Heading, Mono, type Tone } from '@/components/ui'; |
4 | 5 | import { useConnectionLog, ConnectionEvent, ConnectionEventType } from '@/contexts/ConnectionLogContext'; |
5 | 6 | import { getLogPath, readLog, clearLog } from '@/utils/logger'; |
6 | 7 |
|
7 | | -const EVENT_BADGE_COLORS: Record<ConnectionEventType, string> = { |
8 | | - scan_start: '#007AFF', |
9 | | - scan_stop: '#8E8E93', |
10 | | - scan_result: '#5856D6', |
11 | | - connect_start: '#FF9500', |
12 | | - connect_success: '#34C759', |
13 | | - connect_fail: '#FF3B30', |
14 | | - disconnect: '#FF3B30', |
15 | | - battery_read: '#34C759', |
16 | | - audio_start: '#007AFF', |
17 | | - audio_stop: '#8E8E93', |
18 | | - error: '#FF3B30', |
19 | | - health_ping: '#34C759', |
20 | | - reconnect_attempt: '#FF9500', |
21 | | - reconnect_backoff: '#FF9500', |
22 | | - bt_state_change: '#5856D6', |
23 | | - ws_connecting: '#FF9500', |
24 | | - ws_open: '#34C759', |
25 | | - ws_close: '#FF3B30', |
26 | | - ws_error: '#FF3B30', |
27 | | - ws_reconnect: '#FF9500', |
28 | | - ws_reauth: '#AF52DE', |
29 | | - net_change: '#5856D6', |
| 8 | +const EVENT_BADGE_TONES: Record<ConnectionEventType, Tone> = { |
| 9 | + scan_start: 'info', |
| 10 | + scan_stop: 'neutral', |
| 11 | + scan_result: 'suggest', |
| 12 | + connect_start: 'warning', |
| 13 | + connect_success: 'success', |
| 14 | + connect_fail: 'danger', |
| 15 | + disconnect: 'danger', |
| 16 | + battery_read: 'success', |
| 17 | + audio_start: 'info', |
| 18 | + audio_stop: 'neutral', |
| 19 | + error: 'danger', |
| 20 | + health_ping: 'success', |
| 21 | + reconnect_attempt: 'warning', |
| 22 | + reconnect_backoff: 'warning', |
| 23 | + bt_state_change: 'suggest', |
| 24 | + ws_connecting: 'warning', |
| 25 | + ws_open: 'success', |
| 26 | + ws_close: 'danger', |
| 27 | + ws_error: 'danger', |
| 28 | + ws_reconnect: 'warning', |
| 29 | + ws_reauth: 'suggest', |
| 30 | + net_change: 'suggest', |
30 | 31 | }; |
31 | 32 |
|
32 | 33 | function formatTime(date: Date): string { |
33 | 34 | return date.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); |
34 | 35 | } |
35 | 36 |
|
36 | | -function EventItem({ event, colors }: { event: ConnectionEvent; colors: ThemeColors }) { |
37 | | - const badgeColor = EVENT_BADGE_COLORS[event.type] || colors.textTertiary; |
| 37 | +function EventItem({ event, t }: { event: ConnectionEvent; t: Theme }) { |
| 38 | + const s = createItemStyles(t); |
| 39 | + const badgeTone = EVENT_BADGE_TONES[event.type] ?? 'neutral'; |
38 | 40 |
|
39 | 41 | return ( |
40 | | - <View style={[itemStyles.row, { borderBottomColor: colors.separator }]}> |
41 | | - <Text style={[itemStyles.time, { color: colors.textTertiary }]}>{formatTime(event.timestamp)}</Text> |
42 | | - <View style={[itemStyles.badge, { backgroundColor: badgeColor }]}> |
43 | | - <Text style={itemStyles.badgeText}>{event.type.replace(/_/g, ' ')}</Text> |
44 | | - </View> |
45 | | - <View style={itemStyles.details}> |
46 | | - {event.deviceName && <Text style={[itemStyles.device, { color: colors.text }]}>{event.deviceName}</Text>} |
47 | | - {event.details && <Text style={[itemStyles.detail, { color: colors.textSecondary }]} numberOfLines={2}>{event.details}</Text>} |
48 | | - {event.rssi != null && <Text style={[itemStyles.detail, { color: colors.textTertiary }]}>RSSI: {event.rssi} dBm</Text>} |
| 42 | + <View style={s.row}> |
| 43 | + <Mono style={s.time}>{formatTime(event.timestamp)}</Mono> |
| 44 | + <Badge tone={badgeTone} mono style={s.badge}>{event.type.replace(/_/g, ' ')}</Badge> |
| 45 | + <View style={s.details}> |
| 46 | + {event.deviceName && <Text style={s.device}>{event.deviceName}</Text>} |
| 47 | + {event.details && <Text style={s.detail} numberOfLines={2}>{event.details}</Text>} |
| 48 | + {event.rssi != null && <Text style={s.detailMuted}>RSSI: {event.rssi} dBm</Text>} |
49 | 49 | </View> |
50 | 50 | </View> |
51 | 51 | ); |
52 | 52 | } |
53 | 53 |
|
54 | | -const itemStyles = StyleSheet.create({ |
| 54 | +const createItemStyles = (t: Theme) => StyleSheet.create({ |
55 | 55 | row: { |
56 | 56 | flexDirection: 'row', |
57 | 57 | alignItems: 'flex-start', |
58 | | - paddingVertical: 8, |
59 | | - paddingHorizontal: 12, |
60 | | - borderBottomWidth: 1, |
| 58 | + paddingVertical: t.space[2], |
| 59 | + paddingHorizontal: t.space[3], |
| 60 | + borderBottomWidth: t.borderWidth, |
| 61 | + borderBottomColor: t.color.border.subtle, |
61 | 62 | }, |
62 | 63 | time: { |
63 | | - fontSize: 11, |
64 | | - fontFamily: 'monospace', |
| 64 | + fontFamily: t.font.mono, |
65 | 65 | width: 65, |
66 | 66 | marginTop: 3, |
67 | 67 | }, |
68 | 68 | badge: { |
69 | | - paddingHorizontal: 6, |
70 | | - paddingVertical: 2, |
71 | | - borderRadius: 4, |
72 | | - marginRight: 8, |
73 | | - marginTop: 2, |
74 | | - }, |
75 | | - badgeText: { |
76 | | - color: 'white', |
77 | | - fontSize: 10, |
78 | | - fontWeight: '600', |
79 | | - textTransform: 'uppercase', |
| 69 | + marginRight: t.space[2], |
| 70 | + marginTop: t.space[0.5], |
80 | 71 | }, |
81 | 72 | details: { |
82 | 73 | flex: 1, |
83 | 74 | }, |
84 | 75 | device: { |
85 | | - fontSize: 13, |
86 | | - fontWeight: '500', |
| 76 | + fontFamily: t.font.sans, |
| 77 | + ...t.type.sm, |
| 78 | + fontWeight: t.weight.medium, |
| 79 | + color: t.color.text.primary, |
87 | 80 | }, |
88 | 81 | detail: { |
89 | | - fontSize: 12, |
| 82 | + fontFamily: t.font.sans, |
| 83 | + ...t.type.xs, |
| 84 | + color: t.color.text.secondary, |
| 85 | + marginTop: 1, |
| 86 | + }, |
| 87 | + detailMuted: { |
| 88 | + fontFamily: t.font.sans, |
| 89 | + ...t.type.xs, |
| 90 | + color: t.color.text.muted, |
90 | 91 | marginTop: 1, |
91 | 92 | }, |
92 | 93 | }); |
93 | 94 |
|
94 | 95 | export default function DiagnosticsScreen() { |
95 | | - const { colors } = useTheme(); |
| 96 | + const t = useTheme(); |
| 97 | + const s = createScreenStyles(t); |
96 | 98 | const { events, clearEvents } = useConnectionLog(); |
97 | 99 |
|
98 | 100 | const shareLogFile = async () => { |
@@ -120,102 +122,87 @@ export default function DiagnosticsScreen() { |
120 | 122 | }; |
121 | 123 |
|
122 | 124 | return ( |
123 | | - <SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}> |
124 | | - <View style={[screenStyles.logBar, { borderBottomColor: colors.separator, backgroundColor: colors.card }]}> |
125 | | - <Text style={[screenStyles.logBarTitle, { color: colors.text }]}>Crash Log</Text> |
126 | | - <Text style={[screenStyles.logBarPath, { color: colors.textTertiary }]} numberOfLines={1}>{getLogPath()}</Text> |
127 | | - <View style={screenStyles.logBarRow}> |
128 | | - <TouchableOpacity onPress={shareLogFile} style={[screenStyles.logBtn, { backgroundColor: colors.inputBackground }]}> |
129 | | - <Text style={[screenStyles.logBtnText, { color: colors.text }]}>Share Log File</Text> |
130 | | - </TouchableOpacity> |
131 | | - <TouchableOpacity onPress={wipeLogFile} style={[screenStyles.logBtn, { backgroundColor: colors.inputBackground }]}> |
132 | | - <Text style={[screenStyles.logBtnText, { color: colors.danger }]}>Clear File</Text> |
133 | | - </TouchableOpacity> |
| 125 | + <SafeAreaView style={s.safeArea}> |
| 126 | + <View style={s.logBar}> |
| 127 | + <Text style={s.logBarTitle}>Crash Log</Text> |
| 128 | + <Mono style={s.logBarPath} numberOfLines={1}>{getLogPath()}</Mono> |
| 129 | + <View style={s.logBarRow}> |
| 130 | + <Button variant="secondary" size="sm" style={s.logBtn} onPress={shareLogFile}>Share Log File</Button> |
| 131 | + <Button variant="danger" size="sm" style={s.logBtn} onPress={wipeLogFile}>Clear File</Button> |
134 | 132 | </View> |
135 | 133 | </View> |
136 | | - <View style={[screenStyles.header, { borderBottomColor: colors.separator }]}> |
137 | | - <Text style={[screenStyles.title, { color: colors.text }]}>Connection Log ({events.length})</Text> |
138 | | - <TouchableOpacity onPress={clearEvents} style={[screenStyles.clearButton, { backgroundColor: colors.inputBackground }]}> |
139 | | - <Text style={[screenStyles.clearText, { color: colors.danger }]}>Clear</Text> |
140 | | - </TouchableOpacity> |
| 134 | + <View style={s.header}> |
| 135 | + <Heading>Connection Log ({events.length})</Heading> |
| 136 | + <Button variant="danger" size="sm" onPress={clearEvents}>Clear</Button> |
141 | 137 | </View> |
142 | 138 |
|
143 | 139 | {events.length === 0 ? ( |
144 | | - <View style={screenStyles.empty}> |
145 | | - <Text style={[screenStyles.emptyText, { color: colors.textTertiary }]}>No events recorded yet. Scan or connect a device to see events here.</Text> |
| 140 | + <View style={s.empty}> |
| 141 | + <Text style={s.emptyText}>No events recorded yet. Scan or connect a device to see events here.</Text> |
146 | 142 | </View> |
147 | 143 | ) : ( |
148 | 144 | <FlatList |
149 | 145 | data={events} |
150 | | - renderItem={({ item }) => <EventItem event={item} colors={colors} />} |
| 146 | + renderItem={({ item }) => <EventItem event={item} t={t} />} |
151 | 147 | keyExtractor={(item) => item.id} |
152 | | - style={{ backgroundColor: colors.card }} |
| 148 | + style={s.list} |
153 | 149 | /> |
154 | 150 | )} |
155 | 151 | </SafeAreaView> |
156 | 152 | ); |
157 | 153 | } |
158 | 154 |
|
159 | | -const screenStyles = StyleSheet.create({ |
| 155 | +const createScreenStyles = (t: Theme) => StyleSheet.create({ |
| 156 | + safeArea: { |
| 157 | + flex: 1, |
| 158 | + backgroundColor: t.color.surface.page, |
| 159 | + }, |
| 160 | + list: { |
| 161 | + backgroundColor: t.color.surface.raised, |
| 162 | + }, |
160 | 163 | header: { |
161 | 164 | flexDirection: 'row', |
162 | 165 | justifyContent: 'space-between', |
163 | 166 | alignItems: 'center', |
164 | | - paddingHorizontal: 16, |
165 | | - paddingVertical: 12, |
166 | | - borderBottomWidth: 1, |
167 | | - }, |
168 | | - title: { |
169 | | - fontSize: 17, |
170 | | - fontWeight: '600', |
171 | | - }, |
172 | | - clearButton: { |
173 | | - paddingHorizontal: 12, |
174 | | - paddingVertical: 6, |
175 | | - borderRadius: 6, |
176 | | - }, |
177 | | - clearText: { |
178 | | - fontSize: 14, |
179 | | - fontWeight: '500', |
| 167 | + paddingHorizontal: t.space[4], |
| 168 | + paddingVertical: t.space[3], |
| 169 | + borderBottomWidth: t.borderWidth, |
| 170 | + borderBottomColor: t.color.border.subtle, |
180 | 171 | }, |
181 | 172 | empty: { |
182 | 173 | flex: 1, |
183 | 174 | justifyContent: 'center', |
184 | 175 | alignItems: 'center', |
185 | | - padding: 40, |
| 176 | + padding: t.space[10], |
186 | 177 | }, |
187 | 178 | emptyText: { |
188 | | - fontSize: 15, |
| 179 | + fontFamily: t.font.sans, |
| 180 | + ...t.type.base, |
| 181 | + color: t.color.text.muted, |
189 | 182 | textAlign: 'center', |
190 | 183 | }, |
191 | 184 | logBar: { |
192 | | - paddingHorizontal: 16, |
193 | | - paddingVertical: 10, |
194 | | - borderBottomWidth: 1, |
| 185 | + paddingHorizontal: t.space[4], |
| 186 | + paddingVertical: t.space[3], |
| 187 | + borderBottomWidth: t.borderWidth, |
| 188 | + borderBottomColor: t.color.border.subtle, |
| 189 | + backgroundColor: t.color.surface.raised, |
195 | 190 | }, |
196 | 191 | logBarTitle: { |
197 | | - fontSize: 15, |
198 | | - fontWeight: '600', |
| 192 | + fontFamily: t.font.sans, |
| 193 | + ...t.type.base, |
| 194 | + fontWeight: t.weight.semibold, |
| 195 | + color: t.color.text.primary, |
199 | 196 | }, |
200 | 197 | logBarPath: { |
201 | | - fontSize: 10, |
202 | | - fontFamily: 'monospace', |
203 | | - marginTop: 2, |
204 | | - marginBottom: 8, |
| 198 | + marginTop: t.space[0.5], |
| 199 | + marginBottom: t.space[2], |
205 | 200 | }, |
206 | 201 | logBarRow: { |
207 | 202 | flexDirection: 'row', |
208 | | - gap: 8, |
| 203 | + gap: t.space[2], |
209 | 204 | }, |
210 | 205 | logBtn: { |
211 | 206 | flex: 1, |
212 | | - paddingHorizontal: 12, |
213 | | - paddingVertical: 8, |
214 | | - borderRadius: 6, |
215 | | - alignItems: 'center', |
216 | | - }, |
217 | | - logBtnText: { |
218 | | - fontSize: 13, |
219 | | - fontWeight: '500', |
220 | 207 | }, |
221 | 208 | }); |
0 commit comments