feat: add favorites functionality and enhance detail screen

This commit is contained in:
zimplexing
2025-07-15 17:07:53 +08:00
parent 8985781865
commit 30cbf6846e
7 changed files with 303 additions and 68 deletions

View File

@@ -59,6 +59,7 @@ export default function RootLayout() {
<Stack.Screen name="search" options={{ headerShown: false }} />
<Stack.Screen name="live" options={{ headerShown: false }} />
<Stack.Screen name="settings" options={{ headerShown: false }} />
<Stack.Screen name="favorites" options={{ headerShown: false }} />
<Stack.Screen name="+not-found" />
</Stack>
<Toast />

View File

@@ -1,25 +1,37 @@
import React, { useEffect } from "react";
import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator } from "react-native";
import { View, Text, StyleSheet, Image, ScrollView, ActivityIndicator, Pressable } from "react-native";
import { useLocalSearchParams, useRouter } from "expo-router";
import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
import { StyledButton } from "@/components/StyledButton";
import useDetailStore from "@/stores/detailStore";
import { FontAwesome } from "@expo/vector-icons";
export default function DetailScreen() {
const { q } = useLocalSearchParams<{ q: string }>();
const { q, source, id } = useLocalSearchParams<{ q: string; source?: string; id?: string }>();
const router = useRouter();
const { detail, searchResults, loading, error, allSourcesLoaded, init, setDetail, abort } = useDetailStore();
const {
detail,
searchResults,
loading,
error,
allSourcesLoaded,
init,
setDetail,
abort,
isFavorited,
toggleFavorite,
} = useDetailStore();
useEffect(() => {
if (q) {
init(q);
init(q, source, id);
}
return () => {
abort();
};
}, [abort, init, q]);
}, [abort, init, q, source, id]);
const handlePlay = (episodeIndex: number) => {
if (!detail) return;
@@ -75,6 +87,10 @@ export default function DetailScreen() {
<ThemedText style={styles.metaText}>{detail.year}</ThemedText>
<ThemedText style={styles.metaText}>{detail.type_name}</ThemedText>
</View>
<Pressable onPress={toggleFavorite} style={styles.favoriteButton}>
<FontAwesome name={isFavorited ? "star" : "star-o"} size={24} color={isFavorited ? "#FFD700" : "#ccc"} />
<ThemedText style={styles.favoriteButtonText}>{isFavorited ? "已收藏" : "收藏"}</ThemedText>
</Pressable>
<ScrollView style={styles.descriptionScrollView}>
<ThemedText style={styles.description}>{detail.desc}</ThemedText>
</ScrollView>
@@ -177,6 +193,19 @@ const styles = StyleSheet.create({
color: "#ccc",
lineHeight: 22,
},
favoriteButton: {
flexDirection: "row",
alignItems: "center",
marginTop: 10,
padding: 10,
backgroundColor: "rgba(255, 255, 255, 0.1)",
borderRadius: 5,
alignSelf: "flex-start",
},
favoriteButtonText: {
marginLeft: 8,
fontSize: 16,
},
bottomContainer: {
paddingHorizontal: 20,
},

124
app/favorites.tsx Normal file
View File

@@ -0,0 +1,124 @@
import React, { useEffect } from "react";
import { View, FlatList, StyleSheet, ActivityIndicator, Image, Pressable } from "react-native";
import { useRouter } from "expo-router";
import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
import useFavoritesStore from "@/stores/favoritesStore";
import { Favorite } from "@/services/storage";
export default function FavoritesScreen() {
const router = useRouter();
const { favorites, loading, error, fetchFavorites } = useFavoritesStore();
useEffect(() => {
fetchFavorites();
}, [fetchFavorites]);
const handlePress = (favorite: Favorite & { key: string }) => {
const [source, id] = favorite.key.split("+");
router.push({
pathname: "/detail",
params: { q: favorite.title, source, id },
});
};
if (loading) {
return (
<ThemedView style={styles.centered}>
<ActivityIndicator size="large" />
</ThemedView>
);
}
if (error) {
return (
<ThemedView style={styles.centered}>
<ThemedText type="subtitle">{error}</ThemedText>
</ThemedView>
);
}
if (favorites.length === 0) {
return (
<ThemedView style={styles.centered}>
<ThemedText type="subtitle"></ThemedText>
</ThemedView>
);
}
const renderItem = ({ item }: { item: Favorite & { key: string } }) => (
<Pressable onPress={() => handlePress(item)} style={styles.itemContainer}>
<Image source={{ uri: item.poster }} style={styles.poster} />
<View style={styles.infoContainer}>
<ThemedText style={styles.title} numberOfLines={1}>
{item.title}
</ThemedText>
<ThemedText style={styles.year}>{item.year}</ThemedText>
</View>
</Pressable>
);
return (
<ThemedView style={styles.container}>
<View style={styles.headerContainer}>
<ThemedText style={styles.headerTitle}></ThemedText>
</View>
<FlatList
data={favorites}
renderItem={renderItem}
keyExtractor={(item) => item.key}
numColumns={3}
contentContainerStyle={styles.list}
/>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 40,
},
headerContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 24,
marginBottom: 10,
},
headerTitle: {
fontSize: 32,
fontWeight: "bold",
paddingTop: 16,
},
centered: {
flex: 1,
justifyContent: "center",
alignItems: "center",
},
list: {
padding: 10,
},
itemContainer: {
flex: 1,
margin: 10,
alignItems: "center",
},
poster: {
width: 120,
height: 180,
borderRadius: 8,
},
infoContainer: {
marginTop: 8,
alignItems: "center",
},
title: {
fontSize: 16,
fontWeight: "bold",
},
year: {
fontSize: 14,
color: "#888",
},
});

View File

@@ -5,7 +5,7 @@ import { ThemedText } from "@/components/ThemedText";
import { api } from "@/services/api";
import VideoCard from "@/components/VideoCard.tv";
import { useFocusEffect, useRouter } from "expo-router";
import { Search, Settings, LogOut } from "lucide-react-native";
import { Search, Settings, LogOut, Star } from "lucide-react-native";
import { StyledButton } from "@/components/StyledButton";
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
import useAuthStore from "@/stores/authStore";
@@ -96,7 +96,7 @@ export default function HomeScreen() {
year={item.year}
rate={item.rate}
progress={item.progress}
playTime={item.time}
playTime={item.play_time}
episodeIndex={item.episodeIndex}
sourceName={item.sourceName}
totalEpisodes={item.totalEpisodes}
@@ -124,6 +124,9 @@ export default function HomeScreen() {
</Pressable>
</View>
<View style={styles.rightHeaderButtons}>
<StyledButton style={styles.searchButton} onPress={() => router.push("/favorites")} variant="ghost">
<Star color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
<StyledButton
style={styles.searchButton}
onPress={() => router.push({ pathname: "/search" })}