fix: tvos fixes

This commit is contained in:
Fredrik Burmester
2025-02-21 20:38:31 +01:00
parent 04dce9265b
commit b478fbb6bf
14 changed files with 337 additions and 318 deletions

View File

@@ -15,7 +15,7 @@ import { Image } from "expo-image";
import { useLocalSearchParams, useNavigation } from "expo-router"; import { useLocalSearchParams, useNavigation } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useEffect, useMemo } from "react"; import React, { useEffect, useMemo } from "react";
import { View } from "react-native"; import { Platform, View } from "react-native";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const page: React.FC = () => { const page: React.FC = () => {
@@ -85,6 +85,8 @@ const page: React.FC = () => {
allEpisodes.length > 0 && ( allEpisodes.length > 0 && (
<View className="flex flex-row items-center space-x-2"> <View className="flex flex-row items-center space-x-2">
<AddToFavorites item={item} type="series" /> <AddToFavorites item={item} type="series" />
{!Platform.isTV && (
<>
<DownloadItems <DownloadItems
size="large" size="large"
title={t("item_card.download.download_series")} title={t("item_card.download.download_series")}
@@ -100,6 +102,8 @@ const page: React.FC = () => {
/> />
)} )}
/> />
</>
)}
</View> </View>
), ),
}); });

View File

