mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-19 02:34:17 +01:00
Compare commits
1 Commits
v0.15.0
...
feat/expo-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c4bc68566 |
9
app.json
9
app.json
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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={() => {
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
4
eas.json
4
eas.json
@@ -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"
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
};
|
|
||||||
@@ -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 };
|
|
||||||
};
|
|
||||||
@@ -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;
|
||||||
|
|||||||
32
package.json
32
package.json
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
|
|||||||
Reference in New Issue
Block a user