8 Commits

4 changed files with 135 additions and 60 deletions

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useCallback, useRef, useState } from "react"; import React, { useEffect, useCallback, useRef, useState } from "react";
import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Animated, StatusBar } from "react-native"; import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Animated, StatusBar, Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ThemedView } from "@/components/ThemedView"; import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText"; import { ThemedText } from "@/components/ThemedText";
@@ -15,6 +15,7 @@ import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles"; import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles";
import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation"; import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation";
import { useApiConfig, getApiConfigErrorMessage } from "@/hooks/useApiConfig"; import { useApiConfig, getApiConfigErrorMessage } from "@/hooks/useApiConfig";
import { Colors } from "@/constants/Colors";
const LOAD_MORE_THRESHOLD = 200; const LOAD_MORE_THRESHOLD = 200;
@@ -166,7 +167,7 @@ export default function HomeScreen() {
<View style={dynamicStyles.headerContainer}> <View style={dynamicStyles.headerContainer}>
<View style={{ flexDirection: "row", alignItems: "center" }}> <View style={{ flexDirection: "row", alignItems: "center" }}>
<ThemedText style={dynamicStyles.headerTitle}></ThemedText> <ThemedText style={dynamicStyles.headerTitle}></ThemedText>
<Pressable style={{ marginLeft: 20 }} onPress={() => router.push("/live")}> <Pressable android_ripple={Platform.isTV || deviceType !== 'tv'? { color: 'transparent' } : { color: Colors.dark.link }} style={{ marginLeft: 20 }} onPress={() => router.push("/live")}>
{({ focused }) => ( {({ focused }) => (
<ThemedText style={[dynamicStyles.headerTitle, { color: focused ? "white" : "grey" }]}></ThemedText> <ThemedText style={[dynamicStyles.headerTitle, { color: focused ? "white" : "grey" }]}></ThemedText>
)} )}

View File

@@ -1,8 +1,9 @@
import React, { forwardRef } from "react"; import React, { forwardRef } from "react";
import { Animated, Pressable, StyleSheet, StyleProp, ViewStyle, PressableProps, TextStyle, View } from "react-native"; import { Animated, Pressable, StyleSheet, StyleProp, ViewStyle, PressableProps, TextStyle, View, Platform } from "react-native";
import { ThemedText } from "./ThemedText"; import { ThemedText } from "./ThemedText";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import { useButtonAnimation } from "@/hooks/useAnimation"; import { useButtonAnimation } from "@/hooks/useAnimation";
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
interface StyledButtonProps extends PressableProps { interface StyledButtonProps extends PressableProps {
children?: React.ReactNode; children?: React.ReactNode;
@@ -19,6 +20,7 @@ export const StyledButton = forwardRef<View, StyledButtonProps>(
const colors = Colors[colorScheme]; const colors = Colors[colorScheme];
const [isFocused, setIsFocused] = React.useState(false); const [isFocused, setIsFocused] = React.useState(false);
const animationStyle = useButtonAnimation(isFocused); const animationStyle = useButtonAnimation(isFocused);
const deviceType = useResponsiveLayout().deviceType;
const variantStyles = { const variantStyles = {
default: StyleSheet.create({ default: StyleSheet.create({
@@ -108,6 +110,7 @@ export const StyledButton = forwardRef<View, StyledButtonProps>(
return ( return (
<Animated.View style={[animationStyle, style]}> <Animated.View style={[animationStyle, style]}>
<Pressable <Pressable
android_ripple={Platform.isTV || deviceType !== 'tv'? { color: 'transparent' } : { color: Colors.dark.link }}
ref={ref} ref={ref}
onFocus={() => setIsFocused(true)} onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}

View File

@@ -1,5 +1,5 @@
import React, { useState, useEffect, useCallback, useRef, forwardRef } from "react"; import React, { useState, useEffect, useCallback, useRef, forwardRef } from "react";
import { View, Text, Image, StyleSheet, TouchableOpacity, Alert, Animated } from "react-native"; import { View, Text, Image, StyleSheet, Pressable, TouchableOpacity, Alert, Animated, Platform } from "react-native";
import { useRouter } from "expo-router"; import { useRouter } from "expo-router";
import { Star, Play } from "lucide-react-native"; import { Star, Play } from "lucide-react-native";
import { PlayRecordManager } from "@/services/storage"; import { PlayRecordManager } from "@/services/storage";
@@ -7,6 +7,7 @@ import { API } from "@/services/api";
import { ThemedText } from "@/components/ThemedText"; import { ThemedText } from "@/components/ThemedText";
import { Colors } from "@/constants/Colors"; import { Colors } from "@/constants/Colors";
import Logger from '@/utils/Logger'; import Logger from '@/utils/Logger';
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
const logger = Logger.withTag('VideoCardTV'); const logger = Logger.withTag('VideoCardTV');
@@ -54,6 +55,8 @@ const VideoCard = forwardRef<View, VideoCardProps>(
const scale = useRef(new Animated.Value(1)).current; const scale = useRef(new Animated.Value(1)).current;
const deviceType = useResponsiveLayout().deviceType;
const animatedStyle = { const animatedStyle = {
transform: [{ scale }], transform: [{ scale }],
}; };
@@ -147,63 +150,126 @@ const VideoCard = forwardRef<View, VideoCardProps>(
return ( return (
<Animated.View style={[styles.wrapper, animatedStyle, { opacity: fadeAnim }]}> <Animated.View style={[styles.wrapper, animatedStyle, { opacity: fadeAnim }]}>
<TouchableOpacity {Platform.isTV || deviceType !== 'tv' ? (
onPress={handlePress} <TouchableOpacity
onLongPress={handleLongPress} onPress={handlePress}
onFocus={handleFocus} onLongPress={handleLongPress}
onBlur={handleBlur} onFocus={handleFocus}
style={styles.pressable} onBlur={handleBlur}
activeOpacity={1} style={styles.pressable}
delayLongPress={1000} activeOpacity={1}
> delayLongPress={1000}
<View style={styles.card}> >
<Image source={{ uri: api.getImageProxyUrl(poster) }} style={styles.poster} /> <View style={styles.card}>
{isFocused && ( <Image source={{ uri: api.getImageProxyUrl(poster) }} style={styles.poster} />
<View style={styles.overlay}> {isFocused && (
{isContinueWatching && ( <View style={styles.overlay}>
<View style={styles.continueWatchingBadge}> {isContinueWatching && (
<Play size={16} color="#ffffff" fill="#ffffff" /> <View style={styles.continueWatchingBadge}>
<ThemedText style={styles.continueWatchingText}></ThemedText> <Play size={16} color="#ffffff" fill="#ffffff" />
</View> <ThemedText style={styles.continueWatchingText}></ThemedText>
)} </View>
</View> )}
)} </View>
)}
{/* 进度条 */} {/* 进度条 */}
{isContinueWatching && ( {isContinueWatching && (
<View style={styles.progressContainer}> <View style={styles.progressContainer}>
<View style={[styles.progressBar, { width: `${(progress || 0) * 100}%` }]} /> <View style={[styles.progressBar, { width: `${(progress || 0) * 100}%` }]} />
</View> </View>
)} )}
{rate && (
<View style={styles.ratingContainer}>
<Star size={12} color="#FFD700" fill="#FFD700" />
<ThemedText style={styles.ratingText}>{rate}</ThemedText>
</View>
)}
{year && (
<View style={styles.yearBadge}>
<Text style={styles.badgeText}>{year}</Text>
</View>
)}
{sourceName && (
<View style={styles.sourceNameBadge}>
<Text style={styles.badgeText}>{sourceName}</Text>
</View>
)}
</View>
<View style={styles.infoContainer}>
<ThemedText numberOfLines={1}>{title}</ThemedText>
{isContinueWatching && (
<View style={styles.infoRow}>
<ThemedText style={styles.continueLabel}>
{episodeIndex} {Math.round((progress || 0) * 100)}%
</ThemedText>
</View>
)}
</View>
</TouchableOpacity>
) : (
<Pressable
android_ripple={{ color: Colors.dark.link }}
onPress={handlePress}
onLongPress={handleLongPress}
onFocus={handleFocus}
onBlur={handleBlur}
style={styles.pressable}
// activeOpacity={1}
delayLongPress={1000}
>
<View style={styles.card}>
<Image source={{ uri: api.getImageProxyUrl(poster) }} style={styles.poster} />
{isFocused && (
<View style={styles.overlay}>
{isContinueWatching && (
<View style={styles.continueWatchingBadge}>
<Play size={16} color="#ffffff" fill="#ffffff" />
<ThemedText style={styles.continueWatchingText}></ThemedText>
</View>
)}
</View>
)}
{/* 进度条 */}
{isContinueWatching && (
<View style={styles.progressContainer}>
<View style={[styles.progressBar, { width: `${(progress || 0) * 100}%` }]} />
</View>
)}
{rate && (
<View style={styles.ratingContainer}>
<Star size={12} color="#FFD700" fill="#FFD700" />
<ThemedText style={styles.ratingText}>{rate}</ThemedText>
</View>
)}
{year && (
<View style={styles.yearBadge}>
<Text style={styles.badgeText}>{year}</Text>
</View>
)}
{sourceName && (
<View style={styles.sourceNameBadge}>
<Text style={styles.badgeText}>{sourceName}</Text>
</View>
)}
</View>
<View style={styles.infoContainer}>
<ThemedText numberOfLines={1}>{title}</ThemedText>
{isContinueWatching && (
<View style={styles.infoRow}>
<ThemedText style={styles.continueLabel}>
{episodeIndex} {Math.round((progress || 0) * 100)}%
</ThemedText>
</View>
)}
</View>
</Pressable>
)}
{rate && (
<View style={styles.ratingContainer}>
<Star size={12} color="#FFD700" fill="#FFD700" />
<ThemedText style={styles.ratingText}>{rate}</ThemedText>
</View>
)}
{year && (
<View style={styles.yearBadge}>
<Text style={styles.badgeText}>{year}</Text>
</View>
)}
{sourceName && (
<View style={styles.sourceNameBadge}>
<Text style={styles.badgeText}>{sourceName}</Text>
</View>
)}
</View>
<View style={styles.infoContainer}>
<ThemedText numberOfLines={1}>{title}</ThemedText>
{isContinueWatching && (
<View style={styles.infoRow}>
<ThemedText style={styles.continueLabel}>
{episodeIndex} {Math.round((progress || 0) * 100)}%
</ThemedText>
</View>
)}
</View>
</TouchableOpacity>
</Animated.View> </Animated.View>
); );
} }
@@ -221,9 +287,14 @@ const styles = StyleSheet.create({
marginHorizontal: 8, marginHorizontal: 8,
}, },
pressable: { pressable: {
width: CARD_WIDTH + 20,
height: CARD_HEIGHT + 60,
justifyContent: 'center',
alignItems: "center", alignItems: "center",
overflow: "visible",
}, },
card: { card: {
marginTop: 10,
width: CARD_WIDTH, width: CARD_WIDTH,
height: CARD_HEIGHT, height: CARD_HEIGHT,
borderRadius: 8, borderRadius: 8,

View File

@@ -2,7 +2,7 @@
"name": "OrionTV", "name": "OrionTV",
"private": true, "private": true,
"main": "expo-router/entry", "main": "expo-router/entry",
"version": "1.3.7", "version": "1.3.8",
"scripts": { "scripts": {
"start": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start", "start": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo start",
"android": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android", "android": "EXPO_TV=1 EXPO_USE_METRO_WORKSPACE_ROOT=1 expo run:android",