Compare commits

..

1 Commits

Author SHA1 Message Date
Fredrik Burmester
1c4bc68566 wip 2024-09-19 21:14:03 +02:00
35 changed files with 820 additions and 1164 deletions

View File

@@ -2,7 +2,7 @@
"expo": { "expo": {
"name": "Streamyfin", "name": "Streamyfin",
"slug": "streamyfin", "slug": "streamyfin",
"version": "0.15.0", "version": "0.14.0",
"orientation": "default", "orientation": "default",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "streamyfin", "scheme": "streamyfin",
@@ -33,7 +33,7 @@
}, },
"android": { "android": {
"jsEngine": "hermes", "jsEngine": "hermes",
"versionCode": 41, "versionCode": 40,
"adaptiveIcon": { "adaptiveIcon": {
"foregroundImage": "./assets/images/icon.png" "foregroundImage": "./assets/images/icon.png"
}, },
@@ -82,9 +82,11 @@
"expo-build-properties", "expo-build-properties",
{ {
"ios": { "ios": {
"newArchEnabled": true,
"deploymentTarget": "14.0" "deploymentTarget": "14.0"
}, },
"android": { "android": {
"newArchEnabled": true,
"android": { "android": {
"compileSdkVersion": 34, "compileSdkVersion": 34,
"targetSdkVersion": 34, "targetSdkVersion": 34,
@@ -111,7 +113,8 @@
{ {
"motionPermission": "Allow Streamyfin to access your device motion for landscape video watching." "motionPermission": "Allow Streamyfin to access your device motion for landscape video watching."
} }
] ],
"expo-video"
], ],
"experiments": { "experiments": {
"typedRoutes": true "typedRoutes": true

View File

@@ -26,7 +26,6 @@ export default function IndexLayout() {
onPress={() => { onPress={() => {
router.push("/(auth)/downloads"); router.push("/(auth)/downloads");
}} }}
className="p-2"
> >
<Feather name="download" color={"white"} size={22} /> <Feather name="download" color={"white"} size={22} />
</TouchableOpacity> </TouchableOpacity>
@@ -38,9 +37,10 @@ export default function IndexLayout() {
onPress={() => { onPress={() => {
router.push("/(auth)/settings"); router.push("/(auth)/settings");
}} }}
className="p-2 "
> >
<Feather name="settings" color={"white"} size={22} /> <View className="h-10 aspect-square flex items-center justify-center rounded">
<Feather name="settings" color={"white"} size={22} />
</View>
</TouchableOpacity> </TouchableOpacity>
</View> </View>
), ),

View File

@@ -91,7 +91,9 @@ export default function settings() {
<Text className="font-bold text-lg mb-2">Tests</Text> <Text className="font-bold text-lg mb-2">Tests</Text>
<Button <Button
onPress={() => { onPress={() => {
toast.success("Download started"); toast.success("Download started", {
invert: true,
});
}} }}
color="black" color="black"
> >

View File

@@ -1,48 +0,0 @@
import { FullScreenVideoPlayer } from "@/components/FullScreenVideoPlayer";
import { useSettings } from "@/utils/atoms/settings";
import * as NavigationBar from "expo-navigation-bar";
import { StatusBar } from "expo-status-bar";
import { useEffect, useState } from "react";
import { Platform, View, ViewProps } from "react-native";
import * as ScreenOrientation from "expo-screen-orientation";
interface Props extends ViewProps {}
export default function page() {
const [settings] = useSettings();
useEffect(() => {
if (settings?.autoRotate) {
// Don't need to do anything
} else if (settings?.defaultVideoOrientation) {
ScreenOrientation.lockAsync(settings.defaultVideoOrientation);
}
if (Platform.OS === "android") {
NavigationBar.setVisibilityAsync("hidden");
NavigationBar.setBehaviorAsync("overlay-swipe");
}
return () => {
if (settings?.autoRotate) {
ScreenOrientation.unlockAsync();
} else {
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP
);
}
if (Platform.OS === "android") {
NavigationBar.setVisibilityAsync("visible");
NavigationBar.setBehaviorAsync("inset-swipe");
}
};
}, [settings]);
return (
<View className="">
<StatusBar hidden />
<FullScreenVideoPlayer />
</View>
);
}

View File

@@ -120,30 +120,14 @@ function Layout() {
title: "", title: "",
}} }}
/> />
<Stack.Screen
name="(auth)/play"
options={{ headerShown: false, title: "" }}
/>
<Stack.Screen <Stack.Screen
name="login" name="login"
options={{ headerShown: false, title: "Login" }} options={{ headerShown: false, title: "Login" }}
/> />
<Stack.Screen name="+not-found" /> <Stack.Screen name="+not-found" />
</Stack> </Stack>
{/* <FullScreenVideoPlayer /> */} <FullScreenVideoPlayer />
<Toaster <Toaster />
duration={2000}
toastOptions={{
style: {
backgroundColor: "#262626",
borderColor: "#363639",
borderWidth: 1,
},
titleStyle: {
color: "white",
},
}}
/>
</ThemeProvider> </ThemeProvider>
</PlaybackProvider> </PlaybackProvider>
</JellyfinProvider> </JellyfinProvider>

BIN
bun.lockb

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
// GenreTags.tsx
import React from "react";
import { View } from "react-native";
import { Text } from "./common/Text";
interface GenreTagsProps {
genres?: string[];
}
export const GenreTags: React.FC<GenreTagsProps> = ({ genres }) => {
if (!genres || genres.length === 0) return null;
return (
<View className="flex flex-row flex-wrap mt-2">
{genres.map((genre) => (
<View
key={genre}
className="bg-neutral-800 rounded-full px-2 py-1 mr-1"
>
<Text className="text-xs">{genre}</Text>
</View>
))}
</View>
);
};

View File

@@ -43,7 +43,6 @@ import { Chromecast } from "./Chromecast";
import { ItemHeader } from "./ItemHeader"; import { ItemHeader } from "./ItemHeader";
import { Loader } from "./Loader"; import { Loader } from "./Loader";
import { MediaSourceSelector } from "./MediaSourceSelector"; import { MediaSourceSelector } from "./MediaSourceSelector";
import { MoreMoviesWithActor } from "./MoreMoviesWithActor";
export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => { export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
@@ -363,19 +362,6 @@ export const ItemContent: React.FC<{ id: string }> = React.memo(({ id }) => {
<CastAndCrew item={item} className="mb-4" loading={loading} /> <CastAndCrew item={item} className="mb-4" loading={loading} />
{item?.People && item.People.length > 0 && (
<View className="mb-4">
{item.People.slice(0, 3).map((person) => (
<MoreMoviesWithActor
currentItem={item}
key={person.Id}
actorId={person.Id!}
className="mb-4"
/>
))}
</View>
)}
{item?.Type === "Episode" && ( {item?.Type === "Episode" && (
<CurrentSeries item={item} className="mb-4" /> <CurrentSeries item={item} className="mb-4" />
)} )}

View File

@@ -3,7 +3,6 @@ import { View, ViewProps } from "react-native";
import { MoviesTitleHeader } from "./movies/MoviesTitleHeader"; import { MoviesTitleHeader } from "./movies/MoviesTitleHeader";
import { Ratings } from "./Ratings"; import { Ratings } from "./Ratings";
import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader"; import { EpisodeTitleHeader } from "./series/EpisodeTitleHeader";
import { GenreTags } from "./GenreTags";
interface Props extends ViewProps { interface Props extends ViewProps {
item?: BaseItemDto | null; item?: BaseItemDto | null;
@@ -13,7 +12,7 @@ export const ItemHeader: React.FC<Props> = ({ item, ...props }) => {
if (!item) if (!item)
return ( return (
<View <View
className="flex flex-col space-y-1.5 w-full items-start h-32" className="flex flex-col space-y-1.5 w-full items-start h-24"
{...props} {...props}
> >
<View className="w-1/3 h-6 bg-neutral-900 rounded" /> <View className="w-1/3 h-6 bg-neutral-900 rounded" />
@@ -24,22 +23,16 @@ export const ItemHeader: React.FC<Props> = ({ item, ...props }) => {
); );
return ( return (
<View className="flex flex-col" {...props}> <View
<View className="flex flex-col" {...props}> style={{
<Ratings item={item} className="mb-2" /> minHeight: 96,
{item.Type === "Episode" && ( }}
<> className="flex flex-col"
<EpisodeTitleHeader item={item} /> {...props}
<GenreTags genres={item.Genres!} /> >
</> <Ratings item={item} className="mb-2" />
)} {item.Type === "Episode" && <EpisodeTitleHeader item={item} />}
{item.Type === "Movie" && ( {item.Type === "Movie" && <MoviesTitleHeader item={item} />}
<>
<MoviesTitleHeader item={item} />
<GenreTags genres={item.Genres!} />
</>
)}
</View>
</View> </View>
); );
}; };

View File

@@ -7,7 +7,6 @@ import { useEffect, useMemo } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import * as DropdownMenu from "zeego/dropdown-menu"; import * as DropdownMenu from "zeego/dropdown-menu";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { convertBitsToMegabitsOrGigabits } from "@/utils/bToMb";
interface Props extends React.ComponentProps<typeof View> { interface Props extends React.ComponentProps<typeof View> {
item: BaseItemDto; item: BaseItemDto;
@@ -79,9 +78,7 @@ export const MediaSourceSelector: React.FC<Props> = ({
}} }}
> >
<DropdownMenu.ItemTitle> <DropdownMenu.ItemTitle>
{`${name(source.Name)} - ${convertBitsToMegabitsOrGigabits( {name(source.Name)}
source.Size
)}`}
</DropdownMenu.ItemTitle> </DropdownMenu.ItemTitle>
</DropdownMenu.Item> </DropdownMenu.Item>
))} ))}

View File

@@ -1,100 +0,0 @@
import React from "react";
import { View, ViewProps } from "react-native";
import { Text } from "@/components/common/Text";
import { HorizontalScroll } from "@/components/common/HorrizontalScroll";
import { TouchableItemRouter } from "@/components/common/TouchableItemRouter";
import MoviePoster from "@/components/posters/MoviePoster";
import { ItemCardText } from "@/components/ItemCardText";
import { useAtom } from "jotai";
import { apiAtom, userAtom } from "@/providers/JellyfinProvider";
import { getItemsApi } from "@jellyfin/sdk/lib/utils/api";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import { useQuery } from "@tanstack/react-query";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
interface Props extends ViewProps {
actorId: string;
currentItem: BaseItemDto;
}
export const MoreMoviesWithActor: React.FC<Props> = ({
actorId,
currentItem,
...props
}) => {
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const { data: actor } = useQuery({
queryKey: ["actor", actorId],
queryFn: async () => {
if (!api || !user?.Id) return null;
return await getUserItemData({
api,
userId: user.Id,
itemId: actorId,
});
},
enabled: !!api && !!user?.Id && !!actorId,
});
const { data: items, isLoading } = useQuery({
queryKey: ["actor", "movies", actorId, currentItem.Id],
queryFn: async () => {
if (!api || !user?.Id) return [];
const response = await getItemsApi(api).getItems({
userId: user.Id,
personIds: [actorId],
limit: 20,
sortOrder: ["Descending"],
includeItemTypes: ["Movie", "Series"],
recursive: true,
fields: ["ParentId", "PrimaryImageAspectRatio"],
sortBy: ["PremiereDate"],
collapseBoxSetItems: false,
excludeItemIds: [currentItem.SeriesId || "", currentItem.Id || ""],
});
// Remove duplicates based on item ID
const uniqueItems =
response.data.Items?.reduce((acc, current) => {
const x = acc.find((item) => item.Id === current.Id);
if (!x) {
return acc.concat([current]);
} else {
return acc;
}
}, [] as BaseItemDto[]) || [];
return uniqueItems;
},
enabled: !!api && !!user?.Id && !!actorId,
});
if (items?.length === 0) return null;
return (
<View {...props}>
<Text className="text-lg font-bold mb-2 px-4">
More with {actor?.Name}
</Text>
<HorizontalScroll
data={items}
loading={isLoading}
height={247}
renderItem={(item: BaseItemDto, idx: number) => (
<TouchableItemRouter
key={idx}
item={item}
className="flex flex-col w-28"
>
<View>
<MoviePoster item={item} />
<ItemCardText item={item} />
</View>
</TouchableItemRouter>
)}
/>
</View>
);
};

View File

@@ -27,7 +27,6 @@ import Animated, {
} from "react-native-reanimated"; } from "react-native-reanimated";
import { Button } from "./Button"; import { Button } from "./Button";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { useRouter } from "expo-router";
interface Props extends React.ComponentProps<typeof Button> { interface Props extends React.ComponentProps<typeof Button> {
item?: BaseItemDto | null; item?: BaseItemDto | null;
@@ -46,8 +45,6 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
const [colorAtom] = useAtom(itemThemeColorAtom); const [colorAtom] = useAtom(itemThemeColorAtom);
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const router = useRouter();
const memoizedItem = useMemo(() => item, [item?.Id]); // Memoize the item const memoizedItem = useMemo(() => item, [item?.Id]); // Memoize the item
const memoizedColor = useMemo(() => colorAtom, [colorAtom]); // Memoize the color const memoizedColor = useMemo(() => colorAtom, [colorAtom]); // Memoize the color
@@ -66,7 +63,6 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
if (!url || !item) return; if (!url || !item) return;
if (!client) { if (!client) {
setCurrentlyPlayingState({ item, url }); setCurrentlyPlayingState({ item, url });
router.push("/play");
return; return;
} }
const options = ["Chromecast", "Device", "Cancel"]; const options = ["Chromecast", "Device", "Cancel"];
@@ -163,9 +159,7 @@ export const PlayButton: React.FC<Props> = ({ item, url, ...props }) => {
}); });
break; break;
case 1: case 1:
console.log("Device");
setCurrentlyPlayingState({ item, url }); setCurrentlyPlayingState({ item, url });
router.push("/play");
break; break;
case cancelButtonIndex: case cancelButtonIndex:
break; break;

