This commit is contained in:
Fredrik Burmester
2025-01-31 15:36:49 +01:00
parent c4d4475aa9
commit a71a646743
7 changed files with 8498 additions and 203 deletions

View File

@@ -9,8 +9,7 @@
"userInterfaceStyle": "dark", "userInterfaceStyle": "dark",
"splash": { "splash": {
"image": "./assets/images/splash.png", "image": "./assets/images/splash.png",
"resizeMode": "contain", "resizeMode": "contain"
"backgroundColor": "#2E2E2E"
}, },
"jsEngine": "hermes", "jsEngine": "hermes",
"assetBundlePatterns": ["**/*"], "assetBundlePatterns": ["**/*"],
@@ -89,7 +88,9 @@
"jniLibs": { "jniLibs": {
"useLegacyPackaging": true "useLegacyPackaging": true
} }
} },
"useAndroidX": true,
"enableJetifier": true
} }
} }
], ],

View File

@@ -1,5 +1,5 @@
import "@/augmentations"; import "@/augmentations";
import { Text } from "@/components/common/Text"; import i18n from "@/i18n";
import { DownloadProvider } from "@/providers/DownloadProvider"; import { DownloadProvider } from "@/providers/DownloadProvider";
import { import {
getOrSetDeviceId, getOrSetDeviceId,
@@ -8,9 +8,11 @@ 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 {
SplashScreenProvider,
useSplashScreenLoading,
} from "@/providers/SplashScreenProvider";
import { WebSocketProvider } from "@/providers/WebSocketProvider"; import { WebSocketProvider } from "@/providers/WebSocketProvider";
import { orientationAtom } from "@/utils/atoms/orientation";
import { Settings, useSettings } from "@/utils/atoms/settings"; import { Settings, useSettings } from "@/utils/atoms/settings";
import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks"; import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks";
import { LogProvider, writeToLog } from "@/utils/log"; import { LogProvider, writeToLog } from "@/utils/log";
@@ -30,19 +32,17 @@ import * as BackgroundFetch from "expo-background-fetch";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import { useFonts } from "expo-font"; import { useFonts } from "expo-font";
import { useKeepAwake } from "expo-keep-awake"; import { useKeepAwake } from "expo-keep-awake";
import * as Linking from "expo-linking"; import { getLocales } from "expo-localization";
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 TaskManager from "expo-task-manager"; import * as TaskManager from "expo-task-manager";
import { Provider as JotaiProvider, useAtom } from "jotai"; import { Provider as JotaiProvider } from "jotai";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Appearance, AppState, TouchableOpacity } from "react-native"; import { I18nextProvider, useTranslation } from "react-i18next";
import { Appearance, AppState } from "react-native";
import { SystemBars } from "react-native-edge-to-edge"; import { SystemBars } from "react-native-edge-to-edge";
import { GestureHandlerRootView } from "react-native-gesture-handler"; import { GestureHandlerRootView } from "react-native-gesture-handler";
import { I18nextProvider, useTranslation } from "react-i18next";
import i18n from "@/i18n";
import { getLocales } from "expo-localization";
import "react-native-reanimated"; import "react-native-reanimated";
import { Toaster } from "sonner-native"; import { Toaster } from "sonner-native";
@@ -214,13 +214,17 @@ export default function RootLayout() {
Appearance.setColorScheme("dark"); Appearance.setColorScheme("dark");
return ( return (
<JotaiProvider> <SplashScreenProvider>
<SplashScreenProvider> <GestureHandlerRootView style={{ flex: 1 }}>
<I18nextProvider i18n={i18n}> <JotaiProvider>
<Layout /> <ActionSheetProvider>
</I18nextProvider> <I18nextProvider i18n={i18n}>
</SplashScreenProvider> <Layout />
</JotaiProvider> </I18nextProvider>
</ActionSheetProvider>
</JotaiProvider>
</GestureHandlerRootView>
</SplashScreenProvider>
); );
} }
@@ -237,8 +241,7 @@ const queryClient = new QueryClient({
}); });
function Layout() { function Layout() {
const [settings, updateSettings] = useSettings(); const [settings] = useSettings();
const [orientation, setOrientation] = useAtom(orientationAtom);
useKeepAwake(); useKeepAwake();
useNotificationObserver(); useNotificationObserver();
@@ -283,104 +286,77 @@ function Layout() {
}; };
}, []); }, []);
useEffect(() => {
const subscription = ScreenOrientation.addOrientationChangeListener(
(event) => {
setOrientation(event.orientationInfo.orientation);
}
);
ScreenOrientation.getOrientationAsync().then((initialOrientation) => {
setOrientation(initialOrientation);
});
return () => {
ScreenOrientation.removeOrientationChangeListener(subscription);
};
}, []);
const url = Linking.useURL();
if (url) {
const { hostname, path, queryParams } = Linking.parse(url);
}
const [loaded] = useFonts({ const [loaded] = useFonts({
SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"),
}); });
// show splash screen until everything loaded useSplashScreenLoading(!loaded);
useSplashScreenLoading(!loaded)
if (!loaded) { if (!loaded) {
return null; return null;
} }
return ( return (
<GestureHandlerRootView style={{ flex: 1 }}> <QueryClientProvider client={queryClient}>
<QueryClientProvider client={queryClient}> <JobQueueProvider>
<ActionSheetProvider> <JellyfinProvider>
<JobQueueProvider> <PlaySettingsProvider>
<JellyfinProvider> <LogProvider>
<PlaySettingsProvider> <WebSocketProvider>
<LogProvider> <DownloadProvider>
<WebSocketProvider> <BottomSheetModalProvider>
<DownloadProvider> <SystemBars style="light" hidden={false} />
<BottomSheetModalProvider> <ThemeProvider value={DarkTheme}>
<SystemBars style="light" hidden={false} /> <Stack>
<ThemeProvider value={DarkTheme}> <Stack.Screen
<Stack> name="(auth)/(tabs)"
<Stack.Screen options={{
name="(auth)/(tabs)" headerShown: false,
options={{ title: "",
headerShown: false, header: () => null,
title: "", }}
header: () => null, />
}} <Stack.Screen
/> name="(auth)/player"
<Stack.Screen options={{
name="(auth)/player" headerShown: false,
options={{ title: "",
headerShown: false, header: () => null,
title: "", }}
header: () => null, />
}} <Stack.Screen
/> name="login"
<Stack.Screen options={{
name="login" headerShown: true,
options={{ title: "",
headerShown: true, headerTransparent: true,
title: "", }}
headerTransparent: true, />
}} <Stack.Screen name="+not-found" />
/> </Stack>
<Stack.Screen name="+not-found" /> <Toaster
</Stack> duration={4000}
<Toaster toastOptions={{
duration={4000} style: {
toastOptions={{ backgroundColor: "#262626",
style: { borderColor: "#363639",
backgroundColor: "#262626", borderWidth: 1,
borderColor: "#363639", },
borderWidth: 1, titleStyle: {
}, color: "white",
titleStyle: { },
color: "white", }}
}, closeButton
}} />
closeButton </ThemeProvider>
/> </BottomSheetModalProvider>
</ThemeProvider> </DownloadProvider>
</BottomSheetModalProvider> </WebSocketProvider>
</DownloadProvider> </LogProvider>
</WebSocketProvider> </PlaySettingsProvider>
</LogProvider> </JellyfinProvider>
</PlaySettingsProvider> </JobQueueProvider>
</JellyfinProvider> </QueryClientProvider>
</JobQueueProvider>
</ActionSheetProvider>
</QueryClientProvider>
</GestureHandlerRootView>
); );
} }

