diff --git a/app/(tabs)/_layout.tsx b/app/(tabs)/_layout.tsx
index 54e11d0..6a5a436 100644
--- a/app/(tabs)/_layout.tsx
+++ b/app/(tabs)/_layout.tsx
@@ -1,33 +1,76 @@
-import { Tabs } from 'expo-router';
-import React from 'react';
+import { Tabs } from "expo-router";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { Platform } from "react-native";
-import { HapticTab } from '@/components/haptic-tab';
-import { IconSymbol } from '@/components/ui/icon-symbol';
-import { Colors } from '@/constants/theme';
-import { useColorScheme } from '@/hooks/use-color-scheme';
+import { HapticTab } from "@/components/haptic-tab";
+import { IconSymbol } from "@/components/ui/icon-symbol";
+import TabBarBackground from "@/components/ui/tab-bar-background";
+import { Colors } from "@/constants/theme";
+import { useTheme } from "@/context/ThemeContext";
export default function TabLayout() {
- const colorScheme = useColorScheme();
+ const { theme } = useTheme();
+ const { t } = useTranslation();
return (
+ tabBarBackground: TabBarBackground,
+ tabBarStyle: Platform.select({
+ ios: {
+ position: "absolute",
+ },
+ default: {},
+ }),
+ }}
+ >
,
+ title: t("tabs.all"),
+ tabBarIcon: ({ color }) => (
+
+ ),
}}
/>
,
+ title: t("tabs.live"),
+ tabBarIcon: ({ color }) => (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
+ }}
+ />
+ (
+
+ ),
}}
/>
diff --git a/app/(tabs)/explore.tsx b/app/(tabs)/explore.tsx
deleted file mode 100644
index 71518f9..0000000
--- a/app/(tabs)/explore.tsx
+++ /dev/null
@@ -1,112 +0,0 @@
-import { Image } from 'expo-image';
-import { Platform, StyleSheet } from 'react-native';
-
-import { Collapsible } from '@/components/ui/collapsible';
-import { ExternalLink } from '@/components/external-link';
-import ParallaxScrollView from '@/components/parallax-scroll-view';
-import { ThemedText } from '@/components/themed-text';
-import { ThemedView } from '@/components/themed-view';
-import { IconSymbol } from '@/components/ui/icon-symbol';
-import { Fonts } from '@/constants/theme';
-
-export default function TabTwoScreen() {
- return (
-
- }>
-
-
- Explore
-
-
- This app includes example code to help you get started.
-
-
- This app has two screens:{' '}
- app/(tabs)/index.tsx and{' '}
- app/(tabs)/explore.tsx
-
-
- The layout file in app/(tabs)/_layout.tsx{' '}
- sets up the tab navigator.
-
-
- Learn more
-
-
-
-
- You can open this project on Android, iOS, and the web. To open the web version, press{' '}
- w in the terminal running this project.
-
-
-
-
- For static images, you can use the @2x and{' '}
- @3x suffixes to provide files for
- different screen densities
-
-
-
- Learn more
-
-
-
-
- This template has light and dark mode support. The{' '}
- useColorScheme() hook lets you inspect
- what the user's current color scheme is, and so you can adjust UI colors accordingly.
-
-
- Learn more
-
-
-
-
- This template includes an example of an animated component. The{' '}
- components/HelloWave.tsx component uses
- the powerful{' '}
-
- react-native-reanimated
- {' '}
- library to create a waving hand animation.
-
- {Platform.select({
- ios: (
-
- The components/ParallaxScrollView.tsx{' '}
- component provides a parallax effect for the header image.
-
- ),
- })}
-
-
- );
-}
-
-const styles = StyleSheet.create({
- headerImage: {
- color: '#808080',
- bottom: -90,
- left: -35,
- position: 'absolute',
- },
- titleContainer: {
- flexDirection: 'row',
- gap: 8,
- },
-});
diff --git a/app/(tabs)/favorite.tsx b/app/(tabs)/favorite.tsx
new file mode 100644
index 0000000..e4e9b6e
--- /dev/null
+++ b/app/(tabs)/favorite.tsx
@@ -0,0 +1,19 @@
+import { ThemedText } from "@/components/themed-text";
+import { ThemedView } from "@/components/themed-view";
+import { StyleSheet } from "react-native";
+
+export default function FavoriteScreen() {
+ return (
+
+ Favorites
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+});
diff --git a/app/(tabs)/finished.tsx b/app/(tabs)/finished.tsx
new file mode 100644
index 0000000..c10a665
--- /dev/null
+++ b/app/(tabs)/finished.tsx
@@ -0,0 +1,19 @@
+import { ThemedText } from "@/components/themed-text";
+import { ThemedView } from "@/components/themed-view";
+import { StyleSheet } from "react-native";
+
+export default function FinishedScreen() {
+ return (
+
+ Finished Events
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+});
diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx
index 786b736..7c649c2 100644
--- a/app/(tabs)/index.tsx
+++ b/app/(tabs)/index.tsx
@@ -1,98 +1,24 @@
-import { Image } from 'expo-image';
-import { Platform, StyleSheet } from 'react-native';
-
-import { HelloWave } from '@/components/hello-wave';
-import ParallaxScrollView from '@/components/parallax-scroll-view';
-import { ThemedText } from '@/components/themed-text';
-import { ThemedView } from '@/components/themed-view';
-import { Link } from 'expo-router';
+import { HomeHeader } from "@/components/home-header";
+import { ThemedView } from "@/components/themed-view";
+import React from "react";
+import { ScrollView, StyleSheet } from "react-native";
export default function HomeScreen() {
return (
-
- }>
-
- Welcome!
-
-
-
- Step 1: Try it
-
- Edit app/(tabs)/index.tsx to see changes.
- Press{' '}
-
- {Platform.select({
- ios: 'cmd + d',
- android: 'cmd + m',
- web: 'F12',
- })}
- {' '}
- to open developer tools.
-
-
-
-
-
- Step 2: Explore
-
-
-
- alert('Action pressed')} />
- alert('Share pressed')}
- />
-
- alert('Delete pressed')}
- />
-
-
-
-
-
- {`Tap the Explore tab to learn more about what's included in this starter app.`}
-
-
-
- Step 3: Get a fresh start
-
- {`When you're ready, run `}
- npm run reset-project to get a fresh{' '}
- app directory. This will move the current{' '}
- app to{' '}
- app-example.
-
-
-
+
+
+
+ {/* Placeholder for future content */}
+
+
);
}
const styles = StyleSheet.create({
- titleContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
+ container: {
+ flex: 1,
},
- stepContainer: {
- gap: 8,
- marginBottom: 8,
- },
- reactLogo: {
- height: 178,
- width: 290,
- bottom: 0,
- left: 0,
- position: 'absolute',
+ content: {
+ padding: 16,
},
});
diff --git a/app/(tabs)/live.tsx b/app/(tabs)/live.tsx
new file mode 100644
index 0000000..a27240a
--- /dev/null
+++ b/app/(tabs)/live.tsx
@@ -0,0 +1,19 @@
+import { ThemedText } from "@/components/themed-text";
+import { ThemedView } from "@/components/themed-view";
+import { StyleSheet } from "react-native";
+
+export default function LiveScreen() {
+ return (
+
+ Live Streams
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+});
diff --git a/app/(tabs)/upcoming.tsx b/app/(tabs)/upcoming.tsx
new file mode 100644
index 0000000..c92bd5e
--- /dev/null
+++ b/app/(tabs)/upcoming.tsx
@@ -0,0 +1,19 @@
+import { ThemedText } from "@/components/themed-text";
+import { ThemedView } from "@/components/themed-view";
+import { StyleSheet } from "react-native";
+
+export default function UpcomingScreen() {
+ return (
+
+ Upcoming Events
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ alignItems: "center",
+ justifyContent: "center",
+ },
+});
diff --git a/app/_layout.tsx b/app/_layout.tsx
index f518c9b..9ff029f 100644
--- a/app/_layout.tsx
+++ b/app/_layout.tsx
@@ -1,24 +1,34 @@
-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 { useColorScheme } from '@/hooks/use-color-scheme';
+import { ThemeProvider } from '@/context/ThemeContext';
+import '@/i18n'; // Initialize i18n
export const unstable_settings = {
anchor: '(tabs)',
};
export default function RootLayout() {
+ return (
+
+
+
+ );
+}
+
+function RootLayoutNav() {
const colorScheme = useColorScheme();
return (
-
+
-
-
+
+
);
}
diff --git a/app/profile.tsx b/app/profile.tsx
new file mode 100644
index 0000000..327e33b
--- /dev/null
+++ b/app/profile.tsx
@@ -0,0 +1,193 @@
+import { Image } from "expo-image";
+import { Stack } from "expo-router";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { ScrollView, StyleSheet, TouchableOpacity, View } from "react-native";
+
+import { ThemedText } from "@/components/themed-text";
+import { IconSymbol } from "@/components/ui/icon-symbol";
+import { useTheme } from "@/context/ThemeContext";
+import { changeLanguage } from "@/i18n";
+
+export default function ProfileScreen() {
+ const { theme, toggleTheme, setTheme, isSystemTheme, useSystemTheme } =
+ useTheme();
+ const { t, i18n } = useTranslation();
+ const isDark = theme === "dark";
+
+ const currentLanguage = i18n.language;
+
+ const toggleLanguage = () => {
+ const nextLang = currentLanguage.startsWith("en") ? "zh" : "en";
+ changeLanguage(nextLang);
+ };
+
+ const iconColor = isDark ? "#FFFFFF" : "#000000";
+ const textColor = isDark ? "#FFFFFF" : "#000000";
+ const subTextColor = isDark ? "#AAAAAA" : "#666666";
+
+ return (
+ <>
+
+
+ {/* User Info Section */}
+
+
+
+
+ {t("profile.name")}
+
+ user@example.com
+
+
+
+
+
+ {/* Settings Section */}
+
+ {t("settings.title")}
+
+
+
+ {/* Theme Setting */}
+
+
+
+ {t("settings.theme")}
+
+
+
+
+ {theme === "light" ? t("settings.light") : t("settings.dark")}
+
+
+
+
+
+ {/* Language Setting */}
+
+
+
+ {t("settings.language")}
+
+
+
+
+ {currentLanguage.startsWith("zh")
+ ? t("settings.chinese")
+ : t("settings.english")}
+
+
+
+
+
+
+ >
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ },
+ section: {
+ marginTop: 20,
+ paddingVertical: 10,
+ paddingHorizontal: 16,
+ borderRadius: 10, // iOS style groups
+ marginHorizontal: 16,
+ },
+ sectionTitle: {
+ marginLeft: 32,
+ marginTop: 20,
+ marginBottom: 5,
+ fontSize: 13,
+ textTransform: "uppercase",
+ opacity: 0.6,
+ },
+ profileHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ paddingVertical: 10,
+ },
+ avatar: {
+ width: 60,
+ height: 60,
+ borderRadius: 30,
+ backgroundColor: "#ccc",
+ },
+ profileInfo: {
+ marginLeft: 15,
+ },
+ settingItem: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ paddingVertical: 12,
+ },
+ settingLabel: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ settingControl: {
+ flexDirection: "row",
+ alignItems: "center",
+ },
+ button: {
+ paddingHorizontal: 10,
+ paddingVertical: 5,
+ },
+});
diff --git a/assets/images/partial-react-logo.png b/assets/images/partial-react-logo.png
deleted file mode 100644
index 66fd957..0000000
Binary files a/assets/images/partial-react-logo.png and /dev/null differ
diff --git a/assets/images/react-logo.png b/assets/images/react-logo.png
deleted file mode 100644
index 9d72a9f..0000000
Binary files a/assets/images/react-logo.png and /dev/null differ
diff --git a/assets/images/react-logo@2x.png b/assets/images/react-logo@2x.png
deleted file mode 100644
index 2229b13..0000000
Binary files a/assets/images/react-logo@2x.png and /dev/null differ
diff --git a/assets/images/react-logo@3x.png b/assets/images/react-logo@3x.png
deleted file mode 100644
index a99b203..0000000
Binary files a/assets/images/react-logo@3x.png and /dev/null differ
diff --git a/components/external-link.tsx b/components/external-link.tsx
deleted file mode 100644
index 883e515..0000000
--- a/components/external-link.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { Href, Link } from 'expo-router';
-import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser';
-import { type ComponentProps } from 'react';
-
-type Props = Omit, 'href'> & { href: Href & string };
-
-export function ExternalLink({ href, ...rest }: Props) {
- return (
- {
- if (process.env.EXPO_OS !== 'web') {
- // Prevent the default behavior of linking to the default browser on native.
- event.preventDefault();
- // Open the link in an in-app browser.
- await openBrowserAsync(href, {
- presentationStyle: WebBrowserPresentationStyle.AUTOMATIC,
- });
- }
- }}
- />
- );
-}
diff --git a/components/hello-wave.tsx b/components/hello-wave.tsx
deleted file mode 100644
index 5def547..0000000
--- a/components/hello-wave.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import Animated from 'react-native-reanimated';
-
-export function HelloWave() {
- return (
-
- 👋
-
- );
-}
diff --git a/components/home-header.tsx b/components/home-header.tsx
new file mode 100644
index 0000000..406144b
--- /dev/null
+++ b/components/home-header.tsx
@@ -0,0 +1,73 @@
+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 { useRouter } from "expo-router";
+import React from "react";
+import { useTranslation } from "react-i18next";
+import { StyleSheet, TouchableOpacity } from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+export function HomeHeader() {
+ const router = useRouter();
+ const { theme } = useTheme();
+ const { t } = useTranslation();
+ const iconColor = theme === "light" ? Colors.light.icon : Colors.dark.icon;
+
+ return (
+
+
+
+
+ {t("home.title")}
+
+
+ {
+ /* TODO: Search Action */
+ }}
+ >
+
+
+ router.push("/profile")}
+ >
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ paddingTop: 0,
+ },
+ safeArea: {
+ paddingBottom: 10,
+ paddingHorizontal: 16,
+ },
+ headerContent: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ paddingTop: 8,
+ },
+ title: {
+ fontSize: 24,
+ fontWeight: "bold",
+ },
+ actions: {
+ flexDirection: "row",
+ gap: 16,
+ alignItems: "center",
+ },
+ iconButton: {
+ padding: 4,
+ },
+});
diff --git a/components/parallax-scroll-view.tsx b/components/parallax-scroll-view.tsx
deleted file mode 100644
index 6f674a7..0000000
--- a/components/parallax-scroll-view.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-import type { PropsWithChildren, ReactElement } from 'react';
-import { StyleSheet } from 'react-native';
-import Animated, {
- interpolate,
- useAnimatedRef,
- useAnimatedStyle,
- useScrollOffset,
-} from 'react-native-reanimated';
-
-import { ThemedView } from '@/components/themed-view';
-import { useColorScheme } from '@/hooks/use-color-scheme';
-import { useThemeColor } from '@/hooks/use-theme-color';
-
-const HEADER_HEIGHT = 250;
-
-type Props = PropsWithChildren<{
- headerImage: ReactElement;
- headerBackgroundColor: { dark: string; light: string };
-}>;
-
-export default function ParallaxScrollView({
- children,
- headerImage,
- headerBackgroundColor,
-}: Props) {
- const backgroundColor = useThemeColor({}, 'background');
- const colorScheme = useColorScheme() ?? 'light';
- const scrollRef = useAnimatedRef();
- const scrollOffset = useScrollOffset(scrollRef);
- const headerAnimatedStyle = useAnimatedStyle(() => {
- return {
- transform: [
- {
- translateY: interpolate(
- scrollOffset.value,
- [-HEADER_HEIGHT, 0, HEADER_HEIGHT],
- [-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
- ),
- },
- {
- scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
- },
- ],
- };
- });
-
- return (
-
-
- {headerImage}
-
- {children}
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- },
- header: {
- height: HEADER_HEIGHT,
- overflow: 'hidden',
- },
- content: {
- flex: 1,
- padding: 32,
- gap: 16,
- overflow: 'hidden',
- },
-});
diff --git a/components/ui/collapsible.tsx b/components/ui/collapsible.tsx
deleted file mode 100644
index 6345fde..0000000
--- a/components/ui/collapsible.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { PropsWithChildren, useState } from 'react';
-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 { useColorScheme } from '@/hooks/use-color-scheme';
-
-export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
- const [isOpen, setIsOpen] = useState(false);
- const theme = useColorScheme() ?? 'light';
-
- return (
-
- setIsOpen((value) => !value)}
- activeOpacity={0.8}>
-
-
- {title}
-
- {isOpen && {children}}
-
- );
-}
-
-const styles = StyleSheet.create({
- heading: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 6,
- },
- content: {
- marginTop: 6,
- marginLeft: 24,
- },
-});
diff --git a/components/ui/icon-symbol.ios.tsx b/components/ui/icon-symbol.ios.tsx
deleted file mode 100644
index 9177f4d..0000000
--- a/components/ui/icon-symbol.ios.tsx
+++ /dev/null
@@ -1,32 +0,0 @@
-import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
-import { StyleProp, ViewStyle } from 'react-native';
-
-export function IconSymbol({
- name,
- size = 24,
- color,
- style,
- weight = 'regular',
-}: {
- name: SymbolViewProps['name'];
- size?: number;
- color: string;
- style?: StyleProp;
- weight?: SymbolWeight;
-}) {
- return (
-
- );
-}
diff --git a/components/ui/icon-symbol.tsx b/components/ui/icon-symbol.tsx
index b7ece6b..aaba32f 100644
--- a/components/ui/icon-symbol.tsx
+++ b/components/ui/icon-symbol.tsx
@@ -1,29 +1,13 @@
-// Fallback for using MaterialIcons on Android and web.
+// Cross-platform icon component using Ionicons for all platforms.
+import Ionicons from "@expo/vector-icons/Ionicons";
+import { ComponentProps } from "react";
+import { OpaqueColorValue, StyleProp, TextStyle } from "react-native";
-import MaterialIcons from '@expo/vector-icons/MaterialIcons';
-import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
-import { ComponentProps } from 'react';
-import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
-
-type IconMapping = Record['name']>;
-type IconSymbolName = keyof typeof MAPPING;
+// Define the icon names we use in the app
+export type IconSymbolName = ComponentProps["name"];
/**
- * Add your SF Symbols to Material Icons mappings here.
- * - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
- * - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
- */
-const MAPPING = {
- 'house.fill': 'home',
- 'paperplane.fill': 'send',
- 'chevron.left.forwardslash.chevron.right': 'code',
- 'chevron.right': 'chevron-right',
-} as IconMapping;
-
-/**
- * An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
- * This ensures a consistent look across platforms, and optimal resource usage.
- * Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
+ * An icon component that uses Ionicons on all platforms.
*/
export function IconSymbol({
name,
@@ -35,7 +19,7 @@ export function IconSymbol({
size?: number;
color: string | OpaqueColorValue;
style?: StyleProp;
- weight?: SymbolWeight;
+ weight?: string; // Kept for compatibility but unused
}) {
- return ;
+ return ;
}
diff --git a/components/ui/tab-bar-background.ios.tsx b/components/ui/tab-bar-background.ios.tsx
new file mode 100644
index 0000000..a0b6f70
--- /dev/null
+++ b/components/ui/tab-bar-background.ios.tsx
@@ -0,0 +1,22 @@
+import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs";
+import { BlurView } from "expo-blur";
+import { StyleSheet } from "react-native";
+import { useSafeAreaInsets } from "react-native-safe-area-context";
+
+export default function BlurTabBarBackground() {
+ return (
+
+ );
+}
+
+export function useBottomTabOverflow() {
+ const tabHeight = useBottomTabBarHeight();
+ const { bottom } = useSafeAreaInsets();
+ return tabHeight - bottom;
+}
diff --git a/components/ui/tab-bar-background.tsx b/components/ui/tab-bar-background.tsx
new file mode 100644
index 0000000..70d1c3c
--- /dev/null
+++ b/components/ui/tab-bar-background.tsx
@@ -0,0 +1,6 @@
+// This is a shim for web and Android where the tab bar is generally opaque.
+export default undefined;
+
+export function useBottomTabOverflow() {
+ return 0;
+}
diff --git a/context/ThemeContext.tsx b/context/ThemeContext.tsx
new file mode 100644
index 0000000..2347405
--- /dev/null
+++ b/context/ThemeContext.tsx
@@ -0,0 +1,115 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import React, { createContext, useContext, useEffect, useState } from "react";
+import { useColorScheme as useDeviceColorScheme } from "react-native";
+
+type Theme = "light" | "dark";
+
+interface ThemeContextType {
+ theme: Theme;
+ toggleTheme: () => void;
+ setTheme: (theme: Theme) => void;
+ systemTheme: Theme;
+ isSystemTheme: boolean;
+ useSystemTheme: () => void;
+}
+
+const ThemeContext = createContext(undefined);
+
+const THEME_STORAGE_KEY = "user_theme_preference";
+
+export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const deviceColorScheme = useDeviceColorScheme();
+ const systemTheme = deviceColorScheme === "dark" ? "dark" : "light";
+
+ // State to hold the current theme
+ const [theme, setThemeState] = useState(systemTheme);
+ // State to track if we are following system theme
+ const [isSystemTheme, setIsSystemTheme] = useState(true);
+
+ useEffect(() => {
+ loadThemePersistence();
+ }, []);
+
+ useEffect(() => {
+ // If we are following system theme, update when device theme changes
+ if (isSystemTheme) {
+ setThemeState(systemTheme);
+ }
+ }, [systemTheme, isSystemTheme]);
+
+ const loadThemePersistence = async () => {
+ try {
+ const storedTheme = await AsyncStorage.getItem(THEME_STORAGE_KEY);
+ if (storedTheme) {
+ if (storedTheme === "system") {
+ setIsSystemTheme(true);
+ setThemeState(systemTheme);
+ } else {
+ setIsSystemTheme(false);
+ setThemeState(storedTheme as Theme);
+ }
+ } else {
+ // Default to system
+ setIsSystemTheme(true);
+ setThemeState(systemTheme);
+ }
+ } catch (error) {
+ console.warn("Failed to load theme preference", error);
+ // Fallback to system
+ setIsSystemTheme(true);
+ setThemeState(systemTheme);
+ }
+ };
+
+ const saveThemePreference = async (newTheme: Theme | "system") => {
+ try {
+ await AsyncStorage.setItem(THEME_STORAGE_KEY, newTheme);
+ } catch (error) {
+ console.warn("Failed to save theme preference", error);
+ }
+ };
+
+ const toggleTheme = () => {
+ const newTheme = theme === "light" ? "dark" : "light";
+ setIsSystemTheme(false);
+ setThemeState(newTheme);
+ saveThemePreference(newTheme);
+ };
+
+ const setTheme = (newTheme: Theme) => {
+ setIsSystemTheme(false);
+ setThemeState(newTheme);
+ saveThemePreference(newTheme);
+ };
+
+ const useSystemTheme = () => {
+ setIsSystemTheme(true);
+ setThemeState(systemTheme);
+ saveThemePreference("system");
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useTheme = () => {
+ const context = useContext(ThemeContext);
+ if (!context) {
+ throw new Error("useTheme must be used within a ThemeProvider");
+ }
+ return context;
+};
diff --git a/hooks/use-color-scheme.ts b/hooks/use-color-scheme.ts
index 17e3c63..0074ac0 100644
--- a/hooks/use-color-scheme.ts
+++ b/hooks/use-color-scheme.ts
@@ -1 +1,11 @@
-export { useColorScheme } from 'react-native';
+import { useTheme } from "@/context/ThemeContext";
+import { ColorSchemeName } from "react-native";
+
+export function useColorScheme(): ColorSchemeName {
+ try {
+ const { theme } = useTheme();
+ return theme;
+ } catch (e) {
+ return "light";
+ }
+}
diff --git a/i18n/index.ts b/i18n/index.ts
new file mode 100644
index 0000000..9667bcf
--- /dev/null
+++ b/i18n/index.ts
@@ -0,0 +1,55 @@
+import AsyncStorage from "@react-native-async-storage/async-storage";
+import * as Localization from "expo-localization";
+import i18n from "i18next";
+import { initReactI18next } from "react-i18next";
+
+import en from "./locales/en.json";
+import zh from "./locales/zh.json";
+
+const RESOURCES = {
+ en: { translation: en },
+ zh: { translation: zh },
+};
+
+const LANGUAGE_STORAGE_KEY = "user_language_preference";
+
+const initI18n = async () => {
+ let savedLanguage = null;
+ try {
+ savedLanguage = await AsyncStorage.getItem(LANGUAGE_STORAGE_KEY);
+ } catch (error) {
+ console.warn("Failed to load language preference", error);
+ }
+
+ // Get device language (e.g. "en-US" -> "en")
+ // Prefer Localization.getLocales()[0].languageCode; fall back to 'en'
+ const locales = Localization.getLocales?.() ?? [];
+ const deviceLanguageCode = locales[0]?.languageCode ?? "en";
+
+ const languageToUse = savedLanguage || deviceLanguageCode || "en";
+
+ i18n.use(initReactI18next).init({
+ resources: RESOURCES,
+ lng: languageToUse,
+ fallbackLng: "en",
+ interpolation: {
+ escapeValue: false,
+ },
+ react: {
+ useSuspense: false, // For React Native compatibility
+ },
+ });
+};
+
+initI18n();
+
+export default i18n;
+
+export const changeLanguage = async (lang: string) => {
+ try {
+ await i18n.changeLanguage(lang);
+ await AsyncStorage.setItem(LANGUAGE_STORAGE_KEY, lang);
+ } catch (error) {
+ console.warn("Failed to change/save language", error);
+ }
+};
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
new file mode 100644
index 0000000..ff44367
--- /dev/null
+++ b/i18n/locales/en.json
@@ -0,0 +1,28 @@
+{
+ "welcome": "Welcome!",
+ "tabs": {
+ "all": "All",
+ "live": "Live",
+ "upcoming": "Upcoming",
+ "finished": "Finished",
+ "fav": "Favorites"
+ },
+ "settings": {
+ "title": "Settings",
+ "theme": "Theme",
+ "language": "Language",
+ "light": "Light",
+ "dark": "Dark",
+ "system": "System",
+ "english": "English",
+ "chinese": "Chinese"
+ },
+ "home": {
+ "title": "Physical"
+ },
+ "profile": {
+ "title": "My Profile",
+ "name": "User Name",
+ "settings": "Settings"
+ }
+}
\ No newline at end of file
diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json
new file mode 100644
index 0000000..ae3fd2e
--- /dev/null
+++ b/i18n/locales/zh.json
@@ -0,0 +1,28 @@
+{
+ "welcome": "欢迎!",
+ "tabs": {
+ "all": "全部",
+ "live": "直播",
+ "upcoming": "即将到来",
+ "finished": "已完成",
+ "fav": "收藏"
+ },
+ "settings": {
+ "title": "设置",
+ "theme": "主题",
+ "language": "语言",
+ "light": "浅色",
+ "dark": "深色",
+ "system": "跟随系统",
+ "english": "英文",
+ "chinese": "中文"
+ },
+ "home": {
+ "title": "Physical"
+ },
+ "profile": {
+ "title": "我的",
+ "name": "用户名",
+ "settings": "设置"
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 2197716..f0cc777 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,6 @@
"version": "1.0.0",
"scripts": {
"start": "expo start",
- "reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
@@ -59,4 +58,4 @@
"typescript": "~5.9.2"
},
"private": true
-}
+}
\ No newline at end of file
diff --git a/scripts/reset-project.js b/scripts/reset-project.js
deleted file mode 100644
index 51dff15..0000000
--- a/scripts/reset-project.js
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env node
-
-/**
- * This script is used to reset the project to a blank state.
- * It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
- * You can remove the `reset-project` script from package.json and safely delete this file after running it.
- */
-
-const fs = require("fs");
-const path = require("path");
-const readline = require("readline");
-
-const root = process.cwd();
-const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
-const exampleDir = "app-example";
-const newAppDir = "app";
-const exampleDirPath = path.join(root, exampleDir);
-
-const indexContent = `import { Text, View } from "react-native";
-
-export default function Index() {
- return (
-
- Edit app/index.tsx to edit this screen.
-
- );
-}
-`;
-
-const layoutContent = `import { Stack } from "expo-router";
-
-export default function RootLayout() {
- return ;
-}
-`;
-
-const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout,
-});
-
-const moveDirectories = async (userInput) => {
- try {
- if (userInput === "y") {
- // Create the app-example directory
- await fs.promises.mkdir(exampleDirPath, { recursive: true });
- console.log(`📁 /${exampleDir} directory created.`);
- }
-
- // Move old directories to new app-example directory or delete them
- for (const dir of oldDirs) {
- const oldDirPath = path.join(root, dir);
- if (fs.existsSync(oldDirPath)) {
- if (userInput === "y") {
- const newDirPath = path.join(root, exampleDir, dir);
- await fs.promises.rename(oldDirPath, newDirPath);
- console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`);
- } else {
- await fs.promises.rm(oldDirPath, { recursive: true, force: true });
- console.log(`❌ /${dir} deleted.`);
- }
- } else {
- console.log(`➡️ /${dir} does not exist, skipping.`);
- }
- }
-
- // Create new /app directory
- const newAppDirPath = path.join(root, newAppDir);
- await fs.promises.mkdir(newAppDirPath, { recursive: true });
- console.log("\n📁 New /app directory created.");
-
- // Create index.tsx
- const indexPath = path.join(newAppDirPath, "index.tsx");
- await fs.promises.writeFile(indexPath, indexContent);
- console.log("📄 app/index.tsx created.");
-
- // Create _layout.tsx
- const layoutPath = path.join(newAppDirPath, "_layout.tsx");
- await fs.promises.writeFile(layoutPath, layoutContent);
- console.log("📄 app/_layout.tsx created.");
-
- console.log("\n✅ Project reset complete. Next steps:");
- console.log(
- `1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
- userInput === "y"
- ? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
- : ""
- }`
- );
- } catch (error) {
- console.error(`❌ Error during script execution: ${error.message}`);
- }
-};
-
-rl.question(
- "Do you want to move existing files to /app-example instead of deleting them? (Y/n): ",
- (answer) => {
- const userInput = answer.trim().toLowerCase() || "y";
- if (userInput === "y" || userInput === "n") {
- moveDirectories(userInput).finally(() => rl.close());
- } else {
- console.log("❌ Invalid input. Please enter 'Y' or 'N'.");
- rl.close();
- }
- }
-);