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)}
- />
);
};