4dc933ee5a
`AppTheme` never set `android:windowBackground`, so it was inherited from `Theme.AppCompat.DayNight.NoActionBar` and resolved to a near-white colour whenever the system was in light mode — independently of the theme picked inside the app. Nothing between that window and the navigators painted a background of its own: the root view, `GestureHandlerRootView` and `SafeAreaProvider` are all transparent, and only the individual screens carry `contentStyle`. Any frame in which no screen was drawn — mounting a nested navigator such as `MoreStack` or `ReaderStack`, or freezing the screen being left — therefore showed the white window through. Paint the root view with the active theme background so the window stays covered, and give `AppTheme` a day/night `android:windowBackground` so the frames before React draws are themed as well. The chapter screen additionally uses the reader background instead of the app background, so pushing it no longer flashes the app theme before the reader appears. Claude-Session: https://claude.ai/code/session_014uxuC7JVkShhWnN1WhkTaH Co-authored-by: Claude <noreply@anthropic.com>
119 lines
3.6 KiB
TypeScript
119 lines
3.6 KiB
TypeScript
import 'react-native-url-polyfill/auto';
|
||
import { enableFreeze } from 'react-native-screens';
|
||
import { PropsWithChildren, Suspense, useEffect, useMemo } from 'react';
|
||
import { StatusBar, StyleSheet } from 'react-native';
|
||
import { GestureHandlerRootView } from 'react-native-gesture-handler';
|
||
import * as SplashScreen from 'expo-splash-screen';
|
||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||
import {
|
||
MD3DarkTheme,
|
||
MD3LightTheme,
|
||
Provider as PaperProvider,
|
||
} from 'react-native-paper';
|
||
|
||
import AppErrorBoundary, {
|
||
ErrorFallback,
|
||
} from '@components/AppErrorBoundary/AppErrorBoundary';
|
||
|
||
import Main from './src/navigators/Main';
|
||
import { BottomSheetModalProvider } from '@gorhom/bottom-sheet';
|
||
import { useInitDatabase } from '@database/db';
|
||
import { useInitializeAppServices } from '@hooks/common/useInitializeAppServices';
|
||
import { opSqliteAdapter } from './src/rozenite/opSqliteAdapter';
|
||
import { useRozeniteSqlitePlugin } from '@rozenite/sqlite-plugin';
|
||
import { ThemeProvider, useTheme } from '@hooks/persisted/useTheme';
|
||
|
||
enableFreeze(true);
|
||
const sqliteAdapters = __DEV__ && opSqliteAdapter ? [opSqliteAdapter] : [];
|
||
|
||
/**
|
||
* The Android window background is resolved from the system light/dark setting,
|
||
* so it is bright white whenever the system is light – regardless of the theme
|
||
* picked inside the app. Nothing between it and the navigators paints a
|
||
* background of its own, so every frame in which no screen is drawn (mounting a
|
||
* nested navigator, freezing the screen being left) shows through as a flash.
|
||
* Painting the root view keeps the window covered at all times.
|
||
*/
|
||
const ThemedRootView = ({ children }: PropsWithChildren) => {
|
||
const theme = useTheme();
|
||
|
||
return (
|
||
<GestureHandlerRootView
|
||
style={[styles.flex, { backgroundColor: theme.background }]}
|
||
>
|
||
{children}
|
||
</GestureHandlerRootView>
|
||
);
|
||
};
|
||
|
||
const ThemedPaperProvider = ({ children }: PropsWithChildren) => {
|
||
const theme = useTheme();
|
||
const paperTheme = useMemo(() => {
|
||
const baseTheme = theme.isDark ? MD3DarkTheme : MD3LightTheme;
|
||
|
||
return {
|
||
...baseTheme,
|
||
colors: {
|
||
...baseTheme.colors,
|
||
...theme,
|
||
},
|
||
};
|
||
}, [theme]);
|
||
|
||
return <PaperProvider theme={paperTheme}>{children}</PaperProvider>;
|
||
};
|
||
|
||
const App = () => {
|
||
useRozeniteSqlitePlugin({ adapters: sqliteAdapters });
|
||
const { success: databaseReady, error: databaseError } = useInitDatabase();
|
||
const { ready: servicesReady, error: servicesError } =
|
||
useInitializeAppServices(Boolean(databaseReady));
|
||
|
||
useEffect(() => {
|
||
if ((databaseReady && servicesReady) || databaseError || servicesError) {
|
||
SplashScreen.hideAsync();
|
||
}
|
||
}, [databaseReady, databaseError, servicesReady, servicesError]);
|
||
|
||
const initializationError = databaseError || servicesError;
|
||
|
||
if (initializationError) {
|
||
return (
|
||
<ThemeProvider>
|
||
<ErrorFallback error={initializationError} resetError={() => null} />
|
||
</ThemeProvider>
|
||
);
|
||
}
|
||
|
||
if (!databaseReady || !servicesReady) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<Suspense fallback={null}>
|
||
<ThemeProvider>
|
||
<ThemedRootView>
|
||
<AppErrorBoundary>
|
||
<SafeAreaProvider>
|
||
<ThemedPaperProvider>
|
||
<BottomSheetModalProvider>
|
||
<StatusBar translucent={true} backgroundColor="transparent" />
|
||
<Main />
|
||
</BottomSheetModalProvider>
|
||
</ThemedPaperProvider>
|
||
</SafeAreaProvider>
|
||
</AppErrorBoundary>
|
||
</ThemedRootView>
|
||
</ThemeProvider>
|
||
</Suspense>
|
||
);
|
||
};
|
||
|
||
export default App;
|
||
|
||
const styles = StyleSheet.create({
|
||
flex: {
|
||
flex: 1,
|
||
},
|
||
});
|