update theme dark, light mode
This commit is contained in:
163
hooks/use-app-theme.ts
Normal file
163
hooks/use-app-theme.ts
Normal 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>;
|
||||
@@ -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
131
hooks/use-theme-context.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user