130 lines
3.2 KiB
TypeScript
130 lines
3.2 KiB
TypeScript
import React, {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useEffect,
|
|
ReactNode,
|
|
} from "react";
|
|
import { useColorScheme } from "react-native";
|
|
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
import { ColorScheme, lightColors, darkColors } from "../styles/colors";
|
|
import {
|
|
TypographyPresets,
|
|
createTypographyPresets,
|
|
} from "../styles/typography";
|
|
|
|
type ThemeMode = "light" | "dark" | "system";
|
|
type ActiveTheme = "light" | "dark";
|
|
|
|
interface ThemeContextType {
|
|
// Current active theme
|
|
theme: ActiveTheme;
|
|
|
|
// User's theme preference
|
|
themeMode: ThemeMode;
|
|
|
|
// Active color scheme
|
|
colors: ColorScheme;
|
|
|
|
// Typography presets
|
|
typography: TypographyPresets;
|
|
|
|
// Theme actions
|
|
setTheme: (mode: ThemeMode) => void;
|
|
toggleTheme: () => void;
|
|
}
|
|
|
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|
|
|
const THEME_STORAGE_KEY = "@fitai:theme";
|
|
|
|
interface ThemeProviderProps {
|
|
children: ReactNode;
|
|
}
|
|
|
|
export function ThemeProvider({ children }: ThemeProviderProps) {
|
|
const systemColorScheme = useColorScheme();
|
|
const [themeMode, setThemeMode] = useState<ThemeMode>("system");
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
|
|
// Determine active theme based on mode and system preference
|
|
const getActiveTheme = (): ActiveTheme => {
|
|
if (themeMode === "system") {
|
|
return systemColorScheme === "dark" ? "dark" : "light";
|
|
}
|
|
return themeMode;
|
|
};
|
|
|
|
const activeTheme = getActiveTheme();
|
|
const colors = activeTheme === "dark" ? darkColors : lightColors;
|
|
const typography = createTypographyPresets(
|
|
colors.textPrimary,
|
|
colors.textSecondary,
|
|
colors.textTertiary,
|
|
);
|
|
|
|
// Load saved theme preference on mount
|
|
useEffect(() => {
|
|
const loadTheme = async () => {
|
|
try {
|
|
const savedTheme = await AsyncStorage.getItem(THEME_STORAGE_KEY);
|
|
if (savedTheme && ["light", "dark", "system"].includes(savedTheme)) {
|
|
setThemeMode(savedTheme as ThemeMode);
|
|
}
|
|
} catch (error) {
|
|
console.error("Failed to load theme preference:", error);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
loadTheme();
|
|
}, []);
|
|
|
|
// Save theme preference when it changes
|
|
const setTheme = async (mode: ThemeMode) => {
|
|
try {
|
|
setThemeMode(mode);
|
|
await AsyncStorage.setItem(THEME_STORAGE_KEY, mode);
|
|
} catch (error) {
|
|
console.error("Failed to save theme preference:", error);
|
|
}
|
|
};
|
|
|
|
// Toggle between light and dark (sets explicit mode, not system)
|
|
const toggleTheme = () => {
|
|
const newMode = activeTheme === "dark" ? "light" : "dark";
|
|
setTheme(newMode);
|
|
};
|
|
|
|
// Don't render children until theme is loaded
|
|
if (isLoading) {
|
|
return null;
|
|
}
|
|
|
|
const value: ThemeContextType = {
|
|
theme: activeTheme,
|
|
themeMode,
|
|
colors,
|
|
typography,
|
|
setTheme,
|
|
toggleTheme,
|
|
};
|
|
|
|
return (
|
|
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Hook to access theme context
|
|
* @throws Error if used outside ThemeProvider
|
|
*/
|
|
export function useTheme() {
|
|
const context = useContext(ThemeContext);
|
|
if (context === undefined) {
|
|
throw new Error("useTheme must be used within a ThemeProvider");
|
|
}
|
|
return context;
|
|
}
|