feat: seasonal race palette (Giro/Tour/Vuelta) + mobile picker

- theme.ts: useTheme() hook with race calendar (May–Sep windows),
  auto-detects Giro/Tour/Vuelta by date; stores override in SQLite
- All screens (feed, import, activity, tab bar) now use accent/dim
  from useTheme() instead of hardcoded #60a5fa
- Settings: Palette section with Auto/Default/Giro/Tour/Vuelta buttons
  to override the auto-detected palette for testing
This commit is contained in:
Davide Scaini
2026-04-25 15:41:20 +02:00
parent 5330b7b489
commit dfe5307ab4
6 changed files with 111 additions and 50 deletions
+45 -27
View File
@@ -5,6 +5,7 @@ import {
Text, TextInput, View,
} from 'react-native';
import { deleteRemoteActivities, getSetting, setSetting, useSetting } from '@/db/queries';
import { PALETTES, type PaletteKey, useTheme } from '@/theme';
export default function SettingsScreen() {
const db = useSQLiteContext();
@@ -22,6 +23,9 @@ export default function SettingsScreen() {
const [syncMode, setSyncMode] = useState(storedSyncMode);
const [syncUpload, setSyncUpload] = useState(storedSyncUpload);
const [saved, setSaved] = useState(false);
const theme = useTheme();
const storedPalette = (useSetting('palette_override') ?? 'auto') as PaletteKey;
const [palette, setPalette] = useState<PaletteKey>(storedPalette);
const [password, setPassword] = useState('');
const [connecting, setConnecting] = useState(false);
@@ -181,16 +185,10 @@ export default function SettingsScreen() {
<Section title="Sync">
<Text style={styles.subLabel}>Download</Text>
<View style={styles.modeRow}>
<ModeButton
label="Summaries only"
active={syncMode === 'summaries'}
onPress={() => { setSyncMode('summaries'); setSetting(db, 'sync_mode', 'summaries'); }}
/>
<ModeButton
label="Full data"
active={syncMode === 'full'}
onPress={() => { setSyncMode('full'); setSetting(db, 'sync_mode', 'full'); }}
/>
<ModeButton label="Summaries only" active={syncMode === 'summaries'} accent={theme.accent} dim={theme.dim}
onPress={() => { setSyncMode('summaries'); setSetting(db, 'sync_mode', 'summaries'); }} />
<ModeButton label="Full data" active={syncMode === 'full'} accent={theme.accent} dim={theme.dim}
onPress={() => { setSyncMode('full'); setSetting(db, 'sync_mode', 'full'); }} />
</View>
<Text style={styles.hint}>
{syncMode === 'full'
@@ -199,16 +197,10 @@ export default function SettingsScreen() {
</Text>
<Text style={[styles.subLabel, { borderTopWidth: 1, borderTopColor: '#27272a', paddingTop: 12 }]}>Upload</Text>
<View style={styles.modeRow}>
<ModeButton
label="Off"
active={!syncUpload}
onPress={() => { setSyncUpload(false); setSetting(db, 'sync_upload', 'false'); }}
/>
<ModeButton
label="Upload local activities"
active={syncUpload}
onPress={() => { setSyncUpload(true); setSetting(db, 'sync_upload', 'true'); }}
/>
<ModeButton label="Off" active={!syncUpload} accent={theme.accent} dim={theme.dim}
onPress={() => { setSyncUpload(false); setSetting(db, 'sync_upload', 'false'); }} />
<ModeButton label="Upload local activities" active={syncUpload} accent={theme.accent} dim={theme.dim}
onPress={() => { setSyncUpload(true); setSetting(db, 'sync_upload', 'true'); }} />
</View>
<Text style={styles.hint}>
{syncUpload
@@ -217,6 +209,32 @@ export default function SettingsScreen() {
</Text>
</Section>
<Section title="Palette">
<Text style={[styles.hint, { paddingBottom: 0 }]}>
Auto-switches to race colours during Giro, Tour, and Vuelta. Override here for testing.
</Text>
<View style={styles.modeRow}>
{(['auto', 'default', 'giro', 'tour', 'vuelta'] as PaletteKey[]).map(key => {
const label = key === 'auto' ? 'Auto' : PALETTES[key as keyof typeof PALETTES].label;
const keyAccent = key === 'auto' ? theme.accent : PALETTES[key as keyof typeof PALETTES].accent;
const keyDim = key === 'auto' ? theme.dim : PALETTES[key as keyof typeof PALETTES].dim;
return (
<ModeButton
key={key}
label={label}
active={palette === key}
accent={keyAccent}
dim={keyDim}
onPress={() => {
setPalette(key);
setSetting(db, 'palette_override', key);
}}
/>
);
})}
</View>
</Section>
<Section title="Data">
<Pressable
style={[styles.resetButton, resetArmed && styles.resetButtonArmed]}
@@ -274,13 +292,15 @@ function Field({
);
}
function ModeButton({ label, active, onPress }: { label: string; active: boolean; onPress: () => void }) {
function ModeButton({ label, active, accent, dim, onPress }: {
label: string; active: boolean; accent: string; dim: string; onPress: () => void;
}) {
return (
<Pressable
style={[styles.modeButton, active && styles.modeButtonActive]}
style={[styles.modeButton, active && { backgroundColor: dim, borderColor: accent }]}
onPress={onPress}
>
<Text style={[styles.modeButtonText, active && styles.modeButtonTextActive]}>{label}</Text>
<Text style={[styles.modeButtonText, active && { color: accent }]}>{label}</Text>
</Pressable>
);
}
@@ -338,10 +358,8 @@ const styles = StyleSheet.create({
msgErr: { color: '#fca5a5', fontSize: 13, paddingHorizontal: 12, paddingBottom: 10 },
subLabel: { color: '#52525b', fontSize: 11, fontWeight: '600', letterSpacing: 0.6, paddingHorizontal: 12, paddingTop: 12, paddingBottom: 4 },
modeRow: { flexDirection: 'row', gap: 8, padding: 12 },
modeButton: { flex: 1, paddingVertical: 9, borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46', alignItems: 'center' },
modeButtonActive: { backgroundColor: '#1e3a5f', borderColor: '#2563eb' },
modeButtonText: { color: '#71717a', fontSize: 13, fontWeight: '500' },
modeButtonTextActive: { color: '#60a5fa' },
modeButton: { flex: 1, paddingVertical: 9, borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46', alignItems: 'center' },
modeButtonText: { color: '#71717a', fontSize: 13, fontWeight: '500' },
resetButton: {
margin: 12, paddingVertical: 10, alignItems: 'center',
borderRadius: 8, borderWidth: 1, borderColor: '#3f3f46',