diff --git a/MobileApp/app/(tabs)/settings.tsx b/MobileApp/app/(tabs)/settings.tsx index 8697cae..3fc6406 100644 --- a/MobileApp/app/(tabs)/settings.tsx +++ b/MobileApp/app/(tabs)/settings.tsx @@ -1,12 +1,15 @@ -import { StyleSheet } from 'react-native'; +import { StyleSheet, TouchableOpacity } from 'react-native'; import { ThemedText } from '@/components/themed-text'; import { ThemedView } from '@/components/themed-view'; +import { IconSymbol } from '@/components/ui/icon-symbol'; import { Colors } from '@/constants/theme'; +import { useTheme } from '@/context/ThemeContext'; import { useColorScheme } from '@/hooks/use-color-scheme'; export default function SettingsScreen() { const colorScheme = useColorScheme(); + const { themeMode, setThemeMode } = useTheme(); const theme = colorScheme ?? 'light'; const cardStyle = [ @@ -25,21 +28,32 @@ export default function SettingsScreen() { - Account + Appearance - Profile - Notifications - Privacy - + setThemeMode('light')} + > + Light Mode + {themeMode === 'light' && } + - - General - - - About - Help & Support - Sign Out + setThemeMode('dark')} + > + Dark Mode + {themeMode === 'dark' && } + + + setThemeMode('system')} + > + System Default + {themeMode === 'system' && } + ); @@ -71,6 +85,9 @@ const styles = StyleSheet.create({ padding: 16, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: 'rgba(0,0,0,0.05)', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', }, lastItem: { borderBottomWidth: 0, diff --git a/MobileApp/app/_layout.tsx b/MobileApp/app/_layout.tsx index f518c9b..3721865 100644 --- a/MobileApp/app/_layout.tsx +++ b/MobileApp/app/_layout.tsx @@ -1,8 +1,9 @@ -import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'; +import { DarkTheme, DefaultTheme, ThemeProvider as NavigationThemeProvider } from '@react-navigation/native'; import { Stack } from 'expo-router'; import { StatusBar } from 'expo-status-bar'; import 'react-native-reanimated'; +import { ThemeProvider } from '@/context/ThemeContext'; import { useColorScheme } from '@/hooks/use-color-scheme'; export const unstable_settings = { @@ -10,15 +11,23 @@ export const unstable_settings = { }; export default function RootLayout() { + return ( + + + + ); +} + +function RootLayoutNav() { const colorScheme = useColorScheme(); return ( - + - + ); } diff --git a/MobileApp/components/ui/icon-symbol.tsx b/MobileApp/components/ui/icon-symbol.tsx index 53e6750..c04326e 100644 --- a/MobileApp/components/ui/icon-symbol.tsx +++ b/MobileApp/components/ui/icon-symbol.tsx @@ -21,6 +21,7 @@ const MAPPING = { 'bell.fill': 'notifications', 'film.fill': 'video-library', 'gearshape.fill': 'settings', + 'checkmark': 'check', } as IconMapping; /** diff --git a/MobileApp/context/ThemeContext.tsx b/MobileApp/context/ThemeContext.tsx new file mode 100644 index 0000000..0fd9dc3 --- /dev/null +++ b/MobileApp/context/ThemeContext.tsx @@ -0,0 +1,60 @@ +import AsyncStorage from '@react-native-async-storage/async-storage'; +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { useColorScheme as useNativeColorScheme } from 'react-native'; + +type ThemeMode = 'light' | 'dark' | 'system'; + +interface ThemeContextType { + themeMode: ThemeMode; + setThemeMode: (mode: ThemeMode) => void; + colorScheme: 'light' | 'dark'; // The actual active scheme +} + +const ThemeContext = createContext({ + themeMode: 'system', + setThemeMode: () => { }, + colorScheme: 'light', +}); + +export const THEME_STORAGE_KEY = 'user_theme_preference'; + +export function ThemeProvider({ children }: { children: React.ReactNode }) { + const systemColorScheme = useNativeColorScheme(); + const [themeMode, setThemeModeState] = useState('system'); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + // Load preference from storage on mount + AsyncStorage.getItem(THEME_STORAGE_KEY).then((value) => { + if (value && (value === 'light' || value === 'dark' || value === 'system')) { + setThemeModeState(value as ThemeMode); + } + setIsReady(true); + }); + }, []); + + const setThemeMode = async (mode: ThemeMode) => { + setThemeModeState(mode); + await AsyncStorage.setItem(THEME_STORAGE_KEY, mode); + }; + + // Determine effective color scheme + const colorScheme = + themeMode === 'system' + ? (systemColorScheme ?? 'light') + : themeMode; + + if (!isReady) { + return null; // Or a splash screen + } + + return ( + + {children} + + ); +} + +export function useTheme() { + return useContext(ThemeContext); +} diff --git a/MobileApp/hooks/use-color-scheme.ts b/MobileApp/hooks/use-color-scheme.ts index 17e3c63..2f35d88 100644 --- a/MobileApp/hooks/use-color-scheme.ts +++ b/MobileApp/hooks/use-color-scheme.ts @@ -1 +1,6 @@ -export { useColorScheme } from 'react-native'; +import { useTheme } from '@/context/ThemeContext'; + +export function useColorScheme() { + const { colorScheme } = useTheme(); + return colorScheme; +} diff --git a/MobileApp/package-lock.json b/MobileApp/package-lock.json index 6738037..0fe9308 100644 --- a/MobileApp/package-lock.json +++ b/MobileApp/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", @@ -2777,6 +2778,18 @@ } } }, + "node_modules/@react-native-async-storage/async-storage": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz", + "integrity": "sha512-gvRvjR5JAaUZF8tv2Kcq/Gbt3JHwbKFYfmb445rhOj6NUMx3qPLixmDx5pZAyb9at1bYvJ4/eTUipU5aki45xw==", + "license": "MIT", + "dependencies": { + "merge-options": "^3.0.4" + }, + "peerDependencies": { + "react-native": "^0.0.0-0 || >=0.65 <1.0" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", @@ -7810,6 +7823,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -8788,6 +8810,18 @@ "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", "license": "MIT" }, + "node_modules/merge-options": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/merge-options/-/merge-options-3.0.4.tgz", + "integrity": "sha512-2Sug1+knBjkaMsMgf1ctR1Ujx+Ayku4EdJN4Z+C2+JzoeF7A3OZ9KM2GY0CpQS51NR61LTurMJrRKPhSs3ZRTQ==", + "license": "MIT", + "dependencies": { + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", diff --git a/MobileApp/package.json b/MobileApp/package.json index 247e4b4..1b6c39e 100644 --- a/MobileApp/package.json +++ b/MobileApp/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", + "@react-native-async-storage/async-storage": "2.2.0", "@react-navigation/bottom-tabs": "^7.4.0", "@react-navigation/elements": "^2.6.3", "@react-navigation/native": "^7.1.8", @@ -31,11 +32,11 @@ "react-dom": "19.1.0", "react-native": "0.81.5", "react-native-gesture-handler": "~2.28.0", - "react-native-worklets": "0.5.1", "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0" + "react-native-web": "~0.21.0", + "react-native-worklets": "0.5.1" }, "devDependencies": { "@types/react": "~19.1.0",