2026-03-12 21:21:01 +08:00
|
|
|
|
import React, {
|
|
|
|
|
|
useEffect,
|
|
|
|
|
|
useState,
|
|
|
|
|
|
useRef,
|
|
|
|
|
|
useMemo,
|
|
|
|
|
|
useCallback,
|
|
|
|
|
|
} from "react";
|
2026-03-05 18:02:54 +08:00
|
|
|
|
import {
|
2026-03-11 14:38:31 +08:00
|
|
|
|
View,
|
|
|
|
|
|
StyleSheet,
|
|
|
|
|
|
Text,
|
|
|
|
|
|
TouchableOpacity,
|
|
|
|
|
|
ActivityIndicator,
|
|
|
|
|
|
Animated,
|
|
|
|
|
|
Image,
|
|
|
|
|
|
RefreshControl,
|
|
|
|
|
|
ViewToken,
|
2026-03-13 21:52:09 +08:00
|
|
|
|
FlatList,
|
|
|
|
|
|
ScrollView,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
} from "react-native";
|
2026-03-14 18:25:13 +08:00
|
|
|
|
import PagerView from "react-native-pager-view";
|
2026-03-11 14:38:31 +08:00
|
|
|
|
import {
|
|
|
|
|
|
SafeAreaView,
|
|
|
|
|
|
useSafeAreaInsets,
|
|
|
|
|
|
} from "react-native-safe-area-context";
|
|
|
|
|
|
import { useRouter } from "expo-router";
|
|
|
|
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
|
|
|
|
import { VideoCard } from "../components/VideoCard";
|
2026-03-13 21:52:09 +08:00
|
|
|
|
import { LiveCard } from "../components/LiveCard";
|
2026-03-11 14:38:31 +08:00
|
|
|
|
import { LoginModal } from "../components/LoginModal";
|
2026-03-17 22:18:05 +08:00
|
|
|
|
import { DownloadProgressBtn } from "../components/DownloadProgressBtn";
|
2026-03-11 14:38:31 +08:00
|
|
|
|
import { useVideoList } from "../hooks/useVideoList";
|
2026-03-13 21:52:09 +08:00
|
|
|
|
import { useLiveList } from "../hooks/useLiveList";
|
2026-03-11 14:38:31 +08:00
|
|
|
|
import { useAuthStore } from "../store/authStore";
|
2026-03-14 12:27:03 +08:00
|
|
|
|
import {
|
|
|
|
|
|
toListRows,
|
|
|
|
|
|
type ListRow,
|
|
|
|
|
|
type BigRow,
|
|
|
|
|
|
type LiveRow,
|
|
|
|
|
|
} from "../utils/videoRows";
|
2026-03-11 14:38:31 +08:00
|
|
|
|
import { BigVideoCard } from "../components/BigVideoCard";
|
2026-03-19 20:46:16 +08:00
|
|
|
|
import { FollowedLiveStrip } from "../components/FollowedLiveStrip";
|
2026-03-24 21:04:10 +08:00
|
|
|
|
import { useTheme } from "../utils/theme";
|
2026-03-13 21:52:09 +08:00
|
|
|
|
import type { LiveRoom } from "../services/types";
|
2026-03-05 18:02:54 +08:00
|
|
|
|
|
2026-03-10 19:04:18 +08:00
|
|
|
|
const HEADER_H = 44;
|
2026-03-11 14:38:31 +08:00
|
|
|
|
const TAB_H = 38;
|
|
|
|
|
|
const NAV_H = HEADER_H + TAB_H;
|
|
|
|
|
|
|
|
|
|
|
|
const VIEWABILITY_CONFIG = { itemVisiblePercentThreshold: 50 };
|
2026-03-10 19:04:18 +08:00
|
|
|
|
|
2026-03-13 21:52:09 +08:00
|
|
|
|
type TabKey = "hot" | "live";
|
|
|
|
|
|
|
|
|
|
|
|
const TABS: { key: TabKey; label: string }[] = [
|
2026-03-20 14:37:14 +08:00
|
|
|
|
{ key: "hot", label: "热门" },
|
2026-03-13 21:52:09 +08:00
|
|
|
|
{ key: "live", label: "直播" },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const LIVE_AREAS = [
|
|
|
|
|
|
{ id: 0, name: "推荐" },
|
|
|
|
|
|
{ id: 2, name: "网游" },
|
|
|
|
|
|
{ id: 3, name: "手游" },
|
|
|
|
|
|
{ id: 6, name: "单机游戏" },
|
|
|
|
|
|
{ id: 1, name: "娱乐" },
|
|
|
|
|
|
{ id: 9, name: "虚拟主播" },
|
|
|
|
|
|
{ id: 10, name: "生活" },
|
|
|
|
|
|
{ id: 11, name: "知识" },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
2026-03-05 18:02:54 +08:00
|
|
|
|
export default function HomeScreen() {
|
|
|
|
|
|
const router = useRouter();
|
2026-03-14 12:27:03 +08:00
|
|
|
|
const { pages, liveRooms, loading, refreshing, load, refresh } =
|
|
|
|
|
|
useVideoList();
|
2026-03-13 21:52:09 +08:00
|
|
|
|
const {
|
|
|
|
|
|
rooms,
|
|
|
|
|
|
loading: liveLoading,
|
|
|
|
|
|
refreshing: liveRefreshing,
|
|
|
|
|
|
load: liveLoad,
|
|
|
|
|
|
refresh: liveRefresh,
|
|
|
|
|
|
} = useLiveList();
|
2026-03-19 16:34:56 +08:00
|
|
|
|
const { isLoggedIn, face } = useAuthStore();
|
2026-03-05 18:02:54 +08:00
|
|
|
|
const [showLogin, setShowLogin] = useState(false);
|
2026-03-10 19:04:18 +08:00
|
|
|
|
const insets = useSafeAreaInsets();
|
2026-03-13 21:52:09 +08:00
|
|
|
|
const [activeTab, setActiveTab] = useState<TabKey>("hot");
|
|
|
|
|
|
const [liveAreaId, setLiveAreaId] = useState(0);
|
2026-03-10 19:04:18 +08:00
|
|
|
|
|
2026-03-24 21:04:10 +08:00
|
|
|
|
const theme = useTheme();
|
2026-03-11 14:38:31 +08:00
|
|
|
|
const [visibleBigKey, setVisibleBigKey] = useState<string | null>(null);
|
2026-03-14 11:55:45 +08:00
|
|
|
|
const rows = useMemo(() => toListRows(pages, liveRooms), [pages, liveRooms]);
|
2026-03-14 18:25:13 +08:00
|
|
|
|
const pagerRef = useRef<PagerView>(null);
|
2026-03-13 21:52:09 +08:00
|
|
|
|
|
|
|
|
|
|
const hotListRef = useRef<FlatList>(null);
|
|
|
|
|
|
const liveListRef = useRef<FlatList>(null);
|
2026-03-11 14:38:31 +08:00
|
|
|
|
|
|
|
|
|
|
const onViewableItemsChangedRef = useRef(
|
|
|
|
|
|
({ viewableItems }: { viewableItems: ViewToken[] }) => {
|
|
|
|
|
|
const bigRow = viewableItems.find(
|
2026-03-12 21:21:01 +08:00
|
|
|
|
(v) => v.item && (v.item as ListRow).type === "big",
|
2026-03-11 14:38:31 +08:00
|
|
|
|
);
|
2026-03-12 21:21:01 +08:00
|
|
|
|
setVisibleBigKey(bigRow ? (bigRow.item as BigRow).item.bvid : null);
|
2026-03-11 14:38:31 +08:00
|
|
|
|
},
|
2026-03-10 19:04:18 +08:00
|
|
|
|
).current;
|
2026-03-11 14:38:31 +08:00
|
|
|
|
|
|
|
|
|
|
const scrollY = useRef(new Animated.Value(0)).current;
|
2026-03-12 21:21:01 +08:00
|
|
|
|
|
2026-03-11 14:38:31 +08:00
|
|
|
|
const headerTranslate = scrollY.interpolate({
|
2026-03-12 23:57:01 +08:00
|
|
|
|
inputRange: [0, HEADER_H],
|
|
|
|
|
|
outputRange: [0, -HEADER_H],
|
|
|
|
|
|
extrapolate: "clamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const headerOpacity = scrollY.interpolate({
|
|
|
|
|
|
inputRange: [0, HEADER_H * 0.2],
|
|
|
|
|
|
outputRange: [1, 0],
|
2026-03-11 14:38:31 +08:00
|
|
|
|
extrapolate: "clamp",
|
2026-03-10 19:04:18 +08:00
|
|
|
|
});
|
2026-03-05 18:02:54 +08:00
|
|
|
|
|
2026-03-13 21:52:09 +08:00
|
|
|
|
// 直播列表也共用同一个 scrollY
|
|
|
|
|
|
const liveScrollY = useRef(new Animated.Value(0)).current;
|
|
|
|
|
|
|
|
|
|
|
|
const liveHeaderTranslate = liveScrollY.interpolate({
|
|
|
|
|
|
inputRange: [0, HEADER_H],
|
|
|
|
|
|
outputRange: [0, -HEADER_H],
|
|
|
|
|
|
extrapolate: "clamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const liveHeaderOpacity = liveScrollY.interpolate({
|
|
|
|
|
|
inputRange: [0, HEADER_H * 0.2],
|
|
|
|
|
|
outputRange: [1, 0],
|
|
|
|
|
|
extrapolate: "clamp",
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-03-11 14:38:31 +08:00
|
|
|
|
useEffect(() => {
|
|
|
|
|
|
load();
|
|
|
|
|
|
}, []);
|
2026-03-05 18:02:54 +08:00
|
|
|
|
|
2026-03-12 21:21:01 +08:00
|
|
|
|
const onScroll = useMemo(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
Animated.event([{ nativeEvent: { contentOffset: { y: scrollY } } }], {
|
|
|
|
|
|
useNativeDriver: true,
|
|
|
|
|
|
}),
|
|
|
|
|
|
[],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-13 21:52:09 +08:00
|
|
|
|
const onLiveScroll = useMemo(
|
|
|
|
|
|
() =>
|
2026-03-14 12:27:03 +08:00
|
|
|
|
Animated.event([{ nativeEvent: { contentOffset: { y: liveScrollY } } }], {
|
|
|
|
|
|
useNativeDriver: true,
|
|
|
|
|
|
}),
|
2026-03-13 21:52:09 +08:00
|
|
|
|
[],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleTabPress = useCallback(
|
|
|
|
|
|
(key: TabKey) => {
|
|
|
|
|
|
if (key === activeTab) {
|
|
|
|
|
|
// 点击已激活的 tab:滚动到顶部并刷新
|
|
|
|
|
|
if (key === "hot") {
|
|
|
|
|
|
hotListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
|
|
|
|
|
refresh();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
liveListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
|
|
|
|
|
liveRefresh(liveAreaId);
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
// 切换 tab
|
2026-03-14 18:25:13 +08:00
|
|
|
|
pagerRef.current?.setPage(key === "hot" ? 0 : 1);
|
2026-03-13 21:52:09 +08:00
|
|
|
|
setActiveTab(key);
|
|
|
|
|
|
if (key === "live" && rooms.length === 0) {
|
|
|
|
|
|
liveLoad(true, liveAreaId);
|
|
|
|
|
|
}
|
2026-03-14 18:25:13 +08:00
|
|
|
|
},
|
|
|
|
|
|
[activeTab, rooms.length, liveAreaId],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const onPageSelected = useCallback(
|
|
|
|
|
|
(e: any) => {
|
|
|
|
|
|
const key: TabKey = e.nativeEvent.position === 0 ? "hot" : "live";
|
|
|
|
|
|
if (key === activeTab) return;
|
|
|
|
|
|
setActiveTab(key);
|
|
|
|
|
|
if (key === "live" && rooms.length === 0) {
|
|
|
|
|
|
liveLoad(true, liveAreaId);
|
2026-03-13 21:52:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
[activeTab, rooms.length, liveAreaId],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
const handleLiveAreaPress = useCallback(
|
|
|
|
|
|
(areaId: number) => {
|
|
|
|
|
|
if (areaId === liveAreaId) return;
|
|
|
|
|
|
setLiveAreaId(areaId);
|
|
|
|
|
|
liveListRef.current?.scrollToOffset({ offset: 0, animated: false });
|
|
|
|
|
|
liveLoad(true, areaId);
|
|
|
|
|
|
},
|
|
|
|
|
|
[liveAreaId, liveLoad],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2026-03-14 18:25:13 +08:00
|
|
|
|
const visibleBigKeyRef = useRef(visibleBigKey);
|
|
|
|
|
|
visibleBigKeyRef.current = visibleBigKey;
|
|
|
|
|
|
|
2026-03-16 21:13:26 +08:00
|
|
|
|
const renderItem = useCallback(({ item: row }: { item: ListRow }) => {
|
|
|
|
|
|
if (row.type === "big") {
|
|
|
|
|
|
return (
|
|
|
|
|
|
<BigVideoCard
|
|
|
|
|
|
item={row.item}
|
|
|
|
|
|
isVisible={visibleBigKeyRef.current === row.item.bvid}
|
|
|
|
|
|
onPress={() => router.push(`/video/${row.item.bvid}` as any)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (row.type === "live") {
|
2026-03-12 23:57:01 +08:00
|
|
|
|
return (
|
2026-03-19 20:45:33 +08:00
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
|
<View style={styles.leftCol}>
|
|
|
|
|
|
<LiveCard
|
|
|
|
|
|
isLivePulse
|
|
|
|
|
|
item={row.left}
|
|
|
|
|
|
onPress={() => router.push(`/live/${row.left.roomid}` as any)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{row.right && (
|
|
|
|
|
|
<View style={styles.rightCol}>
|
|
|
|
|
|
<LiveCard
|
|
|
|
|
|
isLivePulse
|
|
|
|
|
|
item={row.right}
|
|
|
|
|
|
onPress={() => router.push(`/live/${row.right!.roomid}` as any)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
2026-03-16 21:13:26 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
const right = row.right;
|
|
|
|
|
|
return (
|
|
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
|
<View style={styles.leftCol}>
|
|
|
|
|
|
<VideoCard
|
|
|
|
|
|
item={row.left}
|
|
|
|
|
|
onPress={() => router.push(`/video/${row.left.bvid}` as any)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
{right && (
|
|
|
|
|
|
<View style={styles.rightCol}>
|
2026-03-11 14:38:31 +08:00
|
|
|
|
<VideoCard
|
2026-03-16 21:13:26 +08:00
|
|
|
|
item={right}
|
|
|
|
|
|
onPress={() => router.push(`/video/${right.bvid}` as any)}
|
2026-03-11 14:38:31 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
2026-03-16 21:13:26 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
);
|
|
|
|
|
|
}, []);
|
2026-03-05 18:02:54 +08:00
|
|
|
|
|
2026-03-13 21:52:09 +08:00
|
|
|
|
const renderLiveItem = useCallback(
|
|
|
|
|
|
({ item }: { item: { left: LiveRoom; right?: LiveRoom } }) => (
|
|
|
|
|
|
<View style={styles.row}>
|
|
|
|
|
|
<View style={styles.leftCol}>
|
2026-03-16 16:20:59 +08:00
|
|
|
|
<LiveCard
|
|
|
|
|
|
item={item.left}
|
|
|
|
|
|
onPress={() => router.push(`/live/${item.left.roomid}` as any)}
|
|
|
|
|
|
/>
|
2026-03-13 21:52:09 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
{item.right && (
|
|
|
|
|
|
<View style={styles.rightCol}>
|
2026-03-16 16:20:59 +08:00
|
|
|
|
<LiveCard
|
|
|
|
|
|
item={item.right}
|
|
|
|
|
|
onPress={() => router.push(`/live/${item.right!.roomid}` as any)}
|
|
|
|
|
|
/>
|
2026-03-13 21:52:09 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
),
|
|
|
|
|
|
[],
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// 将直播列表分成两列的行
|
|
|
|
|
|
const liveRows = useMemo(() => {
|
|
|
|
|
|
const result: { left: LiveRoom; right?: LiveRoom }[] = [];
|
|
|
|
|
|
for (let i = 0; i < rooms.length; i += 2) {
|
|
|
|
|
|
result.push({ left: rooms[i], right: rooms[i + 1] });
|
|
|
|
|
|
}
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}, [rooms]);
|
|
|
|
|
|
|
|
|
|
|
|
const currentHeaderTranslate =
|
|
|
|
|
|
activeTab === "hot" ? headerTranslate : liveHeaderTranslate;
|
|
|
|
|
|
const currentHeaderOpacity =
|
|
|
|
|
|
activeTab === "hot" ? headerOpacity : liveHeaderOpacity;
|
|
|
|
|
|
|
2026-03-05 18:02:54 +08:00
|
|
|
|
return (
|
2026-03-24 21:04:10 +08:00
|
|
|
|
<SafeAreaView style={[styles.safe, { backgroundColor: theme.bg }]} edges={["left", "right"]}>
|
2026-03-14 12:27:03 +08:00
|
|
|
|
{/* 滑动切换容器 */}
|
2026-03-14 18:25:13 +08:00
|
|
|
|
<PagerView
|
|
|
|
|
|
ref={pagerRef}
|
|
|
|
|
|
style={styles.pager}
|
|
|
|
|
|
initialPage={0}
|
|
|
|
|
|
scrollEnabled={false}
|
|
|
|
|
|
onPageSelected={onPageSelected}
|
|
|
|
|
|
>
|
|
|
|
|
|
{/* 热门列表 */}
|
|
|
|
|
|
<View key="hot" collapsable={false}>
|
|
|
|
|
|
<Animated.FlatList
|
|
|
|
|
|
ref={hotListRef as any}
|
|
|
|
|
|
style={styles.listContainer}
|
|
|
|
|
|
data={rows}
|
2026-03-19 21:53:08 +08:00
|
|
|
|
keyExtractor={(row: any, index: number) =>
|
2026-03-14 18:25:13 +08:00
|
|
|
|
row.type === "big"
|
|
|
|
|
|
? `big-${row.item.bvid}`
|
|
|
|
|
|
: row.type === "live"
|
2026-03-19 21:53:08 +08:00
|
|
|
|
? `live-${index}-${row.left.roomid}-${row.right?.roomid ?? "empty"}`
|
2026-03-14 18:25:13 +08:00
|
|
|
|
: `pair-${row.left.bvid}-${row.right?.bvid ?? "empty"}`
|
|
|
|
|
|
}
|
|
|
|
|
|
contentContainerStyle={{
|
|
|
|
|
|
paddingTop: insets.top + NAV_H + 6,
|
|
|
|
|
|
paddingBottom: insets.bottom + 16,
|
|
|
|
|
|
}}
|
|
|
|
|
|
renderItem={renderItem}
|
|
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={refreshing}
|
|
|
|
|
|
onRefresh={refresh}
|
|
|
|
|
|
progressViewOffset={insets.top + NAV_H}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
onEndReached={() => load()}
|
|
|
|
|
|
onEndReachedThreshold={0.5}
|
|
|
|
|
|
extraData={visibleBigKey}
|
|
|
|
|
|
viewabilityConfig={VIEWABILITY_CONFIG}
|
|
|
|
|
|
onViewableItemsChanged={onViewableItemsChangedRef}
|
|
|
|
|
|
ListFooterComponent={
|
|
|
|
|
|
<View style={styles.footer}>
|
|
|
|
|
|
{loading && <ActivityIndicator color="#00AEEC" />}
|
|
|
|
|
|
</View>
|
|
|
|
|
|
}
|
|
|
|
|
|
onScroll={onScroll}
|
|
|
|
|
|
scrollEventThrottle={16}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
2026-03-13 21:52:09 +08:00
|
|
|
|
|
2026-03-14 18:25:13 +08:00
|
|
|
|
{/* 直播列表 */}
|
|
|
|
|
|
<View key="live" collapsable={false}>
|
|
|
|
|
|
<Animated.FlatList
|
|
|
|
|
|
ref={liveListRef as any}
|
|
|
|
|
|
style={styles.listContainer}
|
|
|
|
|
|
data={liveRows}
|
|
|
|
|
|
keyExtractor={(item: any, index: number) =>
|
|
|
|
|
|
`live-${index}-${item.left.roomid}-${item.right?.roomid ?? "empty"}`
|
|
|
|
|
|
}
|
|
|
|
|
|
contentContainerStyle={{
|
|
|
|
|
|
paddingTop: insets.top + NAV_H + 6,
|
|
|
|
|
|
paddingBottom: insets.bottom + 16,
|
|
|
|
|
|
}}
|
|
|
|
|
|
renderItem={renderLiveItem}
|
|
|
|
|
|
ListHeaderComponent={
|
2026-03-19 20:46:16 +08:00
|
|
|
|
<View>
|
|
|
|
|
|
<FollowedLiveStrip />
|
|
|
|
|
|
<ScrollView
|
|
|
|
|
|
horizontal
|
|
|
|
|
|
showsHorizontalScrollIndicator={false}
|
|
|
|
|
|
style={styles.areaTabRow}
|
|
|
|
|
|
contentContainerStyle={styles.areaTabContent}
|
|
|
|
|
|
>
|
|
|
|
|
|
{LIVE_AREAS.map((area) => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key={area.id}
|
2026-03-14 12:27:03 +08:00
|
|
|
|
style={[
|
2026-03-19 20:46:16 +08:00
|
|
|
|
styles.areaTab,
|
|
|
|
|
|
liveAreaId === area.id && styles.areaTabActive,
|
2026-03-14 12:27:03 +08:00
|
|
|
|
]}
|
2026-03-19 20:46:16 +08:00
|
|
|
|
onPress={() => handleLiveAreaPress(area.id)}
|
|
|
|
|
|
activeOpacity={0.7}
|
2026-03-14 12:27:03 +08:00
|
|
|
|
>
|
2026-03-19 20:46:16 +08:00
|
|
|
|
<Text
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.areaTabText,
|
|
|
|
|
|
liveAreaId === area.id && styles.areaTabTextActive,
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
{area.name}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ScrollView>
|
|
|
|
|
|
</View>
|
2026-03-14 18:25:13 +08:00
|
|
|
|
}
|
|
|
|
|
|
refreshControl={
|
|
|
|
|
|
<RefreshControl
|
|
|
|
|
|
refreshing={liveRefreshing}
|
|
|
|
|
|
onRefresh={() => liveRefresh(liveAreaId)}
|
|
|
|
|
|
progressViewOffset={insets.top + NAV_H}
|
|
|
|
|
|
/>
|
|
|
|
|
|
}
|
|
|
|
|
|
onEndReached={() => liveLoad()}
|
|
|
|
|
|
onEndReachedThreshold={1.5}
|
|
|
|
|
|
ListFooterComponent={
|
|
|
|
|
|
liveLoading ? (
|
|
|
|
|
|
<View style={styles.footer}>
|
|
|
|
|
|
<ActivityIndicator color="#00AEEC" />
|
|
|
|
|
|
<Text style={styles.footerText}>加载中...</Text>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
onScroll={onLiveScroll}
|
|
|
|
|
|
scrollEventThrottle={16}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</View>
|
|
|
|
|
|
</PagerView>
|
2026-03-13 21:52:09 +08:00
|
|
|
|
|
|
|
|
|
|
{/* 绝对定位导航栏 */}
|
2026-03-10 19:04:18 +08:00
|
|
|
|
<Animated.View
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.navBar,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
{
|
|
|
|
|
|
paddingTop: insets.top,
|
2026-03-24 21:04:10 +08:00
|
|
|
|
backgroundColor: theme.card,
|
2026-03-13 21:52:09 +08:00
|
|
|
|
transform: [{ translateY: currentHeaderTranslate }],
|
2026-03-11 14:38:31 +08:00
|
|
|
|
},
|
2026-03-10 19:04:18 +08:00
|
|
|
|
]}
|
|
|
|
|
|
>
|
2026-03-12 23:57:01 +08:00
|
|
|
|
<Animated.View
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.header,
|
|
|
|
|
|
{
|
2026-03-13 21:52:09 +08:00
|
|
|
|
opacity: currentHeaderOpacity,
|
2026-03-24 21:04:10 +08:00
|
|
|
|
borderBottomColor: theme.border,
|
2026-03-12 23:57:01 +08:00
|
|
|
|
},
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
2026-03-10 19:04:18 +08:00
|
|
|
|
<View style={styles.headerRight}>
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
style={styles.headerBtn}
|
2026-03-19 16:34:56 +08:00
|
|
|
|
onPress={() => (isLoggedIn ? router.push('/settings' as any) : setShowLogin(true))}
|
2026-03-10 19:04:18 +08:00
|
|
|
|
>
|
|
|
|
|
|
{isLoggedIn && face ? (
|
|
|
|
|
|
<Image source={{ uri: face }} style={styles.userAvatar} />
|
|
|
|
|
|
) : (
|
2026-03-11 14:38:31 +08:00
|
|
|
|
<Ionicons
|
|
|
|
|
|
name={isLoggedIn ? "person" : "person-outline"}
|
|
|
|
|
|
size={22}
|
|
|
|
|
|
color="#00AEEC"
|
|
|
|
|
|
/>
|
2026-03-10 19:04:18 +08:00
|
|
|
|
)}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
</View>
|
2026-03-16 21:13:26 +08:00
|
|
|
|
<TouchableOpacity
|
2026-03-24 21:04:10 +08:00
|
|
|
|
style={[styles.searchBar, { backgroundColor: theme.inputBg }]}
|
2026-03-16 21:13:26 +08:00
|
|
|
|
onPress={() => router.push("/search" as any)}
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
2026-03-24 21:04:10 +08:00
|
|
|
|
<Ionicons name="search" size={14} color={theme.textSub} />
|
|
|
|
|
|
<Text style={[styles.searchPlaceholder, { color: theme.textSub }]}>搜索视频、UP主...</Text>
|
2026-03-16 21:13:26 +08:00
|
|
|
|
</TouchableOpacity>
|
2026-03-19 12:55:23 +08:00
|
|
|
|
<DownloadProgressBtn
|
|
|
|
|
|
onPress={() => router.push("/downloads" as any)}
|
|
|
|
|
|
/>
|
2026-03-12 23:57:01 +08:00
|
|
|
|
</Animated.View>
|
2026-03-10 19:04:18 +08:00
|
|
|
|
|
2026-03-24 21:04:10 +08:00
|
|
|
|
<View style={[styles.tabRow, { backgroundColor: theme.card }]}>
|
2026-03-13 21:52:09 +08:00
|
|
|
|
{TABS.map((tab) => (
|
|
|
|
|
|
<TouchableOpacity
|
|
|
|
|
|
key={tab.key}
|
|
|
|
|
|
style={styles.tabItem}
|
|
|
|
|
|
onPress={() => handleTabPress(tab.key)}
|
|
|
|
|
|
activeOpacity={0.7}
|
|
|
|
|
|
>
|
|
|
|
|
|
<Text
|
|
|
|
|
|
style={[
|
|
|
|
|
|
styles.tabText,
|
2026-03-24 21:04:10 +08:00
|
|
|
|
{ color: theme.textSub },
|
2026-03-13 21:52:09 +08:00
|
|
|
|
activeTab === tab.key && styles.tabTextActive,
|
|
|
|
|
|
]}
|
|
|
|
|
|
>
|
|
|
|
|
|
{tab.label}
|
|
|
|
|
|
</Text>
|
|
|
|
|
|
{activeTab === tab.key && <View style={styles.tabUnderline} />}
|
|
|
|
|
|
</TouchableOpacity>
|
|
|
|
|
|
))}
|
2026-03-10 19:04:18 +08:00
|
|
|
|
</View>
|
|
|
|
|
|
</Animated.View>
|
|
|
|
|
|
|
2026-03-05 18:02:54 +08:00
|
|
|
|
<LoginModal visible={showLogin} onClose={() => setShowLogin(false)} />
|
|
|
|
|
|
</SafeAreaView>
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const styles = StyleSheet.create({
|
2026-03-11 14:38:31 +08:00
|
|
|
|
safe: { flex: 1, backgroundColor: "#f4f4f4" },
|
2026-03-14 18:25:13 +08:00
|
|
|
|
pager: { flex: 1 },
|
2026-03-10 19:04:18 +08:00
|
|
|
|
listContainer: { flex: 1 },
|
|
|
|
|
|
navBar: {
|
2026-03-11 14:38:31 +08:00
|
|
|
|
position: "absolute",
|
2026-03-10 19:04:18 +08:00
|
|
|
|
top: 0,
|
|
|
|
|
|
left: 0,
|
|
|
|
|
|
right: 0,
|
|
|
|
|
|
zIndex: 10,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
backgroundColor: "#fff",
|
|
|
|
|
|
overflow: "hidden",
|
2026-03-10 19:04:18 +08:00
|
|
|
|
elevation: 2,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
shadowColor: "#000",
|
2026-03-10 19:04:18 +08:00
|
|
|
|
shadowOffset: { width: 0, height: 1 },
|
|
|
|
|
|
shadowOpacity: 0.06,
|
|
|
|
|
|
shadowRadius: 2,
|
|
|
|
|
|
},
|
|
|
|
|
|
header: {
|
|
|
|
|
|
height: HEADER_H,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
flexDirection: "row",
|
|
|
|
|
|
alignItems: "center",
|
2026-03-10 19:04:18 +08:00
|
|
|
|
paddingHorizontal: 16,
|
2026-03-16 21:13:26 +08:00
|
|
|
|
gap: 10,
|
2026-03-10 19:04:18 +08:00
|
|
|
|
borderBottomWidth: StyleSheet.hairlineWidth,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
borderBottomColor: "#eee",
|
|
|
|
|
|
},
|
|
|
|
|
|
logo: {
|
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
|
fontWeight: "800",
|
|
|
|
|
|
color: "#00AEEC",
|
|
|
|
|
|
letterSpacing: -0.5,
|
2026-03-16 21:13:26 +08:00
|
|
|
|
width: 72,
|
|
|
|
|
|
},
|
|
|
|
|
|
searchBar: {
|
|
|
|
|
|
flex: 1,
|
|
|
|
|
|
height: 30,
|
|
|
|
|
|
backgroundColor: "#f0f0f0",
|
|
|
|
|
|
borderRadius: 15,
|
|
|
|
|
|
flexDirection: "row",
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
paddingHorizontal: 10,
|
|
|
|
|
|
gap: 5,
|
|
|
|
|
|
},
|
2026-03-17 15:14:23 +08:00
|
|
|
|
downloadBtn: {},
|
2026-03-16 21:13:26 +08:00
|
|
|
|
searchPlaceholder: {
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
color: "#999",
|
|
|
|
|
|
flex: 1,
|
2026-03-10 19:04:18 +08:00
|
|
|
|
},
|
2026-03-14 12:27:03 +08:00
|
|
|
|
headerRight: { flexDirection: "row", gap: 8, alignItems: "center" },
|
|
|
|
|
|
headerBtn: { paddingLeft: 0 },
|
2026-03-11 14:38:31 +08:00
|
|
|
|
userAvatar: {
|
2026-03-13 21:52:09 +08:00
|
|
|
|
width: 35,
|
|
|
|
|
|
height: 35,
|
|
|
|
|
|
borderRadius: 50,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
backgroundColor: "#eee",
|
|
|
|
|
|
},
|
2026-03-10 19:04:18 +08:00
|
|
|
|
tabRow: {
|
|
|
|
|
|
height: TAB_H,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
backgroundColor: "#fff",
|
2026-03-10 19:04:18 +08:00
|
|
|
|
paddingHorizontal: 16,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
flexDirection: "row",
|
|
|
|
|
|
alignItems: "center",
|
2026-03-13 21:52:09 +08:00
|
|
|
|
gap: 20,
|
|
|
|
|
|
},
|
|
|
|
|
|
tabItem: {
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
height: TAB_H,
|
|
|
|
|
|
},
|
|
|
|
|
|
tabText: {
|
|
|
|
|
|
fontSize: 15,
|
2026-03-17 22:18:05 +08:00
|
|
|
|
fontWeight: "450",
|
2026-03-13 21:52:09 +08:00
|
|
|
|
color: "#999",
|
|
|
|
|
|
},
|
|
|
|
|
|
tabTextActive: {
|
2026-03-17 22:18:05 +08:00
|
|
|
|
fontWeight: "500",
|
2026-03-13 21:52:09 +08:00
|
|
|
|
color: "#00AEEC",
|
2026-03-10 19:04:18 +08:00
|
|
|
|
},
|
|
|
|
|
|
tabUnderline: {
|
2026-03-11 14:38:31 +08:00
|
|
|
|
position: "absolute",
|
2026-03-12 23:57:01 +08:00
|
|
|
|
bottom: 4,
|
2026-03-10 19:04:18 +08:00
|
|
|
|
width: 24,
|
2026-03-12 23:57:01 +08:00
|
|
|
|
height: 3,
|
2026-03-11 14:38:31 +08:00
|
|
|
|
backgroundColor: "#00AEEC",
|
2026-03-12 23:57:01 +08:00
|
|
|
|
borderRadius: 4,
|
2026-03-10 19:04:18 +08:00
|
|
|
|
},
|
2026-03-12 21:21:01 +08:00
|
|
|
|
row: {
|
|
|
|
|
|
flexDirection: "row",
|
|
|
|
|
|
paddingHorizontal: 1,
|
|
|
|
|
|
justifyContent: "flex-start",
|
|
|
|
|
|
},
|
2026-03-05 18:02:54 +08:00
|
|
|
|
leftCol: { marginLeft: 4, marginRight: 2 },
|
|
|
|
|
|
rightCol: { marginLeft: 2, marginRight: 4 },
|
2026-03-14 12:27:03 +08:00
|
|
|
|
footer: {
|
|
|
|
|
|
height: 48,
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
justifyContent: "center",
|
|
|
|
|
|
flexDirection: "row",
|
|
|
|
|
|
gap: 6,
|
|
|
|
|
|
},
|
2026-03-13 21:52:09 +08:00
|
|
|
|
footerText: { fontSize: 12, color: "#999" },
|
|
|
|
|
|
areaTabRow: {
|
|
|
|
|
|
marginBottom: 6,
|
|
|
|
|
|
},
|
|
|
|
|
|
areaTabContent: {
|
|
|
|
|
|
paddingHorizontal: 8,
|
|
|
|
|
|
gap: 8,
|
|
|
|
|
|
alignItems: "center",
|
|
|
|
|
|
height: 36,
|
|
|
|
|
|
},
|
|
|
|
|
|
areaTab: {
|
|
|
|
|
|
paddingHorizontal: 10,
|
|
|
|
|
|
paddingVertical: 2,
|
|
|
|
|
|
borderRadius: 16,
|
|
|
|
|
|
backgroundColor: "#f0f0f0",
|
|
|
|
|
|
},
|
|
|
|
|
|
areaTabActive: {
|
|
|
|
|
|
backgroundColor: "#00AEEC",
|
|
|
|
|
|
},
|
|
|
|
|
|
areaTabText: {
|
|
|
|
|
|
fontSize: 13,
|
|
|
|
|
|
color: "#333",
|
|
|
|
|
|
fontWeight: "500",
|
|
|
|
|
|
},
|
|
|
|
|
|
areaTabTextActive: {
|
|
|
|
|
|
color: "#fff",
|
|
|
|
|
|
fontWeight: "600",
|
|
|
|
|
|
},
|
2026-03-05 18:02:54 +08:00
|
|
|
|
});
|