Files
OrionTV/app/index.tsx
zimplexing cdf0d72bdc fix(ui): resolve status bar overlay issue across all screens
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.
2025-08-13 19:23:32 +08:00

322 lines
11 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 { 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>;
}