Files
OrionTV/app/index.tsx

300 lines
9.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useEffect, useCallback, useRef, useState } from "react";
import { View, StyleSheet, ActivityIndicator, FlatList, Pressable, Animated, StatusBar } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { ThemedView } from "@/components/ThemedView";
import { ThemedText } from "@/components/ThemedText";
import { api } from "@/services/api";
import VideoCard from "@/components/VideoCard";
import { useFocusEffect, useRouter } from "expo-router";
import { Search, Settings, LogOut, Heart } from "lucide-react-native";
import { StyledButton } from "@/components/StyledButton";
import useHomeStore, { RowItem, Category } from "@/stores/homeStore";
import useAuthStore from "@/stores/authStore";
import CustomScrollView from "@/components/CustomScrollView";
import { useResponsiveLayout } from "@/hooks/useResponsiveLayout";
import { getCommonResponsiveStyles } from "@/utils/ResponsiveStyles";
import ResponsiveNavigation from "@/components/navigation/ResponsiveNavigation";
const LOAD_MORE_THRESHOLD = 200;
export default function HomeScreen() {
const router = useRouter();
const colorScheme = "dark";
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const fadeAnim = useRef(new Animated.Value(0)).current;
const insets = useSafeAreaInsets();
// 响应式布局配置
const responsiveConfig = useResponsiveLayout();
const commonStyles = getCommonResponsiveStyles(responsiveConfig);
const { deviceType, spacing } = responsiveConfig;
const {
categories,
selectedCategory,
contentData,
loading,
loadingMore,
error,
fetchInitialData,
loadMoreData,
selectCategory,
refreshPlayRecords,
} = useHomeStore();
const { isLoggedIn, logout } = useAuthStore();
useFocusEffect(
useCallback(() => {
refreshPlayRecords();
}, [refreshPlayRecords])
);
useEffect(() => {
if (selectedCategory && !selectedCategory.tags) {
fetchInitialData();
} else if (selectedCategory?.tags && !selectedCategory.tag) {
const defaultTag = selectedCategory.tags[0];
setSelectedTag(defaultTag);
selectCategory({ ...selectedCategory, tag: defaultTag });
}
}, [selectedCategory, fetchInitialData, selectCategory]);
useEffect(() => {
if (selectedCategory && selectedCategory.tag) {
fetchInitialData();
}
}, [fetchInitialData, selectedCategory, selectedCategory.tag]);
useEffect(() => {
if (!loading && contentData.length > 0) {
Animated.timing(fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();
} else if (loading) {
fadeAnim.setValue(0);
}
}, [loading, contentData.length, fadeAnim]);
const handleCategorySelect = (category: Category) => {
setSelectedTag(null);
selectCategory(category);
};
const handleTagSelect = (tag: string) => {
setSelectedTag(tag);
if (selectedCategory) {
const categoryWithTag = { ...selectedCategory, tag: tag };
selectCategory(categoryWithTag);
}
};
const renderCategory = ({ item }: { item: Category }) => {
const isSelected = selectedCategory?.title === item.title;
return (
<StyledButton
text={item.title}
onPress={() => handleCategorySelect(item)}
isSelected={isSelected}
style={dynamicStyles.categoryButton}
textStyle={dynamicStyles.categoryText}
/>
);
};
const renderContentItem = ({ item, index }: { item: RowItem; index: number }) => (
<VideoCard
id={item.id}
source={item.source}
title={item.title}
poster={item.poster}
year={item.year}
rate={item.rate}
progress={item.progress}
playTime={item.play_time}
episodeIndex={item.episodeIndex}
sourceName={item.sourceName}
totalEpisodes={item.totalEpisodes}
api={api}
onRecordDeleted={fetchInitialData}
/>
);
const renderFooter = () => {
if (!loadingMore) return null;
return <ActivityIndicator style={{ marginVertical: 20 }} size="large" />;
};
// TV端和平板端的顶部导航
const renderHeader = () => {
if (deviceType === "mobile") {
// 移动端不显示顶部导航使用底部Tab导航
return null;
}
return (
<View style={dynamicStyles.headerContainer}>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<ThemedText style={dynamicStyles.headerTitle}></ThemedText>
<Pressable style={{ marginLeft: 20 }} onPress={() => router.push("/live")}>
{({ focused }) => (
<ThemedText style={[dynamicStyles.headerTitle, { color: focused ? "white" : "grey" }]}></ThemedText>
)}
</Pressable>
</View>
<View style={dynamicStyles.rightHeaderButtons}>
<StyledButton style={dynamicStyles.iconButton} onPress={() => router.push("/favorites")} variant="ghost">
<Heart color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
<StyledButton
style={dynamicStyles.iconButton}
onPress={() => router.push({ pathname: "/search" })}
variant="ghost"
>
<Search color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
<StyledButton style={dynamicStyles.iconButton} onPress={() => router.push("/settings")} variant="ghost">
<Settings color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
{isLoggedIn && (
<StyledButton style={dynamicStyles.iconButton} onPress={logout} variant="ghost">
<LogOut color={colorScheme === "dark" ? "white" : "black"} size={24} />
</StyledButton>
)}
</View>
</View>
);
};
// 动态样式
const dynamicStyles = StyleSheet.create({
container: {
flex: 1,
paddingTop: deviceType === "mobile" ? 0 : 40,
},
headerContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: spacing * 1.5,
marginBottom: spacing,
},
headerTitle: {
fontSize: deviceType === "mobile" ? 24 : deviceType === "tablet" ? 28 : 32,
fontWeight: "bold",
paddingTop: 16,
},
rightHeaderButtons: {
flexDirection: "row",
alignItems: "center",
},
iconButton: {
borderRadius: 30,
marginLeft: spacing / 2,
},
categoryContainer: {
paddingBottom: spacing / 2,
},
categoryListContent: {
paddingHorizontal: spacing,
},
categoryButton: {
paddingHorizontal: deviceType === "tv" ? spacing / 4 : spacing / 2,
paddingVertical: spacing / 2,
borderRadius: deviceType === "mobile" ? 6 : 8,
marginHorizontal: deviceType === "tv" ? spacing / 4 : spacing / 2, // TV端使用更小的间距
},
categoryText: {
fontSize: deviceType === "mobile" ? 14 : 16,
fontWeight: "500",
},
contentContainer: {
flex: 1,
},
});
const content = (
<ThemedView style={[commonStyles.container, dynamicStyles.container]}>
{/* 状态栏 */}
{deviceType === "mobile" && <StatusBar barStyle="light-content" />}
{/* 顶部导航 */}
{renderHeader()}
{/* 分类选择器 */}
<View style={dynamicStyles.categoryContainer}>
<FlatList
data={categories}
renderItem={renderCategory}
keyExtractor={(item) => item.title}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={dynamicStyles.categoryListContent}
/>
</View>
{/* 子分类标签 */}
{selectedCategory && selectedCategory.tags && (
<View style={dynamicStyles.categoryContainer}>
<FlatList
data={selectedCategory.tags}
renderItem={({ item, index }) => {
const isSelected = selectedTag === item;
return (
<StyledButton
hasTVPreferredFocus={index === 0}
text={item}
onPress={() => handleTagSelect(item)}
isSelected={isSelected}
style={dynamicStyles.categoryButton}
textStyle={dynamicStyles.categoryText}
variant="ghost"
/>
);
}}
keyExtractor={(item) => item}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={dynamicStyles.categoryListContent}
/>
</View>
)}
{/* 内容网格 */}
{loading ? (
<View style={commonStyles.center}>
<ActivityIndicator size="large" />
</View>
) : error ? (
<View style={commonStyles.center}>
<ThemedText type="subtitle" style={{ padding: spacing }}>
{error}
</ThemedText>
</View>
) : (
<Animated.View style={[dynamicStyles.contentContainer, { opacity: fadeAnim }]}>
<CustomScrollView
data={contentData}
renderItem={renderContentItem}
loading={loading}
loadingMore={loadingMore}
error={error}
onEndReached={loadMoreData}
loadMoreThreshold={LOAD_MORE_THRESHOLD}
emptyMessage={selectedCategory?.tags ? "请选择一个子分类" : "该分类下暂无内容"}
ListFooterComponent={renderFooter}
/>
</Animated.View>
)}
</ThemedView>
);
// 根据设备类型决定是否包装在响应式导航中
if (deviceType === "tv") {
return content;
}
return <ResponsiveNavigation>{content}</ResponsiveNavigation>;
}