mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-01-29 06:28:24 +00:00
feat(tv): live tv
This commit is contained in:
154
components/livetv/TVGuidePageNavigation.tsx
Normal file
154
components/livetv/TVGuidePageNavigation.tsx
Normal file
@@ -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<NavButtonProps> = ({
|
||||
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 (
|
||||
<Pressable
|
||||
ref={refSetter}
|
||||
onPress={handlePress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
focusable={!disabled}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.navButton,
|
||||
animatedStyle,
|
||||
{
|
||||
backgroundColor: focused ? "#FFFFFF" : "rgba(255, 255, 255, 0.1)",
|
||||
opacity: visuallyDisabled ? 0.3 : 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Ionicons
|
||||
name={icon}
|
||||
size={20}
|
||||
color={focused ? "#000000" : "#FFFFFF"}
|
||||
/>
|
||||
<Text
|
||||
style={[
|
||||
styles.navButtonText,
|
||||
{
|
||||
fontSize: typography.callout,
|
||||
color: focused ? "#000000" : "#FFFFFF",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
export const TVGuidePageNavigation: React.FC<TVGuidePageNavigationProps> = ({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPrevious,
|
||||
onNext,
|
||||
disabled = false,
|
||||
prevButtonRefSetter,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const typography = useScaledTVTypography();
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.buttonsContainer}>
|
||||
<NavButton
|
||||
onPress={onPrevious}
|
||||
icon='chevron-back'
|
||||
label={t("live_tv.previous")}
|
||||
isDisabled={currentPage <= 1}
|
||||
disabled={disabled}
|
||||
refSetter={prevButtonRefSetter}
|
||||
/>
|
||||
|
||||
<NavButton
|
||||
onPress={onNext}
|
||||
icon='chevron-forward'
|
||||
label={t("live_tv.next")}
|
||||
isDisabled={currentPage >= totalPages}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<Text style={[styles.pageText, { fontSize: typography.callout }]}>
|
||||
{t("live_tv.page_of", { current: currentPage, total: totalPages })}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
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)",
|
||||
},
|
||||
});
|
||||
148
components/livetv/TVGuideProgramCell.tsx
Normal file
148
components/livetv/TVGuideProgramCell.tsx
Normal file
@@ -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<TVGuideProgramCellProps> = ({
|
||||
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 (
|
||||
<Pressable
|
||||
ref={refSetter}
|
||||
onPress={onPress}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
disabled={disabled}
|
||||
focusable={!disabled}
|
||||
style={{ width }}
|
||||
>
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: focused
|
||||
? "#FFFFFF"
|
||||
: isCurrentlyAiring
|
||||
? "rgba(255, 255, 255, 0.15)"
|
||||
: "rgba(255, 255, 255, 0.08)",
|
||||
borderColor: focused ? "#FFFFFF" : "rgba(255, 255, 255, 0.1)",
|
||||
},
|
||||
focused && styles.focusedShadow,
|
||||
]}
|
||||
>
|
||||
{/* LIVE badge */}
|
||||
{isCurrentlyAiring && (
|
||||
<View style={styles.liveBadge}>
|
||||
<Text
|
||||
style={[styles.liveBadgeText, { fontSize: typography.callout }]}
|
||||
>
|
||||
LIVE
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Program name */}
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={[
|
||||
styles.programName,
|
||||
{
|
||||
fontSize: typography.callout,
|
||||
color: focused ? "#000000" : "#FFFFFF",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{program.Name}
|
||||
</Text>
|
||||
|
||||
{/* Time range */}
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={[
|
||||
styles.timeText,
|
||||
{
|
||||
fontSize: typography.callout,
|
||||
color: focused
|
||||
? "rgba(0, 0, 0, 0.6)"
|
||||
: "rgba(255, 255, 255, 0.5)",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{formatTime(program.StartDate)} - {formatTime(program.EndDate)}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</Pressable>
|
||||
);
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user