mirror of
https://gh-proxy.org/https://github.com/tiajinsha/JKVideo
synced 2026-07-07 23:18:38 +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;
|
let ScreenOrientation: typeof import('expo-screen-orientation') | null = null;
|
||||||
try { ScreenOrientation = require('expo-screen-orientation'); } catch {}
|
try { ScreenOrientation = require('expo-screen-orientation'); } catch {}
|
||||||
import { useDownloadStore, DownloadTask } from '../store/downloadStore';
|
import { useDownloadStore, DownloadTask } from '../store/downloadStore';
|
||||||
|
import { LanShareModal } from '../components/LanShareModal';
|
||||||
|
|
||||||
function formatFileSize(bytes?: number): string {
|
function formatFileSize(bytes?: number): string {
|
||||||
if (!bytes || bytes <= 0) return '';
|
if (!bytes || bytes <= 0) return '';
|
||||||
@@ -32,6 +33,7 @@ export default function DownloadsScreen() {
|
|||||||
const { tasks, loadFromStorage, removeTask } = useDownloadStore();
|
const { tasks, loadFromStorage, removeTask } = useDownloadStore();
|
||||||
const [playingUri, setPlayingUri] = useState<string | null>(null);
|
const [playingUri, setPlayingUri] = useState<string | null>(null);
|
||||||
const [playingTitle, setPlayingTitle] = useState('');
|
const [playingTitle, setPlayingTitle] = useState('');
|
||||||
|
const [shareTask, setShareTask] = useState<(DownloadTask & { key: string }) | null>(null);
|
||||||
const { width, height } = useWindowDimensions();
|
const { width, height } = useWindowDimensions();
|
||||||
const isLandscape = width > height;
|
const isLandscape = width > height;
|
||||||
|
|
||||||
@@ -89,6 +91,7 @@ export default function DownloadsScreen() {
|
|||||||
if (item.localUri) openPlayer(item.localUri, item.title);
|
if (item.localUri) openPlayer(item.localUri, item.title);
|
||||||
}}
|
}}
|
||||||
onDelete={() => removeTask(item.key)}
|
onDelete={() => removeTask(item.key)}
|
||||||
|
onShare={() => setShareTask(item)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
ItemSeparatorComponent={() => <View style={styles.separator} />}
|
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 */}
|
{/* Local file player modal */}
|
||||||
<Modal
|
<Modal
|
||||||
visible={!!playingUri}
|
visible={!!playingUri}
|
||||||
@@ -134,10 +143,12 @@ function DownloadRow({
|
|||||||
task,
|
task,
|
||||||
onPlay,
|
onPlay,
|
||||||
onDelete,
|
onDelete,
|
||||||
|
onShare,
|
||||||
}: {
|
}: {
|
||||||
task: DownloadTask & { key: string };
|
task: DownloadTask & { key: string };
|
||||||
onPlay: () => void;
|
onPlay: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
onShare: () => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
@@ -165,10 +176,15 @@ function DownloadRow({
|
|||||||
</View>
|
</View>
|
||||||
<View style={styles.actions}>
|
<View style={styles.actions}>
|
||||||
{task.status === 'done' && (
|
{task.status === 'done' && (
|
||||||
<TouchableOpacity style={styles.playBtn} onPress={onPlay}>
|
<>
|
||||||
<Ionicons name="play-circle" size={20} color="#00AEEC" />
|
<TouchableOpacity style={styles.playBtn} onPress={onPlay}>
|
||||||
<Text style={styles.playTxt}>播放</Text>
|
<Ionicons name="play-circle" size={20} color="#00AEEC" />
|
||||||
</TouchableOpacity>
|
<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}>
|
<TouchableOpacity style={styles.deleteBtn} onPress={onDelete}>
|
||||||
<Ionicons name="trash-outline" size={18} color="#bbb" />
|
<Ionicons name="trash-outline" size={18} color="#bbb" />
|
||||||
@@ -230,6 +246,7 @@ const styles = StyleSheet.create({
|
|||||||
actions: { alignItems: 'center', gap: 8 },
|
actions: { alignItems: 'center', gap: 8 },
|
||||||
playBtn: { flexDirection: 'row', alignItems: 'center', gap: 3 },
|
playBtn: { flexDirection: 'row', alignItems: 'center', gap: 3 },
|
||||||
playTxt: { fontSize: 13, color: '#00AEEC' },
|
playTxt: { fontSize: 13, color: '#00AEEC' },
|
||||||
|
shareBtn: { padding: 4 },
|
||||||
deleteBtn: { padding: 4 },
|
deleteBtn: { padding: 4 },
|
||||||
separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#f0f0f0', marginLeft: 108 },
|
separator: { height: StyleSheet.hairlineWidth, backgroundColor: '#f0f0f0', marginLeft: 108 },
|
||||||
// player modal
|
// 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("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [
|
Alert.alert("已保存", "二维码已存入相册,请用哔哩哔哩扫码登录", [
|
||||||
{ text: "关闭", style: "cancel" },
|
{ text: "关闭", style: "cancel" },
|
||||||
{
|
{
|
||||||
text: "打开哔哩哔哩扫一扫",
|
text: "打开哔哩哔哩",
|
||||||
onPress: () => Linking.openURL("bilibili://scan"),
|
onPress: () => Linking.openURL("bilibili://"),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ function findFrameByTime(index: number[], seekTime: number): number {
|
|||||||
|
|
||||||
export interface NativeVideoPlayerRef {
|
export interface NativeVideoPlayerRef {
|
||||||
seek: (t: number) => void;
|
seek: (t: number) => void;
|
||||||
|
setPaused: (v: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -132,6 +133,9 @@ export const NativeVideoPlayer = forwardRef<NativeVideoPlayerRef, Props>(
|
|||||||
seek: (t: number) => {
|
seek: (t: number) => {
|
||||||
videoRef.current?.seek(t);
|
videoRef.current?.seek(t);
|
||||||
},
|
},
|
||||||
|
setPaused: (v: boolean) => {
|
||||||
|
setPaused(v);
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const currentDesc =
|
const currentDesc =
|
||||||
@@ -547,7 +551,7 @@ export const NativeVideoPlayer = forwardRef<NativeVideoPlayerRef, Props>(
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
<TouchableOpacity style={styles.ctrlBtn} onPress={onFullscreen}>
|
<TouchableOpacity style={styles.ctrlBtn} onPress={onFullscreen}>
|
||||||
<Ionicons name="expand" size={16} color="#fff" />
|
<Ionicons name="expand" size={18} color="#fff" />
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
|
|||||||
const [fullscreen, setFullscreen] = useState(false);
|
const [fullscreen, setFullscreen] = useState(false);
|
||||||
const { width, height } = useWindowDimensions();
|
const { width, height } = useWindowDimensions();
|
||||||
const VIDEO_HEIGHT = width * 0.5625;
|
const VIDEO_HEIGHT = width * 0.5625;
|
||||||
// In Expo Go ScreenOrientation is unavailable; simulate landscape via CSS transform
|
|
||||||
const needsRotation = !ScreenOrientation && fullscreen;
|
const needsRotation = !ScreenOrientation && fullscreen;
|
||||||
const lastTimeRef = useRef(0);
|
const lastTimeRef = useRef(0);
|
||||||
const portraitRef = useRef<NativeVideoPlayerRef>(null);
|
const portraitRef = useRef<NativeVideoPlayerRef>(null);
|
||||||
@@ -33,8 +32,9 @@ export function VideoPlayer({ playData, qualities, currentQn, onQualityChange, b
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleExitFullscreen = async () => {
|
const handleExitFullscreen = async () => {
|
||||||
// Seek portrait player to current position before it becomes visible again
|
// 退出全屏:同步进度,竖屏一律暂停
|
||||||
portraitRef.current?.seek(lastTimeRef.current);
|
portraitRef.current?.seek(lastTimeRef.current);
|
||||||
|
portraitRef.current?.setPaused(true);
|
||||||
setFullscreen(false);
|
setFullscreen(false);
|
||||||
if (Platform.OS !== 'web')
|
if (Platform.OS !== 'web')
|
||||||
await ScreenOrientation?.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
|
await ScreenOrientation?.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP);
|
||||||
|
|||||||
129
package-lock.json
generated
129
package-lock.json
generated
@@ -8,15 +8,18 @@
|
|||||||
"name": "reactbilibiliapp",
|
"name": "reactbilibiliapp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dr.pogodin/react-native-static-server": "^0.26.0",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"expo": "~55.0.5",
|
"expo": "~55.0.5",
|
||||||
"expo-av": "^16.0.8",
|
"expo-av": "^16.0.8",
|
||||||
|
"expo-clipboard": "~55.0.9",
|
||||||
"expo-dev-client": "~55.0.11",
|
"expo-dev-client": "~55.0.11",
|
||||||
"expo-file-system": "~55.0.10",
|
"expo-file-system": "~55.0.10",
|
||||||
"expo-linear-gradient": "~55.0.8",
|
"expo-linear-gradient": "~55.0.8",
|
||||||
"expo-media-library": "~55.0.10",
|
"expo-media-library": "~55.0.10",
|
||||||
|
"expo-network": "~55.0.9",
|
||||||
"expo-router": "~55.0.4",
|
"expo-router": "~55.0.4",
|
||||||
"expo-screen-orientation": "~55.0.8",
|
"expo-screen-orientation": "~55.0.8",
|
||||||
"expo-status-bar": "~55.0.4",
|
"expo-status-bar": "~55.0.4",
|
||||||
@@ -1461,6 +1464,58 @@
|
|||||||
"node": ">=6.9.0"
|
"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": {
|
"node_modules/@expo-google-fonts/material-symbols": {
|
||||||
"version": "0.4.25",
|
"version": "0.4.25",
|
||||||
"resolved": "https://registry.npmjs.org/@expo-google-fonts/material-symbols/-/material-symbols-0.4.25.tgz",
|
"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-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": {
|
"node_modules/buffer-from": {
|
||||||
"version": "1.1.2",
|
"version": "1.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
"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": {
|
"node_modules/expo-constants": {
|
||||||
"version": "55.0.7",
|
"version": "55.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-55.0.7.tgz",
|
||||||
@@ -4562,6 +4653,16 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/expo-router": {
|
||||||
"version": "55.0.4",
|
"version": "55.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-55.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-55.0.4.tgz",
|
||||||
@@ -5681,6 +5782,13 @@
|
|||||||
"node": ">= 0.8"
|
"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": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
@@ -5713,6 +5821,27 @@
|
|||||||
"node": ">=0.10.0"
|
"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": {
|
"node_modules/ignore": {
|
||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||||
|
|||||||
@@ -10,15 +10,18 @@
|
|||||||
"proxy": "node dev-proxy.js"
|
"proxy": "node dev-proxy.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@dr.pogodin/react-native-static-server": "^0.26.0",
|
||||||
"@expo/vector-icons": "^15.0.2",
|
"@expo/vector-icons": "^15.0.2",
|
||||||
"@react-native-async-storage/async-storage": "2.2.0",
|
"@react-native-async-storage/async-storage": "2.2.0",
|
||||||
"axios": "^1.13.6",
|
"axios": "^1.13.6",
|
||||||
"expo": "~55.0.5",
|
"expo": "~55.0.5",
|
||||||
"expo-av": "^16.0.8",
|
"expo-av": "^16.0.8",
|
||||||
|
"expo-clipboard": "~55.0.9",
|
||||||
"expo-dev-client": "~55.0.11",
|
"expo-dev-client": "~55.0.11",
|
||||||
"expo-file-system": "~55.0.10",
|
"expo-file-system": "~55.0.10",
|
||||||
"expo-linear-gradient": "~55.0.8",
|
"expo-linear-gradient": "~55.0.8",
|
||||||
"expo-media-library": "~55.0.10",
|
"expo-media-library": "~55.0.10",
|
||||||
|
"expo-network": "~55.0.9",
|
||||||
"expo-router": "~55.0.4",
|
"expo-router": "~55.0.4",
|
||||||
"expo-screen-orientation": "~55.0.8",
|
"expo-screen-orientation": "~55.0.8",
|
||||||
"expo-status-bar": "~55.0.4",
|
"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