import "@/augmentations"; import { DownloadProvider } from "@/providers/DownloadProvider"; import { getOrSetDeviceId, getTokenFromStorage, JellyfinProvider, } from "@/providers/JellyfinProvider"; import { JobQueueProvider } from "@/providers/JobQueueProvider"; import { PlaySettingsProvider } from "@/providers/PlaySettingsProvider"; import { WebSocketProvider } from "@/providers/WebSocketProvider"; import { orientationAtom } from "@/utils/atoms/orientation"; import { Settings, useSettings } from "@/utils/atoms/settings"; import { BACKGROUND_FETCH_TASK } from "@/utils/background-tasks"; import { LogProvider, writeToLog } from "@/utils/log"; import { storage } from "@/utils/mmkv"; import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server"; import { ActionSheetProvider } from "@expo/react-native-action-sheet"; import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { checkForExistingDownloads, completeHandler, download, } from "@kesha-antonov/react-native-background-downloader"; import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import * as BackgroundFetch from "expo-background-fetch"; import * as FileSystem from "expo-file-system"; import { useFonts } from "expo-font"; import { useKeepAwake } from "expo-keep-awake"; import * as Linking from "expo-linking"; import * as Notifications from "expo-notifications"; import { router, Stack } from "expo-router"; import * as ScreenOrientation from "expo-screen-orientation"; import * as SplashScreen from "expo-splash-screen"; import * as TaskManager from "expo-task-manager"; import { Provider as JotaiProvider, useAtom } from "jotai"; import { useEffect, useRef } from "react"; import { Appearance, AppState } from "react-native"; import { SystemBars } from "react-native-edge-to-edge"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import "react-native-reanimated"; import { Toaster } from "sonner-native"; SplashScreen.preventAutoHideAsync(); Notifications.setNotificationHandler({ handleNotification: async () => ({ shouldShowAlert: true, shouldPlaySound: true, shouldSetBadge: false, }), }); function useNotificationObserver() { useEffect(() => { let isMounted = true; function redirect(notification: Notifications.Notification) { const url = notification.request.content.data?.url; if (url) { router.push(url); } } Notifications.getLastNotificationResponseAsync().then((response) => { if (!isMounted || !response?.notification) { return; } redirect(response?.notification); }); const subscription = Notifications.addNotificationResponseReceivedListener( (response) => { redirect(response.notification); } ); return () => { isMounted = false; subscription.remove(); }; }, []); } TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { console.log("TaskManager ~ trigger"); const now = Date.now(); const settingsData = storage.getString("settings"); if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData; const settings: Partial = JSON.parse(settingsData); const url = settings?.optimizedVersionsServerUrl; if (!settings?.autoDownload || !url) return BackgroundFetch.BackgroundFetchResult.NoData; const token = getTokenFromStorage(); const deviceId = getOrSetDeviceId(); const baseDirectory = FileSystem.documentDirectory; if (!token || !deviceId || !baseDirectory) return BackgroundFetch.BackgroundFetchResult.NoData; const jobs = await getAllJobsByDeviceId({ deviceId, authHeader: token, url, }); console.log("TaskManager ~ Active jobs: ", jobs.length); for (let job of jobs) { if (job.status === "completed") { const downloadUrl = url + "download/" + job.id; const tasks = await checkForExistingDownloads(); if (tasks.find((task) => task.id === job.id)) { console.log("TaskManager ~ Download already in progress: ", job.id); continue; } download({ id: job.id, url: downloadUrl, destination: `${baseDirectory}${job.item.Id}.mp4`, headers: { Authorization: token, }, }) .begin(() => { console.log("TaskManager ~ Download started: ", job.id); }) .done(() => { console.log("TaskManager ~ Download completed: ", job.id); saveDownloadedItemInfo(job.item); completeHandler(job.id); cancelJobById({ authHeader: token, id: job.id, url: url, }); Notifications.scheduleNotificationAsync({ content: { title: job.item.Name, body: "Download completed", data: { url: `/downloads`, }, }, trigger: null, }); }) .error((error) => { console.log("TaskManager ~ Download error: ", job.id, error); completeHandler(job.id); Notifications.scheduleNotificationAsync({ content: { title: job.item.Name, body: "Download failed", data: { url: `/downloads`, }, }, trigger: null, }); }); } } console.log(`Auto download started: ${new Date(now).toISOString()}`); // Be sure to return the successful result type! return BackgroundFetch.BackgroundFetchResult.NewData; }); const checkAndRequestPermissions = async () => { try { const hasAskedBefore = storage.getString( "hasAskedForNotificationPermission" ); if (hasAskedBefore !== "true") { const { status } = await Notifications.requestPermissionsAsync(); if (status === "granted") { writeToLog("INFO", "Notification permissions granted."); console.log("Notification permissions granted."); } else { writeToLog("ERROR", "Notification permissions denied."); console.log("Notification permissions denied."); } storage.set("hasAskedForNotificationPermission", "true"); } else { console.log("Already asked for notification permissions before."); } } catch (error) { writeToLog( "ERROR", "Error checking/requesting notification permissions:", error ); console.error("Error checking/requesting notification permissions:", error); } }; export default function RootLayout() { const [loaded] = useFonts({ SpaceMono: require("../assets/fonts/SpaceMono-Regular.ttf"), }); useEffect(() => { if (loaded) { SplashScreen.hideAsync(); } }, [loaded]); Appearance.setColorScheme("dark"); if (!loaded) { return null; } return ( ); } const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 0, refetchOnMount: true, refetchOnReconnect: true, refetchOnWindowFocus: true, retryOnMount: true, }, }, }); function Layout() { const [settings, updateSettings] = useSettings(); const [orientation, setOrientation] = useAtom(orientationAtom); useKeepAwake(); useNotificationObserver(); useEffect(() => { checkAndRequestPermissions(); }, []); useEffect(() => { if (settings?.autoRotate === true) ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT); else ScreenOrientation.lockAsync( ScreenOrientation.OrientationLock.PORTRAIT_UP ); }, [settings]); const appState = useRef(AppState.currentState); useEffect(() => { const subscription = AppState.addEventListener("change", (nextAppState) => { if ( appState.current.match(/inactive|background/) && nextAppState === "active" ) { checkForExistingDownloads(); } }); checkForExistingDownloads(); return () => { subscription.remove(); }; }, []); 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); } return ( ); } function saveDownloadedItemInfo(item: BaseItemDto) { try { const downloadedItems = storage.getString("downloadedItems"); let items: BaseItemDto[] = downloadedItems ? JSON.parse(downloadedItems) : []; const existingItemIndex = items.findIndex((i) => i.Id === item.Id); if (existingItemIndex !== -1) { items[existingItemIndex] = item; } else { items.push(item); } storage.set("downloadedItems", JSON.stringify(items)); } catch (error) { writeToLog("ERROR", "Failed to save downloaded item information:", error); console.error("Failed to save downloaded item information:", error); } }