This commit is contained in:
sarendsen
2025-01-06 13:25:49 +01:00
parent 6a4621c377
commit ab33693dd9
59 changed files with 474 additions and 303 deletions

View File

@@ -23,7 +23,7 @@ export default function IndexLayout() {
headerShadowVisible: false, headerShadowVisible: false,
headerRight: () => ( headerRight: () => (
<View className="flex flex-row items-center space-x-2"> <View className="flex flex-row items-center space-x-2">
<Chromecast /> {!Platform.isTV && <Chromecast />}
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
router.push("/(auth)/settings"); router.push("/(auth)/settings");

View File

@@ -27,6 +27,7 @@ import { QueryFunction, useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigation, useRouter } from "expo-router"; import { useNavigation, useRouter } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Platform } from "react-native";
import { import {
ActivityIndicator, ActivityIndicator,
RefreshControl, RefreshControl,
@@ -64,30 +65,33 @@ export default function index() {
const [isConnected, setIsConnected] = useState<boolean | null>(null); const [isConnected, setIsConnected] = useState<boolean | null>(null);
const [loadingRetry, setLoadingRetry] = useState(false); const [loadingRetry, setLoadingRetry] = useState(false);
const { downloadedFiles, cleanCacheDirectory } = useDownload();
const navigation = useNavigation(); const navigation = useNavigation();
const insets = useSafeAreaInsets(); const insets = useSafeAreaInsets();
useEffect(() => { if (!Platform.isTV) {
const hasDownloads = downloadedFiles && downloadedFiles.length > 0; const { downloadedFiles, cleanCacheDirectory } = useDownload();
navigation.setOptions({
headerLeft: () => ( useEffect(() => {
<TouchableOpacity const hasDownloads = downloadedFiles && downloadedFiles.length > 0;
onPress={() => { navigation.setOptions({
router.push("/(auth)/downloads"); headerLeft: () => (
}} <TouchableOpacity
className="p-2" onPress={() => {
> router.push("/(auth)/downloads");
<Feather }}
name="download" className="p-2"
color={hasDownloads ? Colors.primary : "white"} >
size={22} <Feather
/> name="download"
</TouchableOpacity> color={hasDownloads ? Colors.primary : "white"}
), size={22}
}); />
}, [downloadedFiles, navigation, router]); </TouchableOpacity>
),
});
}, [downloadedFiles, navigation, router]);
}
const checkConnection = useCallback(async () => { const checkConnection = useCallback(async () => {
setLoadingRetry(true); setLoadingRetry(true);
@@ -107,9 +111,11 @@ export default function index() {
setIsConnected(state.isConnected); setIsConnected(state.isConnected);
}); });
cleanCacheDirectory().catch((e) => if (!Platform.isTV) {
console.error("Something went wrong cleaning cache directory") cleanCacheDirectory().catch((e) =>
); console.error("Something went wrong cleaning cache directory")
);
}
return () => { return () => {
unsubscribe(); unsubscribe();
}; };

View File

@@ -1,3 +1,4 @@
import { Platform } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { ListGroup } from "@/components/list/ListGroup"; import { ListGroup } from "@/components/list/ListGroup";
import { ListItem } from "@/components/list/ListItem"; import { ListItem } from "@/components/list/ListItem";
@@ -13,7 +14,8 @@ import { SubtitleToggles } from "@/components/settings/SubtitleToggles";
import { UserInfo } from "@/components/settings/UserInfo"; import { UserInfo } from "@/components/settings/UserInfo";
import { useJellyfin } from "@/providers/JellyfinProvider"; import { useJellyfin } from "@/providers/JellyfinProvider";
import { clearLogs } from "@/utils/log"; import { clearLogs } from "@/utils/log";
import * as Haptics from "expo-haptics"; // const Haptics = !Platform.isTV ? require("expo-haptics") : null;
import * as Haptics from "@/packages/expo-haptics";
import { useNavigation, useRouter } from "expo-router"; import { useNavigation, useRouter } from "expo-router";
import { useEffect } from "react"; import { useEffect } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native"; import { ScrollView, TouchableOpacity, View } from "react-native";
@@ -26,7 +28,9 @@ export default function settings() {
const onClearLogsClicked = async () => { const onClearLogsClicked = async () => {
clearLogs(); clearLogs();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); if (!Platform.isTV) {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}
}; };
const navigation = useNavigation(); const navigation = useNavigation();

View File

@@ -12,7 +12,7 @@ import { useQuery } from "@tanstack/react-query";
import { router, useLocalSearchParams, useNavigation } from "expo-router"; import { router, useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ScrollView, TouchableOpacity, View } from "react-native"; import { Platform, ScrollView, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function page() { export default function page() {
@@ -28,15 +28,17 @@ export default function page() {
const navigation = useNavigation(); const navigation = useNavigation();
useEffect(() => { if (!Platform.isTV) {
navigation.setOptions({ useEffect(() => {
headerRight: () => ( navigation.setOptions({
<View className=""> headerRight: () => (
<Chromecast /> <View className="">
</View> <Chromecast />
), </View>
),
});
}); });
}); }
const { data: album } = useQuery({ const { data: album } = useQuery({
queryKey: ["album", albumId, artistId], queryKey: ["album", albumId, artistId],

View File

@@ -29,7 +29,7 @@ import {
import { FlashList } from "@shopify/flash-list"; import { FlashList } from "@shopify/flash-list";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router"; import { useLocalSearchParams, useNavigation } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useCallback, useEffect, useMemo, useState } from "react"; import React, { useCallback, useEffect, useMemo, useState } from "react";
import { FlatList, View } from "react-native"; import { FlatList, View } from "react-native";

View File

@@ -34,7 +34,7 @@ import {
IssueType, IssueType,
IssueTypeName, IssueTypeName,
} from "@/utils/jellyseerr/server/constants/issue"; } from "@/utils/jellyseerr/server/constants/issue";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { TvDetails } from "@/utils/jellyseerr/server/models/Tv"; import { TvDetails } from "@/utils/jellyseerr/server/models/Tv";
import JellyseerrSeasons from "@/components/series/JellyseerrSeasons"; import JellyseerrSeasons from "@/components/series/JellyseerrSeasons";
import { JellyserrRatings } from "@/components/Ratings"; import { JellyserrRatings } from "@/components/Ratings";

View File

@@ -1,6 +1,6 @@
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useLocalSearchParams, useNavigation } from "expo-router"; import { useLocalSearchParams, useNavigation } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useCallback, useEffect, useMemo } from "react"; import React, { useCallback, useEffect, useMemo } from "react";
import { FlatList, useWindowDimensions, View } from "react-native"; import { FlatList, useWindowDimensions, View } from "react-native";

View File

@@ -3,7 +3,7 @@ import { useSettings } from "@/utils/atoms/settings";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { Stack } from "expo-router"; import { Stack } from "expo-router";
import { Platform } from "react-native"; import { Platform } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
export default function IndexLayout() { export default function IndexLayout() {
const [settings, updateSettings] = useSettings(); const [settings, updateSettings] = useSettings();

View File

@@ -27,7 +27,7 @@ import {
getUserLibraryApi, getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api"; } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import { useFocusEffect, useGlobalSearchParams } from "expo-router"; import { useFocusEffect, useGlobalSearchParams } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import React, { import React, {

View File

@@ -17,7 +17,7 @@ import {
getUserLibraryApi, getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api"; } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";

View File

@@ -20,7 +20,7 @@ import {
getUserLibraryApi, getUserLibraryApi,
} from "@jellyfin/sdk/lib/utils/api"; } from "@jellyfin/sdk/lib/utils/api";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import React, { import React, {

View File

@@ -1,4 +1,5 @@
import "@/augmentations"; import "@/augmentations";
import { Platform } from "react-native";
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { DownloadProvider } from "@/providers/DownloadProvider"; import { DownloadProvider } from "@/providers/DownloadProvider";
import { import {
@@ -18,23 +19,28 @@ import { cancelJobById, getAllJobsByDeviceId } from "@/utils/optimize-server";
import { ActionSheetProvider } from "@expo/react-native-action-sheet"; import { ActionSheetProvider } from "@expo/react-native-action-sheet";
import { BottomSheetModalProvider } from "@gorhom/bottom-sheet"; import { BottomSheetModalProvider } from "@gorhom/bottom-sheet";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { // import {
checkForExistingDownloads, // checkForExistingDownloads,
completeHandler, // completeHandler,
download, // download,
} from "@kesha-antonov/react-native-background-downloader"; // } from "@kesha-antonov/react-native-background-downloader";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;
import { DarkTheme, ThemeProvider } from "@react-navigation/native"; import { DarkTheme, ThemeProvider } from "@react-navigation/native";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import * as BackgroundFetch from "expo-background-fetch"; const BackgroundFetch = !Platform.isTV
? require("expo-background-fetch")
: null;
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 * as Linking from "expo-linking";
import * as Notifications from "expo-notifications"; const Notifications = !Platform.isTV ? require("expo-notifications") : null;
import { router, Stack } from "expo-router"; import { router, Stack } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import * as SplashScreen from "expo-splash-screen"; import * as SplashScreen from "expo-splash-screen";
import * as TaskManager from "expo-task-manager"; const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
import { Provider as JotaiProvider, useAtom } from "jotai"; import { Provider as JotaiProvider, useAtom } from "jotai";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Appearance, AppState, TouchableOpacity } from "react-native"; import { Appearance, AppState, TouchableOpacity } from "react-native";
@@ -45,15 +51,19 @@ import { Toaster } from "sonner-native";
SplashScreen.preventAutoHideAsync(); SplashScreen.preventAutoHideAsync();
Notifications.setNotificationHandler({ if (!Platform.isTV) {
handleNotification: async () => ({ Notifications.setNotificationHandler({
shouldShowAlert: true, handleNotification: async () => ({
shouldPlaySound: true, shouldShowAlert: true,
shouldSetBadge: false, shouldPlaySound: true,
}), shouldSetBadge: false,
}); }),
});
}
function useNotificationObserver() { function useNotificationObserver() {
if (Platform.isTV) return;
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
@@ -84,99 +94,101 @@ function useNotificationObserver() {
}, []); }, []);
} }
TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => { if (!Platform.isTV) {
console.log("TaskManager ~ trigger"); TaskManager.defineTask(BACKGROUND_FETCH_TASK, async () => {
console.log("TaskManager ~ trigger");
const now = Date.now(); const now = Date.now();
const settingsData = storage.getString("settings"); const settingsData = storage.getString("settings");
if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData; if (!settingsData) return BackgroundFetch.BackgroundFetchResult.NoData;
const settings: Partial<Settings> = JSON.parse(settingsData); const settings: Partial<Settings> = JSON.parse(settingsData);
const url = settings?.optimizedVersionsServerUrl; const url = settings?.optimizedVersionsServerUrl;
if (!settings?.autoDownload || !url) if (!settings?.autoDownload || !url)
return BackgroundFetch.BackgroundFetchResult.NoData; return BackgroundFetch.BackgroundFetchResult.NoData;
const token = getTokenFromStorage(); const token = getTokenFromStorage();
const deviceId = getOrSetDeviceId(); const deviceId = getOrSetDeviceId();
const baseDirectory = FileSystem.documentDirectory; const baseDirectory = FileSystem.documentDirectory;
if (!token || !deviceId || !baseDirectory) if (!token || !deviceId || !baseDirectory)
return BackgroundFetch.BackgroundFetchResult.NoData; return BackgroundFetch.BackgroundFetchResult.NoData;
const jobs = await getAllJobsByDeviceId({ const jobs = await getAllJobsByDeviceId({
deviceId, deviceId,
authHeader: token, authHeader: token,
url, url,
}); });
console.log("TaskManager ~ Active jobs: ", jobs.length); console.log("TaskManager ~ Active jobs: ", jobs.length);
for (let job of jobs) { for (let job of jobs) {
if (job.status === "completed") { if (job.status === "completed") {
const downloadUrl = url + "download/" + job.id; const downloadUrl = url + "download/" + job.id;
const tasks = await checkForExistingDownloads(); const tasks = await BackGroundDownloader.checkForExistingDownloads();
if (tasks.find((task) => task.id === job.id)) { if (tasks.find((task) => task.id === job.id)) {
console.log("TaskManager ~ Download already in progress: ", job.id); console.log("TaskManager ~ Download already in progress: ", job.id);
continue; continue;
}
BackGroundDownloader.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);
BackGroundDownloader.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,
});
});
} }
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()}`); console.log(`Auto download started: ${new Date(now).toISOString()}`);
// Be sure to return the successful result type! // Be sure to return the successful result type!
return BackgroundFetch.BackgroundFetchResult.NewData; return BackgroundFetch.BackgroundFetchResult.NewData;
}); });
}
const checkAndRequestPermissions = async () => { const checkAndRequestPermissions = async () => {
try { try {
@@ -250,55 +262,61 @@ function Layout() {
const [orientation, setOrientation] = useAtom(orientationAtom); const [orientation, setOrientation] = useAtom(orientationAtom);
useKeepAwake(); 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); const appState = useRef(AppState.currentState);
useEffect(() => { if (!Platform.isTV) {
const subscription = AppState.addEventListener("change", (nextAppState) => { useNotificationObserver();
if (
appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
checkForExistingDownloads();
}
});
checkForExistingDownloads(); useEffect(() => {
checkAndRequestPermissions();
}, []);
return () => { useEffect(() => {
subscription.remove(); if (settings?.autoRotate === true)
}; ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.DEFAULT);
}, []); else
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP
);
}, [settings]);
useEffect(() => { useEffect(() => {
const subscription = ScreenOrientation.addOrientationChangeListener( const subscription = AppState.addEventListener(
(event) => { "change",
setOrientation(event.orientationInfo.orientation); (nextAppState) => {
} if (
); appState.current.match(/inactive|background/) &&
nextAppState === "active"
) {
BackGroundDownloader.checkForExistingDownloads();
}
}
);
ScreenOrientation.getOrientationAsync().then((initialOrientation) => { BackGroundDownloader.checkForExistingDownloads();
setOrientation(initialOrientation);
});
return () => { return () => {
ScreenOrientation.removeOrientationChangeListener(subscription); 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(); const url = Linking.useURL();

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,7 +1,7 @@
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react"; import { useMemo } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
interface Props extends React.ComponentProps<typeof View> { interface Props extends React.ComponentProps<typeof View> {

View File

@@ -1,5 +1,5 @@
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { useMemo } from "react"; import { useMemo } from "react";

View File

@@ -1,4 +1,4 @@
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import React, { PropsWithChildren, ReactNode, useMemo } from "react"; import React, { PropsWithChildren, ReactNode, useMemo } from "react";
import { Text, TouchableOpacity, View } from "react-native"; import { Text, TouchableOpacity, View } from "react-native";
import { Loader } from "./Loader"; import { Loader } from "./Loader";

View File

View File

@@ -0,0 +1 @@
export * as ContextMenu from "zeego/context-menu";

View File

View File

@@ -0,0 +1 @@
export * as DropdownMenu from "zeego/dropdown-menu";

View File

@@ -24,10 +24,10 @@ import {
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useNavigation } from "expo-router"; import { useNavigation } from "expo-router";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { View } from "react-native"; import { Platform, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Chromecast } from "./Chromecast"; import { Chromecast } from "./Chromecast";
import { ItemHeader } from "./ItemHeader"; import { ItemHeader } from "./ItemHeader";
@@ -81,23 +81,25 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
defaultMediaSource, defaultMediaSource,
]); ]);
useEffect(() => { if (!Platform.isTV) {
navigation.setOptions({ useEffect(() => {
headerRight: () => navigation.setOptions({
item && ( headerRight: () =>
<View className="flex flex-row items-center space-x-2"> item && (
<Chromecast background="blur" width={22} height={22} /> <View className="flex flex-row items-center space-x-2">
{item.Type !== "Program" && ( <Chromecast background="blur" width={22} height={22} />
<View className="flex flex-row items-center space-x-2"> {item.Type !== "Program" && (
<DownloadSingleItem item={item} size="large" /> <View className="flex flex-row items-center space-x-2">
<PlayedStatus item={item} /> <DownloadSingleItem item={item} size="large" />
<AddToFavorites item={item} type="item" /> <PlayedStatus item={item} />
</View> <AddToFavorites item={item} type="item" />
)} </View>
</View> )}
), </View>
}); ),
}, [item]); });
}, [item]);
}
useEffect(() => { useEffect(() => {
if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP) if (orientation !== ScreenOrientation.OrientationLock.PORTRAIT_UP)

View File

@@ -5,7 +5,7 @@ import {
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { convertBitsToMegabitsOrGigabits } from "@/utils/bToMb"; import { convertBitsToMegabitsOrGigabits } from "@/utils/bToMb";

View File

@@ -32,7 +32,7 @@ import Animated, {
import { Button } from "./Button"; import { Button } from "./Button";
import { SelectedOptions } from "./ItemContent"; import { SelectedOptions } from "./ItemContent";
import { chromecastProfile } from "@/utils/profiles/chromecast"; import { chromecastProfile } from "@/utils/profiles/chromecast";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
interface Props extends React.ComponentProps<typeof Button> { interface Props extends React.ComponentProps<typeof Button> {
item: BaseItemDto; item: BaseItemDto;

View File

@@ -6,7 +6,7 @@ import {
TouchableOpacity, TouchableOpacity,
TouchableOpacityProps, TouchableOpacityProps,
} from "react-native"; } from "react-native";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
onPress?: () => void; onPress?: () => void;

View File

@@ -2,7 +2,7 @@ import { tc } from "@/utils/textTools";
import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models"; import { MediaSourceInfo } from "@jellyfin/sdk/lib/generated-client/models";
import { useMemo } from "react"; import { useMemo } from "react";
import { Platform, TouchableOpacity, View } from "react-native"; import { Platform, TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { SubtitleHelper } from "@/utils/SubtitleHelper"; import { SubtitleHelper } from "@/utils/SubtitleHelper";

View File

@@ -1,11 +1,14 @@
import {useRouter, useSegments} from "expo-router"; import { useRouter, useSegments } from "expo-router";
import React, {PropsWithChildren, useCallback, useMemo} from "react"; import React, { PropsWithChildren, useCallback, useMemo } from "react";
import {TouchableOpacity, TouchableOpacityProps} from "react-native"; import { TouchableOpacity, TouchableOpacityProps } from "react-native";
import * as ContextMenu from "zeego/context-menu"; import * as ContextMenu from "@/components/ContextMenu";
import {MovieResult, TvResult} from "@/utils/jellyseerr/server/models/Search"; import { MovieResult, TvResult } from "@/utils/jellyseerr/server/models/Search";
import {useJellyseerr} from "@/hooks/useJellyseerr"; import { useJellyseerr } from "@/hooks/useJellyseerr";
import {hasPermission, Permission} from "@/utils/jellyseerr/server/lib/permissions"; import {
import {MediaType} from "@/utils/jellyseerr/server/constants/media"; hasPermission,
Permission,
} from "@/utils/jellyseerr/server/lib/permissions";
import { MediaType } from "@/utils/jellyseerr/server/constants/media";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
result: MovieResult | TvResult; result: MovieResult | TvResult;
@@ -26,26 +29,27 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
}) => { }) => {
const router = useRouter(); const router = useRouter();
const segments = useSegments(); const segments = useSegments();
const {jellyseerrApi, jellyseerrUser, requestMedia} = useJellyseerr() const { jellyseerrApi, jellyseerrUser, requestMedia } = useJellyseerr();
const from = segments[2]; const from = segments[2];
const autoApprove = useMemo(() => { const autoApprove = useMemo(() => {
return jellyseerrUser && hasPermission( return (
Permission.AUTO_APPROVE, jellyseerrUser &&
jellyseerrUser.permissions, hasPermission(Permission.AUTO_APPROVE, jellyseerrUser.permissions, {
{type: 'or'} type: "or",
) })
}, [jellyseerrApi, jellyseerrUser]) );
}, [jellyseerrApi, jellyseerrUser]);
const request = useCallback(() => const request = useCallback(
() =>
requestMedia(mediaTitle, { requestMedia(mediaTitle, {
mediaId: result.id, mediaId: result.id,
mediaType: result.mediaType mediaType: result.mediaType,
} }),
),
[jellyseerrApi, result] [jellyseerrApi, result]
) );
if (from === "(home)" || from === "(search)" || from === "(libraries)") if (from === "(home)" || from === "(search)" || from === "(libraries)")
return ( return (
@@ -55,7 +59,16 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {
// @ts-ignore // @ts-ignore
router.push({pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`, params: {...result, mediaTitle, releaseYear, canRequest, posterSrc}}); router.push({
pathname: `/(auth)/(tabs)/${from}/jellyseerr/page`,
params: {
...result,
mediaTitle,
releaseYear,
canRequest,
posterSrc,
},
});
}} }}
{...props} {...props}
> >
@@ -71,31 +84,33 @@ export const TouchableJellyseerrRouter: React.FC<PropsWithChildren<Props>> = ({
> >
<ContextMenu.Label key="label-1">Actions</ContextMenu.Label> <ContextMenu.Label key="label-1">Actions</ContextMenu.Label>
{canRequest && result.mediaType === MediaType.MOVIE && ( {canRequest && result.mediaType === MediaType.MOVIE && (
<ContextMenu.Item <ContextMenu.Item
key="item-1" key="item-1"
onSelect={() => { onSelect={() => {
if (autoApprove) { if (autoApprove) {
request() request();
} }
}}
shouldDismissMenuOnSelect
>
<ContextMenu.ItemTitle key="item-1-title">
Request
</ContextMenu.ItemTitle>
<ContextMenu.ItemIcon
ios={{
name: "arrow.down.to.line",
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "purple",
light: "purple",
},
}} }}
shouldDismissMenuOnSelect androidIconName="download"
> />
<ContextMenu.ItemTitle key="item-1-title">Request</ContextMenu.ItemTitle> </ContextMenu.Item>
<ContextMenu.ItemIcon )}
ios={{
name: "arrow.down.to.line",
pointSize: 18,
weight: "semibold",
scale: "medium",
hierarchicalColor: {
dark: "purple",
light: "purple",
},
}}
androidIconName="download"
/>
</ContextMenu.Item>
)}
</ContextMenu.Content> </ContextMenu.Content>
</ContextMenu.Root> </ContextMenu.Root>
</> </>

View File

@@ -6,7 +6,7 @@ import {
import { useRouter, useSegments } from "expo-router"; import { useRouter, useSegments } from "expo-router";
import { PropsWithChildren } from "react"; import { PropsWithChildren } from "react";
import { TouchableOpacity, TouchableOpacityProps } from "react-native"; import { TouchableOpacity, TouchableOpacityProps } from "react-native";
import * as ContextMenu from "zeego/context-menu"; import * as ContextMenu from "@/components/ContextMenu";
interface Props extends TouchableOpacityProps { interface Props extends TouchableOpacityProps {
item: BaseItemDto; item: BaseItemDto;

View File

@@ -5,13 +5,18 @@ import { useSettings } from "@/utils/atoms/settings";
import { JobStatus } from "@/utils/optimize-server"; import { JobStatus } from "@/utils/optimize-server";
import { formatTimeString } from "@/utils/time"; import { formatTimeString } from "@/utils/time";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { checkForExistingDownloads } from "@kesha-antonov/react-native-background-downloader"; // import { checkForExistingDownloads } from "@kesha-antonov/react-native-background-downloader";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { FFmpegKit } from "ffmpeg-kit-react-native"; // import { FFmpegKit } from "ffmpeg-kit-react-native";
const FFmpegKit = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import { import {
ActivityIndicator, ActivityIndicator,
Platform,
TouchableOpacity, TouchableOpacity,
TouchableOpacityProps, TouchableOpacityProps,
View, View,
@@ -64,7 +69,7 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
if (settings?.downloadMethod === "optimized") { if (settings?.downloadMethod === "optimized") {
try { try {
const tasks = await checkForExistingDownloads(); const tasks = await BackGroundDownloader.checkForExistingDownloads();
for (const task of tasks) { for (const task of tasks) {
if (task.id === id) { if (task.id === id) {
task.stop(); task.stop();

View File

@@ -1,5 +1,5 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
import { TouchableOpacity, TouchableOpacityProps, View } from "react-native"; import { TouchableOpacity, TouchableOpacityProps, View } from "react-native";
import { import {

View File

@@ -3,7 +3,7 @@ import {
useActionSheet, useActionSheet,
} from "@expo/react-native-action-sheet"; } from "@expo/react-native-action-sheet";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import React, { useCallback, useMemo } from "react"; import React, { useCallback, useMemo } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";

View File

@@ -22,7 +22,7 @@ import { itemRouter, TouchableItemRouter } from "../common/TouchableItemRouter";
import { Loader } from "../Loader"; import { Loader } from "../Loader";
import { Gesture, GestureDetector } from "react-native-gesture-handler"; import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { useRouter, useSegments } from "expo-router"; import { useRouter, useSegments } from "expo-router";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
interface Props extends ViewProps {} interface Props extends ViewProps {}

View File

@@ -1,7 +1,7 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
type Props = { type Props = {

View File

@@ -1,5 +1,5 @@
import { TouchableOpacity, View, ViewProps } from "react-native"; import { TouchableOpacity, View, ViewProps } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { useMedia } from "./MediaContext"; import { useMedia } from "./MediaContext";
import { Switch } from "react-native-gesture-handler"; import { Switch } from "react-native-gesture-handler";

View File

@@ -6,7 +6,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import React from "react"; import React from "react";
import { Switch, TouchableOpacity, View } from "react-native"; import { Switch, TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup"; import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";

View File

@@ -1,3 +1,4 @@
import { Platform } from "react-native";
import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings"; import { ScreenOrientationEnum, useSettings } from "@/utils/atoms/settings";
import { import {
BACKGROUND_FETCH_TASK, BACKGROUND_FETCH_TASK,
@@ -5,13 +6,15 @@ import {
unregisterBackgroundFetchAsync, unregisterBackgroundFetchAsync,
} from "@/utils/background-tasks"; } from "@/utils/background-tasks";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import * as BackgroundFetch from "expo-background-fetch"; const BackgroundFetch = !Platform.isTV
import * as ScreenOrientation from "expo-screen-orientation"; ? require("expo-background-fetch")
import * as TaskManager from "expo-task-manager"; : null;
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
const TaskManager = !Platform.isTV ? require("expo-task-manager") : null;
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { Linking, Switch, TouchableOpacity, ViewProps } from "react-native"; import { Linking, Switch, TouchableOpacity, ViewProps } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { ListGroup } from "../list/ListGroup"; import { ListGroup } from "../list/ListGroup";
import { ListItem } from "../list/ListItem"; import { ListItem } from "../list/ListItem";
@@ -25,6 +28,8 @@ export const OtherSettings: React.FC = () => {
* Background task * Background task
*******************/ *******************/
const checkStatusAsync = async () => { const checkStatusAsync = async () => {
if (Platform.isTV) return;
await BackgroundFetch.getStatusAsync(); await BackgroundFetch.getStatusAsync();
return await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK); return await TaskManager.isTaskRegisteredAsync(BACKGROUND_FETCH_TASK);
}; };

View File

@@ -7,7 +7,7 @@ import {
BottomSheetView, BottomSheetView,
} from "@gorhom/bottom-sheet"; } from "@gorhom/bottom-sheet";
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api"; import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useCallback, useRef, useState } from "react"; import React, { useCallback, useRef, useState } from "react";
import { Alert, View, ViewProps } from "react-native"; import { Alert, View, ViewProps } from "react-native";

View File

@@ -4,7 +4,7 @@ import { useDownload } from "@/providers/DownloadProvider";
import { clearLogs } from "@/utils/log"; import { clearLogs } from "@/utils/log";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import { View } from "react-native"; import { View } from "react-native";
import * as Progress from "react-native-progress"; import * as Progress from "react-native-progress";
import { toast } from "sonner-native"; import { toast } from "sonner-native";

View File

@@ -1,5 +1,5 @@
import { TouchableOpacity, View, ViewProps } from "react-native"; import { TouchableOpacity, View, ViewProps } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { Text } from "../common/Text"; import { Text } from "../common/Text";
import { useMedia } from "./MediaContext"; import { useMedia } from "./MediaContext";
import { Switch } from "react-native-gesture-handler"; import { Switch } from "react-native-gesture-handler";

View File

@@ -1,8 +1,11 @@
import React, { useEffect, useRef } from "react"; import React, { useEffect, useRef } from "react";
import { View, StyleSheet } from "react-native"; import { View, StyleSheet, Platform } from "react-native";
import { useSharedValue } from "react-native-reanimated"; import { useSharedValue } from "react-native-reanimated";
import { Slider } from "react-native-awesome-slider"; import { Slider } from "react-native-awesome-slider";
import { VolumeManager } from "react-native-volume-manager"; // import { VolumeManager } from "react-native-volume-manager";
const VolumeManager = !Platform.isTV
? require("react-native-volume-manager")
: null;
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
interface AudioSliderProps { interface AudioSliderProps {
@@ -10,6 +13,8 @@ interface AudioSliderProps {
} }
const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => { const AudioSlider: React.FC<AudioSliderProps> = ({ setVisibility }) => {
if (Platform.isTV) return;
const volume = useSharedValue<number>(50); // Explicitly type as number const volume = useSharedValue<number>(50); // Explicitly type as number
const min = useSharedValue<number>(0); // Explicitly type as number const min = useSharedValue<number>(0); // Explicitly type as number
const max = useSharedValue<number>(100); // Explicitly type as number const max = useSharedValue<number>(100); // Explicitly type as number

View File

@@ -1,12 +1,15 @@
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { View, StyleSheet } from "react-native"; import { View, StyleSheet, Platform } from "react-native";
import { useSharedValue } from "react-native-reanimated"; import { useSharedValue } from "react-native-reanimated";
import { Slider } from "react-native-awesome-slider"; import { Slider } from "react-native-awesome-slider";
import * as Brightness from "expo-brightness"; // import * as Brightness from "expo-brightness";
const Brightness = !Platform.isTV ? require("expo-brightness") : null;
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons"; import MaterialCommunityIcons from "@expo/vector-icons/MaterialCommunityIcons";
const BrightnessSlider = () => { const BrightnessSlider = () => {
if (Platform.isTV) return;
const brightness = useSharedValue(50); const brightness = useSharedValue(50);
const min = useSharedValue(0); const min = useSharedValue(0);
const max = useSharedValue(100); const max = useSharedValue(100);

View File

@@ -29,7 +29,7 @@ import {
BaseItemDto, BaseItemDto,
MediaSourceInfo, MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client"; } from "@jellyfin/sdk/lib/generated-client";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { useLocalSearchParams, useRouter } from "expo-router"; import { useLocalSearchParams, useRouter } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";

View File

@@ -1,7 +1,7 @@
import React, { useMemo, useState } from "react"; import React, { useMemo, useState } from "react";
import { View, TouchableOpacity } from "react-native"; import { View, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { useControlContext } from "../contexts/ControlContext"; import { useControlContext } from "../contexts/ControlContext";
import { useVideoContext } from "../contexts/VideoContext"; import { useVideoContext } from "../contexts/VideoContext";
import { EmbeddedSubtitle, ExternalSubtitle } from "../types"; import { EmbeddedSubtitle, ExternalSubtitle } from "../types";

View File

@@ -1,7 +1,7 @@
import React, { useCallback, useMemo, useState } from "react"; import React, { useCallback, useMemo, useState } from "react";
import { View, TouchableOpacity } from "react-native"; import { View, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "@/components/DropdownMenu";
import { useControlContext } from "../contexts/ControlContext"; import { useControlContext } from "../contexts/ControlContext";
import { useVideoContext } from "../contexts/VideoContext"; import { useVideoContext } from "../contexts/VideoContext";
import { TranscodedSubtitle } from "../types"; import { TranscodedSubtitle } from "../types";

View File

@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
import { msToSeconds, secondsToMs } from "@/utils/time"; import { msToSeconds, secondsToMs } from "@/utils/time";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
interface CreditTimestamps { interface CreditTimestamps {
Introduction: { Introduction: {

View File

@@ -10,7 +10,9 @@ import { storage } from "@/utils/mmkv";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client";
import { useAtom, useAtomValue } from "jotai"; import { useAtom, useAtomValue } from "jotai";
import { useEffect, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { getColors } from "react-native-image-colors"; import { Platform } from "react-native";
// import { getColors } from "react-native-image-colors";
const getColors = !Platform.isTV ? require("react-native-image-colors") : null;
/** /**
* Custom hook to extract and manage image colors for a given item. * Custom hook to extract and manage image colors for a given item.
@@ -28,6 +30,8 @@ export const useImageColors = ({
url?: string | null; url?: string | null;
disabled?: boolean; disabled?: boolean;
}) => { }) => {
if (Platform.isTV) return;
const api = useAtomValue(apiAtom); const api = useAtomValue(apiAtom);
const [, setPrimaryColor] = useAtom(itemThemeColorAtom); const [, setPrimaryColor] = useAtom(itemThemeColorAtom);

View File

@@ -5,7 +5,7 @@ import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log"; import { writeToLog } from "@/utils/log";
import { msToSeconds, secondsToMs } from "@/utils/time"; import { msToSeconds, secondsToMs } from "@/utils/time";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
interface IntroTimestamps { interface IntroTimestamps {
EpisodeId: string; EpisodeId: string;

View File

@@ -3,7 +3,7 @@ import { markAsNotPlayed } from "@/utils/jellyfin/playstate/markAsNotPlayed";
import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed"; import { markAsPlayed } from "@/utils/jellyfin/playstate/markAsPlayed";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
export const useMarkAsPlayed = (item: BaseItemDto) => { export const useMarkAsPlayed = (item: BaseItemDto) => {

View File

@@ -1,12 +1,17 @@
import orientationToOrientationLock from "@/utils/OrientationLockConverter"; import orientationToOrientationLock from "@/utils/OrientationLockConverter";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Platform } from "react-native";
export const useOrientation = () => { export const useOrientation = () => {
const [orientation, setOrientation] = useState( const [orientation, setOrientation] = useState(
ScreenOrientation.OrientationLock.UNKNOWN Platform.isTV
? ScreenOrientation.OrientationLock.LANDSCAPE
: ScreenOrientation.OrientationLock.UNKNOWN
); );
if (Platform.isTV) return { orientation, setOrientation };
useEffect(() => { useEffect(() => {
const orientationSubscription = const orientationSubscription =
ScreenOrientation.addOrientationChangeListener((event) => { ScreenOrientation.addOrientationChangeListener((event) => {

View File

@@ -1,8 +1,11 @@
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useEffect } from "react"; import { useEffect } from "react";
import { Platform } from "react-native";
export const useOrientationSettings = () => { export const useOrientationSettings = () => {
if (Platform.isTV) return;
const [settings] = useSettings(); const [settings] = useSettings();
useEffect(() => { useEffect(() => {

View File

@@ -9,7 +9,8 @@ import {
import { useQueryClient } from "@tanstack/react-query"; import { useQueryClient } from "@tanstack/react-query";
import * as FileSystem from "expo-file-system"; import * as FileSystem from "expo-file-system";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native"; // import { FFmpegKit, FFmpegSession, Statistics } from "ffmpeg-kit-react-native";
const FFmpegKit = !Platform.isTV ? require("ffmpeg-kit-react-native") : null;
import { useAtomValue } from "jotai"; import { useAtomValue } from "jotai";
import { useCallback } from "react"; import { useCallback } from "react";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
@@ -18,6 +19,7 @@ import useDownloadHelper from "@/utils/download";
import { Api } from "@jellyfin/sdk"; import { Api } from "@jellyfin/sdk";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { JobStatus } from "@/utils/optimize-server"; import { JobStatus } from "@/utils/optimize-server";
import { Platform } from "react-native";
const createFFmpegCommand = (url: string, output: string) => [ const createFFmpegCommand = (url: string, output: string) => [
"-y", // overwrite output files without asking "-y", // overwrite output files without asking
@@ -53,7 +55,12 @@ export const useRemuxHlsToMp4 = () => {
const [settings] = useSettings(); const [settings] = useSettings();
const { saveImage } = useImageStorage(); const { saveImage } = useImageStorage();
const { saveSeriesPrimaryImage } = useDownloadHelper(); const { saveSeriesPrimaryImage } = useDownloadHelper();
const { saveDownloadedItemInfo, setProcesses, processes, APP_CACHE_DOWNLOAD_DIRECTORY } = useDownload(); const {
saveDownloadedItemInfo,
setProcesses,
processes,
APP_CACHE_DOWNLOAD_DIRECTORY,
} = useDownload();
const onSaveAssets = async (api: Api, item: BaseItemDto) => { const onSaveAssets = async (api: Api, item: BaseItemDto) => {
await saveSeriesPrimaryImage(item); await saveSeriesPrimaryImage(item);
@@ -77,9 +84,9 @@ export const useRemuxHlsToMp4 = () => {
if (returnCode.isValueSuccess()) { if (returnCode.isValueSuccess()) {
const stat = await session.getLastReceivedStatistics(); const stat = await session.getLastReceivedStatistics();
await FileSystem.moveAsync({ await FileSystem.moveAsync({
from: `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`, from: `${APP_CACHE_DOWNLOAD_DIRECTORY}${item.Id}.mp4`,
to: `${FileSystem.documentDirectory}${item.Id}.mp4` to: `${FileSystem.documentDirectory}${item.Id}.mp4`,
}) });
await queryClient.invalidateQueries({ await queryClient.invalidateQueries({
queryKey: ["downloadedItems"], queryKey: ["downloadedItems"],
}); });
@@ -131,12 +138,16 @@ export const useRemuxHlsToMp4 = () => {
const startRemuxing = useCallback( const startRemuxing = useCallback(
async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => { async (item: BaseItemDto, url: string, mediaSource: MediaSourceInfo) => {
const cacheDir = await FileSystem.getInfoAsync(APP_CACHE_DOWNLOAD_DIRECTORY); const cacheDir = await FileSystem.getInfoAsync(
APP_CACHE_DOWNLOAD_DIRECTORY
);
if (!cacheDir.exists) { if (!cacheDir.exists) {
await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {intermediates: true}) await FileSystem.makeDirectoryAsync(APP_CACHE_DOWNLOAD_DIRECTORY, {
intermediates: true,
});
} }
const output = APP_CACHE_DOWNLOAD_DIRECTORY + `${item.Id}.mp4` const output = APP_CACHE_DOWNLOAD_DIRECTORY + `${item.Id}.mp4`;
if (!api) throw new Error("API is not defined"); if (!api) throw new Error("API is not defined");
if (!item.Id) throw new Error("Item must have an Id"); if (!item.Id) throw new Error("Item must have an Id");

View File

1
packages/expo-haptics.ts Normal file
View File

@@ -0,0 +1 @@
export * as Haptics from "expo-haptics";

View File

@@ -0,0 +1,68 @@
// export { Orientation, OrientationLock } from "expo-screen-orientation";
export enum Orientation {
/**
* An unknown screen orientation. For example, the device is flat, perhaps on a table.
*/
UNKNOWN = 0,
/**
* Right-side up portrait interface orientation.
*/
PORTRAIT_UP = 1,
/**
* Upside down portrait interface orientation.
*/
PORTRAIT_DOWN = 2,
/**
* Left landscape interface orientation.
*/
LANDSCAPE_LEFT = 3,
/**
* Right landscape interface orientation.
*/
LANDSCAPE_RIGHT = 4,
}
export enum OrientationLock {
/**
* The default orientation. On iOS, this will allow all orientations except `Orientation.PORTRAIT_DOWN`.
* On Android, this lets the system decide the best orientation.
*/
DEFAULT = 0,
/**
* All four possible orientations
*/
ALL = 1,
/**
* Any portrait orientation.
*/
PORTRAIT = 2,
/**
* Right-side up portrait only.
*/
PORTRAIT_UP = 3,
/**
* Upside down portrait only.
*/
PORTRAIT_DOWN = 4,
/**
* Any landscape orientation.
*/
LANDSCAPE = 5,
/**
* Left landscape only.
*/
LANDSCAPE_LEFT = 6,
/**
* Right landscape only.
*/
LANDSCAPE_RIGHT = 7,
/**
* A platform specific orientation. This is not a valid policy that can be applied in [`lockAsync`](#screenorientationlockasyncorientationlock).
*/
OTHER = 8,
/**
* An unknown screen orientation lock. This is not a valid policy that can be applied in [`lockAsync`](#screenorientationlockasyncorientationlock).
*/
UNKNOWN = 9,
}

View File

@@ -0,0 +1 @@
export * from "expo-screen-orientation";

View File

@@ -13,12 +13,15 @@ import {
BaseItemDto, BaseItemDto,
MediaSourceInfo, MediaSourceInfo,
} from "@jellyfin/sdk/lib/generated-client/models"; } from "@jellyfin/sdk/lib/generated-client/models";
import { // import {
checkForExistingDownloads, // checkForExistingDownloads,
completeHandler, // completeHandler,
download, // download,
setConfig, // setConfig,
} from "@kesha-antonov/react-native-background-downloader"; // } from "@kesha-antonov/react-native-background-downloader";
const BackGroundDownloader = !Platform.isTV
? require("@kesha-antonov/react-native-background-downloader")
: null;
import MMKV from "react-native-mmkv"; import MMKV from "react-native-mmkv";
import { import {
focusManager, focusManager,
@@ -42,13 +45,14 @@ import React, {
import { AppState, AppStateStatus, Platform } from "react-native"; import { AppState, AppStateStatus, Platform } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { apiAtom } from "./JellyfinProvider"; import { apiAtom } from "./JellyfinProvider";
import * as Notifications from "expo-notifications"; // import * as Notifications from "expo-notifications";
const Notifications = !Platform.isTV ? require("expo-notifications") : null;
import { getItemImage } from "@/utils/getItemImage"; import { getItemImage } from "@/utils/getItemImage";
import useImageStorage from "@/hooks/useImageStorage"; import useImageStorage from "@/hooks/useImageStorage";
import { storage } from "@/utils/mmkv"; import { storage } from "@/utils/mmkv";
import useDownloadHelper from "@/utils/download"; import useDownloadHelper from "@/utils/download";
import { FileInfo } from "expo-file-system"; import { FileInfo } from "expo-file-system";
import * as Haptics from "expo-haptics"; import * as Haptics from "@/packages/expo-haptics";
import * as Application from "expo-application"; import * as Application from "expo-application";
export type DownloadedItem = { export type DownloadedItem = {
@@ -67,6 +71,7 @@ const DownloadContext = createContext<ReturnType<
> | null>(null); > | null>(null);
function useDownloadProvider() { function useDownloadProvider() {
if (Platform.isTV) return;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [settings] = useSettings(); const [settings] = useSettings();
const router = useRouter(); const router = useRouter();
@@ -170,7 +175,7 @@ function useDownloadProvider() {
useEffect(() => { useEffect(() => {
const checkIfShouldStartDownload = async () => { const checkIfShouldStartDownload = async () => {
if (processes.length === 0) return; if (processes.length === 0) return;
await checkForExistingDownloads(); await BackGroundDownloader.checkForExistingDownloads();
}; };
checkIfShouldStartDownload(); checkIfShouldStartDownload();
@@ -214,7 +219,7 @@ function useDownloadProvider() {
) )
); );
setConfig({ BackGroundDownloader.setConfig({
isLogsEnabled: true, isLogsEnabled: true,
progressInterval: 500, progressInterval: 500,
headers: { headers: {
@@ -234,7 +239,7 @@ function useDownloadProvider() {
const baseDirectory = FileSystem.documentDirectory; const baseDirectory = FileSystem.documentDirectory;
download({ BackGroundDownloader.download({
id: process.id, id: process.id,
url: settings?.optimizedVersionsServerUrl + "download/" + process.id, url: settings?.optimizedVersionsServerUrl + "download/" + process.id,
destination: `${baseDirectory}/${process.item.Id}.mp4`, destination: `${baseDirectory}/${process.item.Id}.mp4`,
@@ -284,7 +289,7 @@ function useDownloadProvider() {
}, },
}); });
setTimeout(() => { setTimeout(() => {
completeHandler(process.id); BackGroundDownloader.completeHandler(process.id);
removeProcess(process.id); removeProcess(process.id);
}, 1000); }, 1000);
}) })

View File

@@ -1,4 +1,7 @@
import { Orientation, OrientationLock } from "expo-screen-orientation"; import {
Orientation,
OrientationLock,
} from "@/packages/expo-screen-orientation";
function orientationToOrientationLock( function orientationToOrientationLock(
orientation: Orientation orientation: Orientation

View File

@@ -1,4 +1,4 @@
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { atom } from "jotai"; import { atom } from "jotai";
export const orientationAtom = atom<number>( export const orientationAtom = atom<number>(

View File

@@ -1,6 +1,6 @@
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { useEffect } from "react"; import { useEffect } from "react";
import * as ScreenOrientation from "expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { storage } from "../mmkv"; import { storage } from "../mmkv";
import { Platform } from "react-native"; import { Platform } from "react-native";
import { import {

View File

@@ -1,4 +1,7 @@
import * as BackgroundFetch from "expo-background-fetch"; import { Platform } from "react-native";
const BackgroundFetch = !Platform.isTV
? require("expo-background-fetch")
: null;
export const BACKGROUND_FETCH_TASK = "background-fetch"; export const BACKGROUND_FETCH_TASK = "background-fetch";