diff --git a/app/(auth)/(tabs)/(home)/index.tsx b/app/(auth)/(tabs)/(home)/index.tsx index cb0b43bd..c3a48f93 100644 --- a/app/(auth)/(tabs)/(home)/index.tsx +++ b/app/(auth)/(tabs)/(home)/index.tsx @@ -25,7 +25,7 @@ import { import NetInfo from "@react-native-community/netinfo"; import { QueryFunction, useQuery, useQueryClient } from "@tanstack/react-query"; import { useNavigation, useRouter } from "expo-router"; -import { useAtom } from "jotai"; +import { useAtom, useAtomValue } from "jotai"; import { useCallback, useEffect, useMemo, useState } from "react"; import { ActivityIndicator, @@ -57,8 +57,8 @@ export default function index() { const queryClient = useQueryClient(); const router = useRouter(); - const [api] = useAtom(apiAtom); - const [user] = useAtom(userAtom); + const api = useAtomValue(apiAtom); + const user = useAtomValue(userAtom); const [loading, setLoading] = useState(false); const [settings, _] = useSettings(); @@ -226,6 +226,7 @@ export default function index() { await getItemsApi(api).getResumeItems({ userId: user.Id, enableImageTypes: ["Primary", "Backdrop", "Thumb"], + includeItemTypes: ["Movie", "Series", "Episode"], }) ).data.Items || [], type: "ScrollingCollectionList", @@ -340,7 +341,7 @@ export default function index() { const insets = useSafeAreaInsets(); - if (e1 || e2 || !api) + if (e1 || e2) return ( Oops! diff --git a/app/(auth)/(tabs)/(home)/settings.tsx b/app/(auth)/(tabs)/(home)/settings.tsx index 7077a16f..28b9033c 100644 --- a/app/(auth)/(tabs)/(home)/settings.tsx +++ b/app/(auth)/(tabs)/(home)/settings.tsx @@ -74,7 +74,7 @@ export default function settings() { registerBackgroundFetchAsync */} - Information + User Info diff --git a/app/(auth)/play-music.tsx b/app/(auth)/play-music.tsx index d6c3266c..4138ecc2 100644 --- a/app/(auth)/play-music.tsx +++ b/app/(auth)/play-music.tsx @@ -1,12 +1,308 @@ -import { StatusBar } from "expo-status-bar"; -import { View, ViewProps } from "react-native"; - -interface Props extends ViewProps {} +import { Text } from "@/components/common/Text"; +import AlbumCover from "@/components/posters/AlbumCover"; +import { Controls } from "@/components/video-player/Controls"; +import { useAndroidNavigationBar } from "@/hooks/useAndroidNavigationBar"; +import { useOrientation } from "@/hooks/useOrientation"; +import { useOrientationSettings } from "@/hooks/useOrientationSettings"; +import { useWebSocket } from "@/hooks/useWebsockets"; +import { apiAtom } from "@/providers/JellyfinProvider"; +import { + PlaybackType, + usePlaySettings, +} from "@/providers/PlaySettingsProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; +import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; +import { secondsToTicks } from "@/utils/secondsToTicks"; +import { Api } from "@jellyfin/sdk"; +import { getPlaystateApi } from "@jellyfin/sdk/lib/utils/api"; +import * as Haptics from "expo-haptics"; +import { Image } from "expo-image"; +import { useFocusEffect } from "expo-router"; +import { useAtomValue } from "jotai"; +import { debounce } from "lodash"; +import React, { useCallback, useMemo, useRef, useState } from "react"; +import { Dimensions, Pressable, StatusBar, View } from "react-native"; +import { useSharedValue } from "react-native-reanimated"; +import Video, { OnProgressData, VideoRef } from "react-native-video"; export default function page() { + const { playSettings, playUrl, playSessionId } = usePlaySettings(); + const api = useAtomValue(apiAtom); + const [settings] = useSettings(); + const videoRef = useRef(null); + const poster = usePoster(playSettings, api); + const videoSource = useVideoSource(playSettings, api, poster, playUrl); + const firstTime = useRef(true); + + const screenDimensions = Dimensions.get("screen"); + + const [isPlaybackStopped, setIsPlaybackStopped] = useState(false); + const [showControls, setShowControls] = useState(true); + const [ignoreSafeAreas, setIgnoreSafeAreas] = useState(false); + const [isPlaying, setIsPlaying] = useState(false); + const [isBuffering, setIsBuffering] = useState(true); + + const progress = useSharedValue(0); + const isSeeking = useSharedValue(false); + const cacheProgress = useSharedValue(0); + + if (!playSettings || !playUrl || !api || !videoSource || !playSettings.item) + return null; + + const togglePlay = useCallback( + async (ticks: number) => { + console.log("togglePlay"); + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + if (isPlaying) { + videoRef.current?.pause(); + await getPlaystateApi(api).onPlaybackProgress({ + itemId: playSettings.item?.Id!, + audioStreamIndex: playSettings.audioIndex + ? playSettings.audioIndex + : undefined, + subtitleStreamIndex: playSettings.subtitleIndex + ? playSettings.subtitleIndex + : undefined, + mediaSourceId: playSettings.mediaSource?.Id!, + positionTicks: Math.floor(ticks), + isPaused: true, + playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream", + playSessionId: playSessionId ? playSessionId : undefined, + }); + } else { + videoRef.current?.resume(); + await getPlaystateApi(api).onPlaybackProgress({ + itemId: playSettings.item?.Id!, + audioStreamIndex: playSettings.audioIndex + ? playSettings.audioIndex + : undefined, + subtitleStreamIndex: playSettings.subtitleIndex + ? playSettings.subtitleIndex + : undefined, + mediaSourceId: playSettings.mediaSource?.Id!, + positionTicks: Math.floor(ticks), + isPaused: false, + playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream", + playSessionId: playSessionId ? playSessionId : undefined, + }); + } + }, + [isPlaying, api, playSettings?.item?.Id, videoRef, settings] + ); + + const play = useCallback(() => { + console.log("play"); + videoRef.current?.resume(); + reportPlaybackStart(); + }, [videoRef]); + + const pause = useCallback(() => { + console.log("play"); + videoRef.current?.pause(); + }, [videoRef]); + + const stop = useCallback(() => { + console.log("stop"); + setIsPlaybackStopped(true); + videoRef.current?.pause(); + reportPlaybackStopped(); + }, [videoRef]); + + const reportPlaybackStopped = async () => { + await getPlaystateApi(api).onPlaybackStopped({ + itemId: playSettings?.item?.Id!, + mediaSourceId: playSettings.mediaSource?.Id!, + positionTicks: Math.floor(progress.value), + playSessionId: playSessionId ? playSessionId : undefined, + }); + }; + + const reportPlaybackStart = async () => { + await getPlaystateApi(api).onPlaybackStart({ + itemId: playSettings?.item?.Id!, + audioStreamIndex: playSettings.audioIndex + ? playSettings.audioIndex + : undefined, + subtitleStreamIndex: playSettings.subtitleIndex + ? playSettings.subtitleIndex + : undefined, + mediaSourceId: playSettings.mediaSource?.Id!, + playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream", + playSessionId: playSessionId ? playSessionId : undefined, + }); + }; + + const onProgress = useCallback( + async (data: OnProgressData) => { + if (isSeeking.value === true) return; + if (isPlaybackStopped === true) return; + + const ticks = data.currentTime * 10000000; + + progress.value = secondsToTicks(data.currentTime); + cacheProgress.value = secondsToTicks(data.playableDuration); + setIsBuffering(data.playableDuration === 0); + + if (!playSettings?.item?.Id || data.currentTime === 0) return; + + await getPlaystateApi(api).onPlaybackProgress({ + itemId: playSettings.item.Id, + audioStreamIndex: playSettings.audioIndex + ? playSettings.audioIndex + : undefined, + subtitleStreamIndex: playSettings.subtitleIndex + ? playSettings.subtitleIndex + : undefined, + mediaSourceId: playSettings.mediaSource?.Id!, + positionTicks: Math.round(ticks), + isPaused: !isPlaying, + playMethod: playUrl.includes("m3u8") ? "Transcode" : "DirectStream", + playSessionId: playSessionId ? playSessionId : undefined, + }); + }, + [playSettings?.item.Id, isPlaying, api, isPlaybackStopped] + ); + + useFocusEffect( + useCallback(() => { + play(); + + return () => { + stop(); + }; + }, [play, stop]) + ); + + const { orientation } = useOrientation(); + useOrientationSettings(); + useAndroidNavigationBar(); + + useWebSocket({ + isPlaying: isPlaying, + pauseVideo: pause, + playVideo: play, + stopPlayback: stop, + }); + return ( - -