@@ -3,16 +3,21 @@ import React, { useEffect } from "react";
import { SystemBars } from "react-native-edge-to-edge"; import { SystemBars } from "react-native-edge-to-edge";
import * as ScreenOrientation from "@/packages/expo-screen-orientation"; import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { Platform } from "react-native";
export default function Layout() { export default function Layout() {
const [settings] = useSettings(); const [settings] = useSettings();
useEffect(() => { useEffect(() => {
if (Platform.isTV) return;
if (settings.defaultVideoOrientation) { if (settings.defaultVideoOrientation) {
ScreenOrientation.lockAsync(settings.defaultVideoOrientation); ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
} }
return () => { return () => {
if (Platform.isTV) return;
if (settings.autoRotate === true) { if (settings.autoRotate === true) {
ScreenOrientation.unlockAsync(); ScreenOrientation.unlockAsync();
} else { } else {

View File

@@ -270,6 +270,7 @@ function Layout() {
useEffect(() => { useEffect(() => {
// If the user has auto rotate enabled, unlock the orientation // If the user has auto rotate enabled, unlock the orientation
if (Platform.isTV) return;
if (settings.autoRotate === true) { if (settings.autoRotate === true) {
ScreenOrientation.unlockAsync(); ScreenOrientation.unlockAsync();
} else { } else {

View File

@@ -60,7 +60,7 @@
"react-i18next": "^15.4.0", "react-i18next": "^15.4.0",
"react-native": "npm:react-native-tvos@~0.77.0-0", "react-native": "npm:react-native-tvos@~0.77.0-0",
"react-native-awesome-slider": "^2.9.0", "react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "0.8.7", "react-native-bottom-tabs": "0.8.6",
"react-native-circular-progress": "^1.4.1", "react-native-circular-progress": "^1.4.1",
"react-native-compressor": "^1.10.3", "react-native-compressor": "^1.10.3",
"react-native-country-flag": "^2.0.2", "react-native-country-flag": "^2.0.2",
@@ -1826,7 +1826,7 @@
"react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="], "react-native-awesome-slider": ["react-native-awesome-slider@2.9.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-gesture-handler": ">=2.0.0", "react-native-reanimated": ">=3.0.0" } }, "sha512-sc5qgX4YtM6IxjtosjgQLdsal120MvU+YWs0F2MdgQWijps22AXLDCUoBnZZ8vxVhVyJ2WnnIPrmtVBvVJjSuQ=="],
"react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.7", "", { "dependencies": { "react-freeze": "^1.0.0", "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-cVQYs4r8Hb9V9oOO/SqsmBaZ7IzE/3Tpvz4mmRjNXKi1cBWC+ZpKTuqRx6EPjBCYTVK+vbAfoTM6IHS+6NVg4w=="], "react-native-bottom-tabs": ["react-native-bottom-tabs@0.8.6", "", { "dependencies": { "sf-symbols-typescript": "^2.0.0", "use-latest-callback": "^0.2.1" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-N5b3MoSfsEqlmvFyIyL0X0bd+QAtB+cXH1rl/+R2Kr0BefBTC7ZldGcPhgK3FhBbt0vJDpd3kLb/dvmqZd+Eag=="],
"react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="], "react-native-circular-progress": ["react-native-circular-progress@1.4.1", "", { "dependencies": { "prop-types": "^15.8.1" }, "peerDependencies": { "react": ">=16.0.0", "react-native": ">=0.50.0", "react-native-svg": ">=7.0.0" } }, "sha512-HEzvI0WPuWvsCgWE3Ff2HBTMgAEQB2GvTFw0KHyD/t1STAlDDRiolu0mEGhVvihKR3jJu3v3V4qzvSklY/7XzQ=="],

View File

@@ -2,7 +2,7 @@ import { useRemuxHlsToMp4 } from "@/hooks/useRemuxHlsToMp4";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { queueActions, queueAtom } from "@/utils/atoms/queue"; import { queueActions, queueAtom } from "@/utils/atoms/queue";
import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server"; import { saveDownloadItemInfoToDiskTmp } from "@/utils/optimize-server";
@@ -21,7 +21,7 @@ import {
import { Href, router, useFocusEffect } from "expo-router"; import { Href, router, useFocusEffect } from "expo-router";
import { useAtom } from "jotai"; import { useAtom } from "jotai";
import React, { useCallback, useMemo, useRef, useState } from "react"; import React, { useCallback, useMemo, useRef, useState } from "react";
import { Alert, View, ViewProps } from "react-native"; import { Alert, Platform, View, ViewProps } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { AudioTrackSelector } from "./AudioTrackSelector"; import { AudioTrackSelector } from "./AudioTrackSelector";
import { Bitrate, BitrateSelector } from "./BitrateSelector"; import { Bitrate, BitrateSelector } from "./BitrateSelector";
@@ -66,10 +66,12 @@ export const DownloadItems: React.FC<DownloadProps> = ({
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1); const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
const [selectedSubtitleStream, setSelectedSubtitleStream] = const [selectedSubtitleStream, setSelectedSubtitleStream] =
useState<number>(0); useState<number>(0);
const [maxBitrate, setMaxBitrate] = useState<Bitrate>(settings?.defaultBitrate ?? { const [maxBitrate, setMaxBitrate] = useState<Bitrate>(
settings?.defaultBitrate ?? {
key: "Max", key: "Max",
value: undefined, value: undefined,
}); }
);
const userCanDownload = useMemo( const userCanDownload = useMemo(
() => user?.Policy?.EnableContentDownloading, () => user?.Policy?.EnableContentDownloading,
@@ -162,7 +164,9 @@ export const DownloadItems: React.FC<DownloadProps> = ({
); );
} }
} else { } else {
toast.error(t("home.downloads.toasts.you_are_not_allowed_to_download_files")); toast.error(
t("home.downloads.toasts.you_are_not_allowed_to_download_files")
);
} }
}, [ }, [
queue, queue,
@@ -333,7 +337,10 @@ export const DownloadItems: React.FC<DownloadProps> = ({
{title} {title}
</Text> </Text>
<Text className="text-neutral-300"> <Text className="text-neutral-300">
{subtitle || t("item_card.download.download_x_item", {item_count: itemsNotDownloaded.length})} {subtitle ||
t("item_card.download.download_x_item", {
item_count: itemsNotDownloaded.length,
})}
</Text> </Text>
</View> </View>
<View className="flex flex-col space-y-2 w-full items-start"> <View className="flex flex-col space-y-2 w-full items-start">
@@ -391,12 +398,16 @@ export const DownloadSingleItem: React.FC<{
size?: "default" | "large"; size?: "default" | "large";
item: BaseItemDto; item: BaseItemDto;
}> = ({ item, size = "default" }) => { }> = ({ item, size = "default" }) => {
if (Platform.isTV) return;
return ( return (
<DownloadItems <DownloadItems
size={size} size={size}
title={item.Type == "Episode" title={
item.Type == "Episode"
? t("item_card.download.download_episode") ? t("item_card.download.download_episode")
: t("item_card.download.download_movie")} : t("item_card.download.download_movie")
}
subtitle={item.Name!} subtitle={item.Name!}
items={[item]} items={[item]}
MissingDownloadIconComponent={() => ( MissingDownloadIconComponent={() => (

View File

@@ -15,6 +15,7 @@ import { SeasonEpisodesCarousel } from "@/components/series/SeasonEpisodesCarous
import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings"; import useDefaultPlaySettings from "@/hooks/useDefaultPlaySettings";
import { useImageColors } from "@/hooks/useImageColors"; import { useImageColors } from "@/hooks/useImageColors";
import { useOrientation } from "@/hooks/useOrientation"; import { useOrientation } from "@/hooks/useOrientation";
import * as ScreenOrientation from "@/packages/expo-screen-orientation";
import { apiAtom } from "@/providers/JellyfinProvider"; import { apiAtom } from "@/providers/JellyfinProvider";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
@@ -24,17 +25,16 @@ 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 "@/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 { Platform, 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";
const Chromecast = !Platform.isTV ? require("./Chromecast") : null; import { AddToFavorites } from "./AddToFavorites";
import { ItemHeader } from "./ItemHeader"; import { ItemHeader } from "./ItemHeader";
import { ItemTechnicalDetails } from "./ItemTechnicalDetails"; import { ItemTechnicalDetails } from "./ItemTechnicalDetails";
import { MediaSourceSelector } from "./MediaSourceSelector"; import { MediaSourceSelector } from "./MediaSourceSelector";
import { MoreMoviesWithActor } from "./MoreMoviesWithActor"; import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
import { AddToFavorites } from "./AddToFavorites"; const Chromecast = !Platform.isTV ? require("./Chromecast") : null;
export type SelectedOptions = { export type SelectedOptions = {
bitrate: Bitrate; bitrate: Bitrate;
@@ -94,7 +94,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
/> />
{item.Type !== "Program" && ( {item.Type !== "Program" && (
<View className="flex flex-row items-center space-x-2"> <View className="flex flex-row items-center space-x-2">
{!Platform.isTV && (
<DownloadSingleItem item={item} size="large" /> <DownloadSingleItem item={item} size="large" />
)}
<PlayedStatus items={[item]} size="large" /> <PlayedStatus items={[item]} size="large" />
<AddToFavorites item={item} type="item" /> <AddToFavorites item={item} type="item" />
</View> </View>
@@ -164,7 +166,6 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
} }
> >
<View className="flex flex-col bg-transparent shrink"> <View className="flex flex-col bg-transparent shrink">
{/* {!Platform.isTV && ( */}
<View className="flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink"> <View className="flex flex-col px-4 w-full space-y-2 pt-2 mb-2 shrink">
<ItemHeader item={item} className="mb-4" /> <ItemHeader item={item} className="mb-4" />
{item.Type !== "Program" && !Platform.isTV && ( {item.Type !== "Program" && !Platform.isTV && (
@@ -222,13 +223,11 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
</View> </View>
)} )}
{/* {!Platform.isTV && ( */}
<PlayButton <PlayButton
className="grow" className="grow"
selectedOptions={selectedOptions} selectedOptions={selectedOptions}
item={item} item={item}
/> />
{/* )} */}
</View> </View>
{item.Type === "Episode" && ( {item.Type === "Episode" && (

View File

@@ -1,4 +1,4 @@
import { Platform } from "react-native"; import { Platform, Pressable } from "react-native";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { itemThemeColorAtom } from "@/utils/atoms/primaryColor"; import { itemThemeColorAtom } from "@/utils/atoms/primaryColor";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
@@ -79,6 +79,7 @@ export const PlayButton: React.FC<Props> = ({
); );
const onPress = useCallback(async () => { const onPress = useCallback(async () => {
console.log("onPress");
if (!item) return; if (!item) return;
lightHapticFeedback(); lightHapticFeedback();
@@ -113,7 +114,6 @@ export const PlayButton: React.FC<Props> = ({
switch (selectedIndex) { switch (selectedIndex) {
case 0: case 0:
if (!Platform.isTV) {
await CastContext.getPlayServicesState().then(async (state) => { await CastContext.getPlayServicesState().then(async (state) => {
if (state && state !== PlayServicesState.SUCCESS) { if (state && state !== PlayServicesState.SUCCESS) {
CastContext.showPlayServicesErrorDialog(state); CastContext.showPlayServicesErrorDialog(state);
@@ -211,7 +211,6 @@ export const PlayButton: React.FC<Props> = ({
} }
} }
}); });
}
break; break;
case 1: case 1:
goToPlayer(queryString, selectedOptions.bitrate?.value); goToPlayer(queryString, selectedOptions.bitrate?.value);
@@ -323,7 +322,6 @@ export const PlayButton: React.FC<Props> = ({
*/ */
return ( return (
<View>
<TouchableOpacity <TouchableOpacity
disabled={!item} disabled={!item}
accessibilityLabel="Play button" accessibilityLabel="Play button"
@@ -381,17 +379,5 @@ export const PlayButton: React.FC<Props> = ({
</View> </View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
{/* <View className="mt-2 flex flex-row items-center">
<Ionicons
name="information-circle"
size={12}
className=""
color={"#9BA1A6"}
/>
<Text className="text-neutral-500 ml-1">
{directStream ? "Direct stream" : "Transcoded stream"}
</Text>
</View> */}
</View>
); );
}; };

View File

@@ -63,7 +63,8 @@ export const PlayButton: React.FC<Props> = ({
[router] [router]
); );
const onPress = useCallback(async () => { const onPress = () => {
console.log("onpress");
if (!item) return; if (!item) return;
lightHapticFeedback(); lightHapticFeedback();
@@ -79,15 +80,7 @@ export const PlayButton: React.FC<Props> = ({
const queryString = queryParams.toString(); const queryString = queryParams.toString();
goToPlayer(queryString, selectedOptions.bitrate?.value); goToPlayer(queryString, selectedOptions.bitrate?.value);
return; return;
}, [ };
item,
settings,
api,
user,
router,
showActionSheetWithOptions,
selectedOptions,
]);
const derivedTargetWidth = useDerivedValue(() => { const derivedTargetWidth = useDerivedValue(() => {
if (!item || !item.RunTimeTicks) return 0; if (!item || !item.RunTimeTicks) return 0;
@@ -179,9 +172,7 @@ export const PlayButton: React.FC<Props> = ({
*/ */
return ( return (
<View>
<TouchableOpacity <TouchableOpacity
disabled={!item}
accessibilityLabel="Play button" accessibilityLabel="Play button"
accessibilityHint="Tap to play the media" accessibilityHint="Tap to play the media"
onPress={onPress} onPress={onPress}
@@ -231,17 +222,5 @@ export const PlayButton: React.FC<Props> = ({
</View> </View>
</View> </View>
</TouchableOpacity> </TouchableOpacity>
{/* <View className="mt-2 flex flex-row items-center">
<Ionicons
name="information-circle"
size={12}
className=""
color={"#9BA1A6"}
/>
<Text className="text-neutral-500 ml-1">
{directStream ? "Direct stream" : "Transcoded stream"}
</Text>
</View> */}
</View>
); );
}; };

View File

@@ -1,14 +1,22 @@
import React from "react"; import React from "react";
import { TextProps } from "react-native"; import { Platform, TextProps } from "react-native";
import { UITextView } from "react-native-uitextview"; import { UITextView } from "react-native-uitextview";
import { Text as RNText } from "react-native";
export function Text( export function Text(
props: TextProps & { props: TextProps & {
uiTextView?: boolean; uiTextView?: boolean;
} }
) { ) {
const { style, ...otherProps } = props; const { style, ...otherProps } = props;
if (Platform.isTV)
return (
<RNText
allowFontScaling={false}
style={[{ color: "white" }, style]}
{...otherProps}
/>
);
else
return ( return (
<UITextView <UITextView
allowFontScaling={false} allowFontScaling={false}

View File

@@ -1,16 +1,15 @@
import { Text } from "@/components/common/Text"; import { Text } from "@/components/common/Text";
import { useDownload } from "@/providers/DownloadProvider"; import { useDownload } from "@/providers/DownloadProvider";
import {DownloadMethod, useSettings} from "@/utils/atoms/settings"; import { DownloadMethod, useSettings } from "@/utils/atoms/settings";
import { storage } from "@/utils/mmkv";
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";
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 { Image } from "expo-image";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
const FFmpegKitProvider = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; import { t } from "i18next";
import { useAtom } from "jotai"; import { useMemo } from "react";
import { import {
ActivityIndicator, ActivityIndicator,
Platform, Platform,
@@ -21,10 +20,12 @@ import {
} from "react-native"; } from "react-native";
import { toast } from "sonner-native"; import { toast } from "sonner-native";
import { Button } from "../Button"; import { Button } from "../Button";
import { Image } from "expo-image"; const BackGroundDownloader = !Platform.isTV
import { useMemo } from "react"; ? require("@kesha-antonov/react-native-background-downloader")
import { storage } from "@/utils/mmkv"; : null;
import { t } from "i18next"; const FFmpegKitProvider = !Platform.isTV
? require("ffmpeg-kit-react-native")
: null;
interface Props extends ViewProps {} interface Props extends ViewProps {}
@@ -33,14 +34,20 @@ export const ActiveDownloads: React.FC<Props> = ({ ...props }) => {
if (processes?.length === 0) if (processes?.length === 0)
return ( return (
<View {...props} className="bg-neutral-900 p-4 rounded-2xl"> <View {...props} className="bg-neutral-900 p-4 rounded-2xl">
<Text className="text-lg font-bold">{t("home.downloads.active_download")}</Text> <Text className="text-lg font-bold">
<Text className="opacity-50">{t("home.downloads.no_active_downloads")}</Text> {t("home.downloads.active_download")}
</Text>
<Text className="opacity-50">
{t("home.downloads.no_active_downloads")}
</Text>
</View> </View>
); );
return ( return (
<View {...props} className="bg-neutral-900 p-4 rounded-2xl"> <View {...props} className="bg-neutral-900 p-4 rounded-2xl">
<Text className="text-lg font-bold mb-2">{t("home.downloads.active_downloads")}</Text> <Text className="text-lg font-bold mb-2">
{t("home.downloads.active_downloads")}
</Text>
<View className="space-y-2"> <View className="space-y-2">
{processes?.map((p: JobStatus) => ( {processes?.map((p: JobStatus) => (
<DownloadCard key={p.item.Id} process={p} /> <DownloadCard key={p.item.Id} process={p} />
@@ -81,7 +88,9 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
} }
} else { } else {
FFmpegKitProvider.FFmpegKit.cancel(Number(id)); FFmpegKitProvider.FFmpegKit.cancel(Number(id));
setProcesses((prev: any[]) => prev.filter((p: { id: string; }) => p.id !== id)); setProcesses((prev: any[]) =>
prev.filter((p: { id: string }) => p.id !== id)
);
} }
}, },
onSuccess: () => { onSuccess: () => {
@@ -156,7 +165,9 @@ const DownloadCard = ({ process, ...props }: DownloadCardProps) => {
<Text className="text-xs">{process.speed?.toFixed(2)}x</Text> <Text className="text-xs">{process.speed?.toFixed(2)}x</Text>
)} )}
{eta(process) && ( {eta(process) && (
<Text className="text-xs">{t("home.downloads.eta", {eta: eta(process)})}</Text> <Text className="text-xs">
{t("home.downloads.eta", { eta: eta(process) })}
</Text>
)} )}
</View> </View>

View File

@@ -540,6 +540,7 @@ export const Controls: React.FC<Props> = ({
pointerEvents={showControls ? "auto" : "none"} pointerEvents={showControls ? "auto" : "none"}
className={`flex flex-row w-full pt-2`} className={`flex flex-row w-full pt-2`}
> >
{!Platform.isTV && (
<View className="mr-auto"> <View className="mr-auto">
<VideoProvider <VideoProvider
getAudioTracks={getAudioTracks} getAudioTracks={getAudioTracks}
@@ -551,6 +552,7 @@ export const Controls: React.FC<Props> = ({
<DropdownView showControls={showControls} /> <DropdownView showControls={showControls} />
</VideoProvider> </VideoProvider>
</View> </View>
)}
<View className="flex flex-row items-center space-x-2 "> <View className="flex flex-row items-center space-x-2 ">
{!Platform.isTV && ( {!Platform.isTV && (

View File

@@ -11,7 +11,9 @@ 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 FFMPEGKitReactNative = !Platform.isTV ? require("ffmpeg-kit-react-native") : null; const FFMPEGKitReactNative = !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";
@@ -24,8 +26,10 @@ import { Platform } from "react-native";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession; type FFmpegSession = typeof FFMPEGKitReactNative.FFmpegSession;
type Statistics = typeof FFMPEGKitReactNative.Statistics type Statistics = typeof FFMPEGKitReactNative.Statistics;
const FFmpegKit = FFMPEGKitReactNative.FFmpegKit; const FFmpegKit = Platform.isTV
? null
: (FFMPEGKitReactNative.FFmpegKit as typeof FFMPEGKitReactNative.FFmpegKit);
const createFFmpegCommand = (url: string, output: string) => [ const createFFmpegCommand = (url: string, output: string) => [
"-y", // overwrite output files without asking "-y", // overwrite output files without asking
"-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options "-thread_queue_size 512", // https://ffmpeg.org/ffmpeg.html#toc-Advanced-options
@@ -101,7 +105,10 @@ export const useRemuxHlsToMp4 = () => {
} }
setProcesses((prev: any[]) => { setProcesses((prev: any[]) => {
return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id); return prev.filter(
(process: { itemId: string | undefined }) =>
process.itemId !== item.Id
);
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -126,7 +133,7 @@ export const useRemuxHlsToMp4 = () => {
if (!item.Id) throw new Error("Item is undefined"); if (!item.Id) throw new Error("Item is undefined");
setProcesses((prev: any[]) => { setProcesses((prev: any[]) => {
return prev.map((process: { itemId: string | undefined; }) => { return prev.map((process: { itemId: string | undefined }) => {
if (process.itemId === item.Id) { if (process.itemId === item.Id) {
return { return {
...process, ...process,
@@ -161,7 +168,9 @@ export const useRemuxHlsToMp4 = () => {
// First lets save any important assets we want to present to the user offline // First lets save any important assets we want to present to the user offline
await onSaveAssets(api, item); await onSaveAssets(api, item);
toast.success(t("home.downloads.toasts.download_started_for", {item: item.Name}), { toast.success(
t("home.downloads.toasts.download_started_for", { item: item.Name }),
{
action: { action: {
label: "Go to download", label: "Go to download",
onClick: () => { onClick: () => {
@@ -169,7 +178,8 @@ export const useRemuxHlsToMp4 = () => {
toast.dismiss(); toast.dismiss();
}, },
}, },
}); }
);
try { try {
const job: JobStatus = { const job: JobStatus = {
@@ -201,7 +211,10 @@ export const useRemuxHlsToMp4 = () => {
Error: ${error.message}, Stack: ${error.stack}` Error: ${error.message}, Stack: ${error.stack}`
); );
setProcesses((prev: any[]) => { setProcesses((prev: any[]) => {
return prev.filter((process: { itemId: string | undefined; }) => process.itemId !== item.Id); return prev.filter(
(process: { itemId: string | undefined }) =>
process.itemId !== item.Id
);
}); });
throw error; // Re-throw the error to propagate it to the caller throw error; // Re-throw the error to propagate it to the caller
} }

View File

@@ -5,7 +5,7 @@ Pod::Spec.new do |s|
s.description = 'A sample project description' s.description = 'A sample project description'
s.author = '' s.author = ''
s.homepage = 'https://docs.expo.dev/modules/' s.homepage = 'https://docs.expo.dev/modules/'
s.platforms = { :ios => '13.4', :tvos => '13.4' } s.platforms = { :ios => '13.4', :tvos => '16' }
s.source = { git: '' } s.source = { git: '' }
s.static_framework = true s.static_framework = true

View File

@@ -74,7 +74,7 @@
"react-i18next": "^15.4.0", "react-i18next": "^15.4.0",
"react-native": "npm:react-native-tvos@~0.77.0-0", "react-native": "npm:react-native-tvos@~0.77.0-0",
"react-native-awesome-slider": "^2.9.0", "react-native-awesome-slider": "^2.9.0",
"react-native-bottom-tabs": "0.8.7", "react-native-bottom-tabs": "0.8.6",
"react-native-circular-progress": "^1.4.1", "react-native-circular-progress": "^1.4.1",
"react-native-compressor": "^1.10.3", "react-native-compressor": "^1.10.3",
"react-native-country-flag": "^2.0.2", "react-native-country-flag": "^2.0.2",