From efc7af4a4afb44bcd5483bcea887a259b9f57f1a Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 3 Jun 2026 10:00:27 +0200 Subject: [PATCH] feat: ThemeContext + Settings tabs (Interface / App / Sync) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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() --- App.tsx | 14 +- src/ThemeContext.tsx | 75 +++++++ src/navigation/AppNavigator.tsx | 32 +-- src/screens/RecordingScreen.tsx | 100 +++++---- src/screens/SavedRecordingsScreen.tsx | 10 +- src/screens/SensorPairingScreen.tsx | 22 +- src/screens/SettingsScreen.tsx | 290 ++++++++++++++++---------- src/theme.ts | 20 +- 8 files changed, 366 insertions(+), 197 deletions(-) create mode 100644 src/ThemeContext.tsx diff --git a/App.tsx b/App.tsx index 6a713c7..ff33968 100644 --- a/App.tsx +++ b/App.tsx @@ -2,17 +2,17 @@ import { useEffect } from 'react'; import { StatusBar } from 'expo-status-bar'; import * as Notifications from 'expo-notifications'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { ThemeProvider } from './src/ThemeContext'; import { AppNavigator } from './src/navigation/AppNavigator'; import { requestNotificationPermissions } from './src/services/gps'; import { promptBatteryOptimizationIfNeeded } from './src/services/batteryOptimization'; -// Show notifications even when the app is in the foreground (iOS suppresses by default) Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowBanner: true, - shouldShowList: true, - shouldPlaySound: true, - shouldSetBadge: false, + shouldShowList: true, + shouldPlaySound: true, + shouldSetBadge: false, }), }); @@ -24,8 +24,10 @@ export default function App() { return ( - - + + + + ); } diff --git a/src/ThemeContext.tsx b/src/ThemeContext.tsx new file mode 100644 index 0000000..8bf7ea8 --- /dev/null +++ b/src/ThemeContext.tsx @@ -0,0 +1,75 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import { PALETTES, FONT_SCALE, type PaletteKey, type FontSizeKey } from './theme'; + +interface ThemeValue { + accent: string; + accentDim: string; + palette: PaletteKey; + setPalette: (p: PaletteKey) => void; + fontSize: FontSizeKey; + setFontSize: (s: FontSizeKey) => void; + boldLabels: boolean; + setBoldLabels: (b: boolean) => void; + scale: number; +} + +const ThemeContext = createContext({ + ...PALETTES.default, + palette: 'default', + setPalette: () => {}, + fontSize: 'medium', + setFontSize: () => {}, + boldLabels: false, + setBoldLabels: () => {}, + scale: 1, +}); + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const [palette, setPaletteState] = useState('default'); + const [fontSize, setFontSizeState] = useState('medium'); + const [boldLabels, setBoldLabelsState] = useState(false); + + useEffect(() => { + (async () => { + const [p, f, b] = await Promise.all([ + AsyncStorage.getItem('themePalette'), + AsyncStorage.getItem('themeFontSize'), + AsyncStorage.getItem('themeBoldLabels'), + ]); + if (p && p in PALETTES) setPaletteState(p as PaletteKey); + if (f && f in FONT_SCALE) setFontSizeState(f as FontSizeKey); + if (b !== null) setBoldLabelsState(b === 'true'); + })(); + }, []); + + function setPalette(p: PaletteKey) { + setPaletteState(p); + AsyncStorage.setItem('themePalette', p); + } + function setFontSize(f: FontSizeKey) { + setFontSizeState(f); + AsyncStorage.setItem('themeFontSize', f); + } + function setBoldLabels(b: boolean) { + setBoldLabelsState(b); + AsyncStorage.setItem('themeBoldLabels', String(b)); + } + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/src/navigation/AppNavigator.tsx b/src/navigation/AppNavigator.tsx index 8375a48..3aaf05d 100644 --- a/src/navigation/AppNavigator.tsx +++ b/src/navigation/AppNavigator.tsx @@ -4,16 +4,17 @@ import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { Text } from 'react-native'; -import { RecordingScreen } from '../screens/RecordingScreen'; -import { PostRecordingScreen } from '../screens/PostRecordingScreen'; -import { SensorPairingScreen } from '../screens/SensorPairingScreen'; +import { RecordingScreen } from '../screens/RecordingScreen'; +import { PostRecordingScreen } from '../screens/PostRecordingScreen'; +import { SensorPairingScreen } from '../screens/SensorPairingScreen'; import { SavedRecordingsScreen } from '../screens/SavedRecordingsScreen'; -import { SettingsScreen } from '../screens/SettingsScreen'; +import { SettingsScreen } from '../screens/SettingsScreen'; import { RootStackParamList, TabParamList } from '../types'; import { colors } from '../theme'; +import { useTheme } from '../ThemeContext'; const Stack = createNativeStackNavigator(); -const Tab = createBottomTabNavigator(); +const Tab = createBottomTabNavigator(); const TAB_ICONS: Record = { Recording: '◉', @@ -22,13 +23,14 @@ const TAB_ICONS: Record = { }; function Tabs() { + const { accent } = useTheme(); return ( ({ - headerStyle: { backgroundColor: colors.bg }, - headerTintColor: colors.text, - tabBarStyle: { backgroundColor: colors.surface, borderTopColor: colors.border }, - tabBarActiveTintColor: colors.accent, + headerStyle: { backgroundColor: colors.bg }, + headerTintColor: colors.text, + tabBarStyle: { backgroundColor: colors.surface, borderTopColor: colors.border }, + tabBarActiveTintColor: accent, tabBarInactiveTintColor: colors.textMuted, tabBarIcon: ({ color }) => ( {TAB_ICONS[route.name]} @@ -36,8 +38,8 @@ function Tabs() { })} > - - + + ); } @@ -47,14 +49,14 @@ export function AppNavigator() { - + - + ); diff --git a/src/screens/RecordingScreen.tsx b/src/screens/RecordingScreen.tsx index e207f83..7b2bb1b 100644 --- a/src/screens/RecordingScreen.tsx +++ b/src/screens/RecordingScreen.tsx @@ -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; export function RecordingScreen() { const nav = useNavigation