mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-15 23:59:08 +00:00
fix: refactor
This commit is contained in:
@@ -28,6 +28,7 @@ import { chromecastProfile } from "@/utils/profiles/chromecast";
|
||||
import ios12 from "@/utils/profiles/ios12";
|
||||
import {
|
||||
currentlyPlayingItemAtom,
|
||||
playingAtom,
|
||||
triggerPlayAtom,
|
||||
} from "@/components/CurrentlyPlayingBar";
|
||||
import { AudioTrackSelector } from "@/components/AudioTrackSelector";
|
||||
@@ -47,6 +48,10 @@ const page: React.FC = () => {
|
||||
|
||||
const castDevice = useCastDevice();
|
||||
|
||||
const [, setCurrentlyPlying] = useAtom(currentlyPlayingItemAtom);
|
||||
const [, setPlaying] = useAtom(playingAtom);
|
||||
|
||||
const client = useRemoteMediaClient();
|
||||
const chromecastReady = useMemo(() => !!castDevice?.deviceId, [castDevice]);
|
||||
const [selectedAudioStream, setSelectedAudioStream] = useState<number>(-1);
|
||||
const [selectedSubtitleStream, setSelectedSubtitleStream] =
|
||||
@@ -68,22 +73,6 @@ const page: React.FC = () => {
|
||||
staleTime: 60,
|
||||
});
|
||||
|
||||
const backdropUrl = useMemo(
|
||||
() =>
|
||||
getBackdropUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 1000,
|
||||
}),
|
||||
[item],
|
||||
);
|
||||
|
||||
const logoUrl = useMemo(
|
||||
() => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null),
|
||||
[item],
|
||||
);
|
||||
|
||||
const { data: sessionData } = useQuery({
|
||||
queryKey: ["sessionData", item?.Id],
|
||||
queryFn: async () => {
|
||||
@@ -131,10 +120,6 @@ const page: React.FC = () => {
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const [, setCp] = useAtom(currentlyPlayingItemAtom);
|
||||
const client = useRemoteMediaClient();
|
||||
const [, setPlayTrigger] = useAtom(triggerPlayAtom);
|
||||
|
||||
const onPressPlay = useCallback(
|
||||
async (type: "device" | "cast" = "device") => {
|
||||
if (!playbackUrl || !item) return;
|
||||
@@ -159,18 +144,33 @@ const page: React.FC = () => {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setCp({
|
||||
setCurrentlyPlying({
|
||||
item,
|
||||
playbackUrl,
|
||||
});
|
||||
|
||||
// Use this trigger to initiate playback in another component (CurrentlyPlayingBar)
|
||||
setPlayTrigger((prev) => prev + 1);
|
||||
setPlaying(true);
|
||||
}
|
||||
},
|
||||
[playbackUrl, item],
|
||||
);
|
||||
|
||||
const backdropUrl = useMemo(
|
||||
() =>
|
||||
getBackdropUrl({
|
||||
api,
|
||||
item,
|
||||
quality: 90,
|
||||
width: 1000,
|
||||
}),
|
||||
[item],
|
||||
);
|
||||
|
||||
const logoUrl = useMemo(
|
||||
() => (item?.Type === "Movie" ? getLogoImageUrlById({ api, item }) : null),
|
||||
[item],
|
||||
);
|
||||
|
||||
if (l1)
|
||||
return (
|
||||
<View className="justify-center items-center h-full">
|
||||
|
||||
189
app/_layout.tsx
189
app/_layout.tsx
@@ -15,7 +15,6 @@ import { useJobProcessor } from "@/utils/atoms/queue";
|
||||
import { JobQueueProvider } from "@/providers/JobQueueProvider";
|
||||
import { useKeepAwake } from "expo-keep-awake";
|
||||
import { useSettings } from "@/utils/atoms/settings";
|
||||
import { StreamProvider } from "@/providers/StreamProvider";
|
||||
|
||||
// Prevent the splash screen from auto-hiding before asset loading is complete.
|
||||
SplashScreen.preventAutoHideAsync();
|
||||
@@ -79,101 +78,99 @@ function Layout() {
|
||||
<JobQueueProvider>
|
||||
<ActionSheetProvider>
|
||||
<JellyfinProvider>
|
||||
<StreamProvider>
|
||||
<StatusBar style="light" backgroundColor="#000" />
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<Stack initialRouteName="/home">
|
||||
<Stack.Screen
|
||||
name="(auth)/(tabs)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/settings"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "Settings",
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/downloads"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "Downloads",
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/items/[id]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/collections/[collectionId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/artists/page"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/artists/[artistId]/page"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/albums/[albumId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/songs/[songId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/series/[id]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{ headerShown: false, title: "Login" }}
|
||||
/>
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<CurrentlyPlayingBar />
|
||||
</ThemeProvider>
|
||||
</StreamProvider>
|
||||
<StatusBar style="light" backgroundColor="#000" />
|
||||
<ThemeProvider value={DarkTheme}>
|
||||
<Stack initialRouteName="/home">
|
||||
<Stack.Screen
|
||||
name="(auth)/(tabs)"
|
||||
options={{
|
||||
headerShown: false,
|
||||
title: "",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/settings"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "Settings",
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/downloads"
|
||||
options={{
|
||||
headerShown: true,
|
||||
title: "Downloads",
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/items/[id]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/collections/[collectionId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/artists/page"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/artists/[artistId]/page"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/albums/[albumId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: true,
|
||||
headerStyle: { backgroundColor: "black" },
|
||||
headerShadowVisible: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/songs/[songId]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="(auth)/series/[id]"
|
||||
options={{
|
||||
title: "",
|
||||
headerShown: false,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="login"
|
||||
options={{ headerShown: false, title: "Login" }}
|
||||
/>
|
||||
<Stack.Screen name="+not-found" />
|
||||
</Stack>
|
||||
<CurrentlyPlayingBar />
|
||||
</ThemeProvider>
|
||||
</JellyfinProvider>
|
||||
</ActionSheetProvider>
|
||||
</JobQueueProvider>
|
||||
|
||||
@@ -44,17 +44,21 @@ export const currentlyPlayingItemAtom = atom<{
|
||||
|
||||
export const triggerPlayAtom = atom(0);
|
||||
|
||||
export const CurrentlyPlayingBar: React.FC = () => {
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const [cp, setCp] = useAtom(currentlyPlayingItemAtom);
|
||||
export const playingAtom = atom(false);
|
||||
|
||||
export const CurrentlyPlayingBar: React.FC = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const segments = useSegments();
|
||||
const [settings] = useSettings();
|
||||
|
||||
const [api] = useAtom(apiAtom);
|
||||
const [user] = useAtom(userAtom);
|
||||
const [playing, setPlaying] = useAtom(playingAtom);
|
||||
const [currentlyPlaying, setCurrentlyPlaying] = useAtom(
|
||||
currentlyPlayingItemAtom,
|
||||
);
|
||||
|
||||
const videoRef = useRef<VideoRef | null>(null);
|
||||
const [paused, setPaused] = useState(true);
|
||||
const [progress, setProgress] = useState(0);
|
||||
const [fullScreen, setFullScreen] = useState(false);
|
||||
|
||||
@@ -113,81 +117,83 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
// }, [settings, fullScreen]);
|
||||
|
||||
const { data: item } = useQuery({
|
||||
queryKey: ["item", cp?.item.Id],
|
||||
queryKey: ["item", currentlyPlaying?.item.Id],
|
||||
queryFn: async () =>
|
||||
await getUserItemData({
|
||||
api,
|
||||
userId: user?.Id,
|
||||
itemId: cp?.item.Id,
|
||||
itemId: currentlyPlaying?.item.Id,
|
||||
}),
|
||||
enabled: !!cp?.item.Id && !!api,
|
||||
enabled: !!currentlyPlaying?.item.Id && !!api,
|
||||
staleTime: 60,
|
||||
});
|
||||
|
||||
const { data: sessionData } = useQuery({
|
||||
queryKey: ["sessionData", cp?.item.Id],
|
||||
queryKey: ["sessionData", currentlyPlaying?.item.Id],
|
||||
queryFn: async () => {
|
||||
if (!cp?.item.Id) return null;
|
||||
if (!currentlyPlaying?.item.Id) return null;
|
||||
const playbackData = await getMediaInfoApi(api!).getPlaybackInfo({
|
||||
itemId: cp?.item.Id,
|
||||
itemId: currentlyPlaying?.item.Id,
|
||||
userId: user?.Id,
|
||||
});
|
||||
return playbackData.data;
|
||||
},
|
||||
enabled: !!cp?.item.Id && !!api && !!user?.Id,
|
||||
enabled: !!currentlyPlaying?.item.Id && !!api && !!user?.Id,
|
||||
staleTime: 0,
|
||||
});
|
||||
|
||||
const onProgress = useCallback(
|
||||
({ currentTime }: OnProgressData) => {
|
||||
if (!currentTime || !sessionData?.PlaySessionId || paused) return;
|
||||
if (
|
||||
!currentTime ||
|
||||
!sessionData?.PlaySessionId ||
|
||||
!playing ||
|
||||
!api ||
|
||||
!currentlyPlaying?.item.Id
|
||||
)
|
||||
return;
|
||||
const newProgress = currentTime * 10000000;
|
||||
setProgress(newProgress);
|
||||
reportPlaybackProgress({
|
||||
api,
|
||||
itemId: cp?.item.Id,
|
||||
itemId: currentlyPlaying?.item.Id,
|
||||
positionTicks: newProgress,
|
||||
sessionId: sessionData.PlaySessionId,
|
||||
});
|
||||
},
|
||||
[sessionData?.PlaySessionId, item, api, paused]
|
||||
[sessionData?.PlaySessionId, api, playing, currentlyPlaying?.item.Id],
|
||||
);
|
||||
|
||||
const play = () => {
|
||||
if (videoRef.current) {
|
||||
videoRef.current.resume();
|
||||
setPaused(false);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (playing) {
|
||||
videoRef.current?.resume();
|
||||
} else {
|
||||
videoRef.current?.pause();
|
||||
if (progress > 0 && sessionData?.PlaySessionId && api)
|
||||
reportPlaybackStopped({
|
||||
api,
|
||||
itemId: item?.Id,
|
||||
positionTicks: progress,
|
||||
sessionId: sessionData?.PlaySessionId,
|
||||
});
|
||||
|
||||
const pause = useCallback(() => {
|
||||
videoRef.current?.pause();
|
||||
setPaused(true);
|
||||
|
||||
if (progress > 0)
|
||||
reportPlaybackStopped({
|
||||
api,
|
||||
itemId: item?.Id,
|
||||
positionTicks: progress,
|
||||
sessionId: sessionData?.PlaySessionId,
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["nextUp", item?.SeriesId],
|
||||
refetchType: "all",
|
||||
});
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["nextUp", item?.SeriesId],
|
||||
refetchType: "all",
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["episodes"],
|
||||
refetchType: "all",
|
||||
});
|
||||
}, [api, item, progress, sessionData, queryClient]);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["episodes"],
|
||||
refetchType: "all",
|
||||
});
|
||||
}
|
||||
}, [playing, progress, item, sessionData]);
|
||||
|
||||
const startPosition = useMemo(
|
||||
() =>
|
||||
item?.UserData?.PlaybackPositionTicks
|
||||
? Math.round(item.UserData.PlaybackPositionTicks / 10000)
|
||||
: 0,
|
||||
[item]
|
||||
[item],
|
||||
);
|
||||
|
||||
const backdropUrl = useMemo(
|
||||
@@ -198,7 +204,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
quality: 70,
|
||||
width: 200,
|
||||
}),
|
||||
[item]
|
||||
[item],
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -207,24 +213,24 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
*
|
||||
* The trigger playback is triggered from the button component.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (cp?.playbackUrl) {
|
||||
play();
|
||||
if (settings?.openFullScreenVideoPlayerByDefault) {
|
||||
videoRef.current?.presentFullscreenPlayer();
|
||||
}
|
||||
}
|
||||
}, [cp?.playbackUrl]);
|
||||
// useEffect(() => {
|
||||
// if (currentlyPlaying?.playbackUrl) {
|
||||
// play();
|
||||
// if (settings?.openFullScreenVideoPlayerByDefault) {
|
||||
// videoRef.current?.presentFullscreenPlayer();
|
||||
// }
|
||||
// }
|
||||
// }, [currentlyPlaying?.playbackUrl]);
|
||||
|
||||
const [triggerPlay] = useAtom(triggerPlayAtom);
|
||||
useEffect(() => {
|
||||
play();
|
||||
if (settings?.openFullScreenVideoPlayerByDefault) {
|
||||
videoRef.current?.presentFullscreenPlayer();
|
||||
}
|
||||
}, [triggerPlay]);
|
||||
// const [triggerPlay] = useAtom(triggerPlayAtom);
|
||||
// useEffect(() => {
|
||||
// setPlaying(true)
|
||||
// if (settings?.openFullScreenVideoPlayerByDefault) {
|
||||
// videoRef.current?.presentFullscreenPlayer();
|
||||
// }
|
||||
// }, [triggerPlay]);
|
||||
|
||||
if (!cp || !api) return null;
|
||||
if (!currentlyPlaying || !api) return null;
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
@@ -254,7 +260,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
${item?.Type === "Audio" ? "aspect-square" : "aspect-video"}
|
||||
`}
|
||||
>
|
||||
{cp.playbackUrl && (
|
||||
{currentlyPlaying.playbackUrl && (
|
||||
<Video
|
||||
ref={videoRef}
|
||||
allowsExternalPlayback
|
||||
@@ -274,13 +280,13 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
enable: true,
|
||||
thread: true,
|
||||
}}
|
||||
paused={paused}
|
||||
paused={!playing}
|
||||
onProgress={(e) => onProgress(e)}
|
||||
subtitleStyle={{
|
||||
fontSize: 16,
|
||||
}}
|
||||
source={{
|
||||
uri: cp.playbackUrl,
|
||||
uri: currentlyPlaying.playbackUrl,
|
||||
isNetwork: true,
|
||||
startPosition,
|
||||
headers: getAuthHeaders(api),
|
||||
@@ -296,11 +302,11 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
}}
|
||||
onPlaybackStateChanged={(e) => {
|
||||
if (e.isPlaying) {
|
||||
setPaused(false);
|
||||
setPlaying(true);
|
||||
} else if (e.isSeeking) {
|
||||
return;
|
||||
} else {
|
||||
pause();
|
||||
setPlaying(false);
|
||||
}
|
||||
}}
|
||||
progressUpdateInterval={1000}
|
||||
@@ -308,7 +314,7 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
console.log(e);
|
||||
writeToLog(
|
||||
"ERROR",
|
||||
"Video playback error: " + JSON.stringify(e)
|
||||
"Video playback error: " + JSON.stringify(e),
|
||||
);
|
||||
}}
|
||||
renderLoader={
|
||||
@@ -364,20 +370,20 @@ export const CurrentlyPlayingBar: React.FC = () => {
|
||||
<View className="flex flex-row items-center space-x-2">
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
if (paused) play();
|
||||
else pause();
|
||||
if (playing) setPlaying(false);
|
||||
else setPlaying(true);
|
||||
}}
|
||||
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
||||
>
|
||||
{paused ? (
|
||||
<Ionicons name="play" size={24} color="white" />
|
||||
) : (
|
||||
{playing ? (
|
||||
<Ionicons name="pause" size={24} color="white" />
|
||||
) : (
|
||||
<Ionicons name="play" size={24} color="white" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setCp(null);
|
||||
setCurrentlyPlaying(null);
|
||||
}}
|
||||
className="aspect-square rounded flex flex-col items-center justify-center p-2"
|
||||
>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Feather, Ionicons } from "@expo/vector-icons";
|
||||
import { runtimeTicksToMinutes } from "@/utils/time";
|
||||
import { useActionSheet } from "@expo/react-native-action-sheet";
|
||||
import { View } from "react-native";
|
||||
import { useAtom } from "jotai";
|
||||
import { playingAtom } from "./CurrentlyPlayingBar";
|
||||
|
||||
interface Props extends React.ComponentProps<typeof Button> {
|
||||
item: BaseItemDto;
|
||||
|
||||
Reference in New Issue
Block a user