feat: Improve dark mode support and splash screen handling across app
- Add dark mode background to splash and global styles - Replace LoadingScreen with SplashScreenComponent for unified splash handling - Ensure navigation and paper themes sync with dark mode - Set initial theme based on system preference - Apply background color to navigation containers and scenes
This commit is contained in:
@@ -34,7 +34,10 @@
|
|||||||
"image": "./assets/images/racoon-splash.png",
|
"image": "./assets/images/racoon-splash.png",
|
||||||
"imageWidth": 200,
|
"imageWidth": 200,
|
||||||
"resizeMode": "contain",
|
"resizeMode": "contain",
|
||||||
"backgroundColor": "#ffffff"
|
"backgroundColor": "#ffffff",
|
||||||
|
"dark": {
|
||||||
|
"backgroundColor": "#121212"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"expo-secure-store",
|
"expo-secure-store",
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ export default function TabLayout() {
|
|||||||
</Link>
|
</Link>
|
||||||
),
|
),
|
||||||
tabBarActiveTintColor: theme.colors.primary,
|
tabBarActiveTintColor: theme.colors.primary,
|
||||||
|
sceneContainerStyle: {
|
||||||
|
backgroundColor: theme.colors.background,
|
||||||
|
},
|
||||||
tabBarInactiveTintColor: theme.colors.onSurfaceVariant,
|
tabBarInactiveTintColor: theme.colors.onSurfaceVariant,
|
||||||
tabBarStyle: {
|
tabBarStyle: {
|
||||||
backgroundColor: theme.colors.surface,
|
backgroundColor: theme.colors.surface,
|
||||||
|
|||||||
@@ -3,3 +3,12 @@
|
|||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #121212 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,28 +2,21 @@ import React, { useEffect } from "react";
|
|||||||
import {useAuth} from "@/src/stores/authStore";
|
import {useAuth} from "@/src/stores/authStore";
|
||||||
import {useThemeStore} from "@/src/stores/themeStore";
|
import {useThemeStore} from "@/src/stores/themeStore";
|
||||||
import NotAuthenticated from "@/src/components/NotAuthenticated";
|
import NotAuthenticated from "@/src/components/NotAuthenticated";
|
||||||
import LoadingScreen from "@/src/components/LoadingScreen";
|
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
import * as SplashScreen from 'expo-splash-screen';
|
||||||
|
import SplashScreenComponent from "@/src/components/SplashScreenComponent";
|
||||||
|
|
||||||
const AuthenticatedWrapper: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
const AuthenticatedWrapper: React.FC<{ children: React.ReactNode }> = ({children}) => {
|
||||||
const {isAuthenticated, loading, authenticatedUser, isHydrated: authHydrated} = useAuth();
|
const {isAuthenticated, loading, authenticatedUser, isHydrated: authHydrated} = useAuth();
|
||||||
const {isHydrated: themeHydrated} = useThemeStore();
|
const {isHydrated: themeHydrated} = useThemeStore();
|
||||||
|
|
||||||
// Verstecke den Splash Screen erst wenn beide Stores hydratiert sind
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authHydrated && themeHydrated) {
|
if (authHydrated && themeHydrated) {
|
||||||
SplashScreen.hideAsync().catch(() => {});
|
SplashScreen.hideAsync().catch(() => {});
|
||||||
}
|
}
|
||||||
}, [authHydrated, themeHydrated]);
|
}, [authHydrated, themeHydrated]);
|
||||||
|
|
||||||
// Zeige nichts (Splash Screen bleibt) bis beide Stores hydratiert sind
|
if (!authHydrated || !themeHydrated || loading) {
|
||||||
if (!authHydrated || !themeHydrated) {
|
return <SplashScreenComponent/>;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeige LoadingScreen während Auth Status geprüft wird
|
|
||||||
if (loading) {
|
|
||||||
return <LoadingScreen />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isAuthenticated || !authenticatedUser) {
|
if (!isAuthenticated || !authenticatedUser) {
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { View, ActivityIndicator } from "react-native";
|
|
||||||
import Logo from "./Logo";
|
|
||||||
import { useThemeStore } from "@/src/stores/themeStore";
|
|
||||||
|
|
||||||
export default function LoadingScreen() {
|
|
||||||
const isDark = useThemeStore((state) => state.isDark);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
className="flex-1 items-center justify-center"
|
|
||||||
style={{
|
|
||||||
backgroundColor: isDark ? '#121212' : '#ffffff'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Logo size="large" />
|
|
||||||
<ActivityIndicator
|
|
||||||
size="large"
|
|
||||||
color={isDark ? '#BB86FC' : '#6200EE'}
|
|
||||||
style={{ marginTop: 20 }}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { View, Image, useColorScheme } from 'react-native';
|
||||||
|
import { useThemeStore } from '@/src/stores/themeStore';
|
||||||
|
|
||||||
|
export default function SplashScreenComponent() {
|
||||||
|
const systemColorScheme = useColorScheme();
|
||||||
|
|
||||||
|
const { isDark, isHydrated } = useThemeStore();
|
||||||
|
|
||||||
|
const effectiveIsDark = isHydrated ? isDark : systemColorScheme === 'dark';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
backgroundColor: effectiveIsDark ? '#121212' : '#ffffff',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={require('@/assets/images/racoon-splash.png')}
|
||||||
|
style={{ width: 200, resizeMode: 'contain' }}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,26 +1,40 @@
|
|||||||
import React, {ReactNode, useEffect} from "react";
|
import React, {ReactNode, useEffect} from "react";
|
||||||
import {Provider as PaperProvider} from "react-native-paper";
|
import {adaptNavigationTheme, Provider as PaperProvider} from "react-native-paper";
|
||||||
import {useThemeStore} from "@/src/stores/themeStore";
|
import {useThemeStore} from "@/src/stores/themeStore";
|
||||||
import {useColorScheme} from "nativewind";
|
import {useColorScheme} from "nativewind";
|
||||||
import LoadingScreen from "@/src/components/LoadingScreen";
|
import SplashScreenComponent from "@/src/components/SplashScreenComponent";
|
||||||
import * as SplashScreen from 'expo-splash-screen';
|
|
||||||
|
import { ThemeProvider as NavThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native';
|
||||||
|
import {View} from "react-native";
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync().catch(() => {});
|
|
||||||
|
|
||||||
export const ThemeProvider = ({children}: { children: ReactNode }) => {
|
export const ThemeProvider = ({children}: { children: ReactNode }) => {
|
||||||
const { theme, isDark, isHydrated } = useThemeStore();
|
const { theme, isDark, isHydrated } = useThemeStore();
|
||||||
const { setColorScheme } = useColorScheme();
|
const { setColorScheme } = useColorScheme();
|
||||||
|
|
||||||
|
const { LightTheme: NavLightTheme, DarkTheme: NavDarkTheme } = adaptNavigationTheme({
|
||||||
|
reactNavigationLight: DefaultTheme,
|
||||||
|
reactNavigationDark: DarkTheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
const navigationTheme = isDark ? NavDarkTheme : NavLightTheme;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColorScheme(isDark ? 'dark' : 'light');
|
setColorScheme(isDark ? 'dark' : 'light');
|
||||||
}, [isDark, setColorScheme]);
|
}, [isDark, setColorScheme, isHydrated]);
|
||||||
|
|
||||||
if (!isHydrated) {
|
if (!isHydrated) {
|
||||||
return <LoadingScreen />;
|
return <SplashScreenComponent />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PaperProvider theme={theme}>{children}</PaperProvider>
|
<NavThemeProvider value={navigationTheme}>
|
||||||
|
<PaperProvider theme={theme}>
|
||||||
|
<View style={{ flex: 1, backgroundColor: theme.colors.background }}>
|
||||||
|
{children}
|
||||||
|
</View>
|
||||||
|
</PaperProvider>
|
||||||
|
</NavThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -1,10 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import {Slot, Stack} from "expo-router";
|
import {Slot, Stack} from "expo-router";
|
||||||
|
import {useThemeStore} from "@/src/stores";
|
||||||
|
|
||||||
|
|
||||||
export default function CustomStack() {
|
export default function CustomStack() {
|
||||||
|
const { theme } = useThemeStore();
|
||||||
return (
|
return (
|
||||||
<Stack screenOptions={{ headerShown: false }}>
|
<Stack screenOptions={{ headerShown: false, contentStyle: { backgroundColor: theme.colors.background } }}>
|
||||||
<Slot />
|
<Slot />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
+29
-18
@@ -1,8 +1,15 @@
|
|||||||
import { create } from 'zustand';
|
import {create} from 'zustand';
|
||||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
import {persist, createJSONStorage} from 'zustand/middleware';
|
||||||
import { Platform } from 'react-native';
|
import {Platform} from 'react-native';
|
||||||
import * as SecureStore from 'expo-secure-store';
|
import * as SecureStore from 'expo-secure-store';
|
||||||
import { CustomMD3Theme, darkTheme, lightTheme } from '@/src/core/theme';
|
import {CustomMD3Theme, darkTheme, lightTheme} from '@/src/core/theme';
|
||||||
|
|
||||||
|
const getSystemThemeIsDark = () => {
|
||||||
|
if (Platform.OS === 'web' && typeof window !== 'undefined') {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
const zustandStorage = {
|
const zustandStorage = {
|
||||||
getItem: async (name: string): Promise<string | null> => {
|
getItem: async (name: string): Promise<string | null> => {
|
||||||
@@ -37,23 +44,27 @@ interface ThemeState {
|
|||||||
|
|
||||||
export const useThemeStore = create<ThemeState>()(
|
export const useThemeStore = create<ThemeState>()(
|
||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => {
|
||||||
theme: lightTheme,
|
const initialDark = getSystemThemeIsDark();
|
||||||
isDark: false,
|
|
||||||
isHydrated: false,
|
return {
|
||||||
toggleTheme: () => {
|
theme: initialDark ? darkTheme : lightTheme,
|
||||||
const newIsDark = !get().isDark;
|
isDark: initialDark,
|
||||||
set({
|
isHydrated: false,
|
||||||
isDark: newIsDark,
|
toggleTheme: () => {
|
||||||
theme: newIsDark ? darkTheme : lightTheme,
|
const newIsDark = !get().isDark;
|
||||||
});
|
set({
|
||||||
},
|
isDark: newIsDark,
|
||||||
setHydrated: () => set({ isHydrated: true }),
|
theme: newIsDark ? darkTheme : lightTheme,
|
||||||
}),
|
});
|
||||||
|
},
|
||||||
|
setHydrated: () => set({isHydrated: true}),
|
||||||
|
};
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'theme-storage',
|
name: 'theme-storage',
|
||||||
storage: createJSONStorage(() => zustandStorage),
|
storage: createJSONStorage(() => zustandStorage),
|
||||||
partialize: (state) => ({ isDark: state.isDark }),
|
partialize: (state) => ({isDark: state.isDark}),
|
||||||
onRehydrateStorage: () => (state) => {
|
onRehydrateStorage: () => (state) => {
|
||||||
if (state) {
|
if (state) {
|
||||||
state.theme = state.isDark ? darkTheme : lightTheme;
|
state.theme = state.isDark ? darkTheme : lightTheme;
|
||||||
|
|||||||
Reference in New Issue
Block a user