update theme dark, light mode

This commit is contained in:
2025-11-17 17:01:42 +07:00
parent e725819c01
commit 862c4e42a4
25 changed files with 1274 additions and 442 deletions

163
hooks/use-app-theme.ts Normal file
View File

@@ -0,0 +1,163 @@
/**
* Custom hook for easy theme access throughout the app
* Provides styled components and theme utilities
*/
import { useMemo } from "react";
import { StyleSheet, TextStyle, ViewStyle } from "react-native";
import { useThemeContext } from "@/hooks/use-theme-context";
export function useAppTheme() {
const { colors, colorScheme, themeMode, setThemeMode, getColor } =
useThemeContext();
// Common styled components
const styles = useMemo(
() =>
StyleSheet.create({
// Container styles
container: {
flex: 1,
backgroundColor: colors.background,
} as ViewStyle,
surface: {
backgroundColor: colors.surface,
borderRadius: 12,
padding: 16,
} as ViewStyle,
card: {
backgroundColor: colors.card,
borderRadius: 12,
padding: 16,
shadowColor: colors.text,
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
} as ViewStyle,
// Button styles
primaryButton: {
backgroundColor: colors.primary,
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
} as ViewStyle,
secondaryButton: {
backgroundColor: colors.backgroundSecondary,
borderWidth: 1,
borderColor: colors.border,
paddingVertical: 14,
paddingHorizontal: 20,
borderRadius: 12,
alignItems: "center",
justifyContent: "center",
} as ViewStyle,
// Text styles
primaryButtonText: {
color: "#ffffff",
fontSize: 16,
fontWeight: "600",
} as TextStyle,
secondaryButtonText: {
color: colors.text,
fontSize: 16,
fontWeight: "600",
} as TextStyle,
// Input styles
textInput: {
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.border,
borderRadius: 8,
paddingVertical: 12,
paddingHorizontal: 16,
fontSize: 16,
color: colors.text,
} as ViewStyle & TextStyle,
// Separator
separator: {
height: 1,
backgroundColor: colors.separator,
} as ViewStyle,
// Status styles
successContainer: {
backgroundColor: `${colors.success}20`,
borderColor: colors.success,
borderWidth: 1,
borderRadius: 8,
padding: 12,
} as ViewStyle,
warningContainer: {
backgroundColor: `${colors.warning}20`,
borderColor: colors.warning,
borderWidth: 1,
borderRadius: 8,
padding: 12,
} as ViewStyle,
errorContainer: {
backgroundColor: `${colors.error}20`,
borderColor: colors.error,
borderWidth: 1,
borderRadius: 8,
padding: 12,
} as ViewStyle,
}),
[colors]
);
// Theme utilities
const utils = useMemo(
() => ({
// Get opacity color
getOpacityColor: (
colorName: keyof typeof colors,
opacity: number = 0.1
) => {
const color = colors[colorName];
const hex = color.replace("#", "");
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
return `rgba(${r}, ${g}, ${b}, ${opacity})`;
},
// Check if current theme is dark
isDark: colorScheme === "dark",
// Check if current theme is light
isLight: colorScheme === "light",
// Toggle between light and dark (ignoring system)
toggleTheme: () => {
const newMode = colorScheme === "dark" ? "light" : "dark";
setThemeMode(newMode);
},
}),
[colors, colorScheme, setThemeMode]
);
return {
colors,
styles,
utils,
colorScheme,
themeMode,
setThemeMode,
getColor,
};
}
export type AppTheme = ReturnType<typeof useAppTheme>;

View File

@@ -3,19 +3,19 @@
* https://docs.expo.dev/guides/color-schemes/
*/
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { ColorName } from "@/constants/theme";
import { useThemeContext } from "@/hooks/use-theme-context";
export function useThemeColor(
props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
colorName: ColorName
) {
const theme = useColorScheme() ?? 'light';
const colorFromProps = props[theme];
const { colorScheme, getColor } = useThemeContext();
const colorFromProps = props[colorScheme];
if (colorFromProps) {
return colorFromProps;
} else {
return Colors[theme][colorName];
return getColor(colorName);
}
}

131
hooks/use-theme-context.tsx Normal file
View File

@@ -0,0 +1,131 @@
/**
* Theme Context Hook for managing app-wide theme state
* Supports Light, Dark, and System (automatic) modes
*
* IMPORTANT: Requires expo-system-ui plugin in app.json for Android support
*/
import { Colors, ColorName } from "@/constants/theme";
import { getStorageItem, setStorageItem } from "@/utils/storage";
import {
createContext,
useContext,
useEffect,
useState,
ReactNode,
} from "react";
import { useColorScheme as useSystemColorScheme } from "react-native";
type ThemeMode = "light" | "dark" | "system";
type ColorScheme = "light" | "dark";
interface ThemeContextType {
themeMode: ThemeMode;
colorScheme: ColorScheme;
colors: typeof Colors.light;
setThemeMode: (mode: ThemeMode) => Promise<void>;
getColor: (colorName: ColorName) => string;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
const THEME_STORAGE_KEY = "theme_mode";
export function ThemeProvider({ children }: { children: ReactNode }) {
// Dùng useColorScheme từ react-native để detect theme thiết bị
const deviceColorScheme = useSystemColorScheme(); // "light" | "dark" | null | undefined
// State lưu user's choice (light/dark/system)
const [themeMode, setThemeModeState] = useState<ThemeMode>("system");
const [isLoaded, setIsLoaded] = useState(false);
// Xác định colorScheme cuối cùng
const colorScheme: ColorScheme =
themeMode === "system"
? deviceColorScheme === "dark"
? "dark"
: "light"
: themeMode;
const colors = Colors[colorScheme];
useEffect(() => {
const loadThemeMode = async () => {
try {
const savedThemeMode = await getStorageItem(THEME_STORAGE_KEY);
if (
savedThemeMode &&
["light", "dark", "system"].includes(savedThemeMode)
) {
setThemeModeState(savedThemeMode as ThemeMode);
}
} catch (error) {
console.warn("Failed to load theme mode:", error);
} finally {
setIsLoaded(true);
}
};
loadThemeMode();
}, []);
const setThemeMode = async (mode: ThemeMode) => {
try {
setThemeModeState(mode);
await setStorageItem(THEME_STORAGE_KEY, mode);
console.log("[Theme] Changed to:", mode);
} catch (error) {
console.warn("Failed to save theme mode:", error);
}
};
const getColor = (colorName: ColorName): string => {
return colors[colorName] || colors.text;
};
// Chờ theme load xong trước khi render
if (!isLoaded) {
// Render với default theme (system) khi đang load
const defaultScheme: ColorScheme =
deviceColorScheme === "dark" ? "dark" : "light";
return (
<ThemeContext.Provider
value={{
themeMode: "system",
colorScheme: defaultScheme,
colors: Colors[defaultScheme],
setThemeMode: async () => {},
getColor: (colorName: ColorName) =>
Colors[defaultScheme][colorName] || Colors[defaultScheme].text,
}}
>
{children}
</ThemeContext.Provider>
);
}
const value: ThemeContextType = {
themeMode,
colorScheme,
colors,
setThemeMode,
getColor,
};
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
);
}
export function useThemeContext(): ThemeContextType {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error("useThemeContext must be used within a ThemeProvider");
}
return context;
}
// Legacy hook cho backward compatibility
export function useColorScheme(): ColorScheme {
const { colorScheme } = useThemeContext();
return colorScheme;
}