174 lines
4.7 KiB
TypeScript
174 lines
4.7 KiB
TypeScript
/**
|
|
* 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,
|
|
Appearance,
|
|
} 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 }) {
|
|
// State để force re-render khi system theme thay đổi
|
|
const [systemTheme, setSystemTheme] = useState<ColorScheme>(() => {
|
|
const current = Appearance.getColorScheme();
|
|
console.log("[Theme] Initial system theme:", current);
|
|
return current === "dark" ? "dark" : "light";
|
|
});
|
|
|
|
// State lưu user's choice (light/dark/system)
|
|
const [themeMode, setThemeModeState] = useState<ThemeMode>("system");
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
|
|
// Listen vào system theme changes - đăng ký ngay từ đầu
|
|
useEffect(() => {
|
|
console.log("[Theme] Registering appearance listener");
|
|
|
|
const subscription = Appearance.addChangeListener(({ colorScheme }) => {
|
|
const newScheme = colorScheme === "dark" ? "dark" : "light";
|
|
console.log(
|
|
"[Theme] System theme changed to:",
|
|
newScheme,
|
|
"at",
|
|
new Date().toLocaleTimeString()
|
|
);
|
|
setSystemTheme(newScheme);
|
|
});
|
|
|
|
// Double check current theme khi mount
|
|
const currentScheme = Appearance.getColorScheme();
|
|
const current = currentScheme === "dark" ? "dark" : "light";
|
|
if (current !== systemTheme) {
|
|
console.log("[Theme] Syncing system theme on mount:", current);
|
|
setSystemTheme(current);
|
|
}
|
|
|
|
return () => {
|
|
console.log("[Theme] Removing appearance listener");
|
|
subscription.remove();
|
|
};
|
|
}, []);
|
|
|
|
// Xác định colorScheme cuối cùng
|
|
const colorScheme: ColorScheme =
|
|
themeMode === "system" ? systemTheme : themeMode;
|
|
|
|
const colors = Colors[colorScheme];
|
|
|
|
// Log để debug
|
|
useEffect(() => {
|
|
console.log(
|
|
"[Theme] Current state - Mode:",
|
|
themeMode,
|
|
"| Scheme:",
|
|
colorScheme,
|
|
"| System:",
|
|
systemTheme
|
|
);
|
|
}, [themeMode, colorScheme, systemTheme]);
|
|
|
|
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
|
|
return (
|
|
<ThemeContext.Provider
|
|
value={{
|
|
themeMode: "system",
|
|
colorScheme: systemTheme,
|
|
colors: Colors[systemTheme],
|
|
setThemeMode: async () => {},
|
|
getColor: (colorName: ColorName) =>
|
|
Colors[systemTheme][colorName] || Colors[systemTheme].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;
|
|
}
|