feat: ThemeContext + Settings tabs (Interface / App / Sync)

- ThemeContext: dynamic palette (Default/Giro/Tour/Vuelta), font size
  (small/medium/large), bold labels — all persisted to AsyncStorage
- Settings: three top tabs; Interface tab has palette picker + font
  size pills + bold labels toggle; App tab has km notifications;
  Sync tab has bincio instance login + autarchive placeholder
- RecordingScreen: stat labels now use theme accent colour and scale
  with fontSize; font weight follows boldLabels setting
- All accent/accentDim usages migrated from static colors to useTheme()
This commit is contained in:
Davide Scaini
2026-06-03 10:00:27 +02:00
parent ea938e5644
commit efc7af4a4a
8 changed files with 366 additions and 197 deletions
+49 -51
View File
@@ -9,20 +9,15 @@ import { useRecordingStore } from '../store/recording';
import { startGpsRecording, stopGpsRecording, requestLocationPermissions } from '../services/gps';
import { RootStackParamList } from '../types';
import { colors } from '../theme';
import { useTheme } from '../ThemeContext';
const MAP_STYLE = 'https://tiles.openfreemap.org/styles/liberty';
const trackLineStyle: LineLayerStyle = {
lineColor: colors.accent,
lineWidth: 3,
lineJoin: 'round',
lineCap: 'round',
};
type Nav = NativeStackNavigationProp<RootStackParamList>;
export function RecordingScreen() {
const nav = useNavigation<Nav>();
const { accent, accentDim, scale, boldLabels } = useTheme();
const { status, ble, keepAwake, trackPoints, start, pause, resume, stop, setKeepAwake, getStats } = useRecordingStore();
const [stats, setStats] = useState(getStats());
const intervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
@@ -33,44 +28,48 @@ export function RecordingScreen() {
}, []);
useEffect(() => {
if (keepAwake && status === 'recording') {
activateKeepAwakeAsync();
} else {
deactivateKeepAwake();
}
if (keepAwake && status === 'recording') activateKeepAwakeAsync();
else deactivateKeepAwake();
}, [keepAwake, status]);
const trackLineStyle = useMemo<LineLayerStyle>(() => ({
lineColor: accent,
lineWidth: 3,
lineJoin: 'round',
lineCap: 'round',
}), [accent]);
const trackGeoJSON = useMemo<GeoJSON.Feature<GeoJSON.LineString>>(() => ({
type: 'Feature',
geometry: { type: 'LineString', coordinates: trackPoints.map((p) => [p.lon, p.lat]) },
properties: {},
}), [trackPoints]);
const statLabelStyle = useMemo(() => ({
color: accent,
fontSize: Math.round(10 * scale),
fontWeight: boldLabels ? '700' as const : '500' as const,
textTransform: 'uppercase' as const,
letterSpacing: 0.5,
}), [accent, scale, boldLabels]);
const statValueStyle = useMemo(() => ({
color: colors.text,
fontSize: Math.round(17 * scale),
fontWeight: '600' as const,
marginTop: 2,
}), [scale]);
async function handleStart() {
const granted = await requestLocationPermissions();
if (!granted) {
Alert.alert('Permission required', 'Location permission is required to record.');
return;
}
if (!granted) { Alert.alert('Permission required', 'Location permission is required to record.'); return; }
start();
await startGpsRecording();
}
async function handlePause() {
await stopGpsRecording();
pause();
}
async function handleResume() {
resume();
await startGpsRecording();
}
async function handleStop() {
await stopGpsRecording();
stop();
nav.navigate('PostRecording');
}
async function handlePause() { await stopGpsRecording(); pause(); }
async function handleResume() { resume(); await startGpsRecording(); }
async function handleStop() { await stopGpsRecording(); stop(); nav.navigate('PostRecording'); }
const formatDuration = (secs: number) => {
const h = Math.floor(secs / 3600).toString().padStart(2, '0');
@@ -82,14 +81,14 @@ export function RecordingScreen() {
return (
<View style={styles.container}>
<View style={styles.statsGrid}>
<StatBox label="Time" value={formatDuration(stats.elapsedSeconds)} />
<StatBox label="Distance" value={`${(stats.distanceMeters / 1000).toFixed(2)} km`} />
<StatBox label="Speed" value={`${stats.currentSpeedKph.toFixed(1)} km/h`} />
<StatBox label="Avg Speed" value={`${stats.avgSpeedKph.toFixed(1)} km/h`} />
<StatBox label="Elevation" value={`+${stats.elevationGainMeters.toFixed(0)} m`} />
<StatBox label="HR" value={ble.hr ? `${ble.hr} bpm` : '—'} />
<StatBox label="Power" value={ble.power ? `${ble.power} W` : '—'} />
<StatBox label="Cadence" value={ble.cadence ? `${ble.cadence} rpm` : '—'} />
<StatBox label="Time" value={formatDuration(stats.elapsedSeconds)} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
<StatBox label="Distance" value={`${(stats.distanceMeters / 1000).toFixed(2)} km`} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
<StatBox label="Speed" value={`${stats.currentSpeedKph.toFixed(1)} km/h`} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
<StatBox label="Avg Speed" value={`${stats.avgSpeedKph.toFixed(1)} km/h`} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
<StatBox label="Elevation" value={`+${stats.elevationGainMeters.toFixed(0)} m`} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
<StatBox label="HR" value={ble.hr ? `${ble.hr} bpm` : '—'} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
<StatBox label="Power" value={ble.power ? `${ble.power} W` : '—'} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
<StatBox label="Cadence" value={ble.cadence ? `${ble.cadence} rpm` : '—'} labelStyle={statLabelStyle} valueStyle={statValueStyle} />
</View>
<View style={styles.mapArea}>
@@ -105,15 +104,14 @@ export function RecordingScreen() {
</GeoJSONSource>
)}
</Map>
<TouchableOpacity style={styles.sensorBtn} onPress={() => nav.navigate('SensorPairing')}>
<Text style={styles.sensorBtnText}> Sensors</Text>
<TouchableOpacity style={[styles.sensorBtn, { borderColor: colors.border }]} onPress={() => nav.navigate('SensorPairing')}>
<Text style={[styles.sensorBtnText, { color: accent }]}> Sensors</Text>
</TouchableOpacity>
</View>
<View style={styles.controls}>
<TouchableOpacity
style={[styles.awakeBtn, keepAwake && styles.awakeBtnOn]}
style={[styles.awakeBtn, keepAwake && { borderColor: accent }]}
onPress={() => setKeepAwake(!keepAwake)}
>
<Text style={styles.awakeBtnText}>{keepAwake ? '◑ Awake' : '◌ Sleep'}</Text>
@@ -149,11 +147,14 @@ export function RecordingScreen() {
);
}
function StatBox({ label, value }: { label: string; value: string }) {
function StatBox({ label, value, labelStyle, valueStyle }: {
label: string; value: string;
labelStyle: object; valueStyle: object;
}) {
return (
<View style={styles.statBox}>
<Text style={styles.statLabel}>{label}</Text>
<Text style={styles.statValue}>{value}</Text>
<Text style={labelStyle}>{label}</Text>
<Text style={valueStyle}>{value}</Text>
</View>
);
}
@@ -162,14 +163,11 @@ const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bg },
statsGrid: { flexDirection: 'row', flexWrap: 'wrap', padding: 8, borderBottomWidth: 1, borderBottomColor: colors.border },
statBox: { width: '25%', padding: 8, alignItems: 'center' },
statLabel: { color: colors.textMuted, fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5 },
statValue: { color: colors.text, fontSize: 17, fontWeight: '600', marginTop: 2 },
mapArea: { flex: 1, overflow: 'hidden' },
sensorBtn: { position: 'absolute', top: 10, right: 10, backgroundColor: 'rgba(9,9,11,0.8)', borderRadius: 8, borderWidth: 1, borderColor: colors.border, paddingVertical: 6, paddingHorizontal: 12 },
sensorBtnText: { color: colors.accent, fontSize: 13, fontWeight: '600' },
sensorBtn: { position: 'absolute', top: 10, right: 10, backgroundColor: 'rgba(9,9,11,0.8)', borderRadius: 8, borderWidth: 1, paddingVertical: 6, paddingHorizontal: 12 },
sensorBtnText: { fontSize: 13, fontWeight: '600' },
controls: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 12, padding: 20, borderTopWidth: 1, borderTopColor: colors.border },
awakeBtn: { backgroundColor: colors.surface, borderRadius: 20, borderWidth: 1, borderColor: colors.border, paddingVertical: 8, paddingHorizontal: 14 },
awakeBtnOn: { borderColor: colors.accent },
awakeBtnText: { color: colors.textSub, fontSize: 13 },
btn: { paddingVertical: 15, paddingHorizontal: 30, borderRadius: 50 },
btnStart: { backgroundColor: colors.btnStart },