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:
2025-12-27 16:09:15 +01:00
parent 9cd53ca7ab
commit dcab4be87d
9 changed files with 99 additions and 62 deletions
+4 -1
View File
@@ -34,7 +34,10 @@
"image": "./assets/images/racoon-splash.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff"
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#121212"
}
}
],
"expo-secure-store",
+3
View File
@@ -41,6 +41,9 @@ export default function TabLayout() {
</Link>
),
tabBarActiveTintColor: theme.colors.primary,
sceneContainerStyle: {
backgroundColor: theme.colors.background,
},
tabBarInactiveTintColor: theme.colors.onSurfaceVariant,
tabBarStyle: {
backgroundColor: theme.colors.surface,
+9
View File
@@ -3,3 +3,12 @@
@tailwind utilities;
body {
background-color: #ffffff;
}
@media (prefers-color-scheme: dark) {
body {
background-color: #121212 !important;
}
}
+3 -10
View File
@@ -2,28 +2,21 @@ import React, { useEffect } from "react";
import {useAuth} from "@/src/stores/authStore";
import {useThemeStore} from "@/src/stores/themeStore";
import NotAuthenticated from "@/src/components/NotAuthenticated";
import LoadingScreen from "@/src/components/LoadingScreen";
import * as SplashScreen from 'expo-splash-screen';
import SplashScreenComponent from "@/src/components/SplashScreenComponent";
const AuthenticatedWrapper: React.FC<{ children: React.ReactNode }> = ({children}) => {
const {isAuthenticated, loading, authenticatedUser, isHydrated: authHydrated} = useAuth();
const {isHydrated: themeHydrated} = useThemeStore();
// Verstecke den Splash Screen erst wenn beide Stores hydratiert sind
useEffect(() => {
if (authHydrated && themeHydrated) {
SplashScreen.hideAsync().catch(() => {});
}
}, [authHydrated, themeHydrated]);
// Zeige nichts (Splash Screen bleibt) bis beide Stores hydratiert sind
if (!authHydrated || !themeHydrated) {
return null;
}
// Zeige LoadingScreen während Auth Status geprüft wird
if (loading) {
return <LoadingScreen />;
if (!authHydrated || !themeHydrated || loading) {
return <SplashScreenComponent/>;
}
if (!isAuthenticated || !authenticatedUser) {
-25
View File
@@ -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>
);
}
+27
View File
@@ -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>
);
}
+21 -7
View File
@@ -1,26 +1,40 @@
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 {useColorScheme} from "nativewind";
import LoadingScreen from "@/src/components/LoadingScreen";
import * as SplashScreen from 'expo-splash-screen';
import SplashScreenComponent from "@/src/components/SplashScreenComponent";
import { ThemeProvider as NavThemeProvider, DarkTheme, DefaultTheme } from '@react-navigation/native';
import {View} from "react-native";
SplashScreen.preventAutoHideAsync().catch(() => {});
export const ThemeProvider = ({children}: { children: ReactNode }) => {
const { theme, isDark, isHydrated } = useThemeStore();
const { setColorScheme } = useColorScheme();
const { LightTheme: NavLightTheme, DarkTheme: NavDarkTheme } = adaptNavigationTheme({
reactNavigationLight: DefaultTheme,
reactNavigationDark: DarkTheme,
});
const navigationTheme = isDark ? NavDarkTheme : NavLightTheme;
useEffect(() => {
setColorScheme(isDark ? 'dark' : 'light');
}, [isDark, setColorScheme]);
}, [isDark, setColorScheme, isHydrated]);
if (!isHydrated) {
return <LoadingScreen />;
return <SplashScreenComponent />;
}
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
View File
@@ -1,10 +1,12 @@
import React from "react";
import {Slot, Stack} from "expo-router";
import {useThemeStore} from "@/src/stores";
export default function CustomStack() {
const { theme } = useThemeStore();
return (
<Stack screenOptions={{ headerShown: false }}>
<Stack screenOptions={{ headerShown: false, contentStyle: { backgroundColor: theme.colors.background } }}>
<Slot />
</Stack>
);
+29 -18
View File
@@ -1,8 +1,15 @@
import { create } from 'zustand';
import { persist, createJSONStorage } from 'zustand/middleware';
import { Platform } from 'react-native';
import {create} from 'zustand';
import {persist, createJSONStorage} from 'zustand/middleware';
import {Platform} from 'react-native';
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 = {
getItem: async (name: string): Promise<string | null> => {
@@ -37,23 +44,27 @@ interface ThemeState {
export const useThemeStore = create<ThemeState>()(
persist(
(set, get) => ({
theme: lightTheme,
isDark: false,
isHydrated: false,
toggleTheme: () => {
const newIsDark = !get().isDark;
set({
isDark: newIsDark,
theme: newIsDark ? darkTheme : lightTheme,
});
},
setHydrated: () => set({ isHydrated: true }),
}),
(set, get) => {
const initialDark = getSystemThemeIsDark();
return {
theme: initialDark ? darkTheme : lightTheme,
isDark: initialDark,
isHydrated: false,
toggleTheme: () => {
const newIsDark = !get().isDark;
set({
isDark: newIsDark,
theme: newIsDark ? darkTheme : lightTheme,
});
},
setHydrated: () => set({isHydrated: true}),
};
},
{
name: 'theme-storage',
storage: createJSONStorage(() => zustandStorage),
partialize: (state) => ({ isDark: state.isDark }),
partialize: (state) => ({isDark: state.isDark}),
onRehydrateStorage: () => (state) => {
if (state) {
state.theme = state.isDark ? darkTheme : lightTheme;