This commit is contained in:
Neil.X.Zhang
2025-07-02 09:09:35 +08:00
parent 3b79d06b7d
commit 05ef835d5d
86 changed files with 2440 additions and 8770 deletions

View File

@@ -1,14 +1,23 @@
import React, { useState, useEffect, useCallback } from "react";
import { View, Text, Image, StyleSheet, Pressable } from "react-native";
import React, { useState, useEffect, useCallback, useRef } from "react";
import {
View,
Text,
Image,
StyleSheet,
Pressable,
TouchableOpacity,
Alert,
} from "react-native";
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from "react-native-reanimated";
import { useRouter } from "expo-router";
import { Heart, Star } from "lucide-react-native";
import { FavoriteManager } from "@/services/storage";
import { MoonTVAPI } from "@/services/api";
import { Heart, Star, Play, Trash2 } from "lucide-react-native";
import { FavoriteManager, PlayRecordManager } from "@/services/storage";
import { API, moonTVApi } from "@/services/api";
import { ThemedText } from "@/components/ThemedText";
interface VideoCardProps {
id: string;
@@ -17,8 +26,13 @@ interface VideoCardProps {
poster: string;
year?: string;
rate?: string;
sourceName?: string;
progress?: number; // 播放进度0-1之间的小数
episodeIndex?: number; // 剧集索引
totalEpisodes?: number; // 总集数
onFocus?: () => void;
api: MoonTVAPI;
onRecordDeleted?: () => void; // 添加回调属性
api: API;
}
export default function VideoCard({
@@ -28,12 +42,18 @@ export default function VideoCard({
poster,
year,
rate,
sourceName,
progress,
episodeIndex,
totalEpisodes,
onFocus,
onRecordDeleted,
api,
}: VideoCardProps) {
const router = useRouter();
const [isFocused, setIsFocused] = useState(false);
const [isFavorited, setIsFavorited] = useState(false);
const longPressTriggered = useRef(false);
const scale = useSharedValue(1);
@@ -43,19 +63,23 @@ export default function VideoCard({
};
});
useEffect(() => {
const checkFavorite = async () => {
const fav = await FavoriteManager.isFavorited(source, id);
setIsFavorited(fav);
};
checkFavorite();
}, [source, id]);
const handlePress = () => {
router.push({
pathname: "/detail",
params: { source, id },
});
if (longPressTriggered.current) {
longPressTriggered.current = false;
return;
}
// 如果有播放进度,直接转到播放页面
if (progress !== undefined && episodeIndex !== undefined) {
router.push({
pathname: "/play",
params: { source, id, episodeIndex },
});
} else {
router.push({
pathname: "/detail",
params: { source, q: title },
});
}
};
const handleFocus = useCallback(() => {
@@ -69,22 +93,57 @@ export default function VideoCard({
scale.value = withSpring(1.0);
}, [scale]);
const handleToggleFavorite = async () => {
const newFavState = await FavoriteManager.toggle(source, id, {
title,
poster,
source_name: source,
});
setIsFavorited(newFavState);
const handleLongPress = () => {
// Only allow long press for items with progress (play records)
if (progress === undefined) return;
longPressTriggered.current = true;
// Show confirmation dialog to delete play record
Alert.alert("删除观看记录", `确定要删除"${title}"的观看记录吗?`, [
{
text: "取消",
style: "cancel",
},
{
text: "删除",
style: "destructive",
onPress: async () => {
try {
// Delete from local storage
await PlayRecordManager.remove(source, id);
// Call the onRecordDeleted callback
if (onRecordDeleted) {
onRecordDeleted();
}
// 如果没有回调函数,则使用导航刷新作为备选方案
else if (router.canGoBack()) {
router.replace("/");
}
} catch (error) {
console.error("Failed to delete play record:", error);
Alert.alert("错误", "删除观看记录失败,请重试");
}
},
},
]);
};
// 是否是继续观看的视频
const isContinueWatching =
progress !== undefined && progress > 0 && progress < 1;
return (
<Animated.View style={[styles.wrapper, animatedStyle]}>
<Pressable
<TouchableOpacity
onPress={handlePress}
onLongPress={handleLongPress}
onFocus={handleFocus}
onBlur={handleBlur}
style={styles.pressable}
activeOpacity={1}
delayLongPress={1000}
>
<View style={styles.card}>
<Image
@@ -93,32 +152,58 @@ export default function VideoCard({
/>
{isFocused && (
<View style={styles.overlay}>
<Pressable
onPress={handleToggleFavorite}
style={styles.favButton}
>
<Heart
size={24}
color={isFavorited ? "red" : "white"}
fill={isFavorited ? "red" : "transparent"}
/>
</Pressable>
{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" />
<Text style={styles.ratingText}>{rate}</Text>
<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}>
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
{year && <Text style={styles.year}>{year}</Text>}
<ThemedText numberOfLines={1}>{title}</ThemedText>
{isContinueWatching && !isFocused && (
<View style={styles.infoRow}>
<ThemedText style={styles.continueLabel}>
{episodeIndex! + 1} {" "}
{Math.round((progress || 0) * 100)}%
</ThemedText>
</View>
)}
</View>
</Pressable>
</TouchableOpacity>
</Animated.View>
);
}
@@ -150,6 +235,16 @@ const styles = StyleSheet.create({
justifyContent: "center",
alignItems: "center",
},
buttonRow: {
position: "absolute",
top: 8,
left: 8,
flexDirection: "row",
gap: 8,
},
iconButton: {
padding: 4,
},
favButton: {
position: "absolute",
top: 8,
@@ -175,7 +270,14 @@ const styles = StyleSheet.create({
infoContainer: {
width: CARD_WIDTH,
marginTop: 8,
alignItems: "center",
alignItems: "flex-start", // Align items to the start
marginBottom: 16,
paddingHorizontal: 4, // Add some padding
},
infoRow: {
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
},
title: {
color: "white",
@@ -183,9 +285,57 @@ const styles = StyleSheet.create({
fontWeight: "bold",
textAlign: "center",
},
year: {
color: "#aaa",
yearBadge: {
position: "absolute",
top: 8,
right: 8,
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
},
sourceNameBadge: {
position: "absolute",
top: 8,
left: 8,
backgroundColor: "rgba(0, 0, 0, 0.7)",
borderRadius: 6,
paddingHorizontal: 6,
paddingVertical: 3,
},
badgeText: {
color: "white",
fontSize: 12,
fontWeight: "bold",
},
progressContainer: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
height: 3,
backgroundColor: "rgba(0, 0, 0, 0.5)",
},
progressBar: {
height: 3,
backgroundColor: "#ff0000",
},
continueWatchingBadge: {
flexDirection: "row",
alignItems: "center",
backgroundColor: "rgba(255, 0, 0, 0.8)",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 5,
},
continueWatchingText: {
color: "white",
marginLeft: 5,
fontSize: 12,
fontWeight: "bold",
},
continueLabel: {
color: "#ff5252",
fontSize: 12,
textAlign: "center",
},
});