mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
设置页面
This commit is contained in:
@@ -5,15 +5,18 @@ import { View } from 'react-native';
|
||||
import { useEffect } from 'react';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useDownloadStore } from '../store/downloadStore';
|
||||
import { useSettingsStore } from '../store/settingsStore';
|
||||
import { MiniPlayer } from '../components/MiniPlayer';
|
||||
|
||||
export default function RootLayout() {
|
||||
const restore = useAuthStore(s => s.restore);
|
||||
const loadDownloads = useDownloadStore(s => s.loadFromStorage);
|
||||
const restoreSettings = useSettingsStore(s => s.restore);
|
||||
|
||||
useEffect(() => {
|
||||
restore();
|
||||
loadDownloads();
|
||||
restoreSettings();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
@@ -53,6 +56,14 @@ export default function RootLayout() {
|
||||
gestureDirection: "horizontal",
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="settings"
|
||||
options={{
|
||||
animation: "slide_from_right",
|
||||
gestureEnabled: true,
|
||||
gestureDirection: "horizontal",
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
<MiniPlayer />
|
||||
</View>
|
||||
|
||||
@@ -76,7 +76,7 @@ export default function HomeScreen() {
|
||||
load: liveLoad,
|
||||
refresh: liveRefresh,
|
||||
} = useLiveList();
|
||||
const { isLoggedIn, face, logout } = useAuthStore();
|
||||
const { isLoggedIn, face } = useAuthStore();
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const insets = useSafeAreaInsets();
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("hot");
|
||||
@@ -411,7 +411,7 @@ export default function HomeScreen() {
|
||||
<View style={styles.headerRight}>
|
||||
<TouchableOpacity
|
||||
style={styles.headerBtn}
|
||||
onPress={() => (isLoggedIn ? logout() : setShowLogin(true))}
|
||||
onPress={() => (isLoggedIn ? router.push('/settings' as any) : setShowLogin(true))}
|
||||
>
|
||||
{isLoggedIn && face ? (
|
||||
<Image source={{ uri: face }} style={styles.userAvatar} />
|
||||
|
||||
@@ -155,6 +155,7 @@ export default function LiveDetailScreen() {
|
||||
onToggle={() => {}}
|
||||
style={[styles.danmakuFull, tab !== "danmaku" && styles.hidden]}
|
||||
hideHeader
|
||||
isLive
|
||||
maxItems={500}
|
||||
giftCounts={giftCounts}
|
||||
/>
|
||||
|
||||
115
app/settings.tsx
Normal file
115
app/settings.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
import { View, Text, TouchableOpacity, StyleSheet } from 'react-native';
|
||||
import { SafeAreaView } from 'react-native-safe-area-context';
|
||||
import { useRouter } from 'expo-router';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useAuthStore } from '../store/authStore';
|
||||
import { useSettingsStore } from '../store/settingsStore';
|
||||
|
||||
export default function SettingsScreen() {
|
||||
const router = useRouter();
|
||||
const { isLoggedIn, logout } = useAuthStore();
|
||||
const { coverQuality, setCoverQuality } = useSettingsStore();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.safe}>
|
||||
<View style={styles.topBar}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backBtn}>
|
||||
<Ionicons name="chevron-back" size={24} color="#212121" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.topTitle}>设置</Text>
|
||||
<View style={styles.spacer} />
|
||||
</View>
|
||||
|
||||
<View style={styles.section}>
|
||||
<Text style={styles.sectionLabel}>封面图清晰度</Text>
|
||||
<View style={styles.optionRow}>
|
||||
<TouchableOpacity
|
||||
style={[styles.option, coverQuality === 'hd' && styles.optionActive]}
|
||||
onPress={() => setCoverQuality('hd')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.optionText, coverQuality === 'hd' && styles.optionTextActive]}>
|
||||
高清
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.option, coverQuality === 'normal' && styles.optionActive]}
|
||||
onPress={() => setCoverQuality('normal')}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={[styles.optionText, coverQuality === 'normal' && styles.optionTextActive]}>
|
||||
普通
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{isLoggedIn && (
|
||||
<TouchableOpacity style={styles.logoutBtn} onPress={handleLogout} activeOpacity={0.8}>
|
||||
<Text style={styles.logoutText}>退出登录</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safe: { flex: 1, backgroundColor: '#f4f4f4' },
|
||||
topBar: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 8,
|
||||
backgroundColor: '#fff',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
backBtn: { padding: 4, width: 32 },
|
||||
spacer: { width: 32 },
|
||||
topTitle: {
|
||||
flex: 1,
|
||||
fontSize: 16,
|
||||
fontWeight: '600',
|
||||
color: '#212121',
|
||||
textAlign: 'center',
|
||||
},
|
||||
section: {
|
||||
backgroundColor: '#fff',
|
||||
marginTop: 16,
|
||||
paddingHorizontal: 16,
|
||||
paddingVertical: 14,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: '#eee',
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: '#eee',
|
||||
},
|
||||
sectionLabel: { fontSize: 13, color: '#999', marginBottom: 10 },
|
||||
optionRow: { flexDirection: 'row', gap: 10 },
|
||||
option: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 6,
|
||||
borderRadius: 20,
|
||||
borderWidth: 1,
|
||||
borderColor: '#e0e0e0',
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
optionActive: { borderColor: '#00AEEC', backgroundColor: '#e8f7fd' },
|
||||
optionText: { fontSize: 14, color: '#666' },
|
||||
optionTextActive: { color: '#00AEEC', fontWeight: '600' },
|
||||
logoutBtn: {
|
||||
margin: 24,
|
||||
paddingVertical: 12,
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#fff',
|
||||
borderWidth: 1,
|
||||
borderColor: '#ff4757',
|
||||
alignItems: 'center',
|
||||
},
|
||||
logoutText: { fontSize: 15, color: '#ff4757', fontWeight: '600' },
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import React, { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
FlatList,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
@@ -19,7 +18,8 @@ import { DanmakuItem } from "../../services/types";
|
||||
import DanmakuList from "../../components/DanmakuList";
|
||||
import { useVideoDetail } from "../../hooks/useVideoDetail";
|
||||
import { useComments } from "../../hooks/useComments";
|
||||
import { formatCount } from "../../utils/format";
|
||||
import { useRelatedVideos } from "../../hooks/useRelatedVideos";
|
||||
import { formatCount, formatDuration } from "../../utils/format";
|
||||
import { proxyImageUrl } from "../../utils/imageUrl";
|
||||
import { DownloadSheet } from "../../components/DownloadSheet";
|
||||
|
||||
@@ -47,6 +47,15 @@ export default function VideoDetailScreen() {
|
||||
const [danmakus, setDanmakus] = useState<DanmakuItem[]>([]);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [showDownload, setShowDownload] = useState(false);
|
||||
const {
|
||||
videos: relatedVideos,
|
||||
loading: relatedLoading,
|
||||
load: loadRelated,
|
||||
} = useRelatedVideos();
|
||||
|
||||
useEffect(() => {
|
||||
loadRelated();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (video?.aid) loadComments();
|
||||
@@ -141,110 +150,185 @@ export default function VideoDetailScreen() {
|
||||
{videoLoading ? (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
) : video ? (
|
||||
tab === "intro" ? (
|
||||
// 简介:视频信息 + 合集 + 简介文本
|
||||
<ScrollView
|
||||
style={styles.tabScroll}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.upRow}>
|
||||
<Image
|
||||
source={{ uri: proxyImageUrl(video.owner.face) }}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<Text style={styles.upName}>{video.owner.name}</Text>
|
||||
<TouchableOpacity style={styles.followBtn}>
|
||||
<Text style={styles.followTxt}>+ 关注</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>{video.title}</Text>
|
||||
<View style={styles.statsRow}>
|
||||
<StatBadge icon="play" count={video.stat.view} />
|
||||
<StatBadge icon="heart" count={video.stat.like} />
|
||||
<StatBadge icon="star" count={video.stat.favorite} />
|
||||
<StatBadge icon="chatbubble" count={video.stat.reply} />
|
||||
</View>
|
||||
</View>
|
||||
{video.ugc_season && (
|
||||
<SeasonSection
|
||||
season={video.ugc_season}
|
||||
currentBvid={bvid as string}
|
||||
onEpisodePress={(epBvid) => router.replace(`/video/${epBvid}`)}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.descBox}>
|
||||
<Text style={styles.descText}>{video.desc || "暂无简介"}</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
) : tab === "danmaku" ? (
|
||||
<>
|
||||
{tab === "intro" && (
|
||||
<FlatList<import("../../services/types").VideoItem>
|
||||
style={styles.tabScroll}
|
||||
data={relatedVideos}
|
||||
keyExtractor={(item) => item.bvid}
|
||||
showsVerticalScrollIndicator={false}
|
||||
onEndReached={() => {
|
||||
if (!relatedLoading) loadRelated();
|
||||
}}
|
||||
onEndReachedThreshold={0.5}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
<View style={styles.upRow}>
|
||||
<Image
|
||||
source={{ uri: proxyImageUrl(video.owner.face) }}
|
||||
style={styles.avatar}
|
||||
/>
|
||||
<Text style={styles.upName}>{video.owner.name}</Text>
|
||||
<TouchableOpacity style={styles.followBtn}>
|
||||
<Text style={styles.followTxt}>+ 关注</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>{video.title}</Text>
|
||||
<View style={styles.statsRow}>
|
||||
<StatBadge icon="play" count={video.stat.view} />
|
||||
<StatBadge icon="heart" count={video.stat.like} />
|
||||
<StatBadge icon="star" count={video.stat.favorite} />
|
||||
<StatBadge icon="chatbubble" count={video.stat.reply} />
|
||||
</View>
|
||||
</View>
|
||||
{video.ugc_season && (
|
||||
<SeasonSection
|
||||
season={video.ugc_season}
|
||||
currentBvid={bvid as string}
|
||||
onEpisodePress={(epBvid) =>
|
||||
router.replace(`/video/${epBvid}`)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<View style={styles.descBox}>
|
||||
<Text style={styles.descText}>
|
||||
{video.desc || "暂无简介"}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.relatedHeader}>
|
||||
<Text style={styles.relatedHeaderText}>推荐视频</Text>
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
renderItem={({ item }) => (
|
||||
<TouchableOpacity
|
||||
style={styles.relatedCard}
|
||||
onPress={() => router.push(`/video/${item.bvid}` as any)}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
<View style={styles.relatedThumbWrap}>
|
||||
<Image
|
||||
source={{ uri: proxyImageUrl(item.pic) }}
|
||||
style={styles.relatedThumb}
|
||||
resizeMode="cover"
|
||||
/>
|
||||
<View style={styles.relatedDuration}>
|
||||
<Text style={styles.relatedDurationText}>
|
||||
{formatDuration(item.duration)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.relatedInfo}>
|
||||
<Text style={styles.relatedTitle} numberOfLines={2}>
|
||||
{item.title}
|
||||
</Text>
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
}}
|
||||
>
|
||||
<Text style={styles.relatedOwner} numberOfLines={1}>
|
||||
{item.owner?.name ?? ""}
|
||||
</Text>
|
||||
<Text style={styles.relatedView}>
|
||||
{formatCount(item.stat?.view ?? 0)} 播放
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
ListEmptyComponent={
|
||||
!relatedLoading ? (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
) : null
|
||||
}
|
||||
ListFooterComponent={
|
||||
relatedLoading ? (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{tab === "comments" && (
|
||||
<FlatList
|
||||
style={styles.tabScroll}
|
||||
data={comments}
|
||||
keyExtractor={(c) => String(c.rpid)}
|
||||
renderItem={({ item }) => <CommentItem item={item} />}
|
||||
onEndReached={() => {
|
||||
if (cmtHasMore && !cmtLoading) loadComments();
|
||||
}}
|
||||
onEndReachedThreshold={0.3}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={
|
||||
<View style={styles.sortRow}>
|
||||
<Text style={styles.sortLabel}>排序</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.sortBtn,
|
||||
commentSort === 2 && styles.sortBtnActive,
|
||||
]}
|
||||
onPress={() => setCommentSort(2)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.sortBtnTxt,
|
||||
commentSort === 2 && styles.sortBtnTxtActive,
|
||||
]}
|
||||
>
|
||||
热门
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.sortBtn,
|
||||
commentSort === 0 && styles.sortBtnActive,
|
||||
]}
|
||||
onPress={() => setCommentSort(0)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.sortBtnTxt,
|
||||
commentSort === 0 && styles.sortBtnTxtActive,
|
||||
]}
|
||||
>
|
||||
最新
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
cmtLoading ? (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
) : !cmtHasMore && comments.length > 0 ? (
|
||||
<Text style={styles.emptyTxt}>已加载全部评论</Text>
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
!cmtLoading ? (
|
||||
<Text style={styles.emptyTxt}>暂无评论</Text>
|
||||
) : null
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 弹幕面板:始终挂载,切 tab 时用 display:none 隐藏而不卸载 */}
|
||||
<DanmakuList
|
||||
danmakus={danmakus}
|
||||
currentTime={currentTime}
|
||||
visible={true}
|
||||
visible={tab === "danmaku"}
|
||||
onToggle={() => {}}
|
||||
style={styles.danmakuTab}
|
||||
hideHeader={true}
|
||||
style={[
|
||||
styles.danmakuTab,
|
||||
tab !== "danmaku" && { display: "none" },
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<FlatList
|
||||
style={styles.tabScroll}
|
||||
data={comments}
|
||||
keyExtractor={(c) => String(c.rpid)}
|
||||
renderItem={({ item }) => <CommentItem item={item} />}
|
||||
onEndReached={() => {
|
||||
if (cmtHasMore && !cmtLoading) loadComments();
|
||||
}}
|
||||
onEndReachedThreshold={0.3}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ListHeaderComponent={
|
||||
<View style={styles.sortRow}>
|
||||
<Text style={styles.sortLabel}>排序</Text>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.sortBtn,
|
||||
commentSort === 2 && styles.sortBtnActive,
|
||||
]}
|
||||
onPress={() => setCommentSort(2)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.sortBtnTxt,
|
||||
commentSort === 2 && styles.sortBtnTxtActive,
|
||||
]}
|
||||
>
|
||||
热门
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.sortBtn,
|
||||
commentSort === 0 && styles.sortBtnActive,
|
||||
]}
|
||||
onPress={() => setCommentSort(0)}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.sortBtnTxt,
|
||||
commentSort === 0 && styles.sortBtnTxtActive,
|
||||
]}
|
||||
>
|
||||
最新
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
ListFooterComponent={
|
||||
cmtLoading ? (
|
||||
<ActivityIndicator style={styles.loader} color="#00AEEC" />
|
||||
) : !cmtHasMore && comments.length > 0 ? (
|
||||
<Text style={styles.emptyTxt}>已加载全部评论</Text>
|
||||
) : null
|
||||
}
|
||||
ListEmptyComponent={
|
||||
!cmtLoading ? <Text style={styles.emptyTxt}>暂无评论</Text> : null
|
||||
}
|
||||
/>
|
||||
)
|
||||
</>
|
||||
) : null}
|
||||
</SafeAreaView>
|
||||
);
|
||||
@@ -444,6 +528,55 @@ const styles = StyleSheet.create({
|
||||
descText: { fontSize: 14, color: "#555", lineHeight: 22 },
|
||||
danmakuTab: { flex: 1 },
|
||||
emptyTxt: { textAlign: "center", color: "#bbb", padding: 30 },
|
||||
relatedHeader: {
|
||||
paddingHorizontal: 14,
|
||||
paddingVertical: 10,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#f0f0f0",
|
||||
backgroundColor: "#f4f4f4",
|
||||
},
|
||||
relatedHeaderText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600" as const,
|
||||
color: "#212121",
|
||||
},
|
||||
relatedCard: {
|
||||
flexDirection: "row",
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
backgroundColor: "#fff",
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: "#f0f0f0",
|
||||
gap: 10,
|
||||
},
|
||||
relatedThumbWrap: {
|
||||
position: "relative",
|
||||
width: 120,
|
||||
height: 68,
|
||||
borderRadius: 4,
|
||||
overflow: "hidden",
|
||||
backgroundColor: "#eee",
|
||||
flexShrink: 0,
|
||||
},
|
||||
relatedThumb: { width: 120, height: 68 },
|
||||
relatedDuration: {
|
||||
position: "absolute",
|
||||
bottom: 3,
|
||||
right: 3,
|
||||
backgroundColor: "rgba(0,0,0,0.6)",
|
||||
borderRadius: 3,
|
||||
paddingHorizontal: 4,
|
||||
paddingVertical: 1,
|
||||
},
|
||||
relatedDurationText: { color: "#fff", fontSize: 10 },
|
||||
relatedInfo: {
|
||||
flex: 1,
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 2,
|
||||
},
|
||||
relatedTitle: { fontSize: 13, color: "#212121", lineHeight: 18 },
|
||||
relatedOwner: { fontSize: 12, color: "#999" },
|
||||
relatedView: { fontSize: 11, color: "#bbb" },
|
||||
sortRow: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
|
||||
Reference in New Issue
Block a user