From 16a236393dccb91a69bdeee509c4ee3312f900c3 Mon Sep 17 00:00:00 2001 From: Fredrik Burmester Date: Mon, 19 Jan 2026 20:01:00 +0100 Subject: [PATCH] refactor(tv): migrate series season selector to navigation-based modal pattern --- components/series/TVSeasonSelector.tsx | 195 ------------------------- components/series/TVSeriesPage.tsx | 42 +++--- 2 files changed, 24 insertions(+), 213 deletions(-) delete mode 100644 components/series/TVSeasonSelector.tsx diff --git a/components/series/TVSeasonSelector.tsx b/components/series/TVSeasonSelector.tsx deleted file mode 100644 index 947bc918..00000000 --- a/components/series/TVSeasonSelector.tsx +++ /dev/null @@ -1,195 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client/models"; -import { BlurView } from "expo-blur"; -import React, { useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; -import { Animated, Easing, Pressable, ScrollView, View } from "react-native"; -import { Text } from "@/components/common/Text"; - -interface TVSeasonSelectorProps { - visible: boolean; - seasons: BaseItemDto[]; - selectedSeasonIndex: number | string | null | undefined; - onSelect: (seasonIndex: number) => void; - onClose: () => void; -} - -const TVSeasonCard: React.FC<{ - season: BaseItemDto; - isSelected: boolean; - hasTVPreferredFocus?: boolean; - onPress: () => void; -}> = ({ season, isSelected, hasTVPreferredFocus, onPress }) => { - const [focused, setFocused] = useState(false); - const scale = useRef(new Animated.Value(1)).current; - - const animateTo = (v: number) => - Animated.timing(scale, { - toValue: v, - duration: 150, - easing: Easing.out(Easing.quad), - useNativeDriver: true, - }).start(); - - const seasonName = useMemo(() => { - if (season.Name) return season.Name; - if (season.IndexNumber !== undefined) return `Season ${season.IndexNumber}`; - return "Season"; - }, [season]); - - return ( - { - setFocused(true); - animateTo(1.05); - }} - onBlur={() => { - setFocused(false); - animateTo(1); - }} - hasTVPreferredFocus={hasTVPreferredFocus} - > - - - {seasonName} - - {isSelected && !focused && ( - - - - )} - - - ); -}; - -export const TVSeasonSelector: React.FC = ({ - visible, - seasons, - selectedSeasonIndex, - onSelect, - onClose, -}) => { - const { t } = useTranslation(); - - const initialFocusIndex = useMemo(() => { - const idx = seasons.findIndex( - (s) => - s.IndexNumber === selectedSeasonIndex || - s.Name === String(selectedSeasonIndex), - ); - return idx >= 0 ? idx : 0; - }, [seasons, selectedSeasonIndex]); - - if (!visible) return null; - - return ( - - - - {/* Title */} - - {t("item_card.select_season")} - - - {/* Horizontal season cards */} - - {seasons.map((season, index) => ( - { - onSelect(season.IndexNumber ?? index); - onClose(); - }} - /> - ))} - - - - - ); -}; diff --git a/components/series/TVSeriesPage.tsx b/components/series/TVSeriesPage.tsx index bd8047a7..f393fe94 100644 --- a/components/series/TVSeriesPage.tsx +++ b/components/series/TVSeriesPage.tsx @@ -31,9 +31,9 @@ import { TV_EPISODE_WIDTH, TVEpisodeCard, } from "@/components/series/TVEpisodeCard"; -import { TVSeasonSelector } from "@/components/series/TVSeasonSelector"; import { TVSeriesHeader } from "@/components/series/TVSeriesHeader"; import useRouter from "@/hooks/useAppRouter"; +import { useTVOptionModal } from "@/hooks/useTVOptionModal"; import { useDownload } from "@/providers/DownloadProvider"; import { apiAtom, userAtom } from "@/providers/JellyfinProvider"; import { useOfflineMode } from "@/providers/OfflineModeProvider"; @@ -227,9 +227,8 @@ export const TVSeriesPage: React.FC = ({ [item.Id, seasonIndexState], ); - // Modal state - const [openModal, setOpenModal] = useState<"season" | null>(null); - const isModalOpen = openModal !== null; + // TV option modal hook + const { showOptions } = useTVOptionModal(); // ScrollView ref for page scrolling const mainScrollRef = useRef(null); @@ -400,6 +399,25 @@ export const TVSeriesPage: React.FC = ({ [item.Id, setSeasonIndexState], ); + // Open season modal + const handleOpenSeasonModal = useCallback(() => { + const options = seasons.map((season: BaseItemDto) => ({ + label: season.Name || `Season ${season.IndexNumber}`, + value: season.IndexNumber ?? 0, + selected: + season.IndexNumber === selectedSeasonIndex || + season.Name === String(selectedSeasonIndex), + })); + + showOptions({ + title: t("item_card.select_season"), + options, + onSelect: handleSeasonSelect, + cardWidth: 180, + cardHeight: 85, + }); + }, [seasons, selectedSeasonIndex, showOptions, t, handleSeasonSelect]); + // Episode list item layout const getItemLayout = useCallback( (_data: ArrayLike | null | undefined, index: number) => ({ @@ -417,13 +435,12 @@ export const TVSeriesPage: React.FC = ({ handleEpisodePress(episode)} - disabled={isModalOpen} onFocus={handleEpisodeFocus} onBlur={handleEpisodeBlur} /> ), - [handleEpisodePress, isModalOpen, handleEpisodeFocus, handleEpisodeBlur], + [handleEpisodePress, handleEpisodeFocus, handleEpisodeBlur], ); // Get play button text @@ -545,7 +562,6 @@ export const TVSeriesPage: React.FC = ({ = ({ {seasons.length > 1 && ( setOpenModal("season")} - disabled={isModalOpen} + onPress={handleOpenSeasonModal} /> )} @@ -621,15 +636,6 @@ export const TVSeriesPage: React.FC = ({ /> - - {/* Season selector modal */} - setOpenModal(null)} - /> ); };