From a4cd3ea6003b3b00bb328a30074096b096af4043 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 07:15:34 +1100 Subject: [PATCH 01/12] WIP --- components/ContinueWatchingPoster.tsx | 36 +- components/series/SeasonPicker.tsx | 24 +- components/video-player/controls/Controls.tsx | 576 ++++++++++-------- .../video-player/controls/EpisodeList.tsx | 225 +++++++ .../video-player/controls/GotoEpisode.tsx | 0 5 files changed, 583 insertions(+), 278 deletions(-) create mode 100644 components/video-player/controls/EpisodeList.tsx create mode 100644 components/video-player/controls/GotoEpisode.tsx diff --git a/components/ContinueWatchingPoster.tsx b/components/ContinueWatchingPoster.tsx index 641927f2..eb000d45 100644 --- a/components/ContinueWatchingPoster.tsx +++ b/components/ContinueWatchingPoster.tsx @@ -1,27 +1,30 @@ import { apiAtom } from "@/providers/JellyfinProvider"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { Image } from "expo-image"; -import { useAtom, useAtomValue } from "jotai"; -import { useMemo, useState } from "react"; +import { useAtomValue } from "jotai"; +import { useMemo } from "react"; import { View } from "react-native"; import { WatchedIndicator } from "./WatchedIndicator"; import React from "react"; +import { Ionicons } from "@expo/vector-icons"; type ContinueWatchingPosterProps = { item: BaseItemDto; useEpisodePoster?: boolean; size?: "small" | "normal"; + showPlayButton?: boolean; }; const ContinueWatchingPoster: React.FC = ({ item, useEpisodePoster = false, size = "normal", + showPlayButton = false, }) => { const api = useAtomValue(apiAtom); /** - * Get horrizontal poster for movie and episode, with failover to primary. + * Get horizontal poster for movie and episode, with failover to primary. */ const url = useMemo(() => { if (!api) return; @@ -73,16 +76,23 @@ const ContinueWatchingPoster: React.FC = ({ ${size === "small" ? "w-32" : "w-44"} `} > - + + + {showPlayButton && ( + + + + )} + {!progress && } {progress > 0 && ( <> diff --git a/components/series/SeasonPicker.tsx b/components/series/SeasonPicker.tsx index d968532b..28ca9ccb 100644 --- a/components/series/SeasonPicker.tsx +++ b/components/series/SeasonPicker.tsx @@ -6,14 +6,17 @@ import { atom, useAtom } from "jotai"; import { useEffect, useMemo, useState } from "react"; import { View } from "react-native"; import ContinueWatchingPoster from "../ContinueWatchingPoster"; -import {DownloadItems, DownloadSingleItem} from "../DownloadItem"; +import { DownloadItems, DownloadSingleItem } from "../DownloadItem"; import { Loader } from "../Loader"; import { Text } from "../common/Text"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; import { TouchableItemRouter } from "../common/TouchableItemRouter"; -import {SeasonDropdown, SeasonIndexState} from "@/components/series/SeasonDropdown"; -import {Ionicons, MaterialCommunityIcons} from "@expo/vector-icons"; +import { + SeasonDropdown, + SeasonIndexState, +} from "@/components/series/SeasonDropdown"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; type Props = { item: BaseItemDto; @@ -119,14 +122,23 @@ export const SeasonPicker: React.FC = ({ item, initialSeasonIndex }) => { ...prev, [item.Id ?? ""]: season.IndexNumber, })); - }} /> + }} + /> ( - + )} DownloadedIconComponent={() => ( - + )} /> diff --git a/components/video-player/controls/Controls.tsx b/components/video-player/controls/Controls.tsx index c74c9c09..fc18d59d 100644 --- a/components/video-player/controls/Controls.tsx +++ b/components/video-player/controls/Controls.tsx @@ -53,6 +53,11 @@ import DropdownViewTranscoding from "./dropdown/DropdownViewTranscoding"; import BrightnessSlider from "./BrightnessSlider"; import SkipButton from "./SkipButton"; import { debounce } from "lodash"; +import { EpisodeList } from "./EpisodeList"; +import { BlurView } from "expo-blur"; +import { getItemById } from "@/utils/jellyfin/user-library/getItemById"; +import { useAtom } from "jotai"; +import { apiAtom } from "@/providers/JellyfinProvider"; interface Props { item: BaseItemDto; @@ -113,6 +118,7 @@ export const Controls: React.FC = ({ const [settings] = useSettings(); const router = useRouter(); const insets = useSafeAreaInsets(); + const [api] = useAtom(apiAtom); const { previousItem, nextItem } = useAdjacentItems({ item }); const { @@ -397,292 +403,344 @@ export const Controls: React.FC = ({ ); }, [trickPlayUrl, trickplayInfo, time]); + const [EpisodeView, setEpisodeView] = useState(false); + + const switchOnEpisodeMode = () => { + setEpisodeView(true); + if (isPlaying) togglePlay(progress.value); + }; + + const gotoEpisode = async (itemId: string) => { + const item = await getItemById(api, itemId); + console.log("Item", item); + if (!settings || !item) return; + + Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); + + const { bitrate, mediaSource, audioIndex, subtitleIndex } = + getDefaultPlaySettings(item, settings); + + const queryParams = new URLSearchParams({ + itemId: item.Id ?? "", // Ensure itemId is a string + audioIndex: audioIndex?.toString() ?? "", + subtitleIndex: subtitleIndex?.toString() ?? "", + mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string + bitrateValue: bitrate.toString(), + }).toString(); + + if (!bitrate.value) { + // @ts-expect-error + router.replace(`player/direct-player?${queryParams}`); + return; + } + // @ts-expect-error + router.replace(`player/transcoding-player?${queryParams}`); + }; + return ( - - {!mediaSource?.TranscodingUrl ? ( - - ) : ( - - )} - - - { - toggleControls(); - }} - style={{ - position: "absolute", - width: Dimensions.get("window").width, - height: Dimensions.get("window").height, - }} - > - - - {previousItem && ( - setEpisodeView(false)} /> + ) : ( + <> + - - - )} + {!mediaSource?.TranscodingUrl ? ( + + ) : ( + + )} + - {nextItem && ( - - - - )} - - {mediaSource?.TranscodingUrl && ( - - - - )} - { - if (stop) await stop(); - router.back(); - }} - className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" - > - - - - - - - - - - { + toggleControls(); }} - > - - + + - {settings?.rewindSkipTime} - - - - - { - togglePlay(progress.value); - }} - > - {!isBuffering ? ( - - ) : ( - - )} - - - - - - - {settings?.forwardSkipTime} - - - - - - - - - {item?.Name} {item?.Type === "Episode" && ( - {item.SeriesName} + { + switchOnEpisodeMode(); + }} + className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + > + + )} - {item?.Type === "Movie" && ( - {item?.ProductionYear} + {previousItem && ( + + + )} - {item?.Type === "Audio" && ( - {item?.Album} + + {nextItem && ( + + + )} + + {mediaSource?.TranscodingUrl && ( + + + + )} + { + if (stop) await stop(); + router.back(); + }} + className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + > + + + - - - - - - - ( - + + + + + - )} - cache={cacheProgress} - onSlidingStart={handleSliderStart} - onSlidingComplete={handleSliderComplete} - onValueChange={handleSliderChange} - containerStyle={{ - borderRadius: 100, + + {settings?.rewindSkipTime} + + + + + { + togglePlay(progress.value); }} - renderBubble={() => isSliding && memoizedRenderBubble()} - sliderHeight={10} - thumbWidth={0} - progress={progress} - minimumValue={min} - maximumValue={max} - /> - - - {formatTimeString(currentTime, isVlc ? "ms" : "s")} - - - -{formatTimeString(remainingTime, isVlc ? "ms" : "s")} - + > + {!isBuffering ? ( + + ) : ( + + )} + + + + + + + {settings?.forwardSkipTime} + + + + + + + + + {item?.Name} + {item?.Type === "Episode" && ( + {item.SeriesName} + )} + {item?.Type === "Movie" && ( + + {item?.ProductionYear} + + )} + {item?.Type === "Audio" && ( + {item?.Album} + )} + + + + + + + + + ( + + )} + cache={cacheProgress} + onSlidingStart={handleSliderStart} + onSlidingComplete={handleSliderComplete} + onValueChange={handleSliderChange} + containerStyle={{ + borderRadius: 100, + }} + renderBubble={() => isSliding && memoizedRenderBubble()} + sliderHeight={10} + thumbWidth={0} + progress={progress} + minimumValue={min} + maximumValue={max} + /> + + + {formatTimeString(currentTime, isVlc ? "ms" : "s")} + + + -{formatTimeString(remainingTime, isVlc ? "ms" : "s")} + + + - - + + )} ); }; diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx new file mode 100644 index 00000000..0795fc6f --- /dev/null +++ b/components/video-player/controls/EpisodeList.tsx @@ -0,0 +1,225 @@ +import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; +import { runtimeTicksToSeconds } from "@/utils/time"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useAtom } from "jotai"; +import { useEffect, useMemo, useState, useRef } from "react"; +import { View, TouchableOpacity } from "react-native"; +import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; +import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; +import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import { Loader } from "@/components/Loader"; +import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; +import { Text } from "@/components/common/Text"; +import { DownloadSingleItem } from "@/components/DownloadItem"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { + HorizontalScroll, + HorizontalScrollRef, +} from "@/components/common/HorrizontalScroll"; +import { router } from "expo-router"; +import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; +import { getItemById } from "@/utils/jellyfin/user-library/getItemById"; +import { useSettings } from "@/utils/atoms/settings"; + +type Props = { + item: BaseItemDto; + close: () => void; +}; + +export const EpisodeList: React.FC = ({ item, close }) => { + const [api] = useAtom(apiAtom); + const [user] = useAtom(userAtom); + const scrollViewRef = useRef(null); // Reference to the HorizontalScroll + const insets = useSafeAreaInsets(); // Get safe area insets + const [settings] = useSettings(); + const SeasonId = item.ParentId; + + const { data: episodes, isFetching } = useQuery({ + queryKey: ["episodes", SeasonId], + queryFn: async () => { + if (!api || !user?.Id || !item.Id || !SeasonId) return []; + const res = await getTvShowsApi(api).getEpisodes({ + seriesId: item.Id, + userId: user.Id, + seasonId: SeasonId, + enableUserData: true, + fields: ["MediaSources", "MediaStreams", "Overview"], + }); + + return res.data.Items; + }, + enabled: !!api && !!user?.Id && !!item.Id && !!SeasonId, + }); + + const queryClient = useQueryClient(); + useEffect(() => { + for (let e of episodes || []) { + queryClient.prefetchQuery({ + queryKey: ["item", e.Id], + queryFn: async () => { + if (!e.Id) return; + const res = await getUserItemData({ + api, + userId: user?.Id, + itemId: e.Id, + }); + return res; + }, + staleTime: 60 * 5 * 1000, + }); + } + }, [episodes]); + + // Used for width calculation + const [nrOfEpisodes, setNrOfEpisodes] = useState(0); + useEffect(() => { + if (episodes && episodes.length > 0) { + setNrOfEpisodes(episodes.length); + } + }, [episodes]); + + // Scroll to the current item when episodes are fetched + useEffect(() => { + if (episodes && scrollViewRef.current) { + const currentItemIndex = episodes.findIndex((e) => e.Id === item.Id); + if (currentItemIndex !== -1) { + scrollViewRef.current.scrollToIndex(currentItemIndex, 16); // Adjust the scroll position based on item width + } + } + }, [episodes, item.Id]); + + const gotoEpisode = async (itemId: string) => { + const item = await getItemById(api, itemId); + console.log("Item", item); + if (!settings || !item) return; + + const { bitrate, mediaSource, audioIndex, subtitleIndex } = + getDefaultPlaySettings(item, settings); + + const queryParams = new URLSearchParams({ + itemId: item.Id ?? "", // Ensure itemId is a string + audioIndex: audioIndex?.toString() ?? "", + subtitleIndex: subtitleIndex?.toString() ?? "", + mediaSourceId: mediaSource?.Id ?? "", // Ensure mediaSourceId is a string + bitrateValue: bitrate.toString(), + }).toString(); + + if (!bitrate.value) { + // @ts-expect-error + router.replace(`player/direct-player?${queryParams}`); + return; + } + // @ts-expect-error + router.replace(`player/transcoding-player?${queryParams}`); + }; + + return ( + + {isFetching ? ( + + + + ) : ( + <> + + { + close(); + }} + className="aspect-square flex flex-col bg-neutral-800/90 rounded-xl items-center justify-center p-2" + > + + + + + ( + + { + gotoEpisode(_item.Id); + }} + > + + + + + {_item.Name} + + + {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`} + + + {runtimeTicksToSeconds(_item.RunTimeTicks)} + + + + + + + {_item.Overview} + + + )} + keyExtractor={(e: BaseItemDto) => e.Id} + estimatedItemSize={200} + showsHorizontalScrollIndicator={false} + contentContainerStyle={{ + paddingHorizontal: 16, + }} + /> + + + )} + + ); +}; diff --git a/components/video-player/controls/GotoEpisode.tsx b/components/video-player/controls/GotoEpisode.tsx new file mode 100644 index 00000000..e69de29b From de4f60f56468faea5eee17a7d030a04f18656df6 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 07:44:35 +1100 Subject: [PATCH 02/12] WIP --- components/series/SeasonDropdown.tsx | 67 ++++++++------- .../video-player/controls/EpisodeList.tsx | 81 +++++++++++++++++-- 2 files changed, 109 insertions(+), 39 deletions(-) diff --git a/components/series/SeasonDropdown.tsx b/components/series/SeasonDropdown.tsx index f34609ea..1d007c64 100644 --- a/components/series/SeasonDropdown.tsx +++ b/components/series/SeasonDropdown.tsx @@ -1,22 +1,22 @@ -import {BaseItemDto} from "@jellyfin/sdk/lib/generated-client/models"; -import {useEffect, useMemo} from "react"; -import {TouchableOpacity, View} from "react-native"; +import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; +import { useEffect, useMemo } from "react"; +import { TouchableOpacity, View } from "react-native"; import * as DropdownMenu from "zeego/dropdown-menu"; -import {Text} from "../common/Text"; +import { Text } from "../common/Text"; type Props = { item: BaseItemDto; seasons: BaseItemDto[]; initialSeasonIndex?: number; state: SeasonIndexState; - onSelect: (season: BaseItemDto) => void + onSelect: (season: BaseItemDto) => void; }; type SeasonKeys = { - id: keyof BaseItemDto, - title: keyof BaseItemDto, - index: keyof BaseItemDto -} + id: keyof BaseItemDto; + title: keyof BaseItemDto; + index: keyof BaseItemDto; +}; export type SeasonIndexState = { [seriesId: string]: number | null | undefined; @@ -27,19 +27,22 @@ export const SeasonDropdown: React.FC = ({ seasons, initialSeasonIndex, state, - onSelect + onSelect, }) => { - const keys = useMemo(() => - item.Type === "Episode" ? { - id: "ParentId", - title: "SeasonName", - index: "ParentIndexNumber" - } - : { - id: "Id", - title: "Name", - index: "IndexNumber" - }, [item] + const keys = useMemo( + () => + item.Type === "Episode" + ? { + id: "ParentId", + title: "SeasonName", + index: "ParentIndexNumber", + } + : { + id: "Id", + title: "Name", + index: "IndexNumber", + }, + [item] ); const seasonIndex = useMemo(() => state[item[keys.id] ?? ""], [state]); @@ -62,28 +65,28 @@ export const SeasonDropdown: React.FC = ({ const season1 = seasons.find((season: any) => season[keys.index] === 1); const season0 = seasons.find((season: any) => season[keys.index] === 0); const firstSeason = season1 || season0 || seasons[0]; - onSelect(firstSeason) + onSelect(firstSeason); } if (initialIndex !== undefined) { - const initialSeason = seasons.find((season: any) => - season[keys.index] === initialIndex - ) + const initialSeason = seasons.find( + (season: any) => season[keys.index] === initialIndex + ); - if (initialSeason) onSelect(initialSeason!) - else throw Error("Initial index could not be found!") + if (initialSeason) onSelect(initialSeason!); + else throw Error("Initial index could not be found!"); } } }, [seasons, seasonIndex, item[keys.id], initialSeasonIndex]); - const sortByIndex = (a: BaseItemDto, b: BaseItemDto) => a[keys.index] - b[keys.index]; + const sortByIndex = (a: BaseItemDto, b: BaseItemDto) => + a[keys.index] - b[keys.index]; return ( - + Season {seasonIndex} @@ -103,7 +106,9 @@ export const SeasonDropdown: React.FC = ({ key={season[keys.title]} onSelect={() => onSelect(season)} > - {season[keys.title]} + + {season[keys.title]} + ))} diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 0795fc6f..cf968aed 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -2,7 +2,7 @@ import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { runtimeTicksToSeconds } from "@/utils/time"; import { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; import { useQuery, useQueryClient } from "@tanstack/react-query"; -import { useAtom } from "jotai"; +import { atom, useAtom } from "jotai"; import { useEffect, useMemo, useState, useRef } from "react"; import { View, TouchableOpacity } from "react-native"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; @@ -21,35 +21,88 @@ import { router } from "expo-router"; import { getDefaultPlaySettings } from "@/utils/jellyfin/getDefaultPlaySettings"; import { getItemById } from "@/utils/jellyfin/user-library/getItemById"; import { useSettings } from "@/utils/atoms/settings"; +import { + SeasonDropdown, + SeasonIndexState, +} from "@/components/series/SeasonDropdown"; +import { Item } from "zeego/dropdown-menu"; type Props = { item: BaseItemDto; close: () => void; }; +export const seasonIndexAtom = atom({}); + export const EpisodeList: React.FC = ({ item, close }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); const scrollViewRef = useRef(null); // Reference to the HorizontalScroll const insets = useSafeAreaInsets(); // Get safe area insets const [settings] = useSettings(); - const SeasonId = item.ParentId; + + const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); + const seasonIndex = seasonIndexState[item.Id ?? ""]; + + const [seriesItem, setSeriesItem] = useState(null); + + useEffect(() => { + if (item.SeriesId) { + getUserItemData({ api, userId: user?.Id, itemId: item.SeriesId }).then( + (res) => { + setSeriesItem(res); + } + ); + } + }, [item.SeriesId]); + + const { data: seasons } = useQuery({ + queryKey: ["seasons", item.SeriesId], + queryFn: async () => { + console.log("Seasons", Boolean(api), user?.Id, item.SeriesId); + if (!api || !user?.Id || !item.SeriesId) return []; + console.log("Seasons", "Fetching"); + const response = await api.axiosInstance.get( + `${api.basePath}/Shows/${item.SeriesId}/Seasons`, + { + params: { + userId: user?.Id, + itemId: item.SeriesId, + Fields: + "ItemCounts,PrimaryImageAspectRatio,CanDelete,MediaSourceCount", + }, + headers: { + Authorization: `MediaBrowser DeviceId="${api.deviceInfo.id}", Token="${api.accessToken}"`, + }, + } + ); + console.log("Response", response.data.Items); + return response.data.Items; + }, + enabled: !!api && !!user?.Id && !!item.SeasonId, + }); + + const selectedSeasonId: string | null = useMemo( + () => + seasons?.find((season: any) => season.IndexNumber === seasonIndex)?.Id, + [seasons, seasonIndex] + ); const { data: episodes, isFetching } = useQuery({ - queryKey: ["episodes", SeasonId], + queryKey: ["episodes", item.SeriesId, item.SeasonId], queryFn: async () => { - if (!api || !user?.Id || !item.Id || !SeasonId) return []; + if (!api || !user?.Id || !item.Id || !item.SeasonId) return []; const res = await getTvShowsApi(api).getEpisodes({ - seriesId: item.Id, + seriesId: item.SeriesId || "", userId: user.Id, - seasonId: SeasonId, + seasonId: item.SeasonId || undefined, enableUserData: true, fields: ["MediaSources", "MediaStreams", "Overview"], }); return res.data.Items; }, - enabled: !!api && !!user?.Id && !!item.Id && !!SeasonId, + enabled: !!api && !!user?.Id && !!item.SeasonId, }); const queryClient = useQueryClient(); @@ -146,6 +199,18 @@ export const EpisodeList: React.FC = ({ item, close }) => { }} className={`flex flex-row items-center space-x-2`} > + { + setSeasonIndexState((prev) => ({ + ...prev, + [item.SeasonId ?? ""]: season.IndexNumber, + })); + }} + /> { close(); @@ -182,7 +247,7 @@ export const EpisodeList: React.FC = ({ item, close }) => { From 5b290fd6677d7ce4be092674d826768f53a92182 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 17:18:44 +1100 Subject: [PATCH 03/12] Got season dropdown to start working --- .../video-player/controls/EpisodeList.tsx | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index cf968aed..a1ef8f7d 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -7,7 +7,7 @@ import { useEffect, useMemo, useState, useRef } from "react"; import { View, TouchableOpacity } from "react-native"; import { getTvShowsApi } from "@jellyfin/sdk/lib/utils/api"; import { getUserItemData } from "@/utils/jellyfin/user-library/getUserItemData"; -import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import { Ionicons } from "@expo/vector-icons"; import { Loader } from "@/components/Loader"; import ContinueWatchingPoster from "@/components/ContinueWatchingPoster"; import { Text } from "@/components/common/Text"; @@ -42,7 +42,7 @@ export const EpisodeList: React.FC = ({ item, close }) => { const [settings] = useSettings(); const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); - const seasonIndex = seasonIndexState[item.Id ?? ""]; + const seasonIndex = seasonIndexState[item.SeriesId ?? ""]; const [seriesItem, setSeriesItem] = useState(null); @@ -59,9 +59,7 @@ export const EpisodeList: React.FC = ({ item, close }) => { const { data: seasons } = useQuery({ queryKey: ["seasons", item.SeriesId], queryFn: async () => { - console.log("Seasons", Boolean(api), user?.Id, item.SeriesId); if (!api || !user?.Id || !item.SeriesId) return []; - console.log("Seasons", "Fetching"); const response = await api.axiosInstance.get( `${api.basePath}/Shows/${item.SeriesId}/Seasons`, { @@ -76,7 +74,6 @@ export const EpisodeList: React.FC = ({ item, close }) => { }, } ); - console.log("Response", response.data.Items); return response.data.Items; }, enabled: !!api && !!user?.Id && !!item.SeasonId, @@ -89,20 +86,20 @@ export const EpisodeList: React.FC = ({ item, close }) => { ); const { data: episodes, isFetching } = useQuery({ - queryKey: ["episodes", item.SeriesId, item.SeasonId], + queryKey: ["episodes", item.SeriesId, selectedSeasonId], queryFn: async () => { - if (!api || !user?.Id || !item.Id || !item.SeasonId) return []; + if (!api || !user?.Id || !item.Id || !selectedSeasonId) return []; const res = await getTvShowsApi(api).getEpisodes({ seriesId: item.SeriesId || "", userId: user.Id, - seasonId: item.SeasonId || undefined, + seasonId: selectedSeasonId || undefined, enableUserData: true, fields: ["MediaSources", "MediaStreams", "Overview"], }); return res.data.Items; }, - enabled: !!api && !!user?.Id && !!item.SeasonId, + enabled: !!api && !!user?.Id && !!selectedSeasonId, }); const queryClient = useQueryClient(); @@ -144,7 +141,6 @@ export const EpisodeList: React.FC = ({ item, close }) => { const gotoEpisode = async (itemId: string) => { const item = await getItemById(api, itemId); - console.log("Item", item); if (!settings || !item) return; const { bitrate, mediaSource, audioIndex, subtitleIndex } = @@ -200,14 +196,13 @@ export const EpisodeList: React.FC = ({ item, close }) => { className={`flex flex-row items-center space-x-2`} > { setSeasonIndexState((prev) => ({ ...prev, - [item.SeasonId ?? ""]: season.IndexNumber, + [item.SeriesId ?? ""]: season.IndexNumber, })); }} /> From a725af114c282a533f16aa9e88a020d2d3771b8b Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 17:26:17 +1100 Subject: [PATCH 04/12] Fixed playbutton showing up on current Episode --- components/video-player/controls/EpisodeList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index a1ef8f7d..80bd4cb1 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -163,6 +163,8 @@ export const EpisodeList: React.FC = ({ item, close }) => { router.replace(`player/transcoding-player?${queryParams}`); }; + console.log("itemId", item.Id); + return ( = ({ item, close }) => { - + {_item.Name} Date: Sun, 8 Dec 2024 17:26:48 +1100 Subject: [PATCH 05/12] Fixed playbutton showing up on current Episode --- components/video-player/controls/EpisodeList.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 80bd4cb1..0acf41e1 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -163,8 +163,6 @@ export const EpisodeList: React.FC = ({ item, close }) => { router.replace(`player/transcoding-player?${queryParams}`); }; - console.log("itemId", item.Id); - return ( = ({ item, close }) => { /> - + {_item.Name} Date: Sun, 8 Dec 2024 18:03:06 +1100 Subject: [PATCH 06/12] Added style changes for episode list --- .../video-player/controls/EpisodeList.tsx | 43 +++++++------------ 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 0acf41e1..3f1aeab9 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -121,14 +121,6 @@ export const EpisodeList: React.FC = ({ item, close }) => { } }, [episodes]); - // Used for width calculation - const [nrOfEpisodes, setNrOfEpisodes] = useState(0); - useEffect(() => { - if (episodes && episodes.length > 0) { - setNrOfEpisodes(episodes.length); - } - }, [episodes]); - // Scroll to the current item when episodes are fetched useEffect(() => { if (episodes && scrollViewRef.current) { @@ -171,16 +163,11 @@ export const EpisodeList: React.FC = ({ item, close }) => { left: insets.left, right: insets.right, bottom: insets.bottom, - backgroundColor: "rgba(0, 0, 0, 0.85)", // Dark transparent background + backgroundColor: "black", // Dark transparent background }} > {isFetching ? ( - + ) : ( @@ -195,17 +182,19 @@ export const EpisodeList: React.FC = ({ item, close }) => { }} className={`flex flex-row items-center space-x-2`} > - { - setSeasonIndexState((prev) => ({ - ...prev, - [item.SeriesId ?? ""]: season.IndexNumber, - })); - }} - /> + {seriesItem && ( + { + setSeasonIndexState((prev) => ({ + ...prev, + [item.SeriesId ?? ""]: season.IndexNumber, + })); + }} + /> + )} { close(); @@ -270,7 +259,7 @@ export const EpisodeList: React.FC = ({ item, close }) => { )} - keyExtractor={(e: BaseItemDto) => e.Id} + keyExtractor={(e: BaseItemDto) => e.Id ?? ""} estimatedItemSize={200} showsHorizontalScrollIndicator={false} contentContainerStyle={{ From b8b083abe26db0a61097ad782e97c4c86d699347 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 18:14:41 +1100 Subject: [PATCH 07/12] Added correct starting season index --- components/video-player/controls/EpisodeList.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 3f1aeab9..46fe9470 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -42,6 +42,16 @@ export const EpisodeList: React.FC = ({ item, close }) => { const [settings] = useSettings(); const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); + + useEffect(() => { + if (item.SeriesId) { + setSeasonIndexState((prev) => ({ + ...prev, + [item.SeriesId ?? ""]: item.ParentIndexNumber ?? 0, + })); + } + }, []); + const seasonIndex = seasonIndexState[item.SeriesId ?? ""]; const [seriesItem, setSeriesItem] = useState(null); From d6165742329c4ceaf82d57d0a969b75b2c6af89a Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 18:25:10 +1100 Subject: [PATCH 08/12] Added scroll to episode when going in player mode --- .../video-player/controls/EpisodeList.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 46fe9470..132c3fc7 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -37,12 +37,15 @@ export const seasonIndexAtom = atom({}); export const EpisodeList: React.FC = ({ item, close }) => { const [api] = useAtom(apiAtom); const [user] = useAtom(userAtom); - const scrollViewRef = useRef(null); // Reference to the HorizontalScroll const insets = useSafeAreaInsets(); // Get safe area insets const [settings] = useSettings(); - const [seasonIndexState, setSeasonIndexState] = useAtom(seasonIndexAtom); + const scrollViewRef = useRef(null); // Reference to the HorizontalScroll + const scrollToIndex = (index: number) => { + scrollViewRef.current?.scrollToIndex(index, 100); + }; + // Set the initial season index useEffect(() => { if (item.SeriesId) { setSeasonIndexState((prev) => ({ @@ -53,9 +56,9 @@ export const EpisodeList: React.FC = ({ item, close }) => { }, []); const seasonIndex = seasonIndexState[item.SeriesId ?? ""]; - const [seriesItem, setSeriesItem] = useState(null); + // This effect fetches the series item data/ useEffect(() => { if (item.SeriesId) { getUserItemData({ api, userId: user?.Id, itemId: item.SeriesId }).then( @@ -112,6 +115,17 @@ export const EpisodeList: React.FC = ({ item, close }) => { enabled: !!api && !!user?.Id && !!selectedSeasonId, }); + useEffect(() => { + if (item?.Type === "Episode" && item.Id) { + const index = episodes?.findIndex((ep) => ep.Id === item.Id); + if (index !== undefined && index !== -1) { + setTimeout(() => { + scrollToIndex(index); + }, 400); + } + } + }, [episodes, item]); + const queryClient = useQueryClient(); useEffect(() => { for (let e of episodes || []) { From 65d3da155f6803bb6ea3cdd638138c495eb5ae96 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 18:34:20 +1100 Subject: [PATCH 09/12] Fixed style issue for devices with bottom safe area --- components/video-player/controls/EpisodeList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 132c3fc7..99f301e2 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -186,7 +186,7 @@ export const EpisodeList: React.FC = ({ item, close }) => { top: insets.top, left: insets.left, right: insets.right, - bottom: insets.bottom, + bottom: 0, backgroundColor: "black", // Dark transparent background }} > From a6b49c42cf83149ca6329f280f30b28d31d42c27 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Sun, 8 Dec 2024 23:50:59 +1100 Subject: [PATCH 10/12] Added style changes --- .../video-player/controls/EpisodeList.tsx | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index 99f301e2..f8100bc8 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -183,11 +183,9 @@ export const EpisodeList: React.FC = ({ item, close }) => { {isFetching ? ( @@ -198,13 +196,9 @@ export const EpisodeList: React.FC = ({ item, close }) => { <> {seriesItem && ( = ({ item, close }) => { ( { @@ -259,12 +251,18 @@ export const EpisodeList: React.FC = ({ item, close }) => { /> - + {_item.Name} {`S${_item.ParentIndexNumber?.toString()}:E${_item.IndexNumber?.toString()}`} From 353ebf3b0c2dd833a2567a78af59bdc21662c806 Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Mon, 9 Dec 2024 00:00:03 +1100 Subject: [PATCH 11/12] Removed opacity for unselected items --- components/video-player/controls/EpisodeList.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/components/video-player/controls/EpisodeList.tsx b/components/video-player/controls/EpisodeList.tsx index f8100bc8..f3ddfbc9 100644 --- a/components/video-player/controls/EpisodeList.tsx +++ b/components/video-player/controls/EpisodeList.tsx @@ -235,9 +235,7 @@ export const EpisodeList: React.FC = ({ item, close }) => { { From 15f32bca6cb72cadd2cf4b20bb80a34b1bb77c1c Mon Sep 17 00:00:00 2001 From: Alex Kim Date: Mon, 9 Dec 2024 00:13:41 +1100 Subject: [PATCH 12/12] Removed useless file --- components/video-player/controls/GotoEpisode.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 components/video-player/controls/GotoEpisode.tsx diff --git a/components/video-player/controls/GotoEpisode.tsx b/components/video-player/controls/GotoEpisode.tsx deleted file mode 100644 index e69de29b..00000000