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(); - } - } -);