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:
@@ -18,6 +18,7 @@ import Video from 'react-native-video';
|
||||
let ScreenOrientation: typeof import('expo-screen-orientation') | null = null;
|
||||
try { ScreenOrientation = require('expo-screen-orientation'); } catch {}
|
||||
import { useDownloadStore, DownloadTask } from '../store/downloadStore';
|
||||
import { LanShareModal } from '../components/LanShareModal';
|
||||
|
||||
function formatFileSize(bytes?: number): string {
|
||||
if (!bytes || bytes <= 0) return '';
|
||||
@@ -32,6 +33,7 @@ export default function DownloadsScreen() {
|
||||
const { tasks, loadFromStorage, removeTask } = useDownloadStore();
|
||||
const [playingUri, setPlayingUri] = useState<string | null>(null);
|
||||
const [playingTitle, setPlayingTitle] = useState('');
|
||||
const [shareTask, setShareTask] = useState<(DownloadTask & { key: string }) | null>(null);
|
||||
const { width, height } = useWindowDimensions();
|
||||
const isLandscape = width > height;
|
||||
|
||||
@@ -89,6 +91,7 @@ export default function DownloadsScreen() {
|
||||
if (item.localUri) openPlayer(item.localUri, item.title);
|
||||
}}
|
||||
onDelete={() => removeTask(item.key)}
|
||||
onShare={() => setShareTask(item)}
|
||||
/>
|
||||
)}
|
||||
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
||||
@@ -96,6 +99,12 @@ export default function DownloadsScreen() {
|
||||
/>
|
||||
)}
|
||||
|
||||
<LanShareModal
|
||||
visible={!!shareTask}
|
||||
task={shareTask}
|
||||
onClose={() => setShareTask(null)}
|
||||
/>
|
||||
|
||||
{/* Local file player modal */}
|
||||
<Modal
|
||||
visible={!!playingUri}
|
||||
@@ -134,10 +143,12 @@ function DownloadRow({
|
||||
task,
|
||||
onPlay,
|
||||
onDelete,
|
||||
onShare,
|
||||
}: {
|
||||
task: DownloadTask & { key: string };
|
||||
onPlay: () => void;
|
||||
onDelete: () => void;
|
||||
onShare: () => void;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.row}>
|
||||
@@ -165,10 +176,15 @@ function DownloadRow({
|
||||
</View>
|
||||
<View style={styles.actions}>
|
||||
{task.status === 'done' && (
|
||||
<TouchableOpacity style={styles.playBtn} onPress={onPlay}>
|
||||
<Ionicons name="play-circle" size={20} color="#00AEEC" />
|
||||
<Text style={styles.playTxt}>播放</Text>
|
||||
</TouchableOpacity>
|
||||
<>
|
||||
<TouchableOpacity style={styles.playBtn} onPress={onPlay}>
|
||||
<Ionicons name="play-circle" size={20} color="#00AEEC" />
|
||||
<Text style={styles.playTxt}>播放</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity style={styles.shareBtn} onPress={onShare}>
|
||||
<Ionicons name="share-social-outline" size={20} color="#00AEEC" />
|
||||
</TouchableOpacity>
|
||||
</>
|
||||
)}
|
||||
<TouchableOpacity style={styles.deleteBtn} onPress={onDelete}>
|
||||
<Ionicons name="trash-outline" size={18} color="#bbb" />
|
||||
@@ -230,6 +246,7 @@ const styles = StyleSheet.create({
|
||||
actions: { alignItems: 'center', gap: 8 },
|
||||
playBtn: { flexDirection: 'row', alignItems: 'center', gap: 3 },
|
||||
playTxt: { fontSize: 13, color: '#00AEEC' },
|
||||
shareBtn: { padding: 4 },
|
||||
deleteBtn: { padding: 4 },
|
||||
separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#f0f0f0', marginLeft: 108 },
|
||||
// player modal
|
||||
|
||||
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);
|
||||
|
||||
129
package-lock.json
generated
129
package-lock.json
generated
@@ -8,15 +8,18 @@
|
||||
"name": "reactbilibiliapp",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@dr.pogodin/react-native-static-server": "^0.26.0",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"axios": "^1.13.6",
|
||||
"expo": "~55.0.5",
|
||||
"expo-av": "^16.0.8",
|
||||
"expo-clipboard": "~55.0.9",
|
||||
"expo-dev-client": "~55.0.11",
|
||||
"expo-file-system": "~55.0.10",
|
||||
"expo-linear-gradient": "~55.0.8",
|
||||
"expo-media-library": "~55.0.10",
|
||||
"expo-network": "~55.0.9",
|
||||
"expo-router": "~55.0.4",
|
||||
"expo-screen-orientation": "~55.0.8",
|
||||
"expo-status-bar": "~55.0.4",
|
||||
@@ -1461,6 +1464,58 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@dr.pogodin/js-utils": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/@dr.pogodin/js-utils/-/js-utils-0.1.6.tgz",
|
||||
"integrity": "sha512-v4hYKgHEL9Nfzoe4Vn4Sq2y7To46Q8V6vEBMpteghGOvitXmbhgmYV03GPh9SUuu9CV6vT+4u4GBnhBcK2m9vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.28.6"
|
||||
}
|
||||
},
|
||||
"node_modules/@dr.pogodin/react-native-fs": {
|
||||
"version": "2.37.0",
|
||||
"resolved": "https://registry.npmjs.org/@dr.pogodin/react-native-fs/-/react-native-fs-2.37.0.tgz",
|
||||
"integrity": "sha512-0LtRxZ+yJm3z4eiNmwkYvdMvglq74MZcqQBBJwaUwILqfSBNYAkO3jJZOvJdIR2+e06FJBoxuQ1P34mSDj8T8w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"dependencies": {
|
||||
"buffer": "^6.0.3",
|
||||
"http-status-codes": "^2.3.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/birdofpreyru"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@dr.pogodin/react-native-static-server": {
|
||||
"version": "0.26.0",
|
||||
"resolved": "https://registry.npmjs.org/@dr.pogodin/react-native-static-server/-/react-native-static-server-0.26.0.tgz",
|
||||
"integrity": "sha512-BDvi9L6SvH3lJFq5mJWO8FMHBWk9C+PHruniT+XVSKusiuL9cVXxNHyjE5aS/+ZzDiqEQKStexDPsiUZE2m3nw==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"example"
|
||||
],
|
||||
"dependencies": {
|
||||
"@dr.pogodin/js-utils": "^0.1.5"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/birdofpreyru"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@dr.pogodin/react-native-fs": ">= 2.22.0",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo-google-fonts/material-symbols": {
|
||||
"version": "0.4.25",
|
||||
"resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.25.tgz",
|
||||
@@ -3596,6 +3651,31 @@
|
||||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer": {
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
|
||||
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.1",
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
@@ -4359,6 +4439,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/expo-clipboard": {
|
||||
"version": "55.0.9",
|
||||
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-55.0.9.tgz",
|
||||
"integrity": "sha512-WJ9ougE8fEDu3/RV5Vz3gE5aNgnj1C0WByXCz3WUj8y/06bJyaIUDMq8FDLv3n0AO3guiErmkUunsD0EzSDUUw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-constants": {
|
||||
"version": "55.0.7",
|
||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.7.tgz",
|
||||
@@ -4562,6 +4653,16 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-network": {
|
||||
"version": "55.0.9",
|
||||
"resolved": "https://registry.npmjs.org/expo-network/-/expo-network-55.0.9.tgz",
|
||||
"integrity": "sha512-vuL7s+Zbsbcqh2XxhaZ45mfuYZVn7ikukL27be7hN3zkLxoABNPgbVt+/gEKOxyv2Gzr/Q5JZvfVxCuwDDh0Fg==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"expo": "*",
|
||||
"react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-router": {
|
||||
"version": "55.0.4",
|
||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-55.0.4.tgz",
|
||||
@@ -5681,6 +5782,13 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/http-status-codes": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz",
|
||||
"integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
@@ -5713,6 +5821,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
|
||||
@@ -10,15 +10,18 @@
|
||||
"proxy": "node dev-proxy.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dr.pogodin/react-native-static-server": "^0.26.0",
|
||||
"@expo/vector-icons": "^15.0.2",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"axios": "^1.13.6",
|
||||
"expo": "~55.0.5",
|
||||
"expo-av": "^16.0.8",
|
||||
"expo-clipboard": "~55.0.9",
|
||||
"expo-dev-client": "~55.0.11",
|
||||
"expo-file-system": "~55.0.10",
|
||||
"expo-linear-gradient": "~55.0.8",
|
||||
"expo-media-library": "~55.0.10",
|
||||
"expo-network": "~55.0.9",
|
||||
"expo-router": "~55.0.4",
|
||||
"expo-screen-orientation": "~55.0.8",
|
||||
"expo-status-bar": "~55.0.4",
|
||||
|
||||
26
utils/lanServer.ts
Normal file
26
utils/lanServer.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import StaticServer from '@dr.pogodin/react-native-static-server';
|
||||
import * as FileSystem from 'expo-file-system/legacy';
|
||||
import * as Network from 'expo-network';
|
||||
|
||||
const PORT = 18080;
|
||||
let server: StaticServer | null = null;
|
||||
|
||||
export async function startLanServer(): Promise<string> {
|
||||
if (server) await server.stop();
|
||||
const root = FileSystem.documentDirectory!.replace('file://', '');
|
||||
server = new StaticServer({ fileDir: root, port: PORT, nonLocal: true });
|
||||
await server.start();
|
||||
const ip = await Network.getIpAddressAsync();
|
||||
return `http://${ip}:${PORT}`;
|
||||
}
|
||||
|
||||
export async function stopLanServer(): Promise<void> {
|
||||
if (server) {
|
||||
await server.stop();
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildVideoUrl(baseUrl: string, bvid: string, qn: number): string {
|
||||
return `${baseUrl}/${bvid}_${qn}.mp4`;
|
||||
}
|
||||
Reference in New Issue
Block a user