mirror of
https://github.com/streamyfin/streamyfin.git
synced 2025-08-20 18:37:18 +02:00
fix: tvos fixes
This commit is contained in:
@@ -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>
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
4
bun.lock
4
bun.lock
@@ -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=="],
|
||||||
|
|
||||||
|
|||||||
@@ -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={() => (
|
||||||
|
|||||||
@@ -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" && (
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user