BIN
bun.lockb

Binary file not shown.

View File

@@ -14,8 +14,9 @@
"postinstall": "patch-package" "postinstall": "patch-package"
}, },
"dependencies": { "dependencies": {
"@bottom-tabs/react-navigation": "0.8.0", "@bottom-tabs/react-navigation": "0.8.3",
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0", "@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
"@expo/config-plugins": "~9.0.0",
"@expo/react-native-action-sheet": "^4.1.0", "@expo/react-native-action-sheet": "^4.1.0",
"@expo/vector-icons": "^14.0.4", "@expo/vector-icons": "^14.0.4",
"@futurejj/react-native-visibility-sensor": "^1.3.5", "@futurejj/react-native-visibility-sensor": "^1.3.5",
@@ -25,8 +26,9 @@
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/netinfo": "11.4.1", "@react-native-community/netinfo": "11.4.1",
"@react-native-menu/menu": "^1.1.6", "@react-native-menu/menu": "^1.1.6",
"@react-navigation/bottom-tabs": "^7.0.0",
"@react-navigation/material-top-tabs": "^7.1.0", "@react-navigation/material-top-tabs": "^7.1.0",
"@react-navigation/native": "^7.0.14", "@react-navigation/native": "^7.0.0",
"@shopify/flash-list": "1.7.1", "@shopify/flash-list": "1.7.1",
"@tanstack/react-query": "^5.59.20", "@tanstack/react-query": "^5.59.20",
"@types/lodash": "^4.17.13", "@types/lodash": "^4.17.13",
@@ -34,13 +36,13 @@
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"add": "^2.0.6", "add": "^2.0.6",
"axios": "^1.7.7", "axios": "^1.7.7",
"expo": "^52.0.0", "expo": "^52.0.28",
"expo-asset": "~11.0.2", "expo-asset": "~11.0.2",
"expo-background-fetch": "~13.0.4", "expo-background-fetch": "~13.0.5",
"expo-blur": "~14.0.3", "expo-blur": "~14.0.3",
"expo-brightness": "~13.0.3", "expo-brightness": "~13.0.3",
"expo-build-properties": "~0.13.2", "expo-build-properties": "~0.13.2",
"expo-constants": "~17.0.4", "expo-constants": "~17.0.5",
"expo-crypto": "~14.0.2", "expo-crypto": "~14.0.2",
"expo-dev-client": "~5.0.10", "expo-dev-client": "~5.0.10",
"expo-device": "~7.0.2", "expo-device": "~7.0.2",
@@ -49,20 +51,21 @@
"expo-image": "~2.0.4", "expo-image": "~2.0.4",
"expo-keep-awake": "~14.0.2", "expo-keep-awake": "~14.0.2",
"expo-linear-gradient": "~14.0.2", "expo-linear-gradient": "~14.0.2",
"expo-linking": "~7.0.4", "expo-linking": "~7.0.5",
"expo-localization": "~16.0.1", "expo-localization": "~16.0.1",
"expo-network": "~7.0.5", "expo-network": "~7.0.5",
"expo-notifications": "~0.29.12", "expo-notifications": "~0.29.13",
"expo-router": "~4.0.17", "expo-router": "~4.0.17",
"expo-screen-orientation": "~8.0.4", "expo-screen-orientation": "~8.0.4",
"expo-sensors": "~14.0.2", "expo-sensors": "~14.0.2",
"expo-splash-screen": "~0.29.21", "expo-splash-screen": "~0.29.21",
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.7", "expo-system-ui": "~4.0.7",
"expo-task-manager": "~12.0.4", "expo-task-manager": "~12.0.5",
"expo-updates": "~0.26.13", "expo-updates": "~0.26.13",
"expo-web-browser": "~14.0.2", "expo-web-browser": "~14.0.2",
"ffmpeg-kit-react-native": "^6.0.2", "ffmpeg-kit-react-native": "^6.0.2",
"i18next": "^24.2.2",
"install": "^0.13.0", "install": "^0.13.0",
"jotai": "^2.10.1", "jotai": "^2.10.1",
"lodash": "^4.17.21", "lodash": "^4.17.21",
@@ -72,7 +75,7 @@
"react-i18next": "^15.4.0", "react-i18next": "^15.4.0",
"react-native": "0.76.6", "react-native": "0.76.6",
"react-native-awesome-slider": "^2.5.6", "react-native-awesome-slider": "^2.5.6",
"react-native-bottom-tabs": "0.8.0", "react-native-bottom-tabs": "0.8.3",
"react-native-circular-progress": "^1.4.1", "react-native-circular-progress": "^1.4.1",
"react-native-compressor": "^1.9.0", "react-native-compressor": "^1.9.0",
"react-native-country-flag": "^2.0.2", "react-native-country-flag": "^2.0.2",
@@ -81,7 +84,7 @@
"react-native-gesture-handler": "~2.20.2", "react-native-gesture-handler": "~2.20.2",
"react-native-get-random-values": "^1.11.0", "react-native-get-random-values": "^1.11.0",
"react-native-google-cast": "^4.8.3", "react-native-google-cast": "^4.8.3",
"react-native-image-colors": "^2.4.0", "react-native-image-colors": "^1.3.1",
"react-native-ios-context-menu": "^2.5.2", "react-native-ios-context-menu": "^2.5.2",
"react-native-ios-utilities": "4.5.3", "react-native-ios-utilities": "4.5.3",
"react-native-mmkv": "^2.12.2", "react-native-mmkv": "^2.12.2",

View File

@@ -3,6 +3,7 @@
<base-config cleartextTrafficPermitted="false"> <base-config cleartextTrafficPermitted="false">
<trust-anchors> <trust-anchors>
<certificates src="system" /> <certificates src="system" />
<certificates src="user" />
</trust-anchors> </trust-anchors>
</base-config> </base-config>
</network-security-config> </network-security-config>

View File

@@ -1,115 +1,103 @@
import { createContext, ReactNode, useContext, useEffect, useMemo, useState } from "react"; import {
import * as Crypto from 'expo-crypto'; createContext,
ReactNode,
useContext,
useEffect,
useState,
useRef,
} from "react";
import * as SplashScreen from "expo-splash-screen"; 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 = { type SplashScreenContextValue = {
splashScreenVisible: boolean, registerLoadingComponent: () => () => void;
componentLoaded: Map<string, boolean> splashScreenVisible: boolean;
} };
const SplashScreenContext = createContext<SplashScreenContextValue | undefined>(undefined) const SplashScreenContext = createContext<SplashScreenContextValue | undefined>(
undefined
);
SplashScreen.preventAutoHideAsync(); // Prevent splash screen from auto-hiding
void SplashScreen.preventAutoHideAsync();
export const SplashScreenProvider: React.FC<{ children: ReactNode }> = ({ export const SplashScreenProvider: React.FC<{ children: ReactNode }> = ({
children, children,
}) => { }) => {
const [splashScreenVisible, setSplashScreenVisible] = useState(true);
const loadingComponentsCount = useRef(0);
const isHidingRef = useRef(false);
const [splashScreenVisible, setSplashScreenVisible] = useState(true) const hideScreenIfNoLoadingComponents = async () => {
if (loadingComponentsCount.current === 0 && !isHidingRef.current) {
const contextValue: SplashScreenContextValue = { try {
splashScreenVisible, isHidingRef.current = true;
componentLoaded: new ChangeListenerMap(({ self }) => { await SplashScreen.hideAsync();
for(const entry of self.entries()) { setSplashScreenVisible(false);
if(!entry[1]) { } catch (error) {
// one component not loaded yet, not hiding splash screen console.warn("Failed to hide splash screen:", error);
return } finally {
} isHidingRef.current = false;
} }
SplashScreen.hideAsync()
setSplashScreenVisible(false)
})
} }
};
return ( const registerLoadingComponent = () => {
<SplashScreenContext.Provider value={contextValue}> loadingComponentsCount.current += 1;
{children}
</SplashScreenContext.Provider> return () => {
) loadingComponentsCount.current -= 1;
} void hideScreenIfNoLoadingComponents();
};
};
const contextValue: SplashScreenContextValue = {
registerLoadingComponent,
splashScreenVisible,
};
return (
<SplashScreenContext.Provider value={contextValue}>
{children}
</SplashScreenContext.Provider>
);
};
/** /**
* Show the Splash Screen until component is ready to be displayed. * 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 * @param isLoading The loading state of the component
* *
* ## Usage * ## Usage
* ``` * ```
* // Example 1:
* const isLoading = loadSomething() * const isLoading = loadSomething()
* useSplashScreenLoading(isLoading) // splash screen visible until isLoading is false * 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) { 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"
);
}
const context = useContext(SplashScreenContext); useEffect(() => {
if(!context) { if (isLoading) {
throw new Error("useSplashScreenLoading must be used within a SplashScreenProvider"); return context.registerLoadingComponent();
} }
}, [isLoading]);
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. * Get the visibility of the Splash Screen.
* @returns the visibility of the Splash Screen * @returns the visibility of the Splash Screen
*
* ## Usage
* ```
* const splashScreenIsVisible = useSplashScreenVisible()
* ```
*/ */
export function useSplashScreenVisible() { export function useSplashScreenVisible() {
const context = useContext(SplashScreenContext); const context = useContext(SplashScreenContext);
if(!context) { if (!context) {
throw new Error("useSplashScreenVisible must be used within a SplashScreenProvider"); throw new Error(
} "useSplashScreenVisible must be used within a SplashScreenProvider"
return context.splashScreenVisible );
}
return context.splashScreenVisible;
} }

8326
yarn.lock Normal file

File diff suppressed because it is too large Load Diff