diff --git a/components/livetv/TVGuidePageNavigation.tsx b/components/livetv/TVGuidePageNavigation.tsx new file mode 100644 index 00000000..5188c54e --- /dev/null +++ b/components/livetv/TVGuidePageNavigation.tsx @@ -0,0 +1,154 @@ +import { Ionicons } from "@expo/vector-icons"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { Animated, Pressable, StyleSheet, View } from "react-native"; +import { Text } from "@/components/common/Text"; +import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; +import { useScaledTVTypography } from "@/constants/TVTypography"; + +interface TVGuidePageNavigationProps { + currentPage: number; + totalPages: number; + onPrevious: () => void; + onNext: () => void; + disabled?: boolean; + prevButtonRefSetter?: (ref: View | null) => void; +} + +interface NavButtonProps { + onPress: () => void; + icon: keyof typeof Ionicons.glyphMap; + label: string; + isDisabled: boolean; + disabled?: boolean; + refSetter?: (ref: View | null) => void; +} + +const NavButton: React.FC = ({ + onPress, + icon, + label, + isDisabled, + disabled = false, + refSetter, +}) => { + const typography = useScaledTVTypography(); + const { focused, handleFocus, handleBlur, animatedStyle } = + useTVFocusAnimation({ + scaleAmount: 1.05, + duration: 120, + }); + + const visuallyDisabled = isDisabled || disabled; + + const handlePress = () => { + if (!visuallyDisabled) { + onPress(); + } + }; + + return ( + + + + + {label} + + + + ); +}; + +export const TVGuidePageNavigation: React.FC = ({ + currentPage, + totalPages, + onPrevious, + onNext, + disabled = false, + prevButtonRefSetter, +}) => { + const { t } = useTranslation(); + const typography = useScaledTVTypography(); + + return ( + + + + + = totalPages} + disabled={disabled} + /> + + + + {t("live_tv.page_of", { current: currentPage, total: totalPages })} + + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + paddingVertical: 16, + }, + buttonsContainer: { + flexDirection: "row", + alignItems: "center", + gap: 12, + }, + navButton: { + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + }, + navButtonText: { + fontWeight: "600", + }, + pageText: { + color: "rgba(255, 255, 255, 0.6)", + }, +}); diff --git a/components/livetv/TVGuideProgramCell.tsx b/components/livetv/TVGuideProgramCell.tsx new file mode 100644 index 00000000..e8287132 --- /dev/null +++ b/components/livetv/TVGuideProgramCell.tsx @@ -0,0 +1,148 @@ +import type { BaseItemDto } from "@jellyfin/sdk/lib/generated-client"; +import React from "react"; +import { Animated, Pressable, StyleSheet, View } from "react-native"; +import { Text } from "@/components/common/Text"; +import { useTVFocusAnimation } from "@/components/tv/hooks/useTVFocusAnimation"; +import { useScaledTVTypography } from "@/constants/TVTypography"; + +interface TVGuideProgramCellProps { + program: BaseItemDto; + width: number; + isCurrentlyAiring: boolean; + onPress: () => void; + disabled?: boolean; + refSetter?: (ref: View | null) => void; +} + +export const TVGuideProgramCell: React.FC = ({ + program, + width, + isCurrentlyAiring, + onPress, + disabled = false, + refSetter, +}) => { + const typography = useScaledTVTypography(); + const { focused, handleFocus, handleBlur } = useTVFocusAnimation({ + scaleAmount: 1, + duration: 120, + }); + + const formatTime = (date: string | null | undefined) => { + if (!date) return ""; + const d = new Date(date); + return d.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + hour12: false, + }); + }; + + return ( + + + {/* LIVE badge */} + {isCurrentlyAiring && ( + + + LIVE + + + )} + + {/* Program name */} + + {program.Name} + + + {/* Time range */} + + {formatTime(program.StartDate)} - {formatTime(program.EndDate)} + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + height: 70, + borderRadius: 8, + borderWidth: 1, + paddingHorizontal: 12, + paddingVertical: 8, + justifyContent: "center", + overflow: "hidden", + }, + focusedShadow: { + shadowColor: "#FFFFFF", + shadowOffset: { width: 0, height: 0 }, + shadowOpacity: 0.4, + shadowRadius: 12, + }, + liveBadge: { + position: "absolute", + top: 6, + right: 6, + backgroundColor: "#EF4444", + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + zIndex: 10, + elevation: 10, + }, + liveBadgeText: { + color: "#FFFFFF", + fontWeight: "bold", + }, + programName: { + fontWeight: "600", + marginBottom: 4, + }, + timeText: { + fontWeight: "400", + }, +});