mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +08:00
feat: 视频推荐关联当前视频、UP主信息展示、直播全屏修复及UI优化
- 推荐列表改用 getVideoRelated 基于当前 bvid 推荐,与首页 feed 解耦 - 视频详情页 UP主名称下方展示粉丝数和视频数 - 推荐视频点击改为 router.replace 避免页面堆叠 - 直播全屏改用 position:absolute,解决退出全屏视频重建暂停问题 - 退出全屏时直播自动暂停 - 直播画质选中状态修复,过滤杜比/4K 画质选项 - 直播画质面板改为居中 Modal 弹出框 - 视频详情 Tab 按钮左对齐,评论排序及设置页按钮统一实心背景风格
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import React, { useState, useRef, useCallback, useEffect } from "react";
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
Modal,
|
||||
Platform,
|
||||
useWindowDimensions,
|
||||
} from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
} from "react-native";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
|
||||
interface Props {
|
||||
hlsUrl: string;
|
||||
@@ -23,16 +23,23 @@ interface Props {
|
||||
const HIDE_DELAY = 3000;
|
||||
|
||||
const HEADERS = {
|
||||
Referer: 'https://live.bilibili.com',
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
Referer: "https://live.bilibili.com",
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
};
|
||||
|
||||
export function LivePlayer({ hlsUrl, flvUrl, isLive, qualities = [], currentQn = 0, onQualityChange }: Props) {
|
||||
export function LivePlayer({
|
||||
hlsUrl,
|
||||
flvUrl,
|
||||
isLive,
|
||||
qualities = [],
|
||||
currentQn = 0,
|
||||
onQualityChange,
|
||||
}: Props) {
|
||||
const { width: SCREEN_W, height: SCREEN_H } = useWindowDimensions();
|
||||
const VIDEO_H = SCREEN_W * 0.5625;
|
||||
|
||||
if (Platform.OS === 'web') {
|
||||
if (Platform.OS === "web") {
|
||||
return (
|
||||
<View style={[styles.container, { width: SCREEN_W, height: VIDEO_H }]}>
|
||||
<Text style={styles.webHint}>请在手机端观看直播</Text>
|
||||
@@ -79,7 +86,7 @@ function NativeLivePlayer({
|
||||
currentQn: number;
|
||||
onQualityChange?: (qn: number) => void;
|
||||
}) {
|
||||
const Video = require('react-native-video').default;
|
||||
const Video = require("react-native-video").default;
|
||||
|
||||
const [showControls, setShowControls] = useState(true);
|
||||
const [paused, setPaused] = useState(false);
|
||||
@@ -97,14 +104,16 @@ function NativeLivePlayer({
|
||||
|
||||
useEffect(() => {
|
||||
resetHideTimer();
|
||||
return () => { if (hideTimer.current) clearTimeout(hideTimer.current); };
|
||||
return () => {
|
||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Lock/unlock orientation on fullscreen toggle
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const ScreenOrientation = require('expo-screen-orientation');
|
||||
const ScreenOrientation = require("expo-screen-orientation");
|
||||
if (isFullscreen) {
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.LANDSCAPE_RIGHT,
|
||||
@@ -114,7 +123,9 @@ function NativeLivePlayer({
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||
);
|
||||
}
|
||||
} catch { /* graceful degradation in Expo Go */ }
|
||||
} catch {
|
||||
/* graceful degradation in Expo Go */
|
||||
}
|
||||
})();
|
||||
}, [isFullscreen]);
|
||||
|
||||
@@ -123,18 +134,23 @@ function NativeLivePlayer({
|
||||
return () => {
|
||||
(async () => {
|
||||
try {
|
||||
const ScreenOrientation = require('expo-screen-orientation');
|
||||
const ScreenOrientation = require("expo-screen-orientation");
|
||||
await ScreenOrientation.lockAsync(
|
||||
ScreenOrientation.OrientationLock.PORTRAIT_UP,
|
||||
);
|
||||
} catch { /* ignore */ }
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
})();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleTap = useCallback(() => {
|
||||
setShowControls(prev => {
|
||||
if (!prev) { resetHideTimer(); return true; }
|
||||
setShowControls((prev) => {
|
||||
if (!prev) {
|
||||
resetHideTimer();
|
||||
return true;
|
||||
}
|
||||
if (hideTimer.current) clearTimeout(hideTimer.current);
|
||||
return false;
|
||||
});
|
||||
@@ -143,23 +159,27 @@ function NativeLivePlayer({
|
||||
const fsW = Math.max(screenW, screenH);
|
||||
const fsH = Math.min(screenW, screenH);
|
||||
const containerStyle = isFullscreen
|
||||
? { width: fsW, height: fsH }
|
||||
? { position: 'absolute' as const, top: 0, left: 0, width: fsW, height: fsH, zIndex: 999, elevation: 999 }
|
||||
: { width: screenW, height: videoH };
|
||||
|
||||
const currentQnDesc = qualities.find(q => q.qn === currentQn)?.desc ?? '';
|
||||
const currentQnDesc = qualities.find((q) => q.qn === currentQn)?.desc ?? "";
|
||||
|
||||
const videoContent = (
|
||||
<View style={[styles.container, containerStyle]}>
|
||||
<Video
|
||||
key={hlsUrl + (isFullscreen ? '_fs' : '_normal')}
|
||||
key={hlsUrl}
|
||||
ref={videoRef}
|
||||
source={{ uri: hlsUrl, headers: HEADERS }}
|
||||
style={StyleSheet.absoluteFill}
|
||||
resizeMode="contain"
|
||||
controls={false}
|
||||
paused={paused}
|
||||
onProgress={({ currentTime: ct }: { currentTime: number }) => { currentTimeRef.current = ct; }}
|
||||
onBuffer={({ isBuffering }: { isBuffering: boolean }) => setBuffering(isBuffering)}
|
||||
onProgress={({ currentTime: ct }: { currentTime: number }) => {
|
||||
currentTimeRef.current = ct;
|
||||
}}
|
||||
onBuffer={({ isBuffering }: { isBuffering: boolean }) =>
|
||||
setBuffering(isBuffering)
|
||||
}
|
||||
onLoad={() => {
|
||||
setBuffering(false);
|
||||
if (currentTimeRef.current > 0) {
|
||||
@@ -183,10 +203,17 @@ function NativeLivePlayer({
|
||||
{/* Center play/pause */}
|
||||
<TouchableOpacity
|
||||
style={styles.centerBtn}
|
||||
onPress={() => { setPaused(p => !p); resetHideTimer(); }}
|
||||
onPress={() => {
|
||||
setPaused((p) => !p);
|
||||
resetHideTimer();
|
||||
}}
|
||||
>
|
||||
<View style={styles.centerBtnBg}>
|
||||
<Ionicons name={paused ? 'play' : 'pause'} size={28} color="#fff" />
|
||||
<Ionicons
|
||||
name={paused ? "play" : "pause"}
|
||||
size={28}
|
||||
color="#fff"
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
@@ -194,93 +221,117 @@ function NativeLivePlayer({
|
||||
<View style={styles.bottomBar}>
|
||||
<TouchableOpacity
|
||||
style={styles.ctrlBtn}
|
||||
onPress={() => { setPaused(p => !p); resetHideTimer(); }}
|
||||
onPress={() => {
|
||||
setPaused((p) => !p);
|
||||
resetHideTimer();
|
||||
}}
|
||||
>
|
||||
<Ionicons name={paused ? 'play' : 'pause'} size={16} color="#fff" />
|
||||
<Ionicons
|
||||
name={paused ? "play" : "pause"}
|
||||
size={16}
|
||||
color="#fff"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
<View style={{ flex: 1 }} />
|
||||
{qualities.length > 0 && (
|
||||
<TouchableOpacity
|
||||
style={styles.qualityBtn}
|
||||
onPress={() => { setShowQualityPanel(true); resetHideTimer(); }}
|
||||
onPress={() => {
|
||||
setShowQualityPanel(true);
|
||||
resetHideTimer();
|
||||
}}
|
||||
>
|
||||
<Text style={styles.qualityText}>{currentQnDesc || '清晰度'}</Text>
|
||||
<Text style={styles.qualityText}>
|
||||
{currentQnDesc || "清晰度"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity
|
||||
style={styles.ctrlBtn}
|
||||
onPress={() => { setIsFullscreen(fs => !fs); resetHideTimer(); }}
|
||||
onPress={() => {
|
||||
if (isFullscreen) setPaused(true);
|
||||
setIsFullscreen((fs) => !fs);
|
||||
resetHideTimer();
|
||||
}}
|
||||
>
|
||||
<Ionicons name={isFullscreen ? 'contract' : 'expand'} size={16} color="#fff" />
|
||||
<Ionicons
|
||||
name={isFullscreen ? "contract" : "expand"}
|
||||
size={16}
|
||||
color="#fff"
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Quality selector panel */}
|
||||
{showQualityPanel && (
|
||||
<TouchableWithoutFeedback onPress={() => setShowQualityPanel(false)}>
|
||||
<View style={styles.qualityOverlay}>
|
||||
<TouchableWithoutFeedback>
|
||||
<View style={styles.qualityPanel}>
|
||||
<Text style={styles.qualityPanelTitle}>清晰度</Text>
|
||||
{qualities.map(q => (
|
||||
<TouchableOpacity
|
||||
key={q.qn}
|
||||
style={[styles.qualityItem, currentQn === q.qn && styles.qualityItemActive]}
|
||||
onPress={() => {
|
||||
onQualityChange?.(q.qn);
|
||||
setShowQualityPanel(false);
|
||||
}}
|
||||
<Modal
|
||||
visible={showQualityPanel}
|
||||
transparent
|
||||
animationType="fade"
|
||||
onRequestClose={() => setShowQualityPanel(false)}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={styles.qualityOverlay}
|
||||
activeOpacity={1}
|
||||
onPress={() => setShowQualityPanel(false)}
|
||||
>
|
||||
<TouchableOpacity activeOpacity={1} onPress={() => {}}>
|
||||
<View style={styles.qualityPanel}>
|
||||
<Text style={styles.qualityPanelTitle}>清晰度</Text>
|
||||
{qualities.map((q) => (
|
||||
<TouchableOpacity
|
||||
key={q.qn}
|
||||
style={[styles.qualityItem]}
|
||||
onPress={() => {
|
||||
onQualityChange?.(q.qn);
|
||||
setShowQualityPanel(false);
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.qualityItemText,
|
||||
currentQn === q.qn && styles.qualityItemTextActive,
|
||||
]}
|
||||
>
|
||||
<Text style={[styles.qualityItemText, currentQn === q.qn && styles.qualityItemTextActive]}>
|
||||
{q.desc}
|
||||
</Text>
|
||||
{currentQn === q.qn && (
|
||||
<Ionicons name="checkmark" size={14} color="#00AEEC" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
)}
|
||||
{q.desc}
|
||||
</Text>
|
||||
{currentQn === q.qn && (
|
||||
<Ionicons name="checkmark" size={14} color="#00AEEC" />
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</TouchableOpacity>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (isFullscreen) {
|
||||
return (
|
||||
<Modal visible transparent animationType="fade" supportedOrientations={['landscape']} onRequestClose={() => setIsFullscreen(false)}>
|
||||
<View style={styles.fsModal}>{videoContent}</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
return videoContent;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
backgroundColor: '#000',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: "#000",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
webHint: { color: '#fff', fontSize: 15 },
|
||||
offlineText: { color: '#999', fontSize: 14, marginTop: 10 },
|
||||
webHint: { color: "#fff", fontSize: 15 },
|
||||
offlineText: { color: "#999", fontSize: 14, marginTop: 10 },
|
||||
bufferingOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
bufferingText: { color: '#fff', fontSize: 13, opacity: 0.8 },
|
||||
bufferingText: { color: "#fff", fontSize: 13, opacity: 0.8 },
|
||||
liveBadge: {
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 12,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 3,
|
||||
borderRadius: 4,
|
||||
@@ -289,79 +340,78 @@ const styles = StyleSheet.create({
|
||||
width: 6,
|
||||
height: 6,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#f00',
|
||||
backgroundColor: "#f00",
|
||||
marginRight: 5,
|
||||
},
|
||||
liveText: { color: '#fff', fontSize: 11, fontWeight: '700', letterSpacing: 1 },
|
||||
liveText: {
|
||||
color: "#fff",
|
||||
fontSize: 11,
|
||||
fontWeight: "700",
|
||||
letterSpacing: 1,
|
||||
},
|
||||
centerBtn: {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
position: "absolute",
|
||||
top: "50%",
|
||||
left: "50%",
|
||||
transform: [{ translateX: -28 }, { translateY: -28 }],
|
||||
},
|
||||
centerBtnBg: {
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
backgroundColor: 'rgba(0,0,0,0.45)',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: "rgba(0,0,0,0.45)",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
bottomBar: {
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: 8,
|
||||
paddingBottom: 8,
|
||||
paddingTop: 24,
|
||||
backgroundColor: 'rgba(0,0,0,0)',
|
||||
backgroundColor: "rgba(0,0,0,0)",
|
||||
},
|
||||
ctrlBtn: { paddingHorizontal: 8, paddingVertical: 4 },
|
||||
qualityBtn: {
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 4,
|
||||
backgroundColor: 'rgba(0,0,0,0.4)',
|
||||
backgroundColor: "rgba(0,0,0,0.4)",
|
||||
borderRadius: 4,
|
||||
marginRight: 4,
|
||||
},
|
||||
qualityText: { color: '#fff', fontSize: 11, fontWeight: '600' },
|
||||
qualityText: { color: "#fff", fontSize: 11, fontWeight: "600" },
|
||||
qualityOverlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
justifyContent: 'flex-end',
|
||||
flex: 1,
|
||||
backgroundColor: "rgba(0,0,0,0.5)",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
qualityPanel: {
|
||||
backgroundColor: 'rgba(20,20,20,0.95)',
|
||||
borderTopLeftRadius: 12,
|
||||
borderTopRightRadius: 12,
|
||||
paddingBottom: 16,
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 12,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
minWidth: 180,
|
||||
},
|
||||
qualityPanelTitle: {
|
||||
color: '#fff',
|
||||
fontSize: 13,
|
||||
fontWeight: '600',
|
||||
textAlign: 'center',
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
borderBottomColor: 'rgba(255,255,255,0.1)',
|
||||
fontSize: 15,
|
||||
fontWeight: "700",
|
||||
color: "#212121",
|
||||
paddingVertical: 10,
|
||||
textAlign: "center",
|
||||
},
|
||||
qualityItem: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
paddingHorizontal: 20,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
paddingVertical: 12,
|
||||
borderTopWidth: StyleSheet.hairlineWidth,
|
||||
borderTopColor: "#eee",
|
||||
},
|
||||
qualityItemActive: { backgroundColor: 'rgba(0,174,236,0.15)' },
|
||||
qualityItemText: { color: '#ccc', fontSize: 14 },
|
||||
qualityItemTextActive: { color: '#00AEEC', fontWeight: '600' },
|
||||
fsModal: {
|
||||
flex: 1,
|
||||
backgroundColor: '#000',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
},
|
||||
qualityItemText: { fontSize: 14, color: "#333" },
|
||||
qualityItemTextActive: { color: "#00AEEC", fontWeight: "700" },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user