Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 13 additions & 14 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ SplashScreen.preventAutoHideAsync();
import { PeaksProvider } from './src/store/PeaksContext';
import { PlannerProvider } from './src/store/PlannerContext';
import { LocaleProvider, useLocale } from './src/store/LocaleContext';
import { ThemeProvider } from './src/store/ThemeContext';
import { Colors } from './src/utils/theme';
import type {
RootStackParamList,
Expand Down Expand Up @@ -88,9 +89,7 @@ function BlocchiStack() {
import { t } from './src/utils/i18n';

function TabNavigator() {
const scheme = useColorScheme();
const isDark = scheme === 'dark';
const colors = isDark ? Colors.dark : Colors.light;
const { colors } = useThemeColors();

return (
<Tab.Navigator
Expand Down Expand Up @@ -191,16 +190,15 @@ import { ActivityIndicator, View, StyleSheet } from 'react-native';
import { useThemeColors } from './src/utils/useThemeColors';

function RootNavigator() {
const scheme = useColorScheme();
const isDark = scheme === 'dark';
const { locale, isReloading } = useLocale();
const { colors } = useThemeColors();
const { colors, isDark } = useThemeColors();

return (
<>
<NavigationContainer theme={isDark ? DarkNavTheme : LightNavTheme} key={locale}>
<TabNavigator />
</NavigationContainer>
<StatusBar style={isDark ? 'light' : 'dark'} />
{isReloading && (
<View
style={[
Expand Down Expand Up @@ -237,14 +235,15 @@ export default function App() {

return (
<SafeAreaProvider>
<LocaleProvider>
<PeaksProvider>
<PlannerProvider>
<RootNavigator />
<StatusBar style="auto" />
</PlannerProvider>
</PeaksProvider>
</LocaleProvider>
<ThemeProvider>
<LocaleProvider>
<PeaksProvider>
<PlannerProvider>
<RootNavigator />
</PlannerProvider>
</PeaksProvider>
</LocaleProvider>
</ThemeProvider>
</SafeAreaProvider>
);
}
138 changes: 136 additions & 2 deletions src/screens/SettingsScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,41 @@ import * as Sharing from 'expo-sharing';
import { Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { useThemeColors } from '../utils/useThemeColors';
import { useThemeColors, type ThemeMode } from '../utils/useThemeColors';
import { Spacing, FontSize, BorderRadius } from '../utils/theme';
import { t, SUPPORTED_LANGUAGES, getSupportedLocale } from '../utils/i18n';
import { useLocale } from '../store/LocaleContext';
import { usePeaks } from '../store/PeaksContext';
import { usePlanner } from '../store/PlannerContext';
import OnboardingModal from '../components/OnboardingModal';

interface ThemeOption {
mode: ThemeMode;
labelKey: string;
icon: keyof typeof MaterialCommunityIcons.glyphMap;
}

const THEME_OPTIONS: ThemeOption[] = [
{ mode: 'system', labelKey: 'settings.themeSystem', icon: 'theme-light-dark' },
{ mode: 'light', labelKey: 'settings.themeLight', icon: 'white-balance-sunny' },
{ mode: 'dark', labelKey: 'settings.themeDark', icon: 'weather-night' },
];

export default function SettingsScreen() {
const { colors } = useThemeColors();
const { colors, themeMode, setThemeMode } = useThemeColors();
const [isImporting, setIsImporting] = useState(false);
const [showOnboarding, setShowOnboarding] = useState(false);
const [showLanguageModal, setShowLanguageModal] = useState(false);
const [showThemeModal, setShowThemeModal] = useState(false);
const { locale, changeLocale } = useLocale();
const { refreshData: refreshPeaks } = usePeaks();
const { refreshData: refreshPlanner } = usePlanner();
const activeLang = getSupportedLocale(locale);
const currentLanguageItem =
SUPPORTED_LANGUAGES.find((item) => item.code === activeLang) ?? SUPPORTED_LANGUAGES[0];
const currentThemeOption =
THEME_OPTIONS.find((opt) => opt.mode === themeMode) ?? THEME_OPTIONS[0];
const currentThemeLabel = t(currentThemeOption.labelKey);

const changeLanguage = async (lang: string) => {
await changeLocale(lang);
Expand Down Expand Up @@ -179,6 +195,39 @@ export default function SettingsScreen() {
</Pressable>
</View>

<View
style={[
styles.card,
{ backgroundColor: colors.surface, borderColor: colors.border, marginTop: Spacing.xl },
]}
>
<View style={{ flexDirection: 'row', alignItems: 'center', marginBottom: Spacing.md }}>
<MaterialCommunityIcons
name="theme-light-dark"
size={24}
color={colors.primary}
style={{ marginRight: Spacing.sm }}
/>
<Text style={[styles.title, { color: colors.text, marginBottom: 0 }]}>
{t('settings.themeTitle')}
</Text>
</View>
<Pressable
style={[
styles.languageTile,
{ backgroundColor: colors.surfaceAlt, borderColor: colors.border },
]}
onPress={() => setShowThemeModal(true)}
accessibilityRole="button"
accessibilityLabel={`${t('settings.themeTitle')}: ${currentThemeLabel}`}
>
<Text style={[styles.languageTileLabel, { color: colors.text }]}>
{currentThemeLabel}
</Text>
<MaterialCommunityIcons name="chevron-right" size={24} color={colors.textSecondary} />
</Pressable>
</View>

<View
style={[
styles.card,
Expand Down Expand Up @@ -422,6 +471,91 @@ export default function SettingsScreen() {
</Pressable>
</Modal>

<Modal
visible={showThemeModal}
transparent
animationType="fade"
onRequestClose={() => setShowThemeModal(false)}
testID="theme-modal"
>
<Pressable
style={[styles.modalOverlay, { backgroundColor: colors.overlay }]}
onPress={() => setShowThemeModal(false)}
testID="theme-modal-overlay"
>
<Pressable
style={[
styles.modalContent,
{ backgroundColor: colors.surface, borderColor: colors.border },
]}
onPress={(e) => e.stopPropagation()}
testID="theme-modal-content"
>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: colors.text }]}>
{t('settings.selectTheme')}
</Text>
<Pressable
onPress={() => setShowThemeModal(false)}
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={t('common.cancel')}
testID="theme-modal-close"
>
<MaterialCommunityIcons name="close" size={24} color={colors.textSecondary} />
</Pressable>
</View>

<View style={styles.languagesList}>
{THEME_OPTIONS.map((opt) => {
const isSelected = themeMode === opt.mode;
return (
<Pressable
key={opt.mode}
style={[
styles.langOption,
{
backgroundColor: isSelected ? colors.surfaceAlt : 'transparent',
borderColor: isSelected ? colors.primary : colors.border,
},
]}
onPress={async () => {
setShowThemeModal(false);
if (!isSelected && setThemeMode) {
await setThemeMode(opt.mode);
}
}}
>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<MaterialCommunityIcons
name={opt.icon}
size={20}
color={isSelected ? colors.primary : colors.textSecondary}
style={{ marginRight: Spacing.sm }}
/>
<Text
style={[
styles.langOptionText,
{
color: isSelected ? colors.primary : colors.text,
fontWeight: isSelected ? '700' : '500',
},
]}
>
{t(opt.labelKey)}
</Text>
</View>
{isSelected && (
<MaterialCommunityIcons name="check" size={20} color={colors.primary} />
)}
</Pressable>
);
})}
</View>
</Pressable>
</Pressable>
</Modal>

<OnboardingModal visible={showOnboarding} onDismiss={() => setShowOnboarding(false)} />
</SafeAreaView>
);
Expand Down
Loading
Loading