diff --git a/app/(auth)/items/[id].tsx b/app/(auth)/items/[id].tsx index 024d0fce..d46168d6 100644 --- a/app/(auth)/items/[id].tsx +++ b/app/(auth)/items/[id].tsx @@ -1,43 +1,42 @@ -import { Text } from "@/components/common/Text"; +import { AudioTrackSelector } from "@/components/AudioTrackSelector"; +import { Bitrate, BitrateSelector } from "@/components/BitrateSelector"; +import { + currentlyPlayingItemAtom, + playingAtom, +} from "@/components/CurrentlyPlayingBar"; import { DownloadItem } from "@/components/DownloadItem"; +import { OverviewText } from "@/components/OverviewText"; +import { PlayButton } from "@/components/PlayButton"; import { PlayedStatus } from "@/components/PlayedStatus"; +import { Ratings } from "@/components/Ratings"; +import { SimilarItems } from "@/components/SimilarItems"; +import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector"; +import { Text } from "@/components/common/Text"; +import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader"; import { CastAndCrew } from "@/components/series/CastAndCrew"; import { CurrentSeries } from "@/components/series/CurrentSeries"; -import { SimilarItems } from "@/components/SimilarItems"; +import { NextEpisodeButton } from "@/components/series/NextEpisodeButton"; +import { SeriesTitleHeader } from "@/components/series/SeriesTitleHeader"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; +import { getLogoImageUrlById } from "@/utils/jellyfin/image/getLogoImageUrlById"; +import { getStreamUrl } from "@/utils/jellyfin/media/getStreamUrl"; +import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; +import { chromecastProfile } from "@/utils/profiles/chromecast"; +import ios12 from "@/utils/profiles/ios12"; +import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; import { useQuery } from "@tanstack/react-query"; import { Image } from "expo-image"; import { useLocalSearchParams } from "expo-router"; import { useAtom } from "jotai"; import { useCallback, useMemo, useState } from "react"; import { ActivityIndicator, ScrollView, View } from "react-native"; -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 CastContext, { PlayServicesState, useCastDevice, useRemoteMediaClient, } from "react-native-google-cast"; -import { chromecastProfile } from "@/utils/profiles/chromecast"; -import ios12 from "@/utils/profiles/ios12"; -import { - currentlyPlayingItemAtom, - playingAtom, - triggerPlayAtom, -} from "@/components/CurrentlyPlayingBar"; -import { AudioTrackSelector } from "@/components/AudioTrackSelector"; -import { SubtitleTrackSelector } from "@/components/SubtitleTrackSelector"; -import { NextEpisodeButton } from "@/components/series/NextEpisodeButton"; -import { Ratings } from "@/components/Ratings"; -import { SeriesTitleHeader } from "@/components/series/SeriesTitleHeader"; -import { MoviesTitleHeader } from "@/components/movies/MoviesTitleHeader"; -import { OverviewText } from "@/components/OverviewText"; +import { ParallaxScrollView } from "../../../components/ParallaxPage"; const page: React.FC = () => { const local = useLocalSearchParams(); diff --git a/components/CurrentlyPlayingBar.tsx b/components/CurrentlyPlayingBar.tsx index be4483f8..2143672e 100644 --- a/components/CurrentlyPlayingBar.tsx +++ b/components/CurrentlyPlayingBar.tsx @@ -1,41 +1,32 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { useSettings } from "@/utils/atoms/settings"; +import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; +import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; +import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress"; +import { reportPlaybackStopped } from "@/utils/jellyfin/playstate/reportPlaybackStopped"; +import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; +import { writeToLog } from "@/utils/log"; +import { Ionicons } from "@expo/vector-icons"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { BlurView } from "expo-blur"; +import { useRouter, useSegments } from "expo-router"; +import { atom, useAtom } from "jotai"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, Platform, TouchableOpacity, View, } from "react-native"; -import { Text } from "./common/Text"; -import { Ionicons } from "@expo/vector-icons"; -import { - Ref, - RefObject, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import Video, { OnProgressData, VideoRef } from "react-native-video"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { atom, useAtom } from "jotai"; -import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; -import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { getMediaInfoApi } from "@jellyfin/sdk/lib/utils/api"; -import { reportPlaybackProgress } from "@/utils/jellyfin/playstate/reportPlaybackProgress"; -import { reportPlaybackStopped } from "@/utils/jellyfin/playstate/reportPlaybackStopped"; import Animated, { useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; -import { useRouter, useSegments } from "expo-router"; -import { BlurView } from "expo-blur"; -import { writeToLog } from "@/utils/log"; -import { getBackdropUrl } from "@/utils/jellyfin/image/getBackdropUrl"; -import { getAuthHeaders } from "@/utils/jellyfin/jellyfin"; -import { useSettings } from "@/utils/atoms/settings"; -import * as ScreenOrientation from "expo-screen-orientation"; +import Video, { OnProgressData, VideoRef } from "react-native-video"; +import { Text } from "./common/Text"; export const currentlyPlayingItemAtom = atom<{ item: BaseItemDto; @@ -108,14 +99,6 @@ export const CurrentlyPlayingBar: React.FC = () => { } }, [segments]); - // TODO: Fix this - // useEffect(() => { - // if (settings?.forceLandscapeInVideoPlayer === true && fullScreen) - // ScreenOrientation.lockAsync( - // ScreenOrientation.OrientationLock.LANDSCAPE_LEFT, - // ); - // }, [settings, fullScreen]); - const { data: item } = useQuery({ queryKey: ["item", currentlyPlaying?.item.Id], queryFn: async () => @@ -207,29 +190,6 @@ export const CurrentlyPlayingBar: React.FC = () => { [item], ); - /** - * These two useEffects are used to start playing the - * video when the playbackUrl is available. - * - * The trigger playback is triggered from the button component. - */ - // useEffect(() => { - // if (currentlyPlaying?.playbackUrl) { - // play(); - // if (settings?.openFullScreenVideoPlayerByDefault) { - // videoRef.current?.presentFullscreenPlayer(); - // } - // } - // }, [currentlyPlaying?.playbackUrl]); - - // const [triggerPlay] = useAtom(triggerPlayAtom); - // useEffect(() => { - // setPlaying(true) - // if (settings?.openFullScreenVideoPlayerByDefault) { - // videoRef.current?.presentFullscreenPlayer(); - // } - // }, [triggerPlay]); - if (!currentlyPlaying || !api) return null; return ( diff --git a/components/PlayButton.tsx b/components/PlayButton.tsx index c915214e..bebcdf09 100644 --- a/components/PlayButton.tsx +++ b/components/PlayButton.tsx @@ -1,11 +1,9 @@ -import { Button } from "./Button"; -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { Feather, Ionicons } from "@expo/vector-icons"; import { runtimeTicksToMinutes } from "@/utils/time"; import { useActionSheet } from "@expo/react-native-action-sheet"; +import { Feather, Ionicons } from "@expo/vector-icons"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { View } from "react-native"; -import { useAtom } from "jotai"; -import { playingAtom } from "./CurrentlyPlayingBar"; +import { Button } from "./Button"; interface Props extends React.ComponentProps { item: BaseItemDto; diff --git a/components/downloads/EpisodeCard.tsx b/components/downloads/EpisodeCard.tsx index 3bcbf89e..f19a6e99 100644 --- a/components/downloads/EpisodeCard.tsx +++ b/components/downloads/EpisodeCard.tsx @@ -1,97 +1,85 @@ -import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import React, { useCallback } from "react"; import { TouchableOpacity } from "react-native"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import * as ContextMenu from "zeego/context-menu"; -import { Text } from "../common/Text"; -import { useFiles } from "@/hooks/useFiles"; import * as Haptics from "expo-haptics"; -import { useCallback } from "react"; import * as FileSystem from "expo-file-system"; import { useAtom } from "jotai"; + +import { Text } from "../common/Text"; +import { useFiles } from "@/hooks/useFiles"; import { currentlyPlayingItemAtom } from "../CurrentlyPlayingBar"; -export const EpisodeCard: React.FC<{ item: BaseItemDto }> = ({ item }) => { +interface EpisodeCardProps { + item: BaseItemDto; +} + +/** + * EpisodeCard component displays an episode with context menu options. + * @param {EpisodeCardProps} props - The component props. + * @returns {React.ReactElement} The rendered EpisodeCard component. + */ +export const EpisodeCard: React.FC = ({ item }) => { const { deleteFile } = useFiles(); - const [_, setCp] = useAtom(currentlyPlayingItemAtom); + const [, setCurrentlyPlaying] = useAtom(currentlyPlayingItemAtom); - // const fetchFileSize = async () => { - // try { - // const filePath = `${FileSystem.documentDirectory}/${item.Id}.mp4`; - // const info = await FileSystem.getInfoAsync(filePath); - // return info.exists ? info.size : null; - // } catch (e) { - // console.log(e); - // return null; - // } - // }; - - // const { data: fileSize } = useQuery({ - // queryKey: ["fileSize", item?.Id], - // queryFn: fetchFileSize, - // }); - - const openFile = useCallback(() => { - setCp({ + /** + * Handles opening the file for playback. + */ + const handleOpenFile = useCallback(() => { + setCurrentlyPlaying({ item, playbackUrl: `${FileSystem.documentDirectory}/${item.Id}.mp4`, }); - }, [item]); + }, [item, setCurrentlyPlaying]); - const options = [ + /** + * Handles deleting the file with haptic feedback. + */ + const handleDeleteFile = useCallback(() => { + if (item.Id) { + deleteFile(item.Id); + Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); + } + }, [deleteFile, item.Id]); + + const contextMenuOptions = [ { label: "Delete", - onSelect: (id: string) => { - deleteFile(id); - Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); - }, + onSelect: handleDeleteFile, destructive: true, }, ]; return ( - <> - - - - {item.Name} - - Episode {item.IndexNumber} - - {/* - Size:{" "} - {fileSize - ? `${(fileSize / 1000000).toFixed(0)} MB` - : "Calculating..."}{" "} - */} - - - + + - {options.map((i) => ( - { - i.onSelect(item.Id!); - }} - key={i.label} - destructive={i.destructive} - > - - {i.label} - - - ))} - - - + {item.Name} + Episode {item.IndexNumber} + + + + {contextMenuOptions.map((option) => ( + + + {option.label} + + + ))} + + ); }; diff --git a/utils/jellyfin/media/getStreamUrl.ts b/utils/jellyfin/media/getStreamUrl.ts index 7c3b0d4a..86587677 100644 --- a/utils/jellyfin/media/getStreamUrl.ts +++ b/utils/jellyfin/media/getStreamUrl.ts @@ -2,8 +2,8 @@ import ios from "@/utils/profiles/ios"; import { Api } from "@jellyfin/sdk"; import { BaseItemDto, - PlaybackInfoResponse, MediaSourceInfo, + PlaybackInfoResponse, } from "@jellyfin/sdk/lib/generated-client/models"; export const getStreamUrl = async ({