View File

@@ -10,8 +10,6 @@ import { ScrollView, TouchableOpacity, View, ViewProps } from "react-native";
import { Text } from "./common/Text"; import { Text } from "./common/Text";
import { ItemCardText } from "./ItemCardText"; import { ItemCardText } from "./ItemCardText";
import { Loader } from "./Loader"; import { Loader } from "./Loader";
import { HorizontalScroll } from "./common/HorrizontalScroll";
import { TouchableItemRouter } from "./common/TouchableItemRouter";
interface SimilarItemsProps extends ViewProps { interface SimilarItemsProps extends ViewProps {
itemId?: string | null; itemId?: string | null;
@@ -48,24 +46,29 @@ export const SimilarItems: React.FC<SimilarItemsProps> = ({
return ( return (
<View {...props}> <View {...props}>
<Text className="px-4 text-lg font-bold mb-2">Similar items</Text> <Text className="px-4 text-lg font-bold mb-2">Similar items</Text>
<HorizontalScroll {isLoading ? (
data={movies} <View className="my-12">
loading={isLoading} <Loader />
height={247} </View>
noItemsText="No similar items found" ) : (
renderItem={(item: BaseItemDto, idx: number) => ( <ScrollView horizontal>
<TouchableItemRouter <View className="px-4 flex flex-row gap-x-2">
key={idx} {movies.map((item) => (
item={item} <TouchableOpacity
className="flex flex-col w-28" key={item.Id}
> onPress={() => router.push(`/items/page?id=${item.Id}`)}
<View> className="flex flex-col w-32"
<MoviePoster item={item} /> >
<ItemCardText item={item} /> <MoviePoster item={item} />
</View> <ItemCardText item={item} />
</TouchableItemRouter> </TouchableOpacity>
)} ))}
/> </View>
</ScrollView>
)}
{movies.length === 0 && (
<Text className="px-4 text-neutral-500">No similar items</Text>
)}
</View> </View>
); );
}; };

View File

@@ -22,7 +22,6 @@ interface HorizontalScrollProps<T>
height?: number; height?: number;
loading?: boolean; loading?: boolean;
extraData?: any; extraData?: any;
noItemsText?: string;
} }
export const HorizontalScroll = forwardRef< export const HorizontalScroll = forwardRef<
@@ -39,7 +38,6 @@ export const HorizontalScroll = forwardRef<
loading = false, loading = false,
height = 164, height = 164,
extraData, extraData,
noItemsText,
...props ...props
}: HorizontalScrollProps<T>, }: HorizontalScrollProps<T>,
ref: React.ForwardedRef<HorizontalScrollRef> ref: React.ForwardedRef<HorizontalScrollRef>
@@ -93,9 +91,7 @@ export const HorizontalScroll = forwardRef<
}} }}
ListEmptyComponent={() => ( ListEmptyComponent={() => (
<View className="flex-1 justify-center items-center"> <View className="flex-1 justify-center items-center">
<Text className="text-center text-gray-500"> <Text className="text-center text-gray-500">No data available</Text>
{noItemsText || "No data available"}
</Text>
</View> </View>
)} )}
{...props} {...props}

