mirror of
https://github.com/zimplexing/OrionTV.git
synced 2026-02-15 04:14:42 +08:00
Add SafeAreaProvider to root layout and implement proper safe area handling: - Wrap app in SafeAreaProvider in _layout.tsx - Update HomeScreen to use safe area insets for proper top padding - Fix SettingsScreen safe area handling for all device types - Update ResponsiveHeader to use SafeAreaContext instead of manual calculation This ensures content is not covered by the status bar on mobile and tablet devices while maintaining TV compatibility.
322 lines
11 KiB
TypeScript
322 lines
11 KiB
TypeScript
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 { useSettingsStore } from "@/stores/settingsStore";
|
||
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,
|
||
clearError,
|
||
} = useHomeStore();
|
||
const { isLoggedIn, logout } = useAuthStore();
|
||
const { apiBaseUrl } = useSettingsStore();
|
||
|
||
useFocusEffect(
|
||
useCallback(() => {
|
||
refreshPlayRecords();
|
||
}, [refreshPlayRecords])
|
||
);
|
||
|
||
useEffect(() => {
|
||
// 只有在 apiBaseUrl 存在时才调用 fetchInitialData,避免时序问题
|
||
if (selectedCategory && !selectedCategory.tags && apiBaseUrl) {
|
||
fetchInitialData();
|
||
} else if (selectedCategory?.tags && !selectedCategory.tag) {
|
||
const defaultTag = selectedCategory.tags[0];
|
||
setSelectedTag(defaultTag);
|
||
selectCategory({ ...selectedCategory, tag: defaultTag });
|
||
}
|
||
}, [selectedCategory, fetchInitialData, selectCategory, apiBaseUrl]);
|
||
|
||
useEffect(() => {
|
||
// 只有在 apiBaseUrl 存在时才调用 fetchInitialData,避免时序问题
|
||
if (selectedCategory && selectedCategory.tag && apiBaseUrl) {
|
||
fetchInitialData();
|
||
}
|
||
}, [fetchInitialData, selectedCategory, selectedCategory.tag, apiBaseUrl]);
|
||
|
||
// 检查是否需要显示API配置提示
|
||
const shouldShowApiConfig = !apiBaseUrl && selectedCategory && !selectedCategory.tags;
|
||
|
||
// 清除错误状态,当API未配置时
|
||
useEffect(() => {
|
||
if (shouldShowApiConfig && error) {
|
||
// 如果需要显示API配置提示,清除之前的错误状态
|
||
clearError();
|
||
}
|
||
}, [shouldShowApiConfig, error, clearError]);
|
||
|
||
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" ? insets.top : deviceType === "tablet" ? insets.top + 20 : 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>
|
||
)}
|
||
|
||
{/* 内容网格 */}
|
||
{shouldShowApiConfig ? (
|
||
<View style={commonStyles.center}>
|
||
<ThemedText type="subtitle" style={{ padding: spacing, textAlign: 'center' }}>
|
||
请点击右上角设置按钮,配置您的服务器地址
|
||
</ThemedText>
|
||
</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>;
|
||
}
|