feat: currently playing floating bar

This commit is contained in:
Fredrik Burmester
2024-08-12 14:03:22 +02:00
parent ed301a9152
commit 49c95a091c
11 changed files with 519 additions and 125 deletions

View File

@@ -11,7 +11,7 @@ import { useQuery } from "@tanstack/react-query";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router";
import { useAtom } from "jotai";
import { useMemo, useState } from "react";
import { useCallback, useMemo, useState } from "react";
import {
ActivityIndicator,
ScrollView,
@@ -22,16 +22,29 @@ import { ParallaxScrollView } from "../../../../components/ParallaxPage";
import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData";
import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl";
import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById";
import { PlayButton } from "@/components/PlayButton";
import { Bitrate, BitrateSelector } from "@/components/BitrateSelector";
import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api";
import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl";
import { useCastDevice } from "react-native-google-cast";
import { chromecastProfile } from "@/utils/profiles/chromecast";
import ios12 from "@/utils/profiles/ios12";
import { currentlyPlayingItemAtom } from "@/components/CurrentlyPlayingBar";
const page: React.FC = () => {
const local = useLocalSearchParams();
const { id } = local as { id: string };
const [playbackURL, setPlaybackURL] = useState<string | null>(null);
const [api] = useAtom(apiAtom);
const [user] = useAtom(userAtom);
const castDevice = useCastDevice();
const [maxBitrate, setMaxBitrate] = useState<Bitrate>({
key: "Max",
value: undefined,
});
const { data: item, isLoading: l1 } = useQuery({
queryKey: ["item", id],
queryFn: async () =>
@@ -60,6 +73,52 @@ const page: React.FC = () => {
[item],
);
const { data: sessionData } = useQuery({
queryKey: ["sessionData", item?.Id],
queryFn: async () => {
if (!api || !user?.Id || !item?.Id) return null;
const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({
itemId: item?.Id,
userId: user?.Id,
});
return playbackData.data;
},
enabled: !!item?.Id && !!api && !!user?.Id,
staleTime: 0,
});
const { data: playbackUrl } = useQuery({
queryKey: ["playbackUrl", item?.Id, maxBitrate, castDevice],
queryFn: async () => {
if (!api || !user?.Id || !sessionData) return null;
const url = await getStreamUrl({
api,
userId: user.Id,
item,
startTimeTicks: item?.UserData?.PlaybackPositionTicks || 0,
maxStreamingBitrate: maxBitrate.value,
sessionData,
deviceProfile: castDevice?.deviceId ? chromecastProfile : ios12,
});
return url;
},
enabled: !!sessionData,
staleTime: 0,
});
const [cp, setCp] = useAtom(currentlyPlayingItemAtom);
const onPressPlay = useCallback(() => {
if (!playbackUrl || !item) return;
setCp({
item,
playbackUrl,
});
}, [playbackUrl, item]);
if (l1)
return (
<View className="justify-center items-center h-full">
@@ -151,20 +210,19 @@ const page: React.FC = () => {
</View>
<View className="flex flex-row justify-between items-center w-full my-4">
{playbackURL && (
<DownloadItem item={item} playbackURL={playbackURL} />
{playbackUrl && (
<DownloadItem item={item} playbackUrl={playbackUrl} />
)}
<Chromecast />
</View>
<Text>{item.Overview}</Text>
</View>
<View className="flex flex-col p-4">
<VideoPlayer
itemId={item.Id}
onChangePlaybackURL={(val) => {
setPlaybackURL(val);
}}
<BitrateSelector
onChange={(val) => setMaxBitrate(val)}
selected={maxBitrate}
/>
<PlayButton item={item} chromecastReady={false} onPress={onPressPlay} />
</View>
<ScrollView horizontal className="flex px-4 mb-4">
<View className="flex flex-row space-x-2 ">

View File

@@ -13,6 +13,12 @@ import "react-native-reanimated";
import Feather from "@expo/vector-icons/Feather";
import { StatusBar } from "expo-status-bar";
import { Colors } from "@/constants/Colors";
import { View } from "react-native";
import { Text } from "@/components/common/Text";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Ionicons } from "@expo/vector-icons";
import Video from "react-native-video";
import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar";
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync();
@@ -40,6 +46,8 @@ export default function RootLayout() {
}),
);
const insets = useSafeAreaInsets();
useEffect(() => {
if (loaded) {
SplashScreen.hideAsync();
@@ -56,7 +64,7 @@ export default function RootLayout() {
<JellyfinProvider>
<StatusBar style="light" backgroundColor="#000" />
<ThemeProvider value={DarkTheme}>
<Stack>
<Stack screenOptions={{}}>
<Stack.Screen
name="(auth)/(tabs)"
options={{
@@ -69,12 +77,8 @@ export default function RootLayout() {
options={{
headerShown: true,
title: "Settings",
presentation: "modal",
headerLeft: () => (
<TouchableOpacity onPress={() => router.back()}>
<Feather name="x-circle" size={24} color="white" />
</TouchableOpacity>
),
headerStyle: { backgroundColor: "black" },
headerShadowVisible: false,
}}
/>
<Stack.Screen
@@ -82,14 +86,8 @@ export default function RootLayout() {
options={{
headerShown: true,
title: "Downloads",
}}
/>
<Stack.Screen
name="(auth)/player/offline/page"
options={{
title: "",
headerShown: true,
headerStyle: { backgroundColor: "transparent" },
headerStyle: { backgroundColor: "black" },
headerShadowVisible: false,
}}
/>
<Stack.Screen
@@ -121,6 +119,7 @@ export default function RootLayout() {
/>
<Stack.Screen name="+not-found" />
</Stack>
<CurrentlyPlayingBar />
</ThemeProvider>
</JellyfinProvider>
</JotaiProvider>

View File

@@ -151,7 +151,6 @@ const Login: React.FC = () => {
returnKeyType="done"
autoCapitalize="none"
textContentType="URL"
clearButtonMode="while-editing"
maxLength={500}
/>
<Button onPress={() => handleConnect(parsedServerURL)}>