mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-03-27 03:31:53 +00:00
fix: move cast button + ask user which device to play on
This commit is contained in:
@@ -3,8 +3,9 @@ import React, { useEffect } from "react";
|
|||||||
import * as NavigationBar from "expo-navigation-bar";
|
import * as NavigationBar from "expo-navigation-bar";
|
||||||
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
|
import { TabBarIcon } from "@/components/navigation/TabBarIcon";
|
||||||
import { Colors } from "@/constants/Colors";
|
import { Colors } from "@/constants/Colors";
|
||||||
import { Platform, TouchableOpacity } from "react-native";
|
import { Platform, TouchableOpacity, View } from "react-native";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import { Chromecast } from "@/components/Chromecast";
|
||||||
|
|
||||||
export default function TabLayout() {
|
export default function TabLayout() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -41,18 +42,23 @@ export default function TabLayout() {
|
|||||||
router.push("/(auth)/downloads");
|
router.push("/(auth)/downloads");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Feather name="download" color={"white"} size={24} />
|
<Feather name="download" color={"white"} size={22} />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
),
|
),
|
||||||
headerRight: () => (
|
headerRight: () => (
|
||||||
<TouchableOpacity
|
<View className="flex flex-row items-center space-x-2">
|
||||||
style={{ marginHorizontal: 17 }}
|
<Chromecast />
|
||||||
onPress={() => {
|
<TouchableOpacity
|
||||||
router.push("/(auth)/settings");
|
style={{ marginRight: 17 }}
|
||||||
}}
|
onPress={() => {
|
||||||
>
|
router.push("/(auth)/settings");
|
||||||
<Feather name="settings" color={"white"} size={24} />
|
}}
|
||||||
</TouchableOpacity>
|
>
|
||||||
|
<View className="h-10 aspect-square flex items-center justify-center rounded">
|
||||||
|
<Feather name="settings" color={"white"} size={22} />
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -137,35 +137,38 @@ const page: React.FC = () => {
|
|||||||
const [cp, setCp] = useAtom(currentlyPlayingItemAtom);
|
const [cp, setCp] = useAtom(currentlyPlayingItemAtom);
|
||||||
const client = useRemoteMediaClient();
|
const client = useRemoteMediaClient();
|
||||||
|
|
||||||
const onPressPlay = useCallback(async () => {
|
const onPressPlay = useCallback(
|
||||||
if (!playbackUrl || !item) return;
|
async (type: "device" | "cast" = "device") => {
|
||||||
|
if (!playbackUrl || !item) return;
|
||||||
|
|
||||||
if (chromecastReady && client) {
|
if (type === "cast" && client) {
|
||||||
await CastContext.getPlayServicesState().then((state) => {
|
await CastContext.getPlayServicesState().then((state) => {
|
||||||
if (state && state !== PlayServicesState.SUCCESS)
|
if (state && state !== PlayServicesState.SUCCESS)
|
||||||
CastContext.showPlayServicesErrorDialog(state);
|
CastContext.showPlayServicesErrorDialog(state);
|
||||||
else {
|
else {
|
||||||
client.loadMedia({
|
client.loadMedia({
|
||||||
mediaInfo: {
|
mediaInfo: {
|
||||||
contentUrl: playbackUrl,
|
contentUrl: playbackUrl,
|
||||||
contentType: "video/mp4",
|
contentType: "video/mp4",
|
||||||
metadata: {
|
metadata: {
|
||||||
type: item.Type === "Episode" ? "tvShow" : "movie",
|
type: item.Type === "Episode" ? "tvShow" : "movie",
|
||||||
title: item.Name || "",
|
title: item.Name || "",
|
||||||
subtitle: item.Overview || "",
|
subtitle: item.Overview || "",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
startTime: 0,
|
||||||
startTime: 0,
|
});
|
||||||
});
|
}
|
||||||
}
|
});
|
||||||
});
|
} else {
|
||||||
} else {
|
setCp({
|
||||||
setCp({
|
item,
|
||||||
item,
|
playbackUrl,
|
||||||
playbackUrl,
|
});
|
||||||
});
|
}
|
||||||
}
|
},
|
||||||
}, [playbackUrl, item]);
|
[playbackUrl, item],
|
||||||
|
);
|
||||||
|
|
||||||
if (l1)
|
if (l1)
|
||||||
return (
|
return (
|
||||||
@@ -262,7 +265,6 @@ const page: React.FC = () => {
|
|||||||
<View className="h-12 aspect-square flex items-center justify-center"></View>
|
<View className="h-12 aspect-square flex items-center justify-center"></View>
|
||||||
)}
|
)}
|
||||||
<PlayedStatus item={item} />
|
<PlayedStatus item={item} />
|
||||||
<Chromecast />
|
|
||||||
</View>
|
</View>
|
||||||
<Text>{item.Overview}</Text>
|
<Text>{item.Overview}</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -287,7 +289,7 @@ const page: React.FC = () => {
|
|||||||
<NextEpisodeButton item={item} type="previous" className="mr-2" />
|
<NextEpisodeButton item={item} type="previous" className="mr-2" />
|
||||||
<PlayButton
|
<PlayButton
|
||||||
item={item}
|
item={item}
|
||||||
chromecastReady={false}
|
chromecastReady={chromecastReady}
|
||||||
onPress={onPressPlay}
|
onPress={onPressPlay}
|
||||||
className="grow"
|
className="grow"
|
||||||
/>
|
/>
|
||||||
|
|||||||
126
app/_layout.tsx
126
app/_layout.tsx
@@ -9,8 +9,8 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import "react-native-reanimated";
|
import "react-native-reanimated";
|
||||||
import * as ScreenOrientation from "expo-screen-orientation";
|
import * as ScreenOrientation from "expo-screen-orientation";
|
||||||
import { StatusBar } from "expo-status-bar";
|
import { StatusBar } from "expo-status-bar";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
||||||
import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar";
|
import { CurrentlyPlayingBar } from "@/components/CurrentlyPlayingBar";
|
||||||
|
import { ActionSheetProvider } from "@expo/react-native-action-sheet";
|
||||||
|
|
||||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||||
SplashScreen.preventAutoHideAsync();
|
SplashScreen.preventAutoHideAsync();
|
||||||
@@ -75,67 +75,69 @@ export default function RootLayout() {
|
|||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClientRef.current}>
|
<QueryClientProvider client={queryClientRef.current}>
|
||||||
<JotaiProvider>
|
<JotaiProvider>
|
||||||
<JellyfinProvider>
|
<ActionSheetProvider>
|
||||||
<StatusBar style="light" backgroundColor="#000" />
|
<JellyfinProvider>
|
||||||
<ThemeProvider value={DarkTheme}>
|
<StatusBar style="light" backgroundColor="#000" />
|
||||||
<Stack>
|
<ThemeProvider value={DarkTheme}>
|
||||||
<Stack.Screen
|
<Stack>
|
||||||
name="(auth)/(tabs)"
|
<Stack.Screen
|
||||||
options={{
|
name="(auth)/(tabs)"
|
||||||
headerShown: false,
|
options={{
|
||||||
title: "Home",
|
headerShown: false,
|
||||||
}}
|
title: "Home",
|
||||||
/>
|
}}
|
||||||
<Stack.Screen
|
/>
|
||||||
name="(auth)/settings"
|
<Stack.Screen
|
||||||
options={{
|
name="(auth)/settings"
|
||||||
headerShown: true,
|
options={{
|
||||||
title: "Settings",
|
headerShown: true,
|
||||||
headerStyle: { backgroundColor: "black" },
|
title: "Settings",
|
||||||
headerShadowVisible: false,
|
headerStyle: { backgroundColor: "black" },
|
||||||
}}
|
headerShadowVisible: false,
|
||||||
/>
|
}}
|
||||||
<Stack.Screen
|
/>
|
||||||
name="(auth)/downloads"
|
<Stack.Screen
|
||||||
options={{
|
name="(auth)/downloads"
|
||||||
headerShown: true,
|
options={{
|
||||||
title: "Downloads",
|
headerShown: true,
|
||||||
headerStyle: { backgroundColor: "black" },
|
title: "Downloads",
|
||||||
headerShadowVisible: false,
|
headerStyle: { backgroundColor: "black" },
|
||||||
}}
|
headerShadowVisible: false,
|
||||||
/>
|
}}
|
||||||
<Stack.Screen
|
/>
|
||||||
name="(auth)/items/[id]/page"
|
<Stack.Screen
|
||||||
options={{
|
name="(auth)/items/[id]/page"
|
||||||
title: "",
|
options={{
|
||||||
headerShown: false,
|
title: "",
|
||||||
}}
|
headerShown: false,
|
||||||
/>
|
}}
|
||||||
<Stack.Screen
|
/>
|
||||||
name="(auth)/collections/[collection]/page"
|
<Stack.Screen
|
||||||
options={{
|
name="(auth)/collections/[collection]/page"
|
||||||
title: "",
|
options={{
|
||||||
headerShown: true,
|
title: "",
|
||||||
headerStyle: { backgroundColor: "black" },
|
headerShown: true,
|
||||||
headerShadowVisible: false,
|
headerStyle: { backgroundColor: "black" },
|
||||||
}}
|
headerShadowVisible: false,
|
||||||
/>
|
}}
|
||||||
<Stack.Screen
|
/>
|
||||||
name="(auth)/series/[id]/page"
|
<Stack.Screen
|
||||||
options={{
|
name="(auth)/series/[id]/page"
|
||||||
title: "",
|
options={{
|
||||||
headerShown: false,
|
title: "",
|
||||||
}}
|
headerShown: false,
|
||||||
/>
|
}}
|
||||||
<Stack.Screen
|
/>
|
||||||
name="login"
|
<Stack.Screen
|
||||||
options={{ headerShown: false, title: "Login" }}
|
name="login"
|
||||||
/>
|
options={{ headerShown: false, title: "Login" }}
|
||||||
<Stack.Screen name="+not-found" />
|
/>
|
||||||
</Stack>
|
<Stack.Screen name="+not-found" />
|
||||||
<CurrentlyPlayingBar />
|
</Stack>
|
||||||
</ThemeProvider>
|
<CurrentlyPlayingBar />
|
||||||
</JellyfinProvider>
|
</ThemeProvider>
|
||||||
|
</JellyfinProvider>
|
||||||
|
</ActionSheetProvider>
|
||||||
</JotaiProvider>
|
</JotaiProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import Animated, {
|
|||||||
useScrollViewOffset,
|
useScrollViewOffset,
|
||||||
} from "react-native-reanimated";
|
} from "react-native-reanimated";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
|
import { Chromecast } from "./Chromecast";
|
||||||
|
|
||||||
const HEADER_HEIGHT = 400;
|
const HEADER_HEIGHT = 400;
|
||||||
|
|
||||||
@@ -72,6 +73,15 @@ export const ParallaxScrollView: React.FC<Props> = ({
|
|||||||
/>
|
/>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
|
<View
|
||||||
|
className="absolute right-4 z-50 bg-black rounded-full p-0.5"
|
||||||
|
style={{
|
||||||
|
top: inset.top + 17,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Chromecast width={22} height={22} />
|
||||||
|
</View>
|
||||||
|
|
||||||
{logo && (
|
{logo && (
|
||||||
<View className="absolute top-[250px] h-[130px] left-0 w-full z-40 px-4 flex justify-center items-center">
|
<View className="absolute top-[250px] h-[130px] left-0 w-full z-40 px-4 flex justify-center items-center">
|
||||||
{logo}
|
{logo}
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import { Button } from "./Button";
|
|||||||
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models";
|
||||||
import { Feather, Ionicons } from "@expo/vector-icons";
|
import { Feather, Ionicons } from "@expo/vector-icons";
|
||||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||||
|
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||||
|
import { View } from "react-native";
|
||||||
|
|
||||||
interface Props extends React.ComponentProps<typeof Button> {
|
interface Props extends React.ComponentProps<typeof Button> {
|
||||||
item: BaseItemDto;
|
item: BaseItemDto;
|
||||||
onPress: () => void;
|
onPress: (type?: "cast" | "device") => void;
|
||||||
chromecastReady: boolean;
|
chromecastReady: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,15 +17,45 @@ export const PlayButton: React.FC<Props> = ({
|
|||||||
chromecastReady,
|
chromecastReady,
|
||||||
...props
|
...props
|
||||||
}) => {
|
}) => {
|
||||||
|
const { showActionSheetWithOptions } = useActionSheet();
|
||||||
|
|
||||||
|
const _onPress = () => {
|
||||||
|
if (!chromecastReady) {
|
||||||
|
onPress("device");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = ["Chromecast", "Device", "Cancel"];
|
||||||
|
const cancelButtonIndex = 2;
|
||||||
|
|
||||||
|
showActionSheetWithOptions(
|
||||||
|
{
|
||||||
|
options,
|
||||||
|
cancelButtonIndex,
|
||||||
|
},
|
||||||
|
(selectedIndex: number | undefined) => {
|
||||||
|
switch (selectedIndex) {
|
||||||
|
case 0:
|
||||||
|
onPress("cast");
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
onPress("device");
|
||||||
|
break;
|
||||||
|
case cancelButtonIndex:
|
||||||
|
console.log("calcel");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
onPress={onPress}
|
onPress={_onPress}
|
||||||
iconRight={
|
iconRight={
|
||||||
chromecastReady ? (
|
<View className="flex flex-row items-center space-x-2">
|
||||||
<Feather name="cast" size={20} color="white" />
|
|
||||||
) : (
|
|
||||||
<Ionicons name="play-circle" size={24} color="white" />
|
<Ionicons name="play-circle" size={24} color="white" />
|
||||||
)
|
{chromecastReady && <Feather name="cast" size={22} color="white" />}
|
||||||
|
</View>
|
||||||
}
|
}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user