View File

@@ -10,7 +10,6 @@ import { Text } from "../common/Text";
import { useFiles } from "@/hooks/useFiles"; import { useFiles } from "@/hooks/useFiles";
import { useSettings } from "@/utils/atoms/settings"; import { useSettings } from "@/utils/atoms/settings";
import { usePlayback } from "@/providers/PlaybackProvider"; import { usePlayback } from "@/providers/PlaybackProvider";
import { useRouter } from "expo-router";
interface EpisodeCardProps { interface EpisodeCardProps {
item: BaseItemDto; item: BaseItemDto;
@@ -23,7 +22,6 @@ interface EpisodeCardProps {
*/ */
export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item }) => { export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item }) => {
const { deleteFile } = useFiles(); const { deleteFile } = useFiles();
const router = useRouter();
const { startDownloadedFilePlayback } = usePlayback(); const { startDownloadedFilePlayback } = usePlayback();
@@ -32,7 +30,6 @@ export const EpisodeCard: React.FC<EpisodeCardProps> = ({ item }) => {
item, item,
url: `${FileSystem.documentDirectory}/${item.Id}.mp4`, url: `${FileSystem.documentDirectory}/${item.Id}.mp4`,
}); });
router.push("/play");
}, [item, startDownloadedFilePlayback]); }, [item, startDownloadedFilePlayback]);
/** /**

View File

@@ -1,16 +1,17 @@
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import * as FileSystem from "expo-file-system";
import * as Haptics from "expo-haptics";
import React, { useCallback } from "react"; import React, { useCallback } from "react";
import { TouchableOpacity, View } from "react-native"; import { TouchableOpacity, View } from "react-native";
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
import * as ContextMenu from "zeego/context-menu"; import * as ContextMenu from "zeego/context-menu";
import * as FileSystem from "expo-file-system";
import * as Haptics from "expo-haptics";
import { useAtom } from "jotai";
import { Text } from "../common/Text";
import { useFiles } from "@/hooks/useFiles"; import { useFiles } from "@/hooks/useFiles";
import { runtimeTicksToMinutes } from "@/utils/time"; import { runtimeTicksToMinutes } from "@/utils/time";
import { Text } from "../common/Text";
import { useSettings } from "@/utils/atoms/settings";
import { usePlayback } from "@/providers/PlaybackProvider"; import { usePlayback } from "@/providers/PlaybackProvider";
import { useRouter } from "expo-router";
interface MovieCardProps { interface MovieCardProps {
item: BaseItemDto; item: BaseItemDto;
@@ -23,7 +24,8 @@ interface MovieCardProps {
*/ */
export const MovieCard: React.FC<MovieCardProps> = ({ item }) => { export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
const { deleteFile } = useFiles(); const { deleteFile } = useFiles();
const router = useRouter(); const [settings] = useSettings();
const { startDownloadedFilePlayback } = usePlayback(); const { startDownloadedFilePlayback } = usePlayback();
const handleOpenFile = useCallback(() => { const handleOpenFile = useCallback(() => {
@@ -31,7 +33,6 @@ export const MovieCard: React.FC<MovieCardProps> = ({ item }) => {
item, item,
url: `${FileSystem.documentDirectory}/${item.Id}.mp4`, url: `${FileSystem.documentDirectory}/${item.Id}.mp4`,
}); });
router.push("/play");
}, [item, startDownloadedFilePlayback]); }, [item, startDownloadedFilePlayback]);
/** /**

View File

@@ -9,9 +9,7 @@ interface Props extends ViewProps {
export const MoviesTitleHeader: React.FC<Props> = ({ item, ...props }) => { export const MoviesTitleHeader: React.FC<Props> = ({ item, ...props }) => {
return ( return (
<View {...props}> <View {...props}>
<Text className=" font-bold text-2xl mb-1" selectable> <Text className=" font-bold text-2xl mb-1">{item?.Name}</Text>
{item?.Name}
</Text>
<Text className=" opacity-50">{item?.ProductionYear}</Text> <Text className=" opacity-50">{item?.ProductionYear}</Text>
</View> </View>
); );

View File

@@ -25,7 +25,6 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
<Text className="text-lg font-bold mb-2 px-4">Cast & Crew</Text> <Text className="text-lg font-bold mb-2 px-4">Cast & Crew</Text>
<HorizontalScroll <HorizontalScroll
loading={loading} loading={loading}
height={247}
data={item?.People || []} data={item?.People || []}
renderItem={(item, index) => ( renderItem={(item, index) => (
<TouchableOpacity <TouchableOpacity
@@ -33,7 +32,7 @@ export const CastAndCrew: React.FC<Props> = ({ item, loading, ...props }) => {
router.push(`/actors/${item.Id}`); router.push(`/actors/${item.Id}`);
}} }}
key={item.Id} key={item.Id}
className="flex flex-col w-28" className="flex flex-col w-32"
> >
<Poster item={item} url={getPrimaryImageUrl({ api, item })} /> <Poster item={item} url={getPrimaryImageUrl({ api, item })} />
<Text className="mt-2">{item.Name}</Text> <Text className="mt-2">{item.Name}</Text>

View File

@@ -21,12 +21,11 @@ export const CurrentSeries: React.FC<Props> = ({ item, ...props }) => {
<Text className="text-lg font-bold mb-2 px-4">Series</Text> <Text className="text-lg font-bold mb-2 px-4">Series</Text>
<HorizontalScroll <HorizontalScroll
data={[item]} data={[item]}
height={247}
renderItem={(item, index) => ( renderItem={(item, index) => (
<TouchableOpacity <TouchableOpacity
key={item.Id} key={item.Id}
onPress={() => router.push(`/series/${item.SeriesId}`)} onPress={() => router.push(`/series/${item.SeriesId}`)}
className="flex flex-col space-y-2 w-28" className="flex flex-col space-y-2 w-32"
> >
<Poster <Poster
item={item} item={item}

View File

@@ -12,9 +12,7 @@ export const EpisodeTitleHeader: React.FC<Props> = ({ item, ...props }) => {
return ( return (
<View {...props}> <View {...props}>
<Text className="font-bold text-2xl" selectable> <Text className="font-bold text-2xl">{item?.Name}</Text>
{item?.Name}
</Text>
<View className="flex flex-row items-center mb-1"> <View className="flex flex-row items-center mb-1">
<TouchableOpacity <TouchableOpacity
onPress={() => { onPress={() => {

View File

@@ -49,7 +49,7 @@ export const NextUp: React.FC<{ seriesId: string }> = ({ seriesId }) => {
renderItem={(item, index) => ( renderItem={(item, index) => (
<TouchableItemRouter <TouchableItemRouter
item={item} item={item}
key={index} key={item.Id}
className="flex flex-col w-44" className="flex flex-col w-44"
> >
<ContinueWatchingPoster item={item} useEpisodePoster /> <ContinueWatchingPoster item={item} useEpisodePoster />

View File

@@ -9,8 +9,6 @@ interface Props extends ViewProps {}
export const MediaToggles: React.FC<Props> = ({ ...props }) => { export const MediaToggles: React.FC<Props> = ({ ...props }) => {
const [settings, updateSettings] = useSettings(); const [settings, updateSettings] = useSettings();
if (!settings) return null;
return ( return (
<View> <View>
<Text className="text-lg font-bold mb-2">Media</Text> <Text className="text-lg font-bold mb-2">Media</Text>
@@ -121,82 +119,6 @@ export const MediaToggles: React.FC<Props> = ({ ...props }) => {
</DropdownMenu.Content> </DropdownMenu.Content>
</DropdownMenu.Root> </DropdownMenu.Root>
</View> </View>
<View
className={`
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
`}
>
<View className="flex flex-col shrink">
<Text className="font-semibold">Forward skip length</Text>
<Text className="text-xs opacity-50">
Choose length in seconds when skipping in video playback.
</Text>
</View>
<View className="flex flex-row items-center">
<TouchableOpacity
onPress={() =>
updateSettings({
forwardSkipTime: Math.max(0, settings.forwardSkipTime - 5),
})
}
className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center"
>
<Text>-</Text>
</TouchableOpacity>
<Text className="w-12 h-8 bg-neutral-800 first-letter:px-3 py-2 flex items-center justify-center">
{settings.forwardSkipTime}s
</Text>
<TouchableOpacity
className="w-8 h-8 bg-neutral-800 rounded-r-lg flex items-center justify-center"
onPress={() =>
updateSettings({
forwardSkipTime: Math.min(60, settings.forwardSkipTime + 5),
})
}
>
<Text>+</Text>
</TouchableOpacity>
</View>
</View>
<View
className={`
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
`}
>
<View className="flex flex-col shrink">
<Text className="font-semibold">Rewind length</Text>
<Text className="text-xs opacity-50">
Choose length in seconds when skipping in video playback.
</Text>
</View>
<View className="flex flex-row items-center">
<TouchableOpacity
onPress={() =>
updateSettings({
rewindSkipTime: Math.max(0, settings.rewindSkipTime - 5),
})
}
className="w-8 h-8 bg-neutral-800 rounded-l-lg flex items-center justify-center"
>
<Text>-</Text>
</TouchableOpacity>
<Text className="w-12 h-8 bg-neutral-800 first-letter:px-3 py-2 flex items-center justify-center">
{settings.rewindSkipTime}s
</Text>
<TouchableOpacity
className="w-8 h-8 bg-neutral-800 rounded-r-lg flex items-center justify-center"
onPress={() =>
updateSettings({
rewindSkipTime: Math.min(60, settings.rewindSkipTime + 5),
})
}
>
<Text>+</Text>
</TouchableOpacity>
</View>
</View>
</View> </View>
</View> </View>
); );

View File

@@ -97,113 +97,6 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
/> />
</View> </View>
<View
pointerEvents={settings.autoRotate ? "none" : "auto"}
className={`
${
settings.autoRotate
? "opacity-50 pointer-events-none"
: "opacity-100"
}
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
`}
>
<View className="flex flex-col shrink">
<Text className="font-semibold">Video orientation</Text>
<Text className="text-xs opacity-50">
Set the full screen video player orientation.
</Text>
</View>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text>
{ScreenOrientationEnum[settings.defaultVideoOrientation]}
</Text>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
loop={true}
side="bottom"
align="start"
alignOffset={0}
avoidCollisions={true}
collisionPadding={8}
sideOffset={8}
>
<DropdownMenu.Label>Orientation</DropdownMenu.Label>
<DropdownMenu.Item
key="1"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.DEFAULT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.DEFAULT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="2"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.PORTRAIT_UP,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.PORTRAIT_UP
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="3"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="4"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
<View className="flex flex-row space-x-2 items-center justify-between bg-neutral-900 p-4"> <View className="flex flex-row space-x-2 items-center justify-between bg-neutral-900 p-4">
<View className="flex flex-col shrink"> <View className="flex flex-col shrink">
<Text className="font-semibold">Use external player (VLC)</Text> <Text className="font-semibold">Use external player (VLC)</Text>
@@ -363,6 +256,106 @@ export const SettingToggles: React.FC<Props> = ({ ...props }) => {
</DropdownMenu.Root> </DropdownMenu.Root>
</View> </View>
<View className="flex flex-col"> <View className="flex flex-col">
<View
className={`
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4
`}
>
<View className="flex flex-col shrink">
<Text className="font-semibold">Video orientation</Text>
<Text className="text-xs opacity-50">
Set the full screen video player orientation.
</Text>
</View>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<TouchableOpacity className="bg-neutral-800 rounded-lg border-neutral-900 border px-3 py-2 flex flex-row items-center justify-between">
<Text>
{ScreenOrientationEnum[settings.defaultVideoOrientation]}
</Text>
</TouchableOpacity>
</DropdownMenu.Trigger>
<DropdownMenu.Content
loop={true}
side="bottom"
align="start"
alignOffset={0}
avoidCollisions={true}
collisionPadding={8}
sideOffset={8}
>
<DropdownMenu.Label>Orientation</DropdownMenu.Label>
<DropdownMenu.Item
key="1"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.DEFAULT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.DEFAULT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="2"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.PORTRAIT_UP,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.PORTRAIT_UP
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="3"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.LANDSCAPE_LEFT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
<DropdownMenu.Item
key="4"
onSelect={() => {
updateSettings({
defaultVideoOrientation:
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
});
}}
>
<DropdownMenu.ItemTitle>
{
ScreenOrientationEnum[
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT
]
}
</DropdownMenu.ItemTitle>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</View>
<View <View
className={` className={`
flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4 flex flex-row items-center space-x-2 justify-between bg-neutral-900 p-4

View File

@@ -21,13 +21,13 @@
} }
}, },
"production": { "production": {
"channel": "0.15.0", "channel": "0.14.0",
"android": { "android": {
"image": "latest" "image": "latest"
} }
}, },
"production-apk": { "production-apk": {
"channel": "0.15.0", "channel": "0.14.0",
"android": { "android": {
"buildType": "apk", "buildType": "apk",
"image": "latest" "image": "latest"

View File

@@ -1,72 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
interface CreditTimestamps {
Introduction: {
Start: number;
End: number;
Valid: boolean;
};
Credits: {
Start: number;
End: number;
Valid: boolean;
};
}
export const useCreditSkipper = (
itemId: string | undefined,
currentTime: number,
videoRef: React.RefObject<any>
) => {
const [api] = useAtom(apiAtom);
const [showSkipCreditButton, setShowSkipCreditButton] = useState(false);
const { data: creditTimestamps } = useQuery<CreditTimestamps | null>({
queryKey: ["creditTimestamps", itemId],
queryFn: async () => {
if (!itemId) {
console.log("No item id");
return null;
}
const res = await api?.axiosInstance.get(
`${api.basePath}/Episode/${itemId}/Timestamps`,
{
headers: getAuthHeaders(api),
}
);
if (res?.status !== 200) {
return null;
}
return res?.data;
},
enabled: !!itemId,
});
useEffect(() => {
if (creditTimestamps) {
setShowSkipCreditButton(
currentTime > creditTimestamps.Credits.Start &&
currentTime < creditTimestamps.Credits.End
);
}
}, [creditTimestamps, currentTime]);
const skipCredit = useCallback(() => {
if (!creditTimestamps || !videoRef.current) return;
try {
videoRef.current.seek(creditTimestamps.Credits.End);
} catch (error) {
writeToLog("ERROR", "Error skipping intro", error);
}
}, [creditTimestamps, videoRef]);
return { showSkipCreditButton, skipCredit };
};

View File

@@ -1,68 +0,0 @@
import { useCallback, useEffect, useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { useAtom } from "jotai";
import { apiAtom } from "@/providers/JellyfinProvider";
import { getAuthHeaders } from "@/utils/jellyfin/jellyfin";
import { writeToLog } from "@/utils/log";
interface IntroTimestamps {
EpisodeId: string;
HideSkipPromptAt: number;
IntroEnd: number;
IntroStart: number;
ShowSkipPromptAt: number;
Valid: boolean;
}
export const useIntroSkipper = (
itemId: string | undefined,
currentTime: number,
videoRef: React.RefObject<any>
) => {
const [api] = useAtom(apiAtom);
const [showSkipButton, setShowSkipButton] = useState(false);
const { data: introTimestamps } = useQuery<IntroTimestamps | null>({
queryKey: ["introTimestamps", itemId],
queryFn: async () => {
if (!itemId) {
console.log("No item id");
return null;
}
const res = await api?.axiosInstance.get(
`${api.basePath}/Episode/${itemId}/IntroTimestamps`,
{
headers: getAuthHeaders(api),
}
);
if (res?.status !== 200) {
return null;
}
return res?.data;
},
enabled: !!itemId,
});
useEffect(() => {
if (introTimestamps) {
setShowSkipButton(
currentTime > introTimestamps.ShowSkipPromptAt &&
currentTime < introTimestamps.HideSkipPromptAt
);
}
}, [introTimestamps, currentTime]);
const skipIntro = useCallback(() => {
if (!introTimestamps || !videoRef.current) return;
try {
videoRef.current.seek(introTimestamps.IntroEnd);
} catch (error) {
writeToLog("ERROR", "Error skipping intro", error);
}
}, [introTimestamps, videoRef]);
return { showSkipButton, skipIntro };
};

View File

@@ -34,7 +34,7 @@ export const useTrickplay = (
const [api] = useAtom(apiAtom); const [api] = useAtom(apiAtom);
const [trickPlayUrl, setTrickPlayUrl] = useState<TrickplayUrl | null>(null); const [trickPlayUrl, setTrickPlayUrl] = useState<TrickplayUrl | null>(null);
const lastCalculationTime = useRef(0); const lastCalculationTime = useRef(0);
const throttleDelay = 200; // 200ms throttle const throttleDelay = 100; // 200ms throttle
const trickplayInfo = useMemo(() => { const trickplayInfo = useMemo(() => {
if (!currentlyPlaying?.item.Id || !currentlyPlaying?.item.Trickplay) { if (!currentlyPlaying?.item.Id || !currentlyPlaying?.item.Trickplay) {
@@ -62,7 +62,7 @@ export const useTrickplay = (
}, [currentlyPlaying]); }, [currentlyPlaying]);
const calculateTrickplayUrl = useCallback( const calculateTrickplayUrl = useCallback(
(progress: number) => { (progress: SharedValue<number>) => {
const now = Date.now(); const now = Date.now();
if (now - lastCalculationTime.current < throttleDelay) { if (now - lastCalculationTime.current < throttleDelay) {
return null; return null;
@@ -80,7 +80,7 @@ export const useTrickplay = (
throw new Error("Invalid trickplay data"); throw new Error("Invalid trickplay data");
} }
const currentSecond = Math.max(0, Math.floor(progress / 10000000)); const currentSecond = Math.max(0, Math.floor(progress.value / 10000000));
const cols = TileWidth; const cols = TileWidth;
const rows = TileHeight; const rows = TileHeight;

View File

@@ -17,27 +17,27 @@
"dependencies": { "dependencies": {
"@config-plugins/ffmpeg-kit-react-native": "^8.0.0", "@config-plugins/ffmpeg-kit-react-native": "^8.0.0",
"@expo/react-native-action-sheet": "^4.1.0", "@expo/react-native-action-sheet": "^4.1.0",
"@expo/vector-icons": "^14.0.3", "@expo/vector-icons": "^14.0.2",
"@gorhom/bottom-sheet": "^4", "@gorhom/bottom-sheet": "^4",
"@jellyfin/sdk": "^0.10.0", "@jellyfin/sdk": "^0.10.0",
"@react-native-async-storage/async-storage": "1.23.1", "@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/netinfo": "11.3.1", "@react-native-community/netinfo": "11.3.1",
"@react-native-menu/menu": "^1.1.3", "@react-native-menu/menu": "^1.1.2",
"@react-navigation/native": "^6.0.2", "@react-navigation/native": "^6.0.2",
"@shopify/flash-list": "1.6.4", "@shopify/flash-list": "1.6.4",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"@types/lodash": "^4.17.9", "@types/lodash": "^4.17.7",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
"axios": "^1.7.7", "axios": "^1.7.7",
"expo": "~51.0.34", "expo": "^51.0.32",
"expo-blur": "~13.0.2", "expo-blur": "~13.0.2",
"expo-build-properties": "~0.12.5", "expo-build-properties": "~0.12.5",
"expo-constants": "~16.0.2", "expo-constants": "~16.0.2",
"expo-dev-client": "~4.0.27", "expo-dev-client": "~4.0.26",
"expo-device": "~6.0.2", "expo-device": "~6.0.2",
"expo-font": "~12.0.10", "expo-font": "~12.0.10",
"expo-haptics": "~13.0.1", "expo-haptics": "~13.0.1",
"expo-image": "~1.13.0", "expo-image": "~1.12.15",
"expo-keep-awake": "~13.0.2", "expo-keep-awake": "~13.0.2",
"expo-linear-gradient": "~13.0.2", "expo-linear-gradient": "~13.0.2",
"expo-linking": "~6.3.1", "expo-linking": "~6.3.1",
@@ -46,13 +46,14 @@
"expo-router": "~3.5.23", "expo-router": "~3.5.23",
"expo-screen-orientation": "~7.0.5", "expo-screen-orientation": "~7.0.5",
"expo-sensors": "~13.0.9", "expo-sensors": "~13.0.9",
"expo-splash-screen": "~0.27.6", "expo-splash-screen": "~0.27.5",
"expo-status-bar": "~1.12.1", "expo-status-bar": "~1.12.1",
"expo-system-ui": "~3.0.7", "expo-system-ui": "~3.0.7",
"expo-updates": "~0.25.25", "expo-updates": "~0.25.24",
"expo-video": "^1.2.6",
"expo-web-browser": "~13.0.3", "expo-web-browser": "~13.0.3",
"ffmpeg-kit-react-native": "^6.0.2", "ffmpeg-kit-react-native": "^6.0.2",
"jotai": "^2.10.0", "jotai": "^2.9.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"nativewind": "^2.0.11", "nativewind": "^2.0.11",
"react": "18.2.0", "react": "18.2.0",
@@ -74,9 +75,9 @@
"react-native-svg": "15.2.0", "react-native-svg": "15.2.0",
"react-native-url-polyfill": "^2.0.0", "react-native-url-polyfill": "^2.0.0",
"react-native-uuid": "^2.0.2", "react-native-uuid": "^2.0.2",
"react-native-video": "^6.6.2", "react-native-video": "^6.5.0",
"react-native-web": "~0.19.10", "react-native-web": "~0.19.10",
"sonner-native": "^0.14.2", "sonner-native": "^0.9.2",
"tailwindcss": "3.3.2", "tailwindcss": "3.3.2",
"use-debounce": "^10.0.3", "use-debounce": "^10.0.3",
"uuid": "^10.0.0", "uuid": "^10.0.0",
@@ -93,5 +94,12 @@
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"typescript": "~5.3.3" "typescript": "~5.3.3"
}, },
"private": true "private": true,
"expo": {
"doctor": {
"reactNativeDirectoryCheck": {
"enabled": true
}
}
}
} }

View File

@@ -63,7 +63,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
setJellyfin( setJellyfin(
() => () =>
new Jellyfin({ new Jellyfin({
clientInfo: { name: "Streamyfin", version: "0.15.0" }, clientInfo: { name: "Streamyfin", version: "0.14.0" },
deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id }, deviceInfo: { name: Platform.OS === "ios" ? "iOS" : "Android", id },
}) })
); );
@@ -97,7 +97,7 @@ export const JellyfinProvider: React.FC<{ children: ReactNode }> = ({
return { return {
authorization: `MediaBrowser Client="Streamyfin", Device=${ authorization: `MediaBrowser Client="Streamyfin", Device=${
Platform.OS === "android" ? "Android" : "iOS" Platform.OS === "android" ? "Android" : "iOS"
}, DeviceId="${deviceId}", Version="0.15.0"`, }, DeviceId="${deviceId}", Version="0.14.0"`,
}; };
}, [deviceId]); }, [deviceId]);

View File

@@ -29,6 +29,7 @@ import {
parseM3U8ForSubtitles, parseM3U8ForSubtitles,
SubtitleTrack, SubtitleTrack,
} from "@/utils/hls/parseM3U8ForSubtitles"; } from "@/utils/hls/parseM3U8ForSubtitles";
import { useVideoPlayer, VideoPlayer } from "expo-video";
export type CurrentlyPlayingState = { export type CurrentlyPlayingState = {
url: string; url: string;
@@ -40,14 +41,10 @@ interface PlaybackContextType {
currentlyPlaying: CurrentlyPlayingState | null; currentlyPlaying: CurrentlyPlayingState | null;
videoRef: React.MutableRefObject<VideoRef | null>; videoRef: React.MutableRefObject<VideoRef | null>;
isPlaying: boolean; isPlaying: boolean;
isFullscreen: boolean;
progressTicks: number | null; progressTicks: number | null;
playVideo: (triggerRef?: boolean) => void; playVideo: (triggerRef?: boolean) => void;
pauseVideo: (triggerRef?: boolean) => void; pauseVideo: (triggerRef?: boolean) => void;
stopPlayback: () => void; stopPlayback: () => void;
presentFullscreenPlayer: () => void;
dismissFullscreenPlayer: () => void;
setIsFullscreen: (isFullscreen: boolean) => void;
setIsPlaying: (isPlaying: boolean) => void; setIsPlaying: (isPlaying: boolean) => void;
isBuffering: boolean; isBuffering: boolean;
setIsBuffering: (val: boolean) => void; setIsBuffering: (val: boolean) => void;
@@ -60,6 +57,7 @@ interface PlaybackContextType {
currentlyPlaying: CurrentlyPlayingState | null currentlyPlaying: CurrentlyPlayingState | null
) => void; ) => void;
subtitles: SubtitleTrack[]; subtitles: SubtitleTrack[];
player: VideoPlayer;
} }
const PlaybackContext = createContext<PlaybackContextType | null>(null); const PlaybackContext = createContext<PlaybackContextType | null>(null);
@@ -71,13 +69,12 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
const [user] = useAtom(userAtom); const [user] = useAtom(userAtom);
const videoRef = useRef<VideoRef | null>(null); const videoRef = useRef<VideoRef | null>(null);
const previousVolume = useRef<number | null>(null);
const [settings] = useSettings(); const [settings] = useSettings();
const previousVolume = useRef<number | null>(null);
const [isPlaying, _setIsPlaying] = useState<boolean>(false); const [isPlaying, _setIsPlaying] = useState<boolean>(false);
const [isBuffering, setIsBuffering] = useState<boolean>(false); const [isBuffering, setIsBuffering] = useState<boolean>(true);
const [isFullscreen, setIsFullscreen] = useState<boolean>(false); const [isFullscreen, setIsFullscreen] = useState<boolean>(false);
const [progressTicks, setProgressTicks] = useState<number | null>(0); const [progressTicks, setProgressTicks] = useState<number | null>(0);
const [volume, _setVolume] = useState<number | null>(null); const [volume, _setVolume] = useState<number | null>(null);
@@ -99,6 +96,10 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
[_setVolume] [_setVolume]
); );
const player = useVideoPlayer(currentlyPlaying?.url || null, () => {
if (player) player.play();
});
const { data: deviceId } = useQuery({ const { data: deviceId } = useQuery({
queryKey: ["deviceId", api], queryKey: ["deviceId", api],
queryFn: getDeviceId, queryFn: getDeviceId,
@@ -175,7 +176,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
const playVideo = useCallback( const playVideo = useCallback(
(triggerRef: boolean = true) => { (triggerRef: boolean = true) => {
if (triggerRef === true) { if (triggerRef === true) {
videoRef.current?.resume(); player.play();
} }
_setIsPlaying(true); _setIsPlaying(true);
reportPlaybackProgress({ reportPlaybackProgress({
@@ -192,7 +193,7 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
const pauseVideo = useCallback( const pauseVideo = useCallback(
(triggerRef: boolean = true) => { (triggerRef: boolean = true) => {
if (triggerRef === true) { if (triggerRef === true) {
videoRef.current?.pause(); player.pause();
} }
_setIsPlaying(false); _setIsPlaying(false);
reportPlaybackProgress({ reportPlaybackProgress({
@@ -253,16 +254,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
[_onProgress] [_onProgress]
); );
const presentFullscreenPlayer = useCallback(() => {
videoRef.current?.presentFullscreenPlayer();
setIsFullscreen(true);
}, []);
const dismissFullscreenPlayer = useCallback(() => {
videoRef.current?.dismissFullscreenPlayer();
setIsFullscreen(false);
}, []);
useEffect(() => { useEffect(() => {
if (!deviceId || !api?.accessToken) return; if (!deviceId || !api?.accessToken) return;
@@ -346,14 +337,13 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
return ( return (
<PlaybackContext.Provider <PlaybackContext.Provider
value={{ value={{
player,
onProgress, onProgress,
isBuffering, isBuffering,
setIsBuffering, setIsBuffering,
progressTicks, progressTicks,
setVolume, setVolume,
setIsPlaying, setIsPlaying,
setIsFullscreen,
isFullscreen,
isPlaying, isPlaying,
currentlyPlaying, currentlyPlaying,
sessionData: session, sessionData: session,
@@ -362,8 +352,6 @@ export const PlaybackProvider: React.FC<{ children: ReactNode }> = ({
setCurrentlyPlayingState, setCurrentlyPlayingState,
pauseVideo, pauseVideo,
stopPlayback, stopPlayback,
presentFullscreenPlayer,
dismissFullscreenPlayer,
startDownloadedFilePlayback, startDownloadedFilePlayback,
subtitles, subtitles,
}} }}

View File

@@ -70,9 +70,8 @@ type Settings = {
defaultAudioLanguage: DefaultLanguageOption | null; defaultAudioLanguage: DefaultLanguageOption | null;
showHomeTitles: boolean; showHomeTitles: boolean;
defaultVideoOrientation: ScreenOrientation.OrientationLock; defaultVideoOrientation: ScreenOrientation.OrientationLock;
forwardSkipTime: number;
rewindSkipTime: number;
}; };
/** /**
* *
* The settings atom is a Jotai atom that stores the user's settings. * The settings atom is a Jotai atom that stores the user's settings.
@@ -104,8 +103,6 @@ const loadSettings = async (): Promise<Settings> => {
defaultSubtitleLanguage: null, defaultSubtitleLanguage: null,
showHomeTitles: true, showHomeTitles: true,
defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT, defaultVideoOrientation: ScreenOrientation.OrientationLock.DEFAULT,
forwardSkipTime: 30,
rewindSkipTime: 10,
}; };
try { try {

View File

@@ -1,19 +0,0 @@
/**
* Convert bits to megabits or gigabits
*
* Return nice looking string
* If under 1000Mb, return XXXMB, else return X.XGB
*/
export function convertBitsToMegabitsOrGigabits(bits?: number | null): string {
if (!bits) return "0MB";
const megabits = bits / 1000000;
if (megabits < 1000) {
return Math.round(megabits) + "MB";
} else {
const gigabits = megabits / 1000;
return gigabits.toFixed(1) + "GB";
}
}

