fix
@@ -22,11 +22,6 @@
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<service android:name="com.brentvatne.exoplayer.VideoPlaybackService" android:exported="false" android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="androidx.media3.session.MediaSessionService"/>
|
||||
</intent-filter>
|
||||
</service>
|
||||
<activity android:name=".MainActivity" android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|uiMode" android:launchMode="singleTask" android:windowSoftInputMode="adjustResize" android:theme="@style/Theme.App.SplashScreen" android:exported="true" android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 174 KiB After Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 283 KiB After Width: | Height: | Size: 282 KiB |
|
Before Width: | Height: | Size: 286 KiB After Width: | Height: | Size: 286 KiB |
15
app.json
@@ -4,11 +4,11 @@
|
||||
"slug": "streamyfin",
|
||||
"version": "0.0.4",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.jpg",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "streamyfin",
|
||||
"userInterfaceStyle": "dark",
|
||||
"splash": {
|
||||
"image": "./assets/images/splash.jpg",
|
||||
"image": "./assets/images/splash.png",
|
||||
"resizeMode": "contain",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
@@ -16,7 +16,8 @@
|
||||
"ios": {
|
||||
"userInterfaceStyle": "dark",
|
||||
"infoPlist": {
|
||||
"NSCameraUsageDescription": "The app needs access to your camera to scan barcodes."
|
||||
"NSCameraUsageDescription": "The app needs access to your camera to scan barcodes.",
|
||||
"NSMicrophoneUsageDescription": "The app needs access to your microphone."
|
||||
},
|
||||
"supportsTablet": true,
|
||||
"bundleIdentifier": "com.fredrikburmester.streamyfin"
|
||||
@@ -24,22 +25,16 @@
|
||||
"android": {
|
||||
"userInterfaceStyle": "light",
|
||||
"adaptiveIcon": {
|
||||
"foregroundImage": "./assets/images/icon.jpg",
|
||||
"foregroundImage": "./assets/images/icon.png",
|
||||
"backgroundColor": "#ffffff"
|
||||
},
|
||||
"package": "com.fredrikburmester.streamyfin"
|
||||
},
|
||||
"web": {
|
||||
"bundler": "metro",
|
||||
"output": "static",
|
||||
"favicon": "./assets/images/favicon.png"
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-font",
|
||||
"expo-video",
|
||||
"react-native-compressor",
|
||||
// "react-native-google-cast",
|
||||
[
|
||||
"react-native-video",
|
||||
{
|
||||
|
||||
@@ -5,11 +5,13 @@ import { ItemCardText } from "@/components/ItemCardText";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { getItemsApi, getSuggestionsApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
RefreshControl,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
@@ -21,7 +23,7 @@ export default function index() {
|
||||
const router = useRouter();
|
||||
|
||||
const { data, isLoading, isError } = useQuery<BaseItemDto[]>({
|
||||
queryKey: ["resumeItems", api, user?.Id],
|
||||
queryKey: ["resumeItems", user?.Id],
|
||||
queryFn: async () => {
|
||||
if (!api || !user?.Id) {
|
||||
return [];
|
||||
@@ -85,6 +87,18 @@ export default function index() {
|
||||
staleTime: 60,
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const refetch = useCallback(async () => {
|
||||
setLoading(true);
|
||||
await queryClient.refetchQueries({ queryKey: ["resumeItems", user?.Id] });
|
||||
await queryClient.refetchQueries({ queryKey: ["items", user?.Id] });
|
||||
await queryClient.refetchQueries({ queryKey: ["suggestions", user?.Id] });
|
||||
setLoading(false);
|
||||
}, [queryClient, user?.Id]);
|
||||
|
||||
if (isError)
|
||||
return (
|
||||
<View className="flex flex-col items-center justify-center h-full -mt-12">
|
||||
@@ -105,7 +119,12 @@ export default function index() {
|
||||
if (!data || data.length === 0) return <Text>No data...</Text>;
|
||||
|
||||
return (
|
||||
<ScrollView nestedScrollEnabled>
|
||||
<ScrollView
|
||||
nestedScrollEnabled
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={loading} onRefresh={refetch} />
|
||||
}
|
||||
>
|
||||
<View className="py-4 gap-y-2">
|
||||
<Text className="px-4 text-2xl font-bold mb-2">Continue Watching</Text>
|
||||
<HorizontalScroll<BaseItemDto>
|
||||
|
||||
@@ -67,8 +67,8 @@ export default function search() {
|
||||
|
||||
return (
|
||||
<ScrollView keyboardDismissMode="on-drag">
|
||||
<View className="p-4 flex flex-col">
|
||||
<View className="mb-4">
|
||||
<View className="flex flex-col py-2">
|
||||
<View className="mb-4 px-4">
|
||||
<Input
|
||||
autoCorrect={false}
|
||||
returnKeyType="done"
|
||||
@@ -79,7 +79,7 @@ export default function search() {
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text className="font-bold text-2xl mb-2">Movies</Text>
|
||||
<Text className="font-bold text-2xl px-4 mb-2">Movies</Text>
|
||||
<SearchItemWrapper
|
||||
ids={movies?.map((m) => m.Id!)}
|
||||
renderItem={(data) => (
|
||||
@@ -101,7 +101,7 @@ export default function search() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Text className="font-bold text-2xl my-2">Series</Text>
|
||||
<Text className="font-bold text-2xl px-4 my-2">Series</Text>
|
||||
<SearchItemWrapper
|
||||
ids={series?.map((m) => m.Id!)}
|
||||
renderItem={(data) => (
|
||||
@@ -123,7 +123,7 @@ export default function search() {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Text className="font-bold text-2xl my-2">Episodes</Text>
|
||||
<Text className="font-bold text-2xl px-4 my-2">Episodes</Text>
|
||||
<SearchItemWrapper
|
||||
ids={episodes?.map((m) => m.Id!)}
|
||||
renderItem={(data) => (
|
||||
@@ -195,7 +195,7 @@ const SearchItemWrapper: React.FC<Props> = ({ ids, renderItem }) => {
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
if (!data) return <Text className="opacity-50 text-xs">No results</Text>;
|
||||
if (!data) return <Text className="opacity-50 text-xs px-4">No results</Text>;
|
||||
|
||||
return renderItem(data);
|
||||
};
|
||||
|
||||
83
app/(auth)/items/[id]/ParallaxPage.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import type { PropsWithChildren, ReactElement } from "react";
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
useColorScheme,
|
||||
} from "react-native";
|
||||
import Animated, {
|
||||
interpolate,
|
||||
useAnimatedRef,
|
||||
useAnimatedStyle,
|
||||
useScrollViewOffset,
|
||||
} from "react-native-reanimated";
|
||||
|
||||
import { ThemedView } from "@/components/ThemedView";
|
||||
|
||||
const HEADER_HEIGHT = 250;
|
||||
|
||||
type Props = PropsWithChildren<{
|
||||
headerImage: ReactElement;
|
||||
onScroll?: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
}>;
|
||||
|
||||
export const ParallaxScrollView: React.FC<Props> = ({
|
||||
children,
|
||||
headerImage,
|
||||
onScroll,
|
||||
}: Props) => {
|
||||
const scrollRef = useAnimatedRef<Animated.ScrollView>();
|
||||
const scrollOffset = useScrollViewOffset(scrollRef);
|
||||
|
||||
const headerAnimatedStyle = useAnimatedStyle(() => {
|
||||
return {
|
||||
transform: [
|
||||
{
|
||||
translateY: interpolate(
|
||||
scrollOffset.value,
|
||||
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
|
||||
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
|
||||
),
|
||||
},
|
||||
{
|
||||
scale: interpolate(
|
||||
scrollOffset.value,
|
||||
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
|
||||
[2, 1, 1]
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<ThemedView style={styles.container}>
|
||||
<Animated.ScrollView ref={scrollRef} scrollEventThrottle={16}>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.header,
|
||||
{ backgroundColor: "white" },
|
||||
headerAnimatedStyle,
|
||||
]}
|
||||
>
|
||||
{headerImage}
|
||||
</Animated.View>
|
||||
<ThemedView style={styles.content}>{children}</ThemedView>
|
||||
</Animated.ScrollView>
|
||||
</ThemedView>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
},
|
||||
header: {
|
||||
height: 250,
|
||||
overflow: "hidden",
|
||||
},
|
||||
content: {
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
},
|
||||
});
|
||||
@@ -13,8 +13,16 @@ import {} from "@jellyfin/sdk/lib/utils/url";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useAtom } from "jotai";
|
||||
import { useEffect } from "react";
|
||||
import { ActivityIndicator, ScrollView, View } from "react-native";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
ScrollView,
|
||||
View,
|
||||
} from "react-native";
|
||||
import { ParallaxScrollView } from "./ParallaxPage";
|
||||
import { Image } from "expo-image";
|
||||
|
||||
const page: React.FC = () => {
|
||||
const local = useLocalSearchParams();
|
||||
@@ -23,8 +31,6 @@ const page: React.FC = () => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const { data: item, isLoading: l1 } = useQuery({
|
||||
queryKey: ["item", id],
|
||||
queryFn: async () =>
|
||||
@@ -34,22 +40,14 @@ const page: React.FC = () => {
|
||||
itemId: id,
|
||||
}),
|
||||
enabled: !!id && !!api,
|
||||
staleTime: Infinity,
|
||||
staleTime: 60,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => {
|
||||
<Ionicons name="accessibility" />;
|
||||
},
|
||||
});
|
||||
}, [item, navigation]);
|
||||
|
||||
const { data: posterUrl } = useQuery({
|
||||
queryKey: ["backdrop", item?.Id],
|
||||
queryFn: async () => getBackdrop(api, item),
|
||||
enabled: !!api && !!item?.Id,
|
||||
staleTime: Infinity,
|
||||
staleTime: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
if (l1)
|
||||
@@ -59,12 +57,23 @@ const page: React.FC = () => {
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!item?.Id) return null;
|
||||
if (!item?.Id || !posterUrl) return null;
|
||||
|
||||
return (
|
||||
<ScrollView style={[{ flex: 1 }]} keyboardDismissMode="on-drag">
|
||||
<LargePoster url={posterUrl} />
|
||||
<View className="flex flex-col px-4 mb-4">
|
||||
<ParallaxScrollView
|
||||
headerImage={
|
||||
<Image
|
||||
source={{
|
||||
uri: posterUrl,
|
||||
}}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: 250,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<View className="flex flex-col px-4 mb-4 pt-4">
|
||||
<View className="flex flex-col">
|
||||
{item.Type === "Episode" ? (
|
||||
<>
|
||||
@@ -127,12 +136,10 @@ const page: React.FC = () => {
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
<View className="px-4 mb-4">
|
||||
<CastAndCrew item={item} />
|
||||
</View>
|
||||
<CastAndCrew item={item} />
|
||||
|
||||
{item.Type === "Episode" && (
|
||||
<View className="px-4 mb-4">
|
||||
<View className="mb-4">
|
||||
<CurrentSeries item={item} />
|
||||
</View>
|
||||
)}
|
||||
@@ -140,7 +147,7 @@ const page: React.FC = () => {
|
||||
<SimilarItems itemId={item.Id} />
|
||||
|
||||
<View className="h-12"></View>
|
||||
</ScrollView>
|
||||
</ParallaxScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ const page: React.FC = () => {
|
||||
const [user] = useAtom(userAtom);
|
||||
|
||||
const { data: item } = useQuery({
|
||||
queryKey: ["item", seriesId],
|
||||
queryKey: ["series", seriesId],
|
||||
queryFn: async () =>
|
||||
await getUserItemData({
|
||||
api,
|
||||
@@ -26,33 +26,23 @@ const page: React.FC = () => {
|
||||
itemId: seriesId,
|
||||
}),
|
||||
enabled: !!seriesId && !!api,
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
const { data: next } = useQuery({
|
||||
queryKey: ["nextUp", seriesId],
|
||||
queryFn: async () =>
|
||||
await nextUp({
|
||||
userId: user?.Id,
|
||||
api,
|
||||
itemId: seriesId,
|
||||
}),
|
||||
enabled: !!api && !!seriesId && !!user?.Id,
|
||||
staleTime: 0,
|
||||
staleTime: 60,
|
||||
});
|
||||
|
||||
if (!item) return null;
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<View className="flex flex-col px-4 pt-4 pb-8">
|
||||
<MoviePoster item={item} />
|
||||
<View className="my-4">
|
||||
<Text className="text-3xl font-bold">{item?.Name}</Text>
|
||||
<Text className="">{item?.Overview}</Text>
|
||||
<View className="flex flex-col pt-4 pb-8">
|
||||
<View className="px-4">
|
||||
<MoviePoster item={item} />
|
||||
<View className="my-4">
|
||||
<Text className="text-3xl font-bold">{item?.Name}</Text>
|
||||
<Text className="">{item?.Overview}</Text>
|
||||
</View>
|
||||
</View>
|
||||
<SeasonPicker item={item} />
|
||||
<NextUp items={next} />
|
||||
<NextUp seriesId={seriesId} />
|
||||
</View>
|
||||
</ScrollView>
|
||||
);
|
||||
|
||||
@@ -34,15 +34,22 @@ const deleteAllFiles = async () => {
|
||||
const deleteFile = async (id: string | null | undefined) => {
|
||||
if (!id) return;
|
||||
|
||||
FileSystem.deleteAsync(`${FileSystem.documentDirectory}/${id}.mp4`).catch(
|
||||
(err) => console.error(err)
|
||||
);
|
||||
try {
|
||||
FileSystem.deleteAsync(`${FileSystem.documentDirectory}/${id}.mp4`).catch(
|
||||
(err) => console.error(err)
|
||||
);
|
||||
|
||||
const currentFiles = JSON.parse(
|
||||
(await AsyncStorage.getItem("downloaded_files")) ?? "[]"
|
||||
);
|
||||
const updatedFiles = currentFiles.filter((f: string) => f !== id);
|
||||
await AsyncStorage.setItem("downloaded_files", JSON.stringify(updatedFiles));
|
||||
const currentFiles = JSON.parse(
|
||||
(await AsyncStorage.getItem("downloaded_files")) ?? "[]"
|
||||
) as BaseItemDto[];
|
||||
const updatedFiles = currentFiles.filter((f) => f.Id !== id);
|
||||
await AsyncStorage.setItem(
|
||||
"downloaded_files",
|
||||
JSON.stringify(updatedFiles)
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
const listDownloadedFiles = async () => {
|
||||
@@ -125,8 +132,8 @@ export default function settings() {
|
||||
subTitle={file.ProductionYear?.toString()}
|
||||
iconAfter={
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
deleteFile(file.Id);
|
||||
onPress={async () => {
|
||||
await deleteFile(file.Id);
|
||||
setKey((prevKey) => prevKey + 1);
|
||||
}}
|
||||
>
|
||||
@@ -142,18 +149,20 @@ export default function settings() {
|
||||
))}
|
||||
</View>
|
||||
) : activeProcess ? (
|
||||
<ListItem
|
||||
title={activeProcess.item.Name}
|
||||
iconAfter={
|
||||
<ProgressCircle
|
||||
size={22}
|
||||
fill={activeProcess.progress}
|
||||
width={3}
|
||||
tintColor="#3498db"
|
||||
backgroundColor="#bdc3c7"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<View className="rounded-xl overflow-hidden mb-2">
|
||||
<ListItem
|
||||
title={activeProcess.item.Name}
|
||||
iconAfter={
|
||||
<ProgressCircle
|
||||
size={22}
|
||||
fill={activeProcess.progress}
|
||||
width={3}
|
||||
tintColor="#3498db"
|
||||
backgroundColor="#bdc3c7"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<Text className="opacity-50">No downloaded files</Text>
|
||||
)}
|
||||
|
||||
@@ -30,12 +30,10 @@ export default function RootLayout() {
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60,
|
||||
refetchOnMount: false,
|
||||
refetchInterval: false,
|
||||
refetchIntervalInBackground: false,
|
||||
refetchOnReconnect: false,
|
||||
refetchOnWindowFocus: false,
|
||||
retryOnMount: false,
|
||||
refetchOnMount: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnWindowFocus: true,
|
||||
retryOnMount: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
BIN
assets/images/icon.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
assets/images/splash.png
Normal file
|
After Width: | Height: | Size: 2.9 MiB |
@@ -23,7 +23,7 @@ const ContinueWatchingPoster: React.FC<ContinueWatchingPosterProps> = ({
|
||||
queryKey: ["backdrop", item.Id],
|
||||
queryFn: async () => getBackdrop(api, item),
|
||||
enabled: !!api && !!item.Id,
|
||||
staleTime: Infinity,
|
||||
staleTime: 60 * 60 * 24 * 7,
|
||||
});
|
||||
|
||||
const [progress, setProgress] = useState(
|
||||
|
||||
@@ -1,182 +1,50 @@
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
|
||||
import { writeToLog } from "@/utils/log";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { runningProcesses } from "@/utils/atoms/downloads";
|
||||
import { getPlaybackInfo, useDownloadMedia } from "@/utils/jellyfin";
|
||||
import Ionicons from "@expo/vector-icons/Ionicons";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { FFmpegKit, FFmpegKitConfig } from "ffmpeg-kit-react-native";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import ProgressCircle from "./ProgressCircle";
|
||||
import { router } from "expo-router";
|
||||
import { getPlaybackInfo, useDownloadMedia } from "@/utils/jellyfin";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import { ProcessItem, runningProcesses } from "@/utils/atoms/downloads";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import ProgressCircle from "./ProgressCircle";
|
||||
import { Text } from "./common/Text";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
type DownloadProps = {
|
||||
item: BaseItemDto;
|
||||
};
|
||||
|
||||
// const useRemuxHlsToMp4 = (inputUrl: string, item: BaseItemDto) => {
|
||||
// if (!item.Id || !item.Name) {
|
||||
// writeToLog("ERROR", "useRemuxHlsToMp4 ~ missing arguments", {
|
||||
// item,
|
||||
// inputUrl,
|
||||
// });
|
||||
// throw new Error("Item must have an Id and Name");
|
||||
// }
|
||||
|
||||
// const [session, setSession] = useAtom<ProcessItem | null>(runningProcesses);
|
||||
|
||||
// const output = `${FileSystem.documentDirectory}${item.Id}.mp4`;
|
||||
|
||||
// const command = `-y -fflags +genpts -i ${inputUrl} -c copy -max_muxing_queue_size 9999 ${output}`;
|
||||
|
||||
// const startRemuxing = useCallback(async () => {
|
||||
// if (!item.Id || !item.Name) {
|
||||
// writeToLog(
|
||||
// "ERROR",
|
||||
// "useRemuxHlsToMp4 ~ startRemuxing ~ missing arguments",
|
||||
// {
|
||||
// item,
|
||||
// inputUrl,
|
||||
// }
|
||||
// );
|
||||
// throw new Error("Item must have an Id and Name");
|
||||
// }
|
||||
|
||||
// writeToLog(
|
||||
// "INFO",
|
||||
// `useRemuxHlsToMp4 ~ startRemuxing for item ${item.Id} with url ${inputUrl}`,
|
||||
// {
|
||||
// item,
|
||||
// inputUrl,
|
||||
// }
|
||||
// );
|
||||
|
||||
// try {
|
||||
// setSession({
|
||||
// item,
|
||||
// progress: 0,
|
||||
// });
|
||||
|
||||
// FFmpegKitConfig.enableStatisticsCallback((statistics) => {
|
||||
// let percentage = 0;
|
||||
|
||||
// const videoLength =
|
||||
// (item.MediaSources?.[0].RunTimeTicks || 0) / 10000000; // In seconds
|
||||
// const fps = item.MediaStreams?.[0].RealFrameRate || 25;
|
||||
// const totalFrames = videoLength * fps;
|
||||
|
||||
// const processedFrames = statistics.getVideoFrameNumber();
|
||||
|
||||
// if (totalFrames > 0) {
|
||||
// percentage = Math.floor((processedFrames / totalFrames) * 100);
|
||||
// }
|
||||
|
||||
// setSession((prev) => {
|
||||
// return prev?.item.Id === item.Id!
|
||||
// ? { ...prev, progress: percentage }
|
||||
// : prev;
|
||||
// });
|
||||
// });
|
||||
|
||||
// await FFmpegKit.executeAsync(command, async (session) => {
|
||||
// const returnCode = await session.getReturnCode();
|
||||
// if (returnCode.isValueSuccess()) {
|
||||
// const currentFiles: BaseItemDto[] = JSON.parse(
|
||||
// (await AsyncStorage.getItem("downloaded_files")) || "[]"
|
||||
// );
|
||||
|
||||
// const otherItems = currentFiles.filter((i) => i.Id !== item.Id);
|
||||
|
||||
// await AsyncStorage.setItem(
|
||||
// "downloaded_files",
|
||||
// JSON.stringify([...otherItems, item])
|
||||
// );
|
||||
|
||||
// writeToLog(
|
||||
// "INFO",
|
||||
// `useRemuxHlsToMp4 ~ remuxing completed successfully for item: ${item.Name}`,
|
||||
// {
|
||||
// item,
|
||||
// inputUrl,
|
||||
// }
|
||||
// );
|
||||
// setSession(null);
|
||||
// } else if (returnCode.isValueError()) {
|
||||
// console.error("Failed to remux:");
|
||||
// writeToLog(
|
||||
// "ERROR",
|
||||
// `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}`,
|
||||
// {
|
||||
// item,
|
||||
// inputUrl,
|
||||
// }
|
||||
// );
|
||||
// setSession(null);
|
||||
// } else if (returnCode.isValueCancel()) {
|
||||
// console.log("Remuxing was cancelled");
|
||||
// writeToLog(
|
||||
// "INFO",
|
||||
// `useRemuxHlsToMp4 ~ remuxing was canceled for item: ${item.Name}`,
|
||||
// {
|
||||
// item,
|
||||
// inputUrl,
|
||||
// }
|
||||
// );
|
||||
// setSession(null);
|
||||
// }
|
||||
// });
|
||||
// } catch (error) {
|
||||
// console.error("Failed to remux:", error);
|
||||
// writeToLog(
|
||||
// "ERROR",
|
||||
// `useRemuxHlsToMp4 ~ remuxing failed for item: ${item.Name}`,
|
||||
// {
|
||||
// item,
|
||||
// inputUrl,
|
||||
// }
|
||||
// );
|
||||
// }
|
||||
// }, [inputUrl, output, item, command]);
|
||||
|
||||
// const cancelRemuxing = useCallback(async () => {
|
||||
// FFmpegKit.cancel();
|
||||
// setSession(null);
|
||||
// console.log("Remuxing cancelled");
|
||||
// }, []);
|
||||
|
||||
// return { session, startRemuxing, cancelRemuxing };
|
||||
// };
|
||||
|
||||
export const DownloadItem: React.FC<DownloadProps> = ({ item }) => {
|
||||
// const { session, startRemuxing, cancelRemuxing } = useRemuxHlsToMp4(
|
||||
// url,
|
||||
// item
|
||||
// );
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const [process] = useAtom(runningProcesses);
|
||||
|
||||
const { downloadMedia, isDownloading, error } = useDownloadMedia(api);
|
||||
const { downloadMedia, isDownloading, error, cancelDownload } =
|
||||
useDownloadMedia(api, user?.Id);
|
||||
|
||||
const { data: playbackInfo, isLoading } = useQuery({
|
||||
queryKey: ["playbackInfo", item.Id],
|
||||
queryFn: async () => getPlaybackInfo(api, item.Id, user?.Id),
|
||||
});
|
||||
|
||||
const downloadFile = useCallback(async () => {
|
||||
const playbackInfo = await getPlaybackInfo(api, item.Id, user?.Id);
|
||||
if (!playbackInfo) return;
|
||||
|
||||
const source = playbackInfo?.MediaSources?.[0];
|
||||
const source = playbackInfo.MediaSources?.[0];
|
||||
|
||||
if (source?.SupportsDirectPlay && item.CanDownload) {
|
||||
downloadMedia(item);
|
||||
} else {
|
||||
console.log("file not supported");
|
||||
throw new Error(
|
||||
"Direct play not supported thus the file cannot be downloaded"
|
||||
);
|
||||
}
|
||||
}, [item, user]);
|
||||
}, [item, user, playbackInfo]);
|
||||
|
||||
const [downloaded, setDownloaded] = useState<boolean>(false);
|
||||
const [key, setKey] = useState<string>("");
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
@@ -186,7 +54,19 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item }) => {
|
||||
|
||||
if (data.find((d) => d.Id === item.Id)) setDownloaded(true);
|
||||
})();
|
||||
}, [key]);
|
||||
}, [process]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ActivityIndicator size={"small"} color={"white"} />;
|
||||
}
|
||||
|
||||
if (playbackInfo?.MediaSources?.[0].SupportsDirectPlay === false) {
|
||||
return (
|
||||
<View style={{ opacity: 0.5 }}>
|
||||
<Ionicons name="cloud-download-outline" size={24} color="white" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (process && process.item.Id !== item.Id!) {
|
||||
return (
|
||||
@@ -201,17 +81,22 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item }) => {
|
||||
{process ? (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
// cancelRemuxing();
|
||||
cancelDownload();
|
||||
}}
|
||||
className="-rotate-45"
|
||||
className="relative"
|
||||
>
|
||||
<ProgressCircle
|
||||
size={22}
|
||||
fill={process.progress}
|
||||
width={3}
|
||||
tintColor="#3498db"
|
||||
backgroundColor="#bdc3c7"
|
||||
/>
|
||||
<View className="-rotate-45">
|
||||
<ProgressCircle
|
||||
size={26}
|
||||
fill={process.progress}
|
||||
width={3}
|
||||
tintColor="#3498db"
|
||||
backgroundColor="#bdc3c7"
|
||||
/>
|
||||
</View>
|
||||
<View className="absolute top-0 left-0 font-bold w-full h-full flex flex-col items-center justify-center">
|
||||
<Text className="text-[6px]">{process.progress.toFixed(0)}%</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
) : downloaded ? (
|
||||
<TouchableOpacity
|
||||
@@ -221,7 +106,7 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item }) => {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Ionicons name="cloud-download" size={28} color="#16a34a" />
|
||||
<Ionicons name="cloud-download" size={26} color="#16a34a" />
|
||||
</TouchableOpacity>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
@@ -229,7 +114,7 @@ export const DownloadItem: React.FC<DownloadProps> = ({ item }) => {
|
||||
downloadFile();
|
||||
}}
|
||||
>
|
||||
<Ionicons name="cloud-download-outline" size={28} color="white" />
|
||||
<Ionicons name="cloud-download-outline" size={26} color="white" />
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Ionicons } from "@expo/vector-icons";
|
||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||
import { useQueryClient, InvalidateQueryFilters } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import React from "react";
|
||||
import React, { useCallback } from "react";
|
||||
import { TouchableOpacity, View } from "react-native";
|
||||
import * as Haptics from "expo-haptics";
|
||||
|
||||
@@ -14,6 +14,29 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidateQueries = useCallback(() => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["item", item.Id],
|
||||
refetchType: "all",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["resumeItems", user?.Id],
|
||||
refetchType: "all",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["nextUp", item.SeriesId],
|
||||
refetchType: "all",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["episodes"],
|
||||
refetchType: "all",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["seasons"],
|
||||
refetchType: "all",
|
||||
});
|
||||
}, [api, item.Id, queryClient, user?.Id]);
|
||||
|
||||
return (
|
||||
<View>
|
||||
{item.UserData?.Played ? (
|
||||
@@ -24,11 +47,8 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
itemId: item?.Id,
|
||||
userId: user?.Id,
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["item", item.Id],
|
||||
refetchType: "all",
|
||||
});
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
invalidateQueries();
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark-circle" size={30} color="white" />
|
||||
@@ -41,11 +61,8 @@ export const PlayedStatus: React.FC<{ item: BaseItemDto }> = ({ item }) => {
|
||||
itemId: item?.Id,
|
||||
userId: user?.Id,
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["item", item.Id],
|
||||
refetchType: "all",
|
||||
});
|
||||
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
|
||||
invalidateQueries();
|
||||
}}
|
||||
>
|
||||
<Ionicons name="checkmark-circle-outline" size={30} color="white" />
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
Switch,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
getStreamUrl,
|
||||
getUserItemData,
|
||||
reportPlaybackProgress,
|
||||
reportPlaybackStopped,
|
||||
} from "@/utils/jellyfin";
|
||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { ActivityIndicator, TouchableOpacity, View } from "react-native";
|
||||
import Video, {
|
||||
OnBufferData,
|
||||
OnPlaybackStateChangedData,
|
||||
@@ -16,17 +19,8 @@ import Video, {
|
||||
OnVideoErrorData,
|
||||
VideoRef,
|
||||
} from "react-native-video";
|
||||
import { apiAtom, useJellyfin, userAtom } from "@/providers/JellyfinProvider";
|
||||
import {
|
||||
getBackdrop,
|
||||
getStreamUrl,
|
||||
getUserItemData,
|
||||
reportPlaybackProgress,
|
||||
reportPlaybackStopped,
|
||||
} from "@/utils/jellyfin";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import * as DropdownMenu from "zeego/dropdown-menu";
|
||||
import { Button } from "./Button";
|
||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
import { Text } from "./common/Text";
|
||||
|
||||
type VideoPlayerProps = {
|
||||
@@ -36,11 +30,7 @@ type VideoPlayerProps = {
|
||||
const BITRATES = [
|
||||
{
|
||||
key: "Max",
|
||||
value: 140000000,
|
||||
},
|
||||
{
|
||||
key: "10 Mb/s",
|
||||
value: 10000000,
|
||||
value: undefined,
|
||||
},
|
||||
{
|
||||
key: "4 Mb/s",
|
||||
@@ -50,10 +40,6 @@ const BITRATES = [
|
||||
key: "2 Mb/s",
|
||||
value: 2000000,
|
||||
},
|
||||
{
|
||||
key: "1 Mb/s",
|
||||
value: 1000000,
|
||||
},
|
||||
{
|
||||
key: "500 Kb/s",
|
||||
value: 500000,
|
||||
@@ -62,12 +48,8 @@ const BITRATES = [
|
||||
|
||||
export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
|
||||
const videoRef = useRef<VideoRef | null>(null);
|
||||
const [showPoster, setShowPoster] = useState(true);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
const [buffering, setBuffering] = useState(false);
|
||||
const [maxBitrate, setMaxbitrate] = useState(140000000);
|
||||
const [maxBitrate, setMaxbitrate] = useState<number | undefined>(undefined);
|
||||
const [paused, setPaused] = useState(true);
|
||||
const [forceTranscoding, setForceTranscoding] = useState<boolean>(false);
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
@@ -81,7 +63,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
|
||||
itemId,
|
||||
}),
|
||||
enabled: !!itemId && !!api,
|
||||
staleTime: 0,
|
||||
staleTime: 60,
|
||||
});
|
||||
|
||||
const { data: sessionData } = useQuery({
|
||||
@@ -182,7 +164,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
{playbackURL && (
|
||||
{enableVideo === true &&
|
||||
playbackURL !== null &&
|
||||
playbackURL !== undefined ? (
|
||||
<Video
|
||||
style={{ width: 0, height: 0 }}
|
||||
source={{
|
||||
@@ -190,6 +174,10 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
|
||||
isNetwork: true,
|
||||
startPosition,
|
||||
}}
|
||||
debug={{
|
||||
enable: true,
|
||||
thread: true,
|
||||
}}
|
||||
ref={videoRef}
|
||||
onBuffer={onBuffer}
|
||||
onSeek={(t) => onSeek(t)}
|
||||
@@ -216,7 +204,7 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
|
||||
backBufferDurationMs: 30 * 1000,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<View className="flex flex-row items-center justify-between">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
@@ -241,9 +229,9 @@ export const VideoPlayer: React.FC<VideoPlayerProps> = ({ itemId }) => {
|
||||
sideOffset={8}
|
||||
>
|
||||
<DropdownMenu.Label>Bitrates</DropdownMenu.Label>
|
||||
{BITRATES?.map((b: any) => (
|
||||
{BITRATES?.map((b: any, index: number) => (
|
||||
<DropdownMenu.Item
|
||||
key={b.value}
|
||||
key={index.toString()}
|
||||
onSelect={() => {
|
||||
setMaxbitrate(b.value);
|
||||
}}
|
||||
|
||||
@@ -8,7 +8,7 @@ import Poster from "../Poster";
|
||||
export const CastAndCrew = ({ item }: { item: BaseItemDto }) => {
|
||||
return (
|
||||
<View>
|
||||
<Text className="text-lg font-bold mb-2">Cast & Crew</Text>
|
||||
<Text className="text-lg font-bold mb-2 px-4">Cast & Crew</Text>
|
||||
<HorizontalScroll<NonNullable<BaseItemDto["People"]>[number]>
|
||||
data={item.People}
|
||||
renderItem={(item, index) => (
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Text } from "../common/Text";
|
||||
export const CurrentSeries = ({ item }: { item: BaseItemDto }) => {
|
||||
return (
|
||||
<View>
|
||||
<Text className="text-lg font-bold mb-2">Series</Text>
|
||||
<Text className="text-lg font-bold mb-2 px-4">Series</Text>
|
||||
<HorizontalScroll<BaseItemDto>
|
||||
data={[item]}
|
||||
renderItem={(item, index) => (
|
||||
|
||||
@@ -7,8 +7,27 @@ import Poster from "../Poster";
|
||||
import ContinueWatchingPoster from "../ContinueWatchingPoster";
|
||||
import { ItemCardText } from "../ItemCardText";
|
||||
import { router } from "expo-router";
|
||||
import { nextUp } from "@/utils/jellyfin";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useAtom } from "jotai";
|
||||
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
|
||||
|
||||
export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
|
||||
const [user] = useAtom(userAtom);
|
||||
const [api] = useAtom(apiAtom);
|
||||
|
||||
const { data: items } = useQuery({
|
||||
queryKey: ["nextUp", seriesId],
|
||||
queryFn: async () =>
|
||||
await nextUp({
|
||||
userId: user?.Id,
|
||||
api,
|
||||
itemId: seriesId,
|
||||
}),
|
||||
enabled: !!api && !!seriesId && !!user?.Id,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
export const NextUp = ({ items }: { items?: BaseItemDto[] | null }) => {
|
||||
if (!items?.length)
|
||||
return (
|
||||
<View>
|
||||
@@ -19,7 +38,7 @@ export const NextUp = ({ items }: { items?: BaseItemDto[] | null }) => {
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Text className="text-lg font-bold mb-2">Next up</Text>
|
||||
<Text className="text-lg font-bold mb-2 px-4">Next up</Text>
|
||||
<HorizontalScroll<BaseItemDto>
|
||||
data={items}
|
||||
renderItem={(item, index) => (
|
||||
|
||||
@@ -84,7 +84,7 @@ export const SeasonPicker: React.FC<Props> = ({ item }) => {
|
||||
<View className="mb-2">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<View className="flex flex-row">
|
||||
<View className="flex flex-row px-4">
|
||||
<TouchableOpacity className="bg-neutral-900 rounded-2xl border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
|
||||
<Text>Season {selectedSeason}</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
PODS:
|
||||
- boost (1.83.0)
|
||||
- ComputableLayout (0.7.0):
|
||||
- DGSwiftUtilities (~> 0.11)
|
||||
- ContextMenuAuxiliaryPreview (0.5.0):
|
||||
- DGSwiftUtilities (~> 0.18.1)
|
||||
- DGSwiftUtilities (0.18.1)
|
||||
- DoubleConversion (1.1.6)
|
||||
- EXConstants (16.0.2):
|
||||
- ExpoModulesCore
|
||||
@@ -221,6 +226,8 @@ PODS:
|
||||
- ExpoModulesCore
|
||||
- ExpoFont (12.0.9):
|
||||
- ExpoModulesCore
|
||||
- ExpoHaptics (13.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoHead (3.5.20):
|
||||
- ExpoModulesCore
|
||||
- ExpoImage (1.12.13):
|
||||
@@ -1264,6 +1271,8 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- react-native-menu (1.1.2):
|
||||
- React
|
||||
- react-native-safe-area-context (4.10.5):
|
||||
- React-Core
|
||||
- react-native-video (6.4.3):
|
||||
@@ -1538,7 +1547,16 @@ PODS:
|
||||
- React-logger (= 0.74.3)
|
||||
- React-perflogger (= 0.74.3)
|
||||
- React-utils (= 0.74.3)
|
||||
- RNCAsyncStorage (1.24.0):
|
||||
- ReactNativeIosContextMenu (2.5.1):
|
||||
- ContextMenuAuxiliaryPreview (~> 0.3)
|
||||
- DGSwiftUtilities
|
||||
- ExpoModulesCore
|
||||
- ReactNativeIosUtilities
|
||||
- ReactNativeIosUtilities (4.4.5):
|
||||
- ComputableLayout (~> 0.7)
|
||||
- DGSwiftUtilities (~> 0.17)
|
||||
- ExpoModulesCore
|
||||
- RNCAsyncStorage (1.23.1):
|
||||
- React-Core
|
||||
- RNGestureHandler (2.16.2):
|
||||
- DoubleConversion
|
||||
@@ -1604,7 +1622,7 @@ PODS:
|
||||
- ReactCommon/turbomodule/bridging
|
||||
- ReactCommon/turbomodule/core
|
||||
- Yoga
|
||||
- RNSVG (15.4.0):
|
||||
- RNSVG (15.2.0):
|
||||
- React-Core
|
||||
- SDWebImage (5.19.2):
|
||||
- SDWebImage/Core (= 5.19.2)
|
||||
@@ -1634,6 +1652,7 @@ DEPENDENCIES:
|
||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||
- ExpoFont (from `../node_modules/expo-font/ios`)
|
||||
- ExpoHaptics (from `../node_modules/expo-haptics/ios`)
|
||||
- ExpoHead (from `../node_modules/expo-router/ios`)
|
||||
- ExpoImage (from `../node_modules/expo-image/ios`)
|
||||
- ExpoKeepAwake (from `../node_modules/expo-keep-awake/ios`)
|
||||
@@ -1675,6 +1694,7 @@ DEPENDENCIES:
|
||||
- React-logger (from `../node_modules/react-native/ReactCommon/logger`)
|
||||
- React-Mapbuffer (from `../node_modules/react-native/ReactCommon`)
|
||||
- react-native-compressor (from `../node_modules/react-native-compressor`)
|
||||
- "react-native-menu (from `../node_modules/@react-native-menu/menu`)"
|
||||
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
|
||||
- react-native-video (from `../node_modules/react-native-video`)
|
||||
- React-nativeconfig (from `../node_modules/react-native/ReactCommon`)
|
||||
@@ -1700,6 +1720,8 @@ DEPENDENCIES:
|
||||
- React-runtimescheduler (from `../node_modules/react-native/ReactCommon/react/renderer/runtimescheduler`)
|
||||
- React-utils (from `../node_modules/react-native/ReactCommon/react/utils`)
|
||||
- ReactCommon/turbomodule/core (from `../node_modules/react-native/ReactCommon`)
|
||||
- ReactNativeIosContextMenu (from `../node_modules/react-native-ios-context-menu`)
|
||||
- ReactNativeIosUtilities (from `../node_modules/react-native-ios-utilities`)
|
||||
- "RNCAsyncStorage (from `../node_modules/@react-native-async-storage/async-storage`)"
|
||||
- RNGestureHandler (from `../node_modules/react-native-gesture-handler`)
|
||||
- RNReanimated (from `../node_modules/react-native-reanimated`)
|
||||
@@ -1709,6 +1731,9 @@ DEPENDENCIES:
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- ComputableLayout
|
||||
- ContextMenuAuxiliaryPreview
|
||||
- DGSwiftUtilities
|
||||
- ffmpeg-kit-ios-https
|
||||
- libavif
|
||||
- libdav1d
|
||||
@@ -1746,6 +1771,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/expo-file-system/ios"
|
||||
ExpoFont:
|
||||
:path: "../node_modules/expo-font/ios"
|
||||
ExpoHaptics:
|
||||
:path: "../node_modules/expo-haptics/ios"
|
||||
ExpoHead:
|
||||
:path: "../node_modules/expo-router/ios"
|
||||
ExpoImage:
|
||||
@@ -1825,6 +1852,8 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
react-native-compressor:
|
||||
:path: "../node_modules/react-native-compressor"
|
||||
react-native-menu:
|
||||
:path: "../node_modules/@react-native-menu/menu"
|
||||
react-native-safe-area-context:
|
||||
:path: "../node_modules/react-native-safe-area-context"
|
||||
react-native-video:
|
||||
@@ -1875,6 +1904,10 @@ EXTERNAL SOURCES:
|
||||
:path: "../node_modules/react-native/ReactCommon/react/utils"
|
||||
ReactCommon:
|
||||
:path: "../node_modules/react-native/ReactCommon"
|
||||
ReactNativeIosContextMenu:
|
||||
:path: "../node_modules/react-native-ios-context-menu"
|
||||
ReactNativeIosUtilities:
|
||||
:path: "../node_modules/react-native-ios-utilities"
|
||||
RNCAsyncStorage:
|
||||
:path: "../node_modules/@react-native-async-storage/async-storage"
|
||||
RNGestureHandler:
|
||||
@@ -1890,6 +1923,9 @@ EXTERNAL SOURCES:
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
boost: d3f49c53809116a5d38da093a8aa78bf551aed09
|
||||
ComputableLayout: c50faffac4ed9f8f05b0ce5e6f3a60df1f6042c8
|
||||
ContextMenuAuxiliaryPreview: 1c73742ff3100dac1e0fb70bbe30284a6e711071
|
||||
DGSwiftUtilities: 9be88816a2057f125bc8fac3da6aca3ae89c1eec
|
||||
DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
|
||||
EXConstants: 409690fbfd5afea964e5e9d6c4eb2c2b59222c59
|
||||
EXJSONUtils: 30c17fd9cc364d722c0946a550dfbf1be92ef6a4
|
||||
@@ -1902,6 +1938,7 @@ SPEC CHECKSUMS:
|
||||
ExpoAsset: 323700f291684f110fb55f0d4022a3362ea9f875
|
||||
ExpoFileSystem: 80bfe850b1f9922c16905822ecbf97acd711dc51
|
||||
ExpoFont: e7f2275c10ca8573c991e007329ad6bf98086485
|
||||
ExpoHaptics: 5a3a88971af384255baf2504f38b41189cec6984
|
||||
ExpoHead: 3e8eacccdad1256f0643b657d89bf972c27afb1d
|
||||
ExpoImage: cbe7617b4e0e7ba064cc4caa51bfc96262e51ef3
|
||||
ExpoKeepAwake: 3b8815d9dd1d419ee474df004021c69fdd316d08
|
||||
@@ -1945,6 +1982,7 @@ SPEC CHECKSUMS:
|
||||
React-logger: fa92ba4d3a5d39ac450f59be2a3cec7b099f0304
|
||||
React-Mapbuffer: 9f68550e7c6839d01411ac8896aea5c868eff63a
|
||||
react-native-compressor: cef7d532563b6c4b190e69c12b3ffd09d8237483
|
||||
react-native-menu: d32728a357dfb360cf01cd5979cf7713c5acbb95
|
||||
react-native-safe-area-context: a240ad4b683349e48b1d51fed1611138d1bdad97
|
||||
react-native-video: b3ba8f424c8c3f54dd9289d47bbe60fbc09bc986
|
||||
React-nativeconfig: fa5de9d8f4dbd5917358f8ad3ad1e08762f01dcb
|
||||
@@ -1970,11 +2008,13 @@ SPEC CHECKSUMS:
|
||||
React-runtimescheduler: 0c80752bceb80924cb8a4babc2a8e3ed70d41e87
|
||||
React-utils: a06061b3887c702235d2dac92dacbd93e1ea079e
|
||||
ReactCommon: f00e436b3925a7ae44dfa294b43ef360fbd8ccc4
|
||||
RNCAsyncStorage: ec53e44dc3e75b44aa2a9f37618a49c3bc080a7a
|
||||
ReactNativeIosContextMenu: e5f972174bd78ab3a552bd6ee4745762ffaa42b3
|
||||
ReactNativeIosUtilities: 638290fbfaa093c1b50d1f926715d77cabcae759
|
||||
RNCAsyncStorage: 826b603ae9c0f88b5ac4e956801f755109fa4d5c
|
||||
RNGestureHandler: 2282cfbcf86c360d29f44ace393203afd5c6cff7
|
||||
RNReanimated: 35f9ac9c3ac42d0497ebd1cce5c39d7687a8493e
|
||||
RNScreens: b32a9ff15bea7fcdbe5dff6477bc503f792b1208
|
||||
RNSVG: cb24fb322de8c1ebf59904e7aca0447bb8dbed5a
|
||||
RNSVG: 43b64ed39c14ce830d840903774154ca0c1f27ec
|
||||
SDWebImage: dfe95b2466a9823cf9f0c6d01217c06550d7b29a
|
||||
SDWebImageAVIFCoder: 00310d246aab3232ce77f1d8f0076f8c4b021d90
|
||||
SDWebImageSVGCoder: 15a300a97ec1c8ac958f009c02220ac0402e936c
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.mm */; };
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; };
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; };
|
||||
3257823D68914DDEBB3B12F1 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 230AA53500BA4DD9B884537F /* noop-file.swift */; };
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */; };
|
||||
64C94651955506861C263566 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 2CAF2D47E225701446B0FD60 /* PrivacyInfo.xcprivacy */; };
|
||||
96905EF65AED1B983A6B3ABC /* libPods-Streamyfin.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Streamyfin.a */; };
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */; };
|
||||
B4F991FB2D65E78D01D256DB /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 2ACC4880CDABD7035AA9CD9D /* PrivacyInfo.xcprivacy */; };
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */ = {isa = PBXBuildFile; fileRef = BB2F792C24A3F905000567C9 /* Expo.plist */; };
|
||||
FC4E9E0516EB46929623ECE3 /* noop-file.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377710512B91474B8B78B27E /* noop-file.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@@ -25,14 +25,14 @@
|
||||
13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = Streamyfin/Images.xcassets; sourceTree = "<group>"; };
|
||||
13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Streamyfin/Info.plist; sourceTree = "<group>"; };
|
||||
13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = Streamyfin/main.m; sourceTree = "<group>"; };
|
||||
2CAF2D47E225701446B0FD60 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Streamyfin/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
377710512B91474B8B78B27E /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Streamyfin/noop-file.swift"; sourceTree = "<group>"; };
|
||||
230AA53500BA4DD9B884537F /* noop-file.swift */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.swift; name = "noop-file.swift"; path = "Streamyfin/noop-file.swift"; sourceTree = "<group>"; };
|
||||
2ACC4880CDABD7035AA9CD9D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = Streamyfin/PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
58EEBF8E8E6FB1BC6CAF49B5 /* libPods-Streamyfin.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Streamyfin.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
6C2E3173556A471DD304B334 /* Pods-Streamyfin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Streamyfin.debug.xcconfig"; path = "Target Support Files/Pods-Streamyfin/Pods-Streamyfin.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
74C714457E3741B898D70F79 /* Streamyfin-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Streamyfin-Bridging-Header.h"; path = "Streamyfin/Streamyfin-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
7A4D352CD337FB3A3BF06240 /* Pods-Streamyfin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Streamyfin.release.xcconfig"; path = "Target Support Files/Pods-Streamyfin/Pods-Streamyfin.release.xcconfig"; sourceTree = "<group>"; };
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; name = SplashScreen.storyboard; path = Streamyfin/SplashScreen.storyboard; sourceTree = "<group>"; };
|
||||
BB2F792C24A3F905000567C9 /* Expo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Expo.plist; sourceTree = "<group>"; };
|
||||
C04FDA7EF00C4288BCABD623 /* Streamyfin-Bridging-Header.h */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 4; includeInIndex = 0; lastKnownFileType = sourcecode.c.h; name = "Streamyfin-Bridging-Header.h"; path = "Streamyfin/Streamyfin-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
ED297162215061F000B7C4FE /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
|
||||
FAC715A2D49A985799AEE119 /* ExpoModulesProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExpoModulesProvider.swift; path = "Pods/Target Support Files/Pods-Streamyfin/ExpoModulesProvider.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -59,9 +59,9 @@
|
||||
13B07FB61A68108700A75B9A /* Info.plist */,
|
||||
13B07FB71A68108700A75B9A /* main.m */,
|
||||
AA286B85B6C04FC6940260E9 /* SplashScreen.storyboard */,
|
||||
377710512B91474B8B78B27E /* noop-file.swift */,
|
||||
74C714457E3741B898D70F79 /* Streamyfin-Bridging-Header.h */,
|
||||
2CAF2D47E225701446B0FD60 /* PrivacyInfo.xcprivacy */,
|
||||
230AA53500BA4DD9B884537F /* noop-file.swift */,
|
||||
C04FDA7EF00C4288BCABD623 /* Streamyfin-Bridging-Header.h */,
|
||||
2ACC4880CDABD7035AA9CD9D /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
name = Streamyfin;
|
||||
sourceTree = "<group>";
|
||||
@@ -147,13 +147,13 @@
|
||||
buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "Streamyfin" */;
|
||||
buildPhases = (
|
||||
08A4A3CD28434E44B6B9DE2E /* [CP] Check Pods Manifest.lock */,
|
||||
FDD61697DC04AF9E42AF4D0B /* [Expo] Configure project */,
|
||||
3552E666C7970D5BA4441A37 /* [Expo] Configure project */,
|
||||
13B07F871A680F5B00A75B9A /* Sources */,
|
||||
13B07F8C1A680F5B00A75B9A /* Frameworks */,
|
||||
13B07F8E1A680F5B00A75B9A /* Resources */,
|
||||
00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */,
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */,
|
||||
C3D83F10504094BBDE73DF71 /* [CP] Embed Pods Frameworks */,
|
||||
605963CCBC2CC9CF367DC9DF /* [CP] Embed Pods Frameworks */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -203,7 +203,7 @@
|
||||
BB2F792D24A3F905000567C9 /* Expo.plist in Resources */,
|
||||
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
|
||||
3E461D99554A48A4959DE609 /* SplashScreen.storyboard in Resources */,
|
||||
64C94651955506861C263566 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
B4F991FB2D65E78D01D256DB /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -247,6 +247,59 @@
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
3552E666C7970D5BA4441A37 /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Streamyfin/expo-configure-project.sh\"\n";
|
||||
};
|
||||
605963CCBC2CC9CF367DC9DF /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Streamyfin/Pods-Streamyfin-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/ffmpegkit.framework/ffmpegkit",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavcodec.framework/libavcodec",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavdevice.framework/libavdevice",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavfilter.framework/libavfilter",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavformat.framework/libavformat",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavutil.framework/libavutil",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libswresample.framework/libswresample",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libswscale.framework/libswscale",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ffmpegkit.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavcodec.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavdevice.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavfilter.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavformat.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavutil.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswresample.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswscale.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Streamyfin/Pods-Streamyfin-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
800E24972A6A228C8D4807E9 /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -281,59 +334,6 @@
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Streamyfin/Pods-Streamyfin-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
C3D83F10504094BBDE73DF71 /* [CP] Embed Pods Frameworks */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Streamyfin/Pods-Streamyfin-frameworks.sh",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/ffmpegkit.framework/ffmpegkit",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavcodec.framework/libavcodec",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavdevice.framework/libavdevice",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavfilter.framework/libavfilter",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavformat.framework/libavformat",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libavutil.framework/libavutil",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libswresample.framework/libswresample",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/ffmpeg-kit-ios-https/libswscale.framework/libswscale",
|
||||
"${PODS_XCFRAMEWORKS_BUILD_DIR}/hermes-engine/Pre-built/hermes.framework/hermes",
|
||||
);
|
||||
name = "[CP] Embed Pods Frameworks";
|
||||
outputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ffmpegkit.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavcodec.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavdevice.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavfilter.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavformat.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libavutil.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswresample.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/libswscale.framework",
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/hermes.framework",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Streamyfin/Pods-Streamyfin-frameworks.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
FDD61697DC04AF9E42AF4D0B /* [Expo] Configure project */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "[Expo] Configure project";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# This script configures Expo modules and generates the modules provider file.\nbash -l -c \"./Pods/Target\\ Support\\ Files/Pods-Streamyfin/expo-configure-project.sh\"\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -344,7 +344,7 @@
|
||||
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B18059E884C0ABDD17F3DC3D /* ExpoModulesProvider.swift in Sources */,
|
||||
FC4E9E0516EB46929623ECE3 /* noop-file.swift in Sources */,
|
||||
3257823D68914DDEBB3B12F1 /* noop-file.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -375,7 +375,7 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.fredrikburmester.streamyfin;
|
||||
PRODUCT_NAME = "Streamyfin";
|
||||
PRODUCT_NAME = Streamyfin;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Streamyfin/Streamyfin-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -403,7 +403,7 @@
|
||||
);
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.fredrikburmester.streamyfin;
|
||||
PRODUCT_NAME = "Streamyfin";
|
||||
PRODUCT_NAME = Streamyfin;
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Streamyfin/Streamyfin-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
|
||||
|
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 1.0 MiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 2.8 MiB |
@@ -51,10 +51,11 @@
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>The app needs access to your camera to scan barcodes.</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>The app needs access to your microphone.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>SplashScreen</string>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@expo/vector-icons": "^14.0.2",
|
||||
"@jellyfin/sdk": "^0.10.0",
|
||||
"@react-native-async-storage/async-storage": "^1.24.0",
|
||||
"@react-native-async-storage/async-storage": "1.23.1",
|
||||
"@react-native-menu/menu": "^1.1.2",
|
||||
"@react-navigation/native": "^6.0.2",
|
||||
"@tanstack/react-query": "^5.51.16",
|
||||
@@ -32,7 +32,6 @@
|
||||
"expo-splash-screen": "~0.27.5",
|
||||
"expo-status-bar": "~1.12.1",
|
||||
"expo-system-ui": "~3.0.7",
|
||||
"expo-video": "^1.2.4",
|
||||
"expo-web-browser": "~13.0.3",
|
||||
"ffmpeg-kit-react-native": "^6.0.2",
|
||||
"jotai": "^2.9.1",
|
||||
@@ -43,13 +42,12 @@
|
||||
"react-native-circular-progress": "^1.4.0",
|
||||
"react-native-compressor": "^1.8.25",
|
||||
"react-native-gesture-handler": "~2.16.1",
|
||||
"react-native-google-cast": "^4.8.2",
|
||||
"react-native-ios-context-menu": "^2.5.1",
|
||||
"react-native-ios-utilities": "^4.4.5",
|
||||
"react-native-reanimated": "~3.10.1",
|
||||
"react-native-safe-area-context": "4.10.5",
|
||||
"react-native-screens": "3.31.1",
|
||||
"react-native-svg": "^15.4.0",
|
||||
"react-native-svg": "15.2.0",
|
||||
"react-native-url-polyfill": "^2.0.0",
|
||||
"react-native-video": "^6.4.3",
|
||||
"react-native-web": "~0.19.10",
|
||||
|
||||
@@ -118,6 +118,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const token = await AsyncStorage.getItem("token");
|
||||
console.log({ token });
|
||||
const serverUrl = await AsyncStorage.getItem("serverUrl");
|
||||
const user = JSON.parse(
|
||||
(await AsyncStorage.getItem("user")) as string
|
||||
|
||||
@@ -8,22 +8,24 @@ import {
|
||||
getMediaInfoApi,
|
||||
getUserLibraryApi,
|
||||
} from "@jellyfin/sdk/lib/utils/api";
|
||||
import { iosProfile } from "./device-profiles";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import * as FileSystem from "expo-file-system";
|
||||
import { useAtom } from "jotai";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { runningProcesses } from "./atoms/downloads";
|
||||
import { useCallback, useState } from "react";
|
||||
import { createVideoUrl } from "./video/createVideoUrl";
|
||||
import { iosProfile } from "./device-profiles";
|
||||
|
||||
export const useDownloadMedia = (api: Api | null) => {
|
||||
export const useDownloadMedia = (api: Api | null, userId?: string | null) => {
|
||||
const [isDownloading, setIsDownloading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [progress, setProgress] = useAtom(runningProcesses);
|
||||
const downloadResumableRef = useRef<FileSystem.DownloadResumable | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const downloadMedia = useCallback(
|
||||
async (item: BaseItemDto | null) => {
|
||||
if (!item?.Id || !api) {
|
||||
if (!item?.Id || !api || !userId) {
|
||||
setError("Invalid item or API");
|
||||
return false;
|
||||
}
|
||||
@@ -33,12 +35,38 @@ export const useDownloadMedia = (api: Api | null) => {
|
||||
|
||||
const itemId = item.Id;
|
||||
|
||||
console.info("Downloading media item", item);
|
||||
|
||||
// const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({
|
||||
// itemId,
|
||||
// userId: userId,
|
||||
// });
|
||||
|
||||
// const url = await getStreamUrl({
|
||||
// api,
|
||||
// userId: userId,
|
||||
// item,
|
||||
// startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
|
||||
// sessionData: playbackData.data,
|
||||
// });
|
||||
|
||||
// if (!url) {
|
||||
// setError("Could not get stream URL");
|
||||
// setIsDownloading(false);
|
||||
// setProgress(null);
|
||||
// return false;
|
||||
// }
|
||||
|
||||
try {
|
||||
const filename = `${itemId}.mp4`;
|
||||
const fileUri = `${FileSystem.documentDirectory}${filename}`;
|
||||
|
||||
const downloadResumable = FileSystem.createDownloadResumable(
|
||||
`${api.basePath}/Items/${itemId}/Download`,
|
||||
const url = `${api.basePath}/Items/${itemId}/Download`;
|
||||
|
||||
console.info("Starting download of media item from URL", url);
|
||||
|
||||
downloadResumableRef.current = FileSystem.createDownloadResumable(
|
||||
url,
|
||||
fileUri,
|
||||
{
|
||||
headers: {
|
||||
@@ -49,7 +77,6 @@ export const useDownloadMedia = (api: Api | null) => {
|
||||
const currentProgress =
|
||||
downloadProgress.totalBytesWritten /
|
||||
downloadProgress.totalBytesExpectedToWrite;
|
||||
console.log(`Download progress: ${currentProgress * 100}%`);
|
||||
|
||||
setProgress({
|
||||
item,
|
||||
@@ -58,7 +85,7 @@ export const useDownloadMedia = (api: Api | null) => {
|
||||
}
|
||||
);
|
||||
|
||||
const res = await downloadResumable.downloadAsync();
|
||||
const res = await downloadResumableRef.current.downloadAsync();
|
||||
const uri = res?.uri;
|
||||
|
||||
console.log("File downloaded to:", uri);
|
||||
@@ -78,6 +105,7 @@ export const useDownloadMedia = (api: Api | null) => {
|
||||
);
|
||||
|
||||
setIsDownloading(false);
|
||||
setProgress(null);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error downloading media:", error);
|
||||
@@ -89,7 +117,22 @@ export const useDownloadMedia = (api: Api | null) => {
|
||||
[api, setProgress]
|
||||
);
|
||||
|
||||
return { downloadMedia, isDownloading, error };
|
||||
const cancelDownload = useCallback(async () => {
|
||||
if (downloadResumableRef.current) {
|
||||
try {
|
||||
await downloadResumableRef.current.pauseAsync();
|
||||
setIsDownloading(false);
|
||||
setError("Download cancelled");
|
||||
setProgress(null);
|
||||
downloadResumableRef.current = null;
|
||||
} catch (error) {
|
||||
console.error("Error cancelling download:", error);
|
||||
setError("Failed to cancel download");
|
||||
}
|
||||
}
|
||||
}, [setProgress]);
|
||||
|
||||
return { downloadMedia, isDownloading, error, cancelDownload };
|
||||
};
|
||||
|
||||
export const markAsNotPlayed = async ({
|
||||
@@ -442,63 +485,72 @@ export const getStreamUrl = async ({
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const itemId = item.Id;
|
||||
const itemId = item.Id;
|
||||
|
||||
const response = await api.axiosInstance.post(
|
||||
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||
{
|
||||
DeviceProfile: {
|
||||
...iosProfile,
|
||||
MaxStaticBitrate: maxStreamingBitrate,
|
||||
MaxStreamingBitrate: maxStreamingBitrate,
|
||||
},
|
||||
UserId: userId,
|
||||
const response = await api.axiosInstance.post(
|
||||
`${api.basePath}/Items/${itemId}/PlaybackInfo`,
|
||||
{
|
||||
DeviceProfile: {
|
||||
...iosProfile,
|
||||
MaxStaticBitrate: maxStreamingBitrate,
|
||||
MaxStreamingBitrate: maxStreamingBitrate,
|
||||
StartTimeTicks: startTimeTicks,
|
||||
EnableTranscoding: maxStreamingBitrate ? true : undefined,
|
||||
AutoOpenLiveStream: true,
|
||||
MediaSourceId: itemId,
|
||||
AllowVideoStreamCopy: maxStreamingBitrate ? false : true,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const mediaSource = response.data.MediaSources?.[0] as MediaSourceInfo;
|
||||
|
||||
if (!mediaSource) {
|
||||
throw new Error("No media source");
|
||||
}
|
||||
if (!sessionData.PlaySessionId) {
|
||||
throw new Error("no PlaySessionId");
|
||||
UserId: userId,
|
||||
MaxStreamingBitrate: maxStreamingBitrate,
|
||||
StartTimeTicks: startTimeTicks,
|
||||
EnableTranscoding: maxStreamingBitrate ? true : undefined,
|
||||
AutoOpenLiveStream: true,
|
||||
MediaSourceId: itemId,
|
||||
AllowVideoStreamCopy: maxStreamingBitrate ? false : true,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const streamParams = new URLSearchParams({
|
||||
itemId,
|
||||
deviceId: "",
|
||||
mediaSourceId: itemId,
|
||||
videoCodec: "h264,h264",
|
||||
audioCodec: "aac",
|
||||
playSessionId: sessionData.PlaySessionId,
|
||||
transcodingMaxAudioChannels: "2",
|
||||
tag: mediaSource.ETag || "",
|
||||
segmentContainer: "mp4",
|
||||
});
|
||||
const mediaSource = response.data.MediaSources?.[0] as MediaSourceInfo;
|
||||
|
||||
if (maxStreamingBitrate) {
|
||||
// streamParams.append("videoBitRate", maxStreamingBitrate.toString());
|
||||
// streamParams.append("transcodeReasons", "ContainerBitrateExceedsLimit");
|
||||
}
|
||||
|
||||
return `/Videos/${itemId}/main.m3u8?${streamParams.toString()}`;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return null;
|
||||
if (!mediaSource) {
|
||||
throw new Error("No media source");
|
||||
}
|
||||
if (!sessionData.PlaySessionId) {
|
||||
throw new Error("no PlaySessionId");
|
||||
}
|
||||
|
||||
const streamParams = new URLSearchParams({
|
||||
Static: "true",
|
||||
api_key: api.accessToken,
|
||||
playSessionId: sessionData.PlaySessionId || "",
|
||||
videoCodec: "h265,h264",
|
||||
audioCodec: "aac",
|
||||
maxAudioChannels: "6",
|
||||
mediaSourceId: itemId,
|
||||
Tag: mediaSource.ETag || "",
|
||||
TranscodingMaxAudioChannels: "2",
|
||||
RequireAvc: "false",
|
||||
SegmentContainer: "mp4",
|
||||
MinSegments: "2",
|
||||
BreakOnNonKeyFrames: "True",
|
||||
context: "Streaming",
|
||||
"h264-level": "40",
|
||||
"h264-videobitdepth": "8",
|
||||
"h264-profile": "high",
|
||||
"h264-audiochannels": "2",
|
||||
"aac-profile": "lc",
|
||||
"h264-rangetype": "SDR",
|
||||
"h264-deinterlace": "true",
|
||||
});
|
||||
|
||||
if (maxStreamingBitrate) {
|
||||
streamParams.append("videoBitRate", maxStreamingBitrate.toString());
|
||||
streamParams.append("transcodeReasons", "ContainerBitrateExceedsLimit");
|
||||
}
|
||||
|
||||
return `${
|
||||
api.basePath
|
||||
}/Videos/${itemId}/main.m3u8?${streamParams.toString()}`;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||