diff --git a/components/video-player/controls/TVSubtitleSheet.tsx b/components/video-player/controls/TVSubtitleSheet.tsx deleted file mode 100644 index 6284b566..00000000 --- a/components/video-player/controls/TVSubtitleSheet.tsx +++ /dev/null @@ -1,560 +0,0 @@ -import { Ionicons } from "@expo/vector-icons"; -import type { - BaseItemDto, - MediaStream, -} from "@jellyfin/sdk/lib/generated-client"; -import { BlurView } from "expo-blur"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; -import { useTranslation } from "react-i18next"; -import { - ActivityIndicator, - Animated, - Easing, - ScrollView, - StyleSheet, - TVFocusGuideView, - View, -} from "react-native"; -import { Text } from "@/components/common/Text"; -import { - TVCancelButton, - TVLanguageCard, - TVSubtitleResultCard, - TVTabButton, - TVTrackCard, -} from "@/components/tv"; -import { - type SubtitleSearchResult, - useRemoteSubtitles, -} from "@/hooks/useRemoteSubtitles"; -import { COMMON_SUBTITLE_LANGUAGES } from "@/utils/opensubtitles/api"; - -interface TVSubtitleSheetProps { - visible: boolean; - item: BaseItemDto; - mediaSourceId?: string | null; - subtitleTracks: MediaStream[]; - currentSubtitleIndex: number; - onSubtitleIndexChange: (index: number) => void; - onClose: () => void; - onServerSubtitleDownloaded?: () => void; - onLocalSubtitleDownloaded?: (path: string) => void; -} - -type TabType = "tracks" | "download"; - -export const TVSubtitleSheet: React.FC = ({ - visible, - item, - mediaSourceId, - subtitleTracks, - currentSubtitleIndex, - onSubtitleIndexChange, - onClose, - onServerSubtitleDownloaded, - onLocalSubtitleDownloaded, -}) => { - const { t } = useTranslation(); - - console.log( - "[TVSubtitleSheet] visible:", - visible, - "tracks:", - subtitleTracks.length, - ); - const [activeTab, setActiveTab] = useState("tracks"); - const [selectedLanguage, setSelectedLanguage] = useState("eng"); - const [downloadingId, setDownloadingId] = useState(null); - const [hasSearchedThisSession, setHasSearchedThisSession] = useState(false); - const [isReady, setIsReady] = useState(false); - const [isTabContentReady, setIsTabContentReady] = useState(false); - const firstTrackRef = useRef(null); - - const { - hasOpenSubtitlesApiKey, - isSearching, - searchError, - searchResults, - search, - downloadAsync, - reset, - } = useRemoteSubtitles({ - itemId: item.Id ?? "", - item, - mediaSourceId, - }); - - const resetRef = useRef(reset); - resetRef.current = reset; - - const overlayOpacity = useRef(new Animated.Value(0)).current; - const sheetTranslateY = useRef(new Animated.Value(300)).current; - - const initialSelectedTrackIndex = useMemo(() => { - if (currentSubtitleIndex === -1) return 0; - const trackIdx = subtitleTracks.findIndex( - (t) => t.Index === currentSubtitleIndex, - ); - return trackIdx >= 0 ? trackIdx + 1 : 0; - }, [subtitleTracks, currentSubtitleIndex]); - - useEffect(() => { - if (visible) { - overlayOpacity.setValue(0); - sheetTranslateY.setValue(300); - - Animated.parallel([ - Animated.timing(overlayOpacity, { - toValue: 1, - duration: 250, - easing: Easing.out(Easing.quad), - useNativeDriver: true, - }), - Animated.timing(sheetTranslateY, { - toValue: 0, - duration: 300, - easing: Easing.out(Easing.cubic), - useNativeDriver: true, - }), - ]).start(); - } - }, [visible, overlayOpacity, sheetTranslateY]); - - useEffect(() => { - if (!visible) { - setHasSearchedThisSession(false); - setActiveTab("tracks"); - resetRef.current(); - setIsReady(false); - } - }, [visible]); - - useEffect(() => { - if (visible) { - const timer = setTimeout(() => setIsReady(true), 100); - return () => clearTimeout(timer); - } - setIsReady(false); - }, [visible]); - - useEffect(() => { - if (visible && activeTab === "download" && !hasSearchedThisSession) { - search({ language: selectedLanguage }); - setHasSearchedThisSession(true); - } - }, [visible, activeTab, hasSearchedThisSession, search, selectedLanguage]); - - useEffect(() => { - if (isReady) { - setIsTabContentReady(false); - const timer = setTimeout(() => setIsTabContentReady(true), 50); - return () => clearTimeout(timer); - } - setIsTabContentReady(false); - }, [activeTab, isReady]); - - const handleLanguageSelect = useCallback( - (code: string) => { - setSelectedLanguage(code); - search({ language: code }); - }, - [search], - ); - - const handleTrackSelect = useCallback( - (index: number) => { - onSubtitleIndexChange(index); - onClose(); - }, - [onSubtitleIndexChange, onClose], - ); - - const handleDownload = useCallback( - async (result: SubtitleSearchResult) => { - setDownloadingId(result.id); - - try { - const downloadResult = await downloadAsync(result); - - if (downloadResult.type === "server") { - onServerSubtitleDownloaded?.(); - } else if (downloadResult.type === "local" && downloadResult.path) { - onLocalSubtitleDownloaded?.(downloadResult.path); - } - - onClose(); - } catch (error) { - console.error("Failed to download subtitle:", error); - } finally { - setDownloadingId(null); - } - }, - [ - downloadAsync, - onServerSubtitleDownloaded, - onLocalSubtitleDownloaded, - onClose, - ], - ); - - const displayLanguages = useMemo( - () => COMMON_SUBTITLE_LANGUAGES.slice(0, 16), - [], - ); - - const trackOptions = useMemo(() => { - const noneOption = { - label: t("item_card.subtitles.none"), - sublabel: undefined as string | undefined, - value: -1, - selected: currentSubtitleIndex === -1, - }; - const options = subtitleTracks.map((track) => ({ - label: - track.DisplayTitle || `${track.Language || "Unknown"} (${track.Codec})`, - sublabel: track.Codec?.toUpperCase(), - value: track.Index!, - selected: track.Index === currentSubtitleIndex, - })); - return [noneOption, ...options]; - }, [subtitleTracks, currentSubtitleIndex, t]); - - if (!visible) return null; - - return ( - - - - - {/* Header with tabs */} - - - {t("item_card.subtitles.label") || "Subtitles"} - - - {/* Tab bar */} - - setActiveTab("tracks")} - /> - setActiveTab("download")} - /> - - - - {/* Tracks Tab Content */} - {activeTab === "tracks" && isTabContentReady && ( - - - {trackOptions.map((option, index) => ( - handleTrackSelect(option.value)} - /> - ))} - - - )} - - {/* Download Tab Content */} - {activeTab === "download" && isTabContentReady && ( - <> - {/* Language Selector */} - - - {t("player.language") || "Language"} - - - {displayLanguages.map((lang, index) => ( - handleLanguageSelect(lang.code)} - /> - ))} - - - - {/* Results Section */} - - - {t("player.results") || "Results"} - {searchResults && ` (${searchResults.length})`} - - - {/* Loading state */} - {isSearching && ( - - - - {t("player.searching") || "Searching..."} - - - )} - - {/* Error state */} - {searchError && !isSearching && ( - - - - {t("player.search_failed") || "Search failed"} - - - {!hasOpenSubtitlesApiKey - ? t("player.no_subtitle_provider") || - "No subtitle provider configured on server" - : String(searchError)} - - - )} - - {/* No results */} - {searchResults && - searchResults.length === 0 && - !isSearching && - !searchError && ( - - - - {t("player.no_subtitles_found") || - "No subtitles found"} - - - )} - - {/* Results list */} - {searchResults && - searchResults.length > 0 && - !isSearching && ( - - {searchResults.map((result, index) => ( - handleDownload(result)} - /> - ))} - - )} - - - {/* API Key hint if no fallback available */} - {!hasOpenSubtitlesApiKey && ( - - - - {t("player.add_opensubtitles_key_hint") || - "Add OpenSubtitles API key in settings for client-side fallback"} - - - )} - - )} - - {/* Cancel button */} - {isReady && ( - - - - )} - - - - - ); -}; - -const styles = StyleSheet.create({ - overlay: { - position: "absolute", - top: 0, - left: 0, - right: 0, - bottom: 0, - backgroundColor: "rgba(0, 0, 0, 0.6)", - justifyContent: "flex-end", - zIndex: 1000, - }, - sheetContainer: { - maxHeight: "70%", - }, - blurContainer: { - borderTopLeftRadius: 24, - borderTopRightRadius: 24, - overflow: "hidden", - }, - content: { - paddingTop: 24, - paddingBottom: 48, - }, - header: { - paddingHorizontal: 48, - marginBottom: 20, - }, - title: { - fontSize: 24, - fontWeight: "600", - color: "#fff", - marginBottom: 16, - }, - tabRow: { - flexDirection: "row", - gap: 24, - }, - section: { - marginBottom: 20, - }, - sectionTitle: { - fontSize: 14, - fontWeight: "500", - color: "rgba(255,255,255,0.5)", - textTransform: "uppercase", - letterSpacing: 1, - marginBottom: 12, - paddingHorizontal: 48, - }, - tracksScroll: { - overflow: "visible", - }, - tracksScrollContent: { - paddingHorizontal: 48, - paddingVertical: 8, - gap: 12, - }, - languageScroll: { - overflow: "visible", - }, - languageScrollContent: { - paddingHorizontal: 48, - paddingVertical: 8, - gap: 10, - }, - resultsScroll: { - overflow: "visible", - }, - resultsScrollContent: { - paddingHorizontal: 48, - paddingVertical: 8, - gap: 12, - }, - loadingContainer: { - paddingVertical: 40, - alignItems: "center", - }, - loadingText: { - color: "rgba(255,255,255,0.6)", - marginTop: 12, - fontSize: 14, - }, - errorContainer: { - paddingVertical: 40, - paddingHorizontal: 48, - alignItems: "center", - }, - errorText: { - color: "rgba(255,100,100,0.9)", - marginTop: 8, - fontSize: 16, - fontWeight: "500", - }, - errorHint: { - color: "rgba(255,255,255,0.5)", - marginTop: 4, - fontSize: 13, - textAlign: "center", - }, - emptyContainer: { - paddingVertical: 40, - alignItems: "center", - }, - emptyText: { - color: "rgba(255,255,255,0.5)", - marginTop: 8, - fontSize: 14, - }, - apiKeyHint: { - flexDirection: "row", - alignItems: "center", - gap: 8, - paddingHorizontal: 48, - paddingTop: 8, - }, - apiKeyHintText: { - color: "rgba(255,255,255,0.4)", - fontSize: 12, - }, - cancelButtonContainer: { - paddingHorizontal: 48, - paddingTop: 20, - alignItems: "flex-start", - }, -});