View File

@@ -1,5 +1,6 @@
// seconds to ticks util // seconds to ticks util
export function secondsToTicks(seconds: number): number { export function secondsToTicks(seconds: number): number {
"worklet";
return seconds * 10000000; return seconds * 10000000;
} }

View File

@@ -6,7 +6,7 @@
* @returns A string formatted as "Xh Ym" where X is hours and Y is minutes. * @returns A string formatted as "Xh Ym" where X is hours and Y is minutes.
*/ */
export const runtimeTicksToMinutes = ( export const runtimeTicksToMinutes = (
ticks: number | null | undefined ticks: number | null | undefined,
): string => { ): string => {
if (!ticks) return "0h 0m"; if (!ticks) return "0h 0m";
@@ -20,7 +20,7 @@ export const runtimeTicksToMinutes = (
}; };
export const runtimeTicksToSeconds = ( export const runtimeTicksToSeconds = (
ticks: number | null | undefined ticks: number | null | undefined,
): string => { ): string => {
if (!ticks) return "0h 0m"; if (!ticks) return "0h 0m";
@@ -34,37 +34,3 @@ export const runtimeTicksToSeconds = (
if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`; if (hours > 0) return `${hours}h ${minutes}m ${seconds}s`;
else return `${minutes}m ${seconds}s`; else return `${minutes}m ${seconds}s`;
}; };
export const formatTimeString = (
t: number | null | undefined,
tick = false
): string => {
if (t === null || t === undefined) return "0:00";
let seconds = t;
if (tick) {
seconds = Math.floor(t / 10000000); // Convert ticks to seconds
}
if (seconds < 0) return "0:00";
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const remainingSeconds = Math.floor(seconds % 60);
if (hours > 0) {
return `${hours}h ${minutes}m ${remainingSeconds}s`;
} else {
return `${minutes}m ${remainingSeconds}s`;
}
};
export const secondsToTicks = (seconds?: number | undefined) => {
if (!seconds) return 0;
return seconds * 10000000;
};
export const ticksToSeconds = (ticks?: number | undefined) => {
if (!ticks) return 0;
return ticks / 10000000;
};