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",