mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-08 07:28:37 +08:00
feat:下载管理页面,集成分享功能
This commit is contained in:
175
components/LanShareModal.tsx
Normal file
175
components/LanShareModal.tsx
Normal file
@@ -0,0 +1,175 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
Modal,
|
||||
View,
|
||||
Text,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
Image,
|
||||
ActivityIndicator,
|
||||
Animated,
|
||||
ScrollView,
|
||||
} from 'react-native';
|
||||
import * as Clipboard from 'expo-clipboard';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { DownloadTask } from '../store/downloadStore';
|
||||
import { startLanServer, stopLanServer, buildVideoUrl } from '../utils/lanServer';
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
task: DownloadTask | null;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export function LanShareModal({ visible, task, onClose }: Props) {
|
||||
const [videoUrl, setVideoUrl] = useState<string | null>(null);
|
||||
const [qrImageLoaded, setQrImageLoaded] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const slideY = useRef(new Animated.Value(400)).current;
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
Animated.spring(slideY, { toValue: 0, useNativeDriver: true, bounciness: 4 }).start();
|
||||
} else {
|
||||
slideY.setValue(400);
|
||||
}
|
||||
}, [visible]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || !task) return;
|
||||
setVideoUrl(null);
|
||||
setQrImageLoaded(false);
|
||||
setLoading(true);
|
||||
console.log(123123,'baseUrlbaseUrl')
|
||||
startLanServer()
|
||||
.then((baseUrl) => {
|
||||
console.log(baseUrl,'baseUrl')
|
||||
setVideoUrl(buildVideoUrl(baseUrl, task.bvid, task.qn));
|
||||
})
|
||||
.catch(() => setVideoUrl(null))
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
return () => {
|
||||
stopLanServer();
|
||||
};
|
||||
}, [visible, task?.bvid, task?.qn]);
|
||||
|
||||
async function handleCopy() {
|
||||
if (!videoUrl) return;
|
||||
await Clipboard.setStringAsync(videoUrl);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
stopLanServer();
|
||||
onClose();
|
||||
}
|
||||
|
||||
const qrSrc = videoUrl
|
||||
? `https://api.qrserver.com/v1/create-qr-code/?data=${encodeURIComponent(videoUrl)}&size=400x400`
|
||||
: null;
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="none" onRequestClose={handleClose}>
|
||||
<View style={styles.overlay} pointerEvents="box-none" />
|
||||
<Animated.View style={[styles.sheetWrapper, { transform: [{ translateY: slideY }] }]}>
|
||||
<View style={styles.sheet}>
|
||||
<Text style={styles.title}>局域网分享</Text>
|
||||
|
||||
{task && (
|
||||
<Text style={styles.taskTitle} numberOfLines={2}>
|
||||
{task.title} · {task.qdesc}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<ActivityIndicator size="large" color="#00AEEC" style={styles.loader} />
|
||||
) : qrSrc ? (
|
||||
<>
|
||||
<View style={styles.qrWrapper}>
|
||||
<Image
|
||||
source={{ uri: qrSrc }}
|
||||
style={styles.qr}
|
||||
onLoad={() => setQrImageLoaded(true)}
|
||||
/>
|
||||
{!qrImageLoaded && (
|
||||
<View style={styles.qrLoader}>
|
||||
<ActivityIndicator size="large" color="#00AEEC" />
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<TouchableOpacity style={styles.urlRow} onPress={handleCopy} activeOpacity={0.7}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={styles.urlScroll}>
|
||||
<Text style={styles.urlText}>{videoUrl}</Text>
|
||||
</ScrollView>
|
||||
<Ionicons
|
||||
name={copied ? 'checkmark-circle' : 'copy-outline'}
|
||||
size={20}
|
||||
color={copied ? '#4caf50' : '#00AEEC'}
|
||||
style={{ marginLeft: 8 }}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
|
||||
<Text style={styles.hint}>同一 WiFi 下,用浏览器扫码或输入链接即可播放</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text style={styles.hint}>启动服务器失败,请确认已连接 WiFi12312</Text>
|
||||
)}
|
||||
|
||||
<TouchableOpacity style={styles.closeBtn} onPress={handleClose}>
|
||||
<Text style={styles.closeTxt}>关闭</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</Animated.View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
overlay: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
backgroundColor: 'rgba(0,0,0,0.5)',
|
||||
},
|
||||
sheetWrapper: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
sheet: {
|
||||
backgroundColor: '#fff',
|
||||
borderTopLeftRadius: 16,
|
||||
borderTopRightRadius: 16,
|
||||
padding: 24,
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: { fontSize: 18, fontWeight: '600', marginBottom: 12 },
|
||||
taskTitle: { fontSize: 13, color: '#666', marginBottom: 16, textAlign: 'center' },
|
||||
loader: { marginVertical: 40 },
|
||||
qrWrapper: { width: 200, height: 200, marginBottom: 16 },
|
||||
qr: { width: 200, height: 200 },
|
||||
qrLoader: {
|
||||
...StyleSheet.absoluteFillObject,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: '#f4f4f4',
|
||||
},
|
||||
urlRow: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
backgroundColor: '#f4f4f4',
|
||||
borderRadius: 8,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 8,
|
||||
marginBottom: 12,
|
||||
maxWidth: '100%',
|
||||
},
|
||||
urlScroll: { maxWidth: 260 },
|
||||
urlText: { fontSize: 12, color: '#333', fontFamily: 'monospace' },
|
||||
hint: { fontSize: 12, color: '#999', marginBottom: 20, textAlign: 'center' },
|
||||
closeBtn: { padding: 12 },
|
||||
closeTxt: { fontSize: 14, color: '#00AEEC' },
|
||||
});
|
||||
@@ -112,8 +112,8 @@ export function LoginModal({ visible, onClose }: Props) {
|
||||
Alert.alert("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [
|
||||
{ text: "关闭", style: "cancel" },
|
||||
{
|
||||
text: "打开哔哩哔哩扫一扫",
|
||||
onPress: () => Linking.openURL("bilibili://scan"),
|
||||
text: "打开哔哩哔哩",
|
||||
onPress: () => Linking.openURL("bilibili://"),
|
||||
},
|
||||
]);
|
||||
} catch {
|
||||
|
||||
@@ -60,6 +60,7 @@ function findFrameByTime(index: number[], seekTime: number): number {
|
||||
|
||||
export interface NativeVideoPlayerRef {
|
||||
seek: (t: number) => void;
|
||||
setPaused: (v: boolean) => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -132,6 +133,9 @@ export const NativeVideoPlayer = forwardRef<NativeVideoPlayerRef, Props>(
|
||||
seek: (t: number) => {
|
||||
videoRef.current?.seek(t);
|
||||
},
|
||||
setPaused: (v: boolean) => {
|
||||
setPaused(v);
|
||||
},
|
||||
}));
|
||||
|
||||
const currentDesc =
|
||||
@@ -547,7 +551,7 @@ export const NativeVideoPlayer = forwardRef<NativeVideoPlayerRef, Props>(
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
<TouchableOpacity style={styles.ctrlBtn} onPress={onFullscreen}>
|
||||
<Ionicons name="expand" size={16} color="#fff" />
|
||||
<Ionicons name="expand" size={18} color="#fff" />
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</LinearGradient>
|
||||
|
||||
@@ -21,7 +21,6 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const VIDEO_HEIGHT = width * 0.5625;
|
||||
// In Expo Go ScreenOrientation is unavailable; simulate landscape via CSS transform
|
||||
const needsRotation = !ScreenOrientation && fullscreen;
|
||||
const lastTimeRef = useRef(0);
|
||||
const portraitRef = useRef<NativeVideoPlayerRef>(null);
|
||||
@@ -33,8 +32,9 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
|
||||
};
|
||||
|
||||
const handleExitFullscreen = async () => {
|
||||
// Seek portrait player to current position before it becomes visible again
|
||||
// 退出全屏:同步进度,竖屏一律暂停
|
||||
portraitRef.current?.seek(lastTimeRef.current);
|
||||
portraitRef.current?.setPaused(true);
|
||||
setFullscreen(false);
|
||||
if (Platform.OS !== 'web')
|
||||
await ScreenOrientation?.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
|
||||
|
||||
Reference in New Issue
Block a user