mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-05 05:28:37 +01:00
Feat/Show Splashcreen until UI loaded (#437)
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
|||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { useSplashScreenLoading, useSplashScreenVisible } from "@/providers/SplashScreenProvider";
|
||||||
|
|
||||||
type ScrollingCollectionListSection = {
|
type ScrollingCollectionListSection = {
|
||||||
type: "ScrollingCollectionList";
|
type: "ScrollingCollectionList";
|
||||||
@@ -146,6 +147,10 @@ export default function index() {
|
|||||||
staleTime: 60 * 1000,
|
staleTime: 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// show splash screen until query loaded
|
||||||
|
useSplashScreenLoading(l1)
|
||||||
|
const splashScreenVisible = useSplashScreenVisible()
|
||||||
|
|
||||||
const userViews = useMemo(
|
const userViews = useMemo(
|
||||||
() => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
|
() => data?.filter((l) => !settings?.hiddenLibraries?.includes(l.Id!)),
|
||||||
[data, settings?.hiddenLibraries]
|
[data, settings?.hiddenLibraries]
|
||||||
@@ -399,7 +404,9 @@ export default function index() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (l1)
|
// this spinner should only show up, when user navigates here
|
||||||
|
// on launch the splash screen is used for loading
|
||||||
|
if (l1 && !splashScreenVisible)
|
||||||
return (
|
return (
|
||||||
<View className="justify-center items-center h-full">
|
<View className="justify-center items-center h-full">
|
||||||
<Loader />
|
<Loader />
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
} from "@/providers/JellyfinProvider";
|
} from "@/providers/JellyfinProvider";
|
||||||
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||||
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider";
|
||||||
|
import { SplashScreenProvider, useSplashScreenLoading } from "@/providers/SplashScreenProvider";
|
||||||
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
import { WebSocketProvider } from "@/providers/WebSocketProvider";
|
||||||
import { orientationAtom } from "@/utils/atoms/orientation";
|
import { orientationAtom } from "@/utils/atoms/orientation";
|
||||||
import { Settings, useSettings } from "@/utils/atoms/settings";
|
import { Settings, useSettings } from "@/utils/atoms/settings";
|
||||||
@@ -33,7 +34,6 @@ import * as Linking from "expo-linking";
|
|||||||
import * as Notifications from "expo-notifications";
|
import * as Notifications from "expo-notifications";
|
||||||
import { router, Stack } from "expo-router";
|
import { router, Stack } from "expo-router";
|
||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
import * as SplashScreen from "expo-splash-screen";
|
|
||||||
import * as TaskManager from "expo-task-manager";
|
import * as TaskManager from "expo-task-manager";
|
||||||
import { Provider as JotaiProvider, useAtom } from "jotai";
|
import { Provider as JotaiProvider, useAtom } from "jotai";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
@@ -46,8 +46,6 @@ import { getLocales } from "expo-localization";
|
|||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import { Toaster } from "sonner-native";
|
import { Toaster } from "sonner-native";
|
||||||
|
|
||||||
SplashScreen.preventAutoHideAsync();
|
|
||||||
|
|
||||||
Notifications.setNotificationHandler({
|
Notifications.setNotificationHandler({
|
||||||
handleNotification: async () => ({
|
handleNotification: async () => ({
|
||||||
shouldShowAlert: true,
|
shouldShowAlert: true,
|
||||||
@@ -213,27 +211,15 @@ const checkAndRequestPermissions = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout() {
|
export default function RootLayout() {
|
||||||
const [loaded] = useFonts({
|
|
||||||
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (loaded) {
|
|
||||||
SplashScreen.hideAsync();
|
|
||||||
}
|
|
||||||
}, [loaded]);
|
|
||||||
|
|
||||||
Appearance.setColorScheme("dark");
|
Appearance.setColorScheme("dark");
|
||||||
|
|
||||||
if (!loaded) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JotaiProvider>
|
<JotaiProvider>
|
||||||
<I18nextProvider i18n={i18n}>
|
<SplashScreenProvider>
|
||||||
<Layout />
|
<I18nextProvider i18n={i18n}>
|
||||||
</I18nextProvider>
|
<Layout />
|
||||||
|
</I18nextProvider>
|
||||||
|
</SplashScreenProvider>
|
||||||
</JotaiProvider>
|
</JotaiProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -319,6 +305,17 @@ function Layout() {
|
|||||||
const { hostname, path, queryParams } = Linking.parse(url);
|
const { hostname, path, queryParams } = Linking.parse(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [loaded] = useFonts({
|
||||||
|
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// show splash screen until everything loaded
|
||||||
|
useSplashScreenLoading(!loaded)
|
||||||
|
|
||||||
|
if (!loaded) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GestureHandlerRootView style={{ flex: 1 }}>
|
<GestureHandlerRootView style={{ flex: 1 }}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
|
|||||||
@@ -46,6 +46,7 @@
|
|||||||
"expo-brightness": "~12.0.1",
|
"expo-brightness": "~12.0.1",
|
||||||
"expo-build-properties": "~0.12.5",
|
"expo-build-properties": "~0.12.5",
|
||||||
"expo-constants": "~16.0.2",
|
"expo-constants": "~16.0.2",
|
||||||
|
"expo-crypto": "^14.0.2",
|
||||||
"expo-dev-client": "~4.0.29",
|
"expo-dev-client": "~4.0.29",
|
||||||
"expo-device": "~6.0.2",
|
"expo-device": "~6.0.2",
|
||||||
"expo-font": "~12.0.10",
|
"expo-font": "~12.0.10",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { getDeviceName } from "react-native-device-info";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useSettings } from "@/utils/atoms/settings";
|
import { useSettings } from "@/utils/atoms/settings";
|
||||||
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
import { JellyseerrApi, useJellyseerr } from "@/hooks/useJellyseerr";
|
||||||
|
import { useSplashScreenLoading, useSplashScreenVisible } from "./SplashScreenProvider";
|
||||||
|
|
||||||
interface Server {
|
interface Server {
|
||||||
address: string;
|
address: string;
|
||||||
@@ -341,11 +342,17 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
initiateQuickConnect,
|
initiateQuickConnect,
|
||||||
};
|
};
|
||||||
|
|
||||||
useProtectedRoute(user, isLoading || isFetching);
|
let isLoadingOrFetching = isLoading || isFetching;
|
||||||
|
useProtectedRoute(user, isLoadingOrFetching);
|
||||||
|
|
||||||
|
// show splash screen until everything loaded
|
||||||
|
useSplashScreenLoading(isLoadingOrFetching)
|
||||||
|
const splashScreenVisible = useSplashScreenVisible()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<JellyfinContext.Provider value={contextValue}>
|
<JellyfinContext.Provider value={contextValue}>
|
||||||
{children}
|
{/* don't render login page when loading and splash screen visible */}
|
||||||
|
{isLoadingOrFetching && splashScreenVisible ? undefined : children}
|
||||||
</JellyfinContext.Provider>
|
</JellyfinContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
115
providers/SplashScreenProvider.tsx
Normal file
115
providers/SplashScreenProvider.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react";
|
||||||
|
import * as Crypto from 'expo-crypto';
|
||||||
|
import * as SplashScreen from "expo-splash-screen";
|
||||||
|
|
||||||
|
class ChangeListenerMap<K, V> extends Map<K, V> {
|
||||||
|
constructor(private readonly onChange: (e: { self: ChangeListenerMap<K, V>, key: K, oldValue: V | undefined, newValue: V }) => void) {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(key: K, value: V): this {
|
||||||
|
const oldValue = this.get(key);
|
||||||
|
super.set(key, value);
|
||||||
|
if(oldValue !== value) {
|
||||||
|
this.onChange({ self: this, key, oldValue, newValue: value })
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SplashScreenContextValue = {
|
||||||
|
splashScreenVisible: boolean,
|
||||||
|
componentLoaded: Map<string, boolean>
|
||||||
|
}
|
||||||
|
|
||||||
|
const SplashScreenContext = createContext<SplashScreenContextValue | undefined>(undefined)
|
||||||
|
|
||||||
|
SplashScreen.preventAutoHideAsync();
|
||||||
|
|
||||||
|
export const SplashScreenProvider: React.FC<{ children: ReactNode }> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
const [splashScreenVisible, setSplashScreenVisible] = useState(true)
|
||||||
|
|
||||||
|
const contextValue: SplashScreenContextValue = {
|
||||||
|
splashScreenVisible,
|
||||||
|
componentLoaded: new ChangeListenerMap(({ self }) => {
|
||||||
|
for(const entry of self.entries()) {
|
||||||
|
if(!entry[1]) {
|
||||||
|
// one component not loaded yet, not hiding splash screen
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SplashScreen.hideAsync()
|
||||||
|
setSplashScreenVisible(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SplashScreenContext.Provider value={contextValue}>
|
||||||
|
{children}
|
||||||
|
</SplashScreenContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Splash Screen until component is ready to be displayed.
|
||||||
|
*
|
||||||
|
* This only has an effect when component is mounted before Splash Screen is hidden,
|
||||||
|
* so it should only be used in components that show up on launch.
|
||||||
|
*
|
||||||
|
* @param isLoading The loading state of the component
|
||||||
|
*
|
||||||
|
* ## Usage
|
||||||
|
* ```
|
||||||
|
* // Example 1:
|
||||||
|
* const isLoading = loadSomething()
|
||||||
|
* useSplashScreenLoading(isLoading) // splash screen visible until isLoading is false
|
||||||
|
* ```
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* // Example 2: multiple loading states
|
||||||
|
* const isLoading1 = loadSomething()
|
||||||
|
* useSplashScreenLoading(isLoading1) // splash screen visible until isLoading1 and isLoading2 are false
|
||||||
|
*
|
||||||
|
* // this could be in different component and still have the same effect
|
||||||
|
* const isLoading2 = loadSomethingElse()
|
||||||
|
* useSplashScreenLoading(isLoading2)
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useSplashScreenLoading(isLoading: boolean) {
|
||||||
|
const id = useMemo(() => Crypto.randomUUID(), []);
|
||||||
|
|
||||||
|
const context = useContext(SplashScreenContext);
|
||||||
|
if(!context) {
|
||||||
|
throw new Error("useSplashScreenLoading must be used within a SplashScreenProvider");
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// update the loading state of component
|
||||||
|
context.componentLoaded.set(id, !isLoading)
|
||||||
|
|
||||||
|
// cleanup when unmounting component
|
||||||
|
return () => {
|
||||||
|
context.componentLoaded.delete(id);
|
||||||
|
};
|
||||||
|
}, [isLoading])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the visiblity of the Splash Screen.
|
||||||
|
* @returns the visibility of the Splash Screen
|
||||||
|
*
|
||||||
|
* ## Usage
|
||||||
|
* ```
|
||||||
|
* const splashScreenIsVisible = useSplashScreenVisible()
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export function useSplashScreenVisible() {
|
||||||
|
const context = useContext(SplashScreenContext);
|
||||||
|
if(!context) {
|
||||||
|
throw new Error("useSplashScreenVisible must be used within a SplashScreenProvider");
|
||||||
|
}
|
||||||
|
return context.splashScreenVisible
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user