mirror of
https://github.com/MatrixSeven/file-transfer-go.git
synced 2026-02-04 03:25:03 +08:00
feat:拆分hooks
This commit is contained in:
@@ -10,7 +10,7 @@ import { WebRTCTextImageTransfer } from '@/components/WebRTCTextImageTransfer';
|
||||
import DesktopShare from '@/components/DesktopShare';
|
||||
import WeChatGroup from '@/components/WeChatGroup';
|
||||
import { WebRTCUnsupportedModal } from '@/components/WebRTCUnsupportedModal';
|
||||
import { useWebRTCSupport } from '@/hooks/useWebRTCSupport';
|
||||
import { useWebRTCSupport } from '@/hooks/connection';
|
||||
|
||||
export default function HomePage() {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useState, useMemo } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore';
|
||||
import { useWebRTCStore } from '@/hooks/index';
|
||||
|
||||
interface ConnectionStatusProps {
|
||||
// 房间信息 - 只需要这个基本信息
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useURLHandler } from '@/hooks/ui';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Share, Monitor } from 'lucide-react';
|
||||
import WebRTCDesktopReceiver from '@/components/webrtc/WebRTCDesktopReceiver';
|
||||
import WebRTCDesktopSender from '@/components/webrtc/WebRTCDesktopSender';
|
||||
import { ConnectionStatus } from '@/components/ConnectionStatus';
|
||||
import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore';
|
||||
import { useWebRTCStore } from '@/hooks/index';
|
||||
|
||||
|
||||
interface DesktopShareProps {
|
||||
@@ -22,55 +21,28 @@ export default function DesktopShare({
|
||||
onStopSharing,
|
||||
onJoinSharing
|
||||
}: DesktopShareProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [mode, setMode] = useState<'share' | 'view'>('share');
|
||||
|
||||
// 使用全局WebRTC状态
|
||||
const webrtcState = useWebRTCStore();
|
||||
|
||||
// 从URL参数中获取初始模式和房间代码
|
||||
useEffect(() => {
|
||||
const urlMode = searchParams.get('mode');
|
||||
const type = searchParams.get('type');
|
||||
const urlCode = searchParams.get('code');
|
||||
|
||||
if (type === 'desktop' && urlMode) {
|
||||
if (urlMode === 'send') {
|
||||
setMode('share');
|
||||
} else if (urlMode === 'receive') {
|
||||
setMode('view');
|
||||
// 如果URL中有房间代码,将在DesktopShareReceiver组件中自动加入
|
||||
}
|
||||
// 使用统一的URL处理器,带模式转换
|
||||
const { updateMode, getCurrentRoomCode } = useURLHandler({
|
||||
featureType: 'desktop',
|
||||
onModeChange: setMode,
|
||||
onAutoJoinRoom: onJoinSharing,
|
||||
modeConverter: {
|
||||
fromURL: (urlMode) => urlMode === 'send' ? 'share' : 'view',
|
||||
toURL: (componentMode) => componentMode === 'share' ? 'send' : 'receive'
|
||||
}
|
||||
}, [searchParams]);
|
||||
|
||||
// 更新URL参数
|
||||
const updateMode = useCallback((newMode: 'share' | 'view') => {
|
||||
setMode(newMode);
|
||||
const currentUrl = new URL(window.location.href);
|
||||
currentUrl.searchParams.set('type', 'desktop');
|
||||
currentUrl.searchParams.set('mode', newMode === 'share' ? 'send' : 'receive');
|
||||
// 清除代码参数,避免模式切换时的混乱
|
||||
currentUrl.searchParams.delete('code');
|
||||
router.replace(currentUrl.pathname + currentUrl.search);
|
||||
}, [router]);
|
||||
});
|
||||
|
||||
// 获取初始房间代码(用于接收者模式)
|
||||
const getInitialCode = useCallback(() => {
|
||||
const urlMode = searchParams.get('mode');
|
||||
const type = searchParams.get('type');
|
||||
const code = searchParams.get('code');
|
||||
console.log('[DesktopShare] getInitialCode 调用, URL参数:', { type, urlMode, code });
|
||||
|
||||
if (type === 'desktop' && urlMode === 'receive') {
|
||||
const result = code || '';
|
||||
console.log('[DesktopShare] getInitialCode 返回:', result);
|
||||
return result;
|
||||
}
|
||||
console.log('[DesktopShare] getInitialCode 返回空字符串');
|
||||
return '';
|
||||
}, [searchParams]);
|
||||
const code = getCurrentRoomCode();
|
||||
console.log('[DesktopShare] getInitialCode 返回:', code);
|
||||
return code;
|
||||
}, [getCurrentRoomCode]);
|
||||
|
||||
// 连接状态变化处理 - 现在不需要了,因为使用全局状态
|
||||
const handleConnectionChange = useCallback((connection: any) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { AlertCircle, Wifi, WifiOff, Loader2, RotateCcw } from 'lucide-react';
|
||||
import { WebRTCConnection } from '@/hooks/webrtc/useSharedWebRTCManager';
|
||||
import { WebRTCConnection } from '@/hooks/connection/useSharedWebRTCManager';
|
||||
|
||||
interface Props {
|
||||
webrtc: WebRTCConnection;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { useSharedWebRTCManager } from '@/hooks/webrtc/useSharedWebRTCManager';
|
||||
import { useFileTransferBusiness } from '@/hooks/webrtc/useFileTransferBusiness';
|
||||
import { useSharedWebRTCManager, useConnectionState, useRoomConnection } from '@/hooks/connection';
|
||||
import { useFileTransferBusiness, useFileListSync, useFileStateManager } from '@/hooks/file-transfer';
|
||||
import { useURLHandler } from '@/hooks/ui';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import { Upload, Download } from 'lucide-react';
|
||||
@@ -20,29 +20,20 @@ interface FileInfo {
|
||||
}
|
||||
|
||||
export const WebRTCFileTransfer: React.FC = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const { showToast } = useToast();
|
||||
|
||||
// 独立的文件状态
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [fileList, setFileList] = useState<FileInfo[]>([]);
|
||||
const [downloadedFiles, setDownloadedFiles] = useState<Map<string, File>>(new Map());
|
||||
// 基础状态
|
||||
const [mode, setMode] = useState<'send' | 'receive'>('send');
|
||||
const [pickupCode, setPickupCode] = useState('');
|
||||
const [currentTransferFile, setCurrentTransferFile] = useState<{
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
progress: number;
|
||||
} | null>(null);
|
||||
|
||||
// 房间状态
|
||||
const [pickupCode, setPickupCode] = useState('');
|
||||
const [mode, setMode] = useState<'send' | 'receive'>('send');
|
||||
const [hasProcessedInitialUrl, setHasProcessedInitialUrl] = useState(false);
|
||||
const [isJoiningRoom, setIsJoiningRoom] = useState(false); // 添加加入房间状态
|
||||
const urlProcessedRef = useRef(false); // 使用 ref 防止重复处理 URL
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
// 创建共享连接 - 使用 useMemo 稳定引用
|
||||
// 创建共享连接
|
||||
const connection = useSharedWebRTCManager();
|
||||
const stableConnection = useMemo(() => connection, [connection.isConnected, connection.isConnecting, connection.isWebSocketConnected, connection.error]);
|
||||
|
||||
@@ -63,268 +54,70 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
onFileProgress
|
||||
} = useFileTransferBusiness(stableConnection);
|
||||
|
||||
// 加入房间 (接收模式) - 提前定义以供 useEffect 使用
|
||||
// 使用自定义 hooks
|
||||
const { syncFileListToReceiver } = useFileListSync({
|
||||
sendFileList,
|
||||
mode,
|
||||
pickupCode,
|
||||
isConnected,
|
||||
isPeerConnected: connection.isPeerConnected,
|
||||
getChannelState: connection.getChannelState
|
||||
});
|
||||
|
||||
const {
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
fileList,
|
||||
setFileList,
|
||||
downloadedFiles,
|
||||
setDownloadedFiles,
|
||||
handleFileSelect,
|
||||
clearFiles,
|
||||
resetFiles,
|
||||
updateFileStatus,
|
||||
updateFileProgress
|
||||
} = useFileStateManager({
|
||||
mode,
|
||||
pickupCode,
|
||||
syncFileListToReceiver,
|
||||
isPeerConnected: connection.isPeerConnected
|
||||
});
|
||||
|
||||
const { joinRoom: originalJoinRoom, isJoiningRoom } = useRoomConnection({
|
||||
connect,
|
||||
isConnecting,
|
||||
isConnected
|
||||
});
|
||||
|
||||
// 包装joinRoom函数以便设置pickupCode
|
||||
const joinRoom = useCallback(async (code: string) => {
|
||||
console.log('=== 加入房间 ===');
|
||||
console.log('取件码:', code);
|
||||
|
||||
const trimmedCode = code.trim();
|
||||
|
||||
// 检查取件码格式
|
||||
if (!trimmedCode || trimmedCode.length !== 6) {
|
||||
showToast('请输入正确的6位取件码', "error");
|
||||
return;
|
||||
}
|
||||
setPickupCode(code);
|
||||
await originalJoinRoom(code);
|
||||
}, [originalJoinRoom]);
|
||||
|
||||
// 防止重复调用 - 检查是否已经在连接或已连接
|
||||
if (isConnecting || isConnected || isJoiningRoom) {
|
||||
console.log('已在连接中或已连接,跳过重复的房间状态检查');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsJoiningRoom(true);
|
||||
|
||||
try {
|
||||
// 先检查房间状态
|
||||
console.log('检查房间状态...');
|
||||
|
||||
const response = await fetch(`/api/room-info?code=${trimmedCode}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: 无法检查房间状态`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
let errorMessage = result.message || '房间不存在或已过期';
|
||||
if (result.message?.includes('expired')) {
|
||||
errorMessage = '房间已过期,请联系发送方重新创建';
|
||||
} else if (result.message?.includes('not found')) {
|
||||
errorMessage = '房间不存在,请检查取件码是否正确';
|
||||
}
|
||||
showToast(errorMessage, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查发送方是否在线 (使用新的字段名)
|
||||
if (!result.sender_online) {
|
||||
showToast('发送方不在线,请确认取件码是否正确或联系发送方', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('房间状态检查通过,开始连接...');
|
||||
setPickupCode(trimmedCode);
|
||||
|
||||
connect(trimmedCode, 'receiver');
|
||||
|
||||
showToast(`正在连接到房间: ${trimmedCode}`, "success");
|
||||
} catch (error) {
|
||||
console.error('检查房间状态失败:', error);
|
||||
let errorMessage = '检查房间状态失败';
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
errorMessage = '网络连接失败,请检查网络状况';
|
||||
} else if (error.message.includes('timeout')) {
|
||||
errorMessage = '请求超时,请重试';
|
||||
} else if (error.message.includes('HTTP 404')) {
|
||||
errorMessage = '房间不存在,请检查取件码';
|
||||
} else if (error.message.includes('HTTP 500')) {
|
||||
errorMessage = '服务器错误,请稍后重试';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
showToast(errorMessage, "error");
|
||||
} finally {
|
||||
setIsJoiningRoom(false); // 重置加入房间状态
|
||||
}
|
||||
}, [isConnecting, isConnected, isJoiningRoom, showToast, connect]); // 添加isJoiningRoom依赖
|
||||
const { updateMode } = useURLHandler({
|
||||
featureType: 'webrtc',
|
||||
onModeChange: setMode,
|
||||
onAutoJoinRoom: joinRoom
|
||||
});
|
||||
|
||||
// 从URL参数中获取初始模式(仅在首次加载时处理)
|
||||
useEffect(() => {
|
||||
// 使用 ref 确保只处理一次,避免严格模式的重复调用
|
||||
if (urlProcessedRef.current) {
|
||||
console.log('URL已处理过,跳过重复处理');
|
||||
return;
|
||||
}
|
||||
|
||||
const urlMode = searchParams.get('mode') as 'send' | 'receive';
|
||||
const type = searchParams.get('type');
|
||||
const code = searchParams.get('code');
|
||||
|
||||
// 只在首次加载且URL中有webrtc类型时处理
|
||||
if (!hasProcessedInitialUrl && type === 'webrtc' && urlMode && ['send', 'receive'].includes(urlMode)) {
|
||||
console.log('=== 处理初始URL参数 ===');
|
||||
console.log('URL模式:', urlMode, '类型:', type, '取件码:', code);
|
||||
|
||||
// 立即标记为已处理,防止重复
|
||||
urlProcessedRef.current = true;
|
||||
|
||||
setMode(urlMode);
|
||||
setHasProcessedInitialUrl(true);
|
||||
|
||||
if (code && urlMode === 'receive') {
|
||||
console.log('URL中有取件码,自动加入房间');
|
||||
// 防止重复调用 - 检查连接状态和加入房间状态
|
||||
if (!isConnecting && !isConnected && !isJoiningRoom) {
|
||||
// 直接调用异步函数,不依赖 joinRoom
|
||||
const autoJoinRoom = async () => {
|
||||
const trimmedCode = code.trim();
|
||||
|
||||
if (!trimmedCode || trimmedCode.length !== 6) {
|
||||
showToast('请输入正确的6位取件码', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsJoiningRoom(true);
|
||||
|
||||
try {
|
||||
console.log('检查房间状态...');
|
||||
const response = await fetch(`/api/room-info?code=${trimmedCode}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: 无法检查房间状态`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
let errorMessage = result.message || '房间不存在或已过期';
|
||||
if (result.message?.includes('expired')) {
|
||||
errorMessage = '房间已过期,请联系发送方重新创建';
|
||||
} else if (result.message?.includes('not found')) {
|
||||
errorMessage = '房间不存在,请检查取件码是否正确';
|
||||
}
|
||||
showToast(errorMessage, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.sender_online) {
|
||||
showToast('发送方不在线,请确认取件码是否正确或联系发送方', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('房间状态检查通过,开始连接...');
|
||||
setPickupCode(trimmedCode);
|
||||
connect(trimmedCode, 'receiver');
|
||||
showToast(`正在连接到房间: ${trimmedCode}`, "success");
|
||||
} catch (error) {
|
||||
console.error('检查房间状态失败:', error);
|
||||
let errorMessage = '检查房间状态失败';
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
errorMessage = '网络连接失败,请检查网络状况';
|
||||
} else if (error.message.includes('timeout')) {
|
||||
errorMessage = '请求超时,请重试';
|
||||
} else if (error.message.includes('HTTP 404')) {
|
||||
errorMessage = '房间不存在,请检查取件码';
|
||||
} else if (error.message.includes('HTTP 500')) {
|
||||
errorMessage = '服务器错误,请稍后重试';
|
||||
} else {
|
||||
errorMessage = error.message;
|
||||
}
|
||||
}
|
||||
|
||||
showToast(errorMessage, "error");
|
||||
} finally {
|
||||
setIsJoiningRoom(false);
|
||||
}
|
||||
};
|
||||
|
||||
autoJoinRoom();
|
||||
} else {
|
||||
console.log('已在连接中或加入房间中,跳过重复处理');
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [searchParams, hasProcessedInitialUrl, isConnecting, isConnected, isJoiningRoom, showToast, connect]); // 添加isJoiningRoom依赖
|
||||
|
||||
// 更新URL参数
|
||||
const updateMode = useCallback((newMode: 'send' | 'receive') => {
|
||||
console.log('=== 手动切换模式 ===');
|
||||
console.log('新模式:', newMode);
|
||||
|
||||
setMode(newMode);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('type', 'webrtc');
|
||||
params.set('mode', newMode);
|
||||
|
||||
// 如果切换到发送模式,移除code参数
|
||||
if (newMode === 'send') {
|
||||
params.delete('code');
|
||||
}
|
||||
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}, [searchParams, router]);
|
||||
useConnectionState({
|
||||
isWebSocketConnected,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
error: error || '',
|
||||
pickupCode,
|
||||
fileListLength: fileList.length,
|
||||
currentTransferFile,
|
||||
setCurrentTransferFile,
|
||||
updateFileListStatus: setFileList
|
||||
});
|
||||
|
||||
// 生成文件ID
|
||||
const generateFileId = () => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
};
|
||||
|
||||
// 文件列表同步防抖标志
|
||||
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 统一的文件列表同步函数,带防抖功能
|
||||
const syncFileListToReceiver = useCallback((fileInfos: FileInfo[], reason: string) => {
|
||||
// 只有在发送模式、连接已建立且有房间时才发送文件列表
|
||||
if (mode !== 'send' || !pickupCode || !isConnected || !connection.isPeerConnected) {
|
||||
console.log('跳过文件列表同步:', { mode, pickupCode: !!pickupCode, isConnected, isPeerConnected: connection.isPeerConnected });
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的延时发送
|
||||
if (syncTimeoutRef.current) {
|
||||
clearTimeout(syncTimeoutRef.current);
|
||||
}
|
||||
|
||||
// 延时发送,避免频繁发送
|
||||
syncTimeoutRef.current = setTimeout(() => {
|
||||
if (connection.isPeerConnected && connection.getChannelState() === 'open') {
|
||||
console.log(`发送文件列表到接收方 (${reason}):`, fileInfos.map(f => f.name));
|
||||
sendFileList(fileInfos);
|
||||
}
|
||||
}, 150);
|
||||
}, [mode, pickupCode, isConnected, connection.isPeerConnected, connection.getChannelState, sendFileList]);
|
||||
|
||||
// 清理防抖定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (syncTimeoutRef.current) {
|
||||
clearTimeout(syncTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 文件选择处理
|
||||
const handleFileSelect = (files: File[]) => {
|
||||
console.log('=== 文件选择 ===');
|
||||
console.log('新文件:', files.map(f => f.name));
|
||||
|
||||
// 更新选中的文件
|
||||
setSelectedFiles(prev => [...prev, ...files]);
|
||||
|
||||
// 创建对应的文件信息
|
||||
const newFileInfos: FileInfo[] = files.map(file => ({
|
||||
id: generateFileId(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
status: 'ready',
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
setFileList(prev => {
|
||||
const updatedList = [...prev, ...newFileInfos];
|
||||
console.log('更新后的文件列表:', updatedList);
|
||||
return updatedList;
|
||||
});
|
||||
};
|
||||
|
||||
// 创建房间 (发送模式)
|
||||
const generateCode = async () => {
|
||||
if (selectedFiles.length === 0) {
|
||||
@@ -390,23 +183,17 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
|
||||
// 清空状态
|
||||
setPickupCode('');
|
||||
setFileList([]);
|
||||
setDownloadedFiles(new Map());
|
||||
resetFiles();
|
||||
|
||||
// 如果是接收模式,更新URL移除code参数
|
||||
if (mode === 'receive') {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete('code');
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}
|
||||
// 如果是接收模式,需要手动更新URL
|
||||
// URL处理逻辑已经移到 hook 中
|
||||
};
|
||||
|
||||
// 处理文件列表更新
|
||||
// 处理文件列表更新
|
||||
useEffect(() => {
|
||||
const cleanup = onFileListReceived((fileInfos: FileInfo[]) => {
|
||||
console.log('=== 收到文件列表更新 ===');
|
||||
console.log('文件列表:', fileInfos);
|
||||
console.log('当前模式:', mode);
|
||||
|
||||
if (mode === 'receive') {
|
||||
setFileList(fileInfos);
|
||||
@@ -416,6 +203,99 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
return cleanup;
|
||||
}, [onFileListReceived, mode]);
|
||||
|
||||
// 处理文件接收
|
||||
useEffect(() => {
|
||||
const cleanup = onFileReceived((fileData: { id: string; file: File }) => {
|
||||
console.log('=== 接收到文件 ===');
|
||||
console.log('文件:', fileData.file.name, 'ID:', fileData.id);
|
||||
|
||||
// 更新下载的文件
|
||||
setDownloadedFiles(prev => new Map(prev.set(fileData.id, fileData.file)));
|
||||
|
||||
// 更新文件状态
|
||||
updateFileStatus(fileData.id, 'completed', 100);
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onFileReceived, updateFileStatus]);
|
||||
|
||||
// 监听文件级别的进度更新
|
||||
useEffect(() => {
|
||||
const cleanup = onFileProgress((progressInfo) => {
|
||||
// 检查连接状态,如果连接断开则忽略进度更新
|
||||
if (!isConnected || error) {
|
||||
console.log('连接已断开,忽略进度更新:', progressInfo.fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== 文件进度更新 ===');
|
||||
console.log('文件:', progressInfo.fileName, 'ID:', progressInfo.fileId, '进度:', progressInfo.progress);
|
||||
|
||||
// 更新当前传输文件信息
|
||||
setCurrentTransferFile({
|
||||
fileId: progressInfo.fileId,
|
||||
fileName: progressInfo.fileName,
|
||||
progress: progressInfo.progress
|
||||
});
|
||||
|
||||
// 更新文件进度
|
||||
updateFileProgress(progressInfo.fileId, progressInfo.fileName, progressInfo.progress);
|
||||
|
||||
// 当传输完成时清理
|
||||
if (progressInfo.progress >= 100 && mode === 'send') {
|
||||
setCurrentTransferFile(null);
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onFileProgress, mode, isConnected, error, updateFileProgress]);
|
||||
|
||||
// 处理文件请求(发送方监听)
|
||||
useEffect(() => {
|
||||
const cleanup = onFileRequested((fileId: string, fileName: string) => {
|
||||
console.log('=== 收到文件请求 ===');
|
||||
console.log('文件:', fileName, 'ID:', fileId, '当前模式:', mode);
|
||||
|
||||
if (mode === 'send') {
|
||||
// 检查连接状态
|
||||
if (!isConnected || error) {
|
||||
console.log('连接已断开,无法发送文件');
|
||||
showToast('连接已断开,无法发送文件', "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 在发送方的selectedFiles中查找对应文件
|
||||
const file = selectedFiles.find(f => f.name === fileName);
|
||||
|
||||
if (!file) {
|
||||
console.error('找不到匹配的文件:', fileName);
|
||||
showToast(`无法找到文件: ${fileName}`, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('找到匹配文件,开始发送:', file.name, 'ID:', fileId, '文件大小:', file.size);
|
||||
|
||||
// 更新发送方文件状态为downloading
|
||||
updateFileStatus(fileId, 'downloading', 0);
|
||||
|
||||
// 发送文件
|
||||
try {
|
||||
sendFile(file, fileId);
|
||||
} catch (sendError) {
|
||||
console.error('发送文件失败:', sendError);
|
||||
showToast(`发送文件失败: ${fileName}`, "error");
|
||||
|
||||
// 重置文件状态
|
||||
updateFileStatus(fileId, 'ready', 0);
|
||||
}
|
||||
} else {
|
||||
console.warn('接收模式下收到文件请求,忽略');
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onFileRequested, mode, selectedFiles, sendFile, isConnected, error, showToast, updateFileStatus]);
|
||||
|
||||
// 处理连接错误
|
||||
const [lastError, setLastError] = useState<string>('');
|
||||
useEffect(() => {
|
||||
@@ -482,52 +362,6 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
return cleanup;
|
||||
}, [onFileReceived]);
|
||||
|
||||
// 监听文件级别的进度更新
|
||||
useEffect(() => {
|
||||
const cleanup = onFileProgress((progressInfo) => {
|
||||
// 检查连接状态,如果连接断开则忽略进度更新
|
||||
if (!isConnected || error) {
|
||||
console.log('连接已断开,忽略进度更新:', progressInfo.fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('=== 文件进度更新 ===');
|
||||
console.log('文件:', progressInfo.fileName, 'ID:', progressInfo.fileId, '进度:', progressInfo.progress);
|
||||
|
||||
// 更新当前传输文件信息
|
||||
setCurrentTransferFile({
|
||||
fileId: progressInfo.fileId,
|
||||
fileName: progressInfo.fileName,
|
||||
progress: progressInfo.progress
|
||||
});
|
||||
|
||||
// 更新文件列表中对应文件的进度
|
||||
setFileList(prev => prev.map(item => {
|
||||
if (item.id === progressInfo.fileId || item.name === progressInfo.fileName) {
|
||||
const newProgress = progressInfo.progress;
|
||||
const newStatus = newProgress >= 100 ? 'completed' as const : 'downloading' as const;
|
||||
|
||||
console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${newProgress}`);
|
||||
return { ...item, progress: newProgress, status: newStatus };
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
|
||||
// 当传输完成时显示提示
|
||||
if (progressInfo.progress >= 100 && mode === 'send') {
|
||||
// 移除不必要的Toast - 传输完成状态在UI中已经显示
|
||||
setCurrentTransferFile(null);
|
||||
}
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
}, [onFileProgress, mode, isConnected, error]);
|
||||
|
||||
// 实时更新传输进度(旧逻辑 - 删除)
|
||||
// useEffect(() => {
|
||||
// ...已删除的旧代码...
|
||||
// }, [...]);
|
||||
|
||||
// 处理文件请求(发送方监听)
|
||||
useEffect(() => {
|
||||
const cleanup = onFileRequested((fileId: string, fileName: string) => {
|
||||
@@ -792,13 +626,6 @@ export const WebRTCFileTransfer: React.FC = () => {
|
||||
fileInputRef.current?.click();
|
||||
};
|
||||
|
||||
// 清空文件
|
||||
const clearFiles = () => {
|
||||
console.log('=== 清空文件 ===');
|
||||
setSelectedFiles([]);
|
||||
setFileList([]);
|
||||
};
|
||||
|
||||
// 下载文件到本地
|
||||
const downloadFile = (fileId: string) => {
|
||||
const file = downloadedFiles.get(fileId);
|
||||
|
||||
@@ -1,68 +1,34 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Send, Download, X } from 'lucide-react';
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import { useURLHandler } from '@/hooks/ui';
|
||||
import { useWebRTCStore } from '@/hooks/ui/webRTCStore';
|
||||
import { WebRTCTextSender } from '@/components/webrtc/WebRTCTextSender';
|
||||
import { WebRTCTextReceiver } from '@/components/webrtc/WebRTCTextReceiver';
|
||||
import { useWebRTCStore } from '@/hooks/webrtc/webRTCStore';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { MessageSquare, Send, Download, X } from 'lucide-react';
|
||||
|
||||
export const WebRTCTextImageTransfer: React.FC = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
// 状态管理
|
||||
const [mode, setMode] = useState<'send' | 'receive'>('send');
|
||||
const [hasProcessedInitialUrl, setHasProcessedInitialUrl] = useState(false);
|
||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||
|
||||
// 使用全局WebRTC状态
|
||||
const webrtcState = useWebRTCStore();
|
||||
|
||||
// 从URL参数中获取初始模式
|
||||
useEffect(() => {
|
||||
const urlMode = searchParams.get('mode') as 'send' | 'receive';
|
||||
const type = searchParams.get('type');
|
||||
const code = searchParams.get('code');
|
||||
|
||||
if (!hasProcessedInitialUrl && type === 'message' && urlMode && ['send', 'receive'].includes(urlMode)) {
|
||||
console.log('=== 处理初始URL参数 ===');
|
||||
console.log('URL模式:', urlMode, '类型:', type, '取件码:', code);
|
||||
|
||||
setMode(urlMode);
|
||||
setHasProcessedInitialUrl(true);
|
||||
}
|
||||
}, [searchParams, hasProcessedInitialUrl]);
|
||||
|
||||
// 更新URL参数
|
||||
const updateMode = useCallback((newMode: 'send' | 'receive') => {
|
||||
console.log('=== 切换模式 ===', newMode);
|
||||
|
||||
setMode(newMode);
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('type', 'message');
|
||||
params.set('mode', newMode);
|
||||
|
||||
if (newMode === 'send') {
|
||||
params.delete('code');
|
||||
}
|
||||
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}, [searchParams, router]);
|
||||
// 使用统一的URL处理器
|
||||
const { updateMode, getCurrentRoomCode, clearURLParams } = useURLHandler({
|
||||
featureType: 'message',
|
||||
onModeChange: setMode
|
||||
});
|
||||
|
||||
// 重新开始函数
|
||||
const handleRestart = useCallback(() => {
|
||||
setPreviewImage(null);
|
||||
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('type', 'message');
|
||||
params.set('mode', mode);
|
||||
params.delete('code');
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}, [searchParams, mode, router]);
|
||||
clearURLParams();
|
||||
}, [clearURLParams]);
|
||||
|
||||
const code = searchParams.get('code') || '';
|
||||
const code = getCurrentRoomCode();
|
||||
|
||||
// 连接状态变化处理 - 现在不需要了,因为使用全局状态
|
||||
const handleConnectionChange = useCallback((connection: any) => {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Monitor, Square } from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness';
|
||||
import { useDesktopShareBusiness } from '@/hooks/desktop-share';
|
||||
import DesktopViewer from '@/components/DesktopViewer';
|
||||
import { ConnectionStatus } from '@/components/ConnectionStatus';
|
||||
|
||||
@@ -19,7 +19,6 @@ export default function WebRTCDesktopReceiver({ className, initialCode, onConnec
|
||||
const [inputCode, setInputCode] = useState(initialCode || '');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isJoiningRoom, setIsJoiningRoom] = useState(false); // 添加加入房间状态
|
||||
const [showDebug, setShowDebug] = useState(false);
|
||||
const hasTriedAutoJoin = React.useRef(false); // 添加 ref 来跟踪是否已尝试自动加入
|
||||
const { showToast } = useToast();
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Share, Monitor, Copy, Play, Square, Repeat, Users, Wifi, WifiOff } from 'lucide-react';
|
||||
import { Share, Monitor, Play, Square, Repeat } from 'lucide-react';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import { useDesktopShareBusiness } from '@/hooks/webrtc/useDesktopShareBusiness';
|
||||
import { useDesktopShareBusiness } from '@/hooks/desktop-share';
|
||||
import RoomInfoDisplay from '@/components/RoomInfoDisplay';
|
||||
import { ConnectionStatus } from '@/components/ConnectionStatus';
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useSharedWebRTCManager } from '@/hooks/webrtc/useSharedWebRTCManager';
|
||||
import { useTextTransferBusiness } from '@/hooks/webrtc/useTextTransferBusiness';
|
||||
import { useFileTransferBusiness } from '@/hooks/webrtc/useFileTransferBusiness';
|
||||
import { useSharedWebRTCManager } from '@/hooks/connection';
|
||||
import { useTextTransferBusiness } from '@/hooks/text-transfer';
|
||||
import { useFileTransferBusiness } from '@/hooks/file-transfer';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
||||
import { useSharedWebRTCManager } from '@/hooks/webrtc/useSharedWebRTCManager';
|
||||
import { useTextTransferBusiness } from '@/hooks/webrtc/useTextTransferBusiness';
|
||||
import { useFileTransferBusiness } from '@/hooks/webrtc/useFileTransferBusiness';
|
||||
import { useSharedWebRTCManager } from '@/hooks/connection';
|
||||
import { useTextTransferBusiness } from '@/hooks/text-transfer';
|
||||
import { useFileTransferBusiness } from '@/hooks/file-transfer';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
import { MessageSquare, Image, Send, Copy } from 'lucide-react';
|
||||
|
||||
6
chuan-next/src/hooks/connection/index.ts
Normal file
6
chuan-next/src/hooks/connection/index.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
// 连接相关的hooks
|
||||
export { useConnectionState } from './useConnectionState';
|
||||
export { useRoomConnection } from './useRoomConnection';
|
||||
export { useSharedWebRTCManager } from './useSharedWebRTCManager';
|
||||
export { useWebRTCManager } from './useWebRTCManager';
|
||||
export { useWebRTCSupport } from './useWebRTCSupport';
|
||||
137
chuan-next/src/hooks/connection/useConnectionState.ts
Normal file
137
chuan-next/src/hooks/connection/useConnectionState.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
|
||||
interface UseConnectionStateProps {
|
||||
isWebSocketConnected: boolean;
|
||||
isConnected: boolean;
|
||||
isConnecting: boolean;
|
||||
error: string;
|
||||
pickupCode: string;
|
||||
fileListLength: number;
|
||||
currentTransferFile: any;
|
||||
setCurrentTransferFile: (file: any) => void;
|
||||
updateFileListStatus: (callback: (prev: any[]) => any[]) => void;
|
||||
}
|
||||
|
||||
export const useConnectionState = ({
|
||||
isWebSocketConnected,
|
||||
isConnected,
|
||||
isConnecting,
|
||||
error,
|
||||
pickupCode,
|
||||
fileListLength,
|
||||
currentTransferFile,
|
||||
setCurrentTransferFile,
|
||||
updateFileListStatus
|
||||
}: UseConnectionStateProps) => {
|
||||
const { showToast } = useToast();
|
||||
const [lastError, setLastError] = useState<string>('');
|
||||
|
||||
// 处理连接错误
|
||||
useEffect(() => {
|
||||
if (error && error !== lastError) {
|
||||
console.log('=== 连接错误处理 ===');
|
||||
console.log('错误信息:', error);
|
||||
|
||||
// 根据错误类型显示不同的提示
|
||||
let errorMessage = error;
|
||||
|
||||
if (error.includes('WebSocket')) {
|
||||
errorMessage = '服务器连接失败,请检查网络连接或稍后重试';
|
||||
} else if (error.includes('数据通道')) {
|
||||
errorMessage = '数据通道连接失败,请重新尝试连接';
|
||||
} else if (error.includes('连接超时')) {
|
||||
errorMessage = '连接超时,请检查网络状况或重新尝试';
|
||||
} else if (error.includes('连接失败')) {
|
||||
errorMessage = 'WebRTC连接失败,可能是网络环境限制,请尝试刷新页面';
|
||||
} else if (error.includes('信令错误')) {
|
||||
errorMessage = '信令服务器错误,请稍后重试';
|
||||
} else if (error.includes('创建连接失败')) {
|
||||
errorMessage = '无法建立P2P连接,请检查网络设置';
|
||||
}
|
||||
|
||||
// 显示错误提示
|
||||
showToast(errorMessage, "error");
|
||||
setLastError(error);
|
||||
|
||||
// 如果是严重连接错误,清理传输状态
|
||||
if (error.includes('连接失败') || error.includes('数据通道连接失败') || error.includes('WebSocket')) {
|
||||
console.log('严重连接错误,清理传输状态');
|
||||
setCurrentTransferFile(null);
|
||||
|
||||
// 重置所有正在传输的文件状态
|
||||
updateFileListStatus((prev: any[]) => prev.map(item =>
|
||||
item.status === 'downloading'
|
||||
? { ...item, status: 'ready' as const, progress: 0 }
|
||||
: item
|
||||
));
|
||||
}
|
||||
}
|
||||
}, [error, lastError, showToast, setCurrentTransferFile, updateFileListStatus]);
|
||||
|
||||
// 监听连接状态变化和清理传输状态
|
||||
useEffect(() => {
|
||||
console.log('=== 连接状态变化 ===');
|
||||
console.log('WebSocket连接状态:', isWebSocketConnected);
|
||||
console.log('WebRTC连接状态:', isConnected);
|
||||
console.log('连接中状态:', isConnecting);
|
||||
|
||||
// 当连接断开或有错误时,清理所有传输状态
|
||||
const shouldCleanup = (!isWebSocketConnected && !isConnected && !isConnecting && pickupCode) ||
|
||||
((!isConnected && !isConnecting) || error);
|
||||
|
||||
if (shouldCleanup) {
|
||||
const hasCurrentTransfer = !!currentTransferFile;
|
||||
const hasFileList = fileListLength > 0;
|
||||
|
||||
// 只有在之前有连接活动时才显示断开提示和清理状态
|
||||
if (hasFileList || hasCurrentTransfer) {
|
||||
if (!isWebSocketConnected && pickupCode) {
|
||||
showToast('与服务器的连接已断开,请重新连接', "error");
|
||||
}
|
||||
|
||||
console.log('连接断开,清理传输状态');
|
||||
|
||||
if (currentTransferFile) {
|
||||
setCurrentTransferFile(null);
|
||||
}
|
||||
|
||||
// 重置所有正在下载的文件状态
|
||||
updateFileListStatus((prev: any[]) => {
|
||||
const hasDownloadingFiles = prev.some(item => item.status === 'downloading');
|
||||
if (hasDownloadingFiles) {
|
||||
console.log('重置正在传输的文件状态');
|
||||
return prev.map(item =>
|
||||
item.status === 'downloading'
|
||||
? { ...item, status: 'ready' as const, progress: 0 }
|
||||
: item
|
||||
);
|
||||
}
|
||||
return prev;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket连接成功时的提示
|
||||
if (isWebSocketConnected && isConnecting && !isConnected) {
|
||||
console.log('WebSocket已连接,正在建立P2P连接...');
|
||||
}
|
||||
|
||||
}, [isWebSocketConnected, isConnected, isConnecting, pickupCode, error, showToast, currentTransferFile, fileListLength, setCurrentTransferFile, updateFileListStatus]);
|
||||
|
||||
// 监听连接状态变化并提供日志
|
||||
useEffect(() => {
|
||||
console.log('=== WebRTC连接状态变化 ===');
|
||||
console.log('连接状态:', {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
isWebSocketConnected,
|
||||
pickupCode,
|
||||
fileListLength
|
||||
});
|
||||
}, [isConnected, isConnecting, isWebSocketConnected, pickupCode, fileListLength]);
|
||||
|
||||
return {
|
||||
lastError
|
||||
};
|
||||
};
|
||||
103
chuan-next/src/hooks/connection/useRoomConnection.ts
Normal file
103
chuan-next/src/hooks/connection/useRoomConnection.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useState, useCallback } from 'react';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
|
||||
interface UseRoomConnectionProps {
|
||||
connect: (code: string, role: 'sender' | 'receiver') => void;
|
||||
isConnecting: boolean;
|
||||
isConnected: boolean;
|
||||
}
|
||||
|
||||
export const useRoomConnection = ({ connect, isConnecting, isConnected }: UseRoomConnectionProps) => {
|
||||
const { showToast } = useToast();
|
||||
const [isJoiningRoom, setIsJoiningRoom] = useState(false);
|
||||
|
||||
const validateRoomCode = (code: string): string | null => {
|
||||
const trimmedCode = code.trim();
|
||||
if (!trimmedCode || trimmedCode.length !== 6) {
|
||||
return '请输入正确的6位取件码';
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const checkRoomStatus = async (code: string) => {
|
||||
const response = await fetch(`/api/room-info?code=${code}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: 无法检查房间状态`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success) {
|
||||
let errorMessage = result.message || '房间不存在或已过期';
|
||||
if (result.message?.includes('expired')) {
|
||||
errorMessage = '房间已过期,请联系发送方重新创建';
|
||||
} else if (result.message?.includes('not found')) {
|
||||
errorMessage = '房间不存在,请检查取件码是否正确';
|
||||
}
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
|
||||
if (!result.sender_online) {
|
||||
throw new Error('发送方不在线,请确认取件码是否正确或联系发送方');
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const handleNetworkError = (error: Error): string => {
|
||||
if (error.message.includes('network') || error.message.includes('fetch')) {
|
||||
return '网络连接失败,请检查网络状况';
|
||||
} else if (error.message.includes('timeout')) {
|
||||
return '请求超时,请重试';
|
||||
} else if (error.message.includes('HTTP 404')) {
|
||||
return '房间不存在,请检查取件码';
|
||||
} else if (error.message.includes('HTTP 500')) {
|
||||
return '服务器错误,请稍后重试';
|
||||
} else {
|
||||
return error.message;
|
||||
}
|
||||
};
|
||||
|
||||
// 加入房间 (接收模式)
|
||||
const joinRoom = useCallback(async (code: string) => {
|
||||
console.log('=== 加入房间 ===');
|
||||
console.log('取件码:', code);
|
||||
|
||||
// 验证输入
|
||||
const validationError = validateRoomCode(code);
|
||||
if (validationError) {
|
||||
showToast(validationError, "error");
|
||||
return;
|
||||
}
|
||||
|
||||
// 防止重复调用
|
||||
if (isConnecting || isConnected || isJoiningRoom) {
|
||||
console.log('已在连接中或已连接,跳过重复的房间状态检查');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsJoiningRoom(true);
|
||||
|
||||
try {
|
||||
console.log('检查房间状态...');
|
||||
await checkRoomStatus(code.trim());
|
||||
|
||||
console.log('房间状态检查通过,开始连接...');
|
||||
connect(code.trim(), 'receiver');
|
||||
showToast(`正在连接到房间: ${code.trim()}`, "success");
|
||||
|
||||
} catch (error) {
|
||||
console.error('检查房间状态失败:', error);
|
||||
const errorMessage = error instanceof Error ? handleNetworkError(error) : '检查房间状态失败';
|
||||
showToast(errorMessage, "error");
|
||||
} finally {
|
||||
setIsJoiningRoom(false);
|
||||
}
|
||||
}, [isConnecting, isConnected, isJoiningRoom, showToast, connect]);
|
||||
|
||||
return {
|
||||
joinRoom,
|
||||
isJoiningRoom
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useCallback } from 'react';
|
||||
import { getWsUrl } from '@/lib/config';
|
||||
import { useWebRTCStore } from './webRTCStore';
|
||||
import { useWebRTCStore } from '../ui/webRTCStore';
|
||||
|
||||
// 基础连接状态
|
||||
interface WebRTCState {
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { WebRTCManager } from './core/WebRTCManager';
|
||||
import { WebRTCConnectionState, WebRTCMessage, MessageHandler, DataHandler } from './core/types';
|
||||
import { WebRTCManager } from '../webrtc/WebRTCManager';
|
||||
import { WebRTCConnectionState, WebRTCMessage, MessageHandler, DataHandler } from '../webrtc/types';
|
||||
import { WebRTCConnection } from './useSharedWebRTCManager';
|
||||
|
||||
interface WebRTCManagerConfig {
|
||||
2
chuan-next/src/hooks/desktop-share/index.ts
Normal file
2
chuan-next/src/hooks/desktop-share/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// 桌面共享相关的hooks
|
||||
export { useDesktopShareBusiness } from './useDesktopShareBusiness';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { useSharedWebRTCManager } from './useSharedWebRTCManager';
|
||||
import { useSharedWebRTCManager } from '../connection/useSharedWebRTCManager';
|
||||
|
||||
interface DesktopShareState {
|
||||
isSharing: boolean;
|
||||
4
chuan-next/src/hooks/file-transfer/index.ts
Normal file
4
chuan-next/src/hooks/file-transfer/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
// 文件传输相关的hooks
|
||||
export { useFileTransferBusiness } from './useFileTransferBusiness';
|
||||
export { useFileStateManager } from './useFileStateManager';
|
||||
export { useFileListSync } from './useFileListSync';
|
||||
65
chuan-next/src/hooks/file-transfer/useFileListSync.ts
Normal file
65
chuan-next/src/hooks/file-transfer/useFileListSync.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useRef, useCallback, useEffect } from 'react';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
status: 'ready' | 'downloading' | 'completed';
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface UseFileListSyncProps {
|
||||
sendFileList: (fileInfos: FileInfo[]) => void;
|
||||
mode: 'send' | 'receive';
|
||||
pickupCode: string;
|
||||
isConnected: boolean;
|
||||
isPeerConnected: boolean;
|
||||
getChannelState: () => string;
|
||||
}
|
||||
|
||||
export const useFileListSync = ({
|
||||
sendFileList,
|
||||
mode,
|
||||
pickupCode,
|
||||
isConnected,
|
||||
isPeerConnected,
|
||||
getChannelState
|
||||
}: UseFileListSyncProps) => {
|
||||
const syncTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
// 统一的文件列表同步函数,带防抖功能
|
||||
const syncFileListToReceiver = useCallback((fileInfos: FileInfo[], reason: string) => {
|
||||
// 只有在发送模式、连接已建立且有房间时才发送文件列表
|
||||
if (mode !== 'send' || !pickupCode || !isConnected || !isPeerConnected) {
|
||||
console.log('跳过文件列表同步:', { mode, pickupCode: !!pickupCode, isConnected, isPeerConnected });
|
||||
return;
|
||||
}
|
||||
|
||||
// 清除之前的延时发送
|
||||
if (syncTimeoutRef.current) {
|
||||
clearTimeout(syncTimeoutRef.current);
|
||||
}
|
||||
|
||||
// 延时发送,避免频繁发送
|
||||
syncTimeoutRef.current = setTimeout(() => {
|
||||
if (isPeerConnected && getChannelState() === 'open') {
|
||||
console.log(`发送文件列表到接收方 (${reason}):`, fileInfos.map(f => f.name));
|
||||
sendFileList(fileInfos);
|
||||
}
|
||||
}, 150);
|
||||
}, [mode, pickupCode, isConnected, isPeerConnected, getChannelState, sendFileList]);
|
||||
|
||||
// 清理防抖定时器
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (syncTimeoutRef.current) {
|
||||
clearTimeout(syncTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
syncFileListToReceiver
|
||||
};
|
||||
};
|
||||
166
chuan-next/src/hooks/file-transfer/useFileStateManager.ts
Normal file
166
chuan-next/src/hooks/file-transfer/useFileStateManager.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
interface FileInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
status: 'ready' | 'downloading' | 'completed';
|
||||
progress: number;
|
||||
}
|
||||
|
||||
interface UseFileStateManagerProps {
|
||||
mode: 'send' | 'receive';
|
||||
pickupCode: string;
|
||||
syncFileListToReceiver: (fileInfos: FileInfo[], reason: string) => void;
|
||||
isPeerConnected: boolean;
|
||||
}
|
||||
|
||||
export const useFileStateManager = ({
|
||||
mode,
|
||||
pickupCode,
|
||||
syncFileListToReceiver,
|
||||
isPeerConnected
|
||||
}: UseFileStateManagerProps) => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
const [fileList, setFileList] = useState<FileInfo[]>([]);
|
||||
const [downloadedFiles, setDownloadedFiles] = useState<Map<string, File>>(new Map());
|
||||
|
||||
// 生成文件ID
|
||||
const generateFileId = useCallback(() => {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2);
|
||||
}, []);
|
||||
|
||||
// 文件选择处理
|
||||
const handleFileSelect = useCallback((files: File[]) => {
|
||||
console.log('=== 文件选择 ===');
|
||||
console.log('新文件:', files.map(f => f.name));
|
||||
|
||||
// 更新选中的文件
|
||||
setSelectedFiles(prev => [...prev, ...files]);
|
||||
|
||||
// 创建对应的文件信息
|
||||
const newFileInfos: FileInfo[] = files.map(file => ({
|
||||
id: generateFileId(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
status: 'ready',
|
||||
progress: 0
|
||||
}));
|
||||
|
||||
setFileList(prev => {
|
||||
const updatedList = [...prev, ...newFileInfos];
|
||||
console.log('更新后的文件列表:', updatedList);
|
||||
return updatedList;
|
||||
});
|
||||
}, [generateFileId]);
|
||||
|
||||
// 清空文件
|
||||
const clearFiles = useCallback(() => {
|
||||
console.log('=== 清空文件 ===');
|
||||
setSelectedFiles([]);
|
||||
setFileList([]);
|
||||
}, []);
|
||||
|
||||
// 重置状态
|
||||
const resetFiles = useCallback(() => {
|
||||
console.log('=== 重置文件状态 ===');
|
||||
setSelectedFiles([]);
|
||||
setFileList([]);
|
||||
setDownloadedFiles(new Map());
|
||||
}, []);
|
||||
|
||||
// 更新文件状态
|
||||
const updateFileStatus = useCallback((fileId: string, status: FileInfo['status'], progress?: number) => {
|
||||
setFileList(prev => prev.map(item =>
|
||||
item.id === fileId
|
||||
? { ...item, status, progress: progress ?? item.progress }
|
||||
: item
|
||||
));
|
||||
}, []);
|
||||
|
||||
// 更新文件进度
|
||||
const updateFileProgress = useCallback((fileId: string, fileName: string, progress: number) => {
|
||||
const newStatus = progress >= 100 ? 'completed' as const : 'downloading' as const;
|
||||
setFileList(prev => prev.map(item => {
|
||||
if (item.id === fileId || item.name === fileName) {
|
||||
console.log(`更新文件 ${item.name} 进度: ${item.progress} -> ${progress}`);
|
||||
return { ...item, progress, status: newStatus };
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
}, []);
|
||||
|
||||
// 数据通道第一次打开时初始化
|
||||
useEffect(() => {
|
||||
if (isPeerConnected && mode === 'send' && fileList.length > 0) {
|
||||
console.log('P2P连接已建立,数据通道首次打开,初始化文件列表');
|
||||
syncFileListToReceiver(fileList, '数据通道初始化');
|
||||
}
|
||||
}, [isPeerConnected, mode, syncFileListToReceiver]);
|
||||
|
||||
// 监听fileList大小变化并同步
|
||||
useEffect(() => {
|
||||
if (isPeerConnected && mode === 'send' && pickupCode) {
|
||||
console.log('fileList大小变化,同步到接收方:', fileList.length);
|
||||
syncFileListToReceiver(fileList, 'fileList大小变化');
|
||||
}
|
||||
}, [fileList.length, isPeerConnected, mode, pickupCode, syncFileListToReceiver]);
|
||||
|
||||
// 监听selectedFiles变化,同步更新fileList
|
||||
useEffect(() => {
|
||||
// 只有在发送模式下且已有房间时才处理文件列表同步
|
||||
if (mode !== 'send' || !pickupCode) return;
|
||||
|
||||
console.log('=== selectedFiles变化,同步文件列表 ===', {
|
||||
selectedFilesCount: selectedFiles.length,
|
||||
fileListCount: fileList.length,
|
||||
selectedFileNames: selectedFiles.map(f => f.name)
|
||||
});
|
||||
|
||||
// 根据selectedFiles创建新的文件信息列表
|
||||
const newFileInfos: FileInfo[] = selectedFiles.map(file => {
|
||||
// 尝试找到现有的文件信息,保持已有的状态
|
||||
const existingFileInfo = fileList.find(info => info.name === file.name && info.size === file.size);
|
||||
return existingFileInfo || {
|
||||
id: generateFileId(),
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
type: file.type,
|
||||
status: 'ready' as const,
|
||||
progress: 0
|
||||
};
|
||||
});
|
||||
|
||||
// 检查文件列表是否真正发生变化
|
||||
const fileListChanged =
|
||||
newFileInfos.length !== fileList.length ||
|
||||
newFileInfos.some(newFile =>
|
||||
!fileList.find(oldFile => oldFile.name === newFile.name && oldFile.size === newFile.size)
|
||||
);
|
||||
|
||||
if (fileListChanged) {
|
||||
console.log('文件列表发生变化,更新:', {
|
||||
before: fileList.map(f => f.name),
|
||||
after: newFileInfos.map(f => f.name)
|
||||
});
|
||||
|
||||
setFileList(newFileInfos);
|
||||
}
|
||||
}, [selectedFiles, mode, pickupCode, fileList, generateFileId]);
|
||||
|
||||
return {
|
||||
selectedFiles,
|
||||
setSelectedFiles,
|
||||
fileList,
|
||||
setFileList,
|
||||
downloadedFiles,
|
||||
setDownloadedFiles,
|
||||
handleFileSelect,
|
||||
clearFiles,
|
||||
resetFiles,
|
||||
updateFileStatus,
|
||||
updateFileProgress
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { WebRTCConnection } from './useSharedWebRTCManager';
|
||||
import type { WebRTCConnection } from '../connection/useSharedWebRTCManager';
|
||||
|
||||
// 文件传输状态
|
||||
interface FileTransferState {
|
||||
@@ -17,7 +17,6 @@ interface FileTransferState {
|
||||
interface FileReceiveProgress {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
receivedChunks: number;
|
||||
totalChunks: number;
|
||||
progress: number;
|
||||
}
|
||||
@@ -178,7 +177,6 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
|
||||
receiveProgress.current.set(metadata.id, {
|
||||
fileId: metadata.id,
|
||||
fileName: metadata.name,
|
||||
receivedChunks: 0,
|
||||
totalChunks,
|
||||
progress: 0
|
||||
});
|
||||
@@ -301,16 +299,22 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经接收过这个块,避免重复计数
|
||||
const alreadyReceived = fileInfo.chunks[chunkIndex] !== undefined;
|
||||
|
||||
// 数据有效,保存到缓存
|
||||
fileInfo.chunks[chunkIndex] = data;
|
||||
fileInfo.receivedChunks++;
|
||||
|
||||
// 只有在首次接收时才增加计数
|
||||
if (!alreadyReceived) {
|
||||
fileInfo.receivedChunks++;
|
||||
}
|
||||
|
||||
// 更新接收进度跟踪
|
||||
// 更新接收进度跟踪 - 使用 fileInfo 的计数,避免双重计数
|
||||
const progressInfo = receiveProgress.current.get(fileId);
|
||||
if (progressInfo) {
|
||||
progressInfo.receivedChunks++;
|
||||
progressInfo.progress = progressInfo.totalChunks > 0 ?
|
||||
(progressInfo.receivedChunks / progressInfo.totalChunks) * 100 : 0;
|
||||
(fileInfo.receivedChunks / progressInfo.totalChunks) * 100 : 0;
|
||||
|
||||
// 只有当这个文件是当前活跃文件时才更新全局进度
|
||||
if (activeReceiveFile.current === fileId) {
|
||||
@@ -537,8 +541,8 @@ export function useFileTransferBusiness(connection: WebRTCConnection) {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
const progress = (status.acknowledgedChunks.size / totalChunks) * 100;
|
||||
// 更新进度 - 基于已发送的块数,这样与接收方的进度更同步
|
||||
const progress = ((chunkIndex + 1) / totalChunks) * 100;
|
||||
updateState({ progress });
|
||||
|
||||
fileProgressCallbacks.current.forEach(cb => cb({
|
||||
19
chuan-next/src/hooks/index.ts
Normal file
19
chuan-next/src/hooks/index.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
// 按功能分类的hooks导出
|
||||
|
||||
// 连接相关
|
||||
export * from './connection';
|
||||
|
||||
// 文件传输相关
|
||||
export * from './file-transfer';
|
||||
|
||||
// 桌面共享相关
|
||||
export * from './desktop-share';
|
||||
|
||||
// 文本传输相关
|
||||
export * from './text-transfer';
|
||||
|
||||
// UI状态管理相关
|
||||
export * from './ui';
|
||||
|
||||
// 核心WebRTC功能
|
||||
export * from './webrtc';
|
||||
2
chuan-next/src/hooks/text-transfer/index.ts
Normal file
2
chuan-next/src/hooks/text-transfer/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// 文本传输相关的hooks
|
||||
export { useTextTransferBusiness } from './useTextTransferBusiness';
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
import type { WebRTCConnection } from './useSharedWebRTCManager';
|
||||
import type { WebRTCConnection } from '../connection/useSharedWebRTCManager';
|
||||
|
||||
// 文本传输状态
|
||||
interface TextTransferState {
|
||||
3
chuan-next/src/hooks/ui/index.ts
Normal file
3
chuan-next/src/hooks/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// UI状态管理相关的hooks
|
||||
export { useURLHandler } from './useURLHandler';
|
||||
export { useWebRTCStore } from './webRTCStore';
|
||||
123
chuan-next/src/hooks/ui/useURLHandler.ts
Normal file
123
chuan-next/src/hooks/ui/useURLHandler.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useSearchParams, useRouter } from 'next/navigation';
|
||||
import { useToast } from '@/components/ui/toast-simple';
|
||||
|
||||
// 支持的功能类型
|
||||
export type FeatureType = 'webrtc' | 'message' | 'desktop';
|
||||
|
||||
// 支持的模式映射
|
||||
const MODE_MAPPINGS: Record<FeatureType, { send: string; receive: string }> = {
|
||||
webrtc: { send: 'send', receive: 'receive' },
|
||||
message: { send: 'send', receive: 'receive' },
|
||||
desktop: { send: 'send', receive: 'receive' } // desktop内部可能使用 share/view,但URL统一使用send/receive
|
||||
};
|
||||
|
||||
interface UseURLHandlerProps<T = string> {
|
||||
featureType: FeatureType;
|
||||
onModeChange: (mode: T) => void;
|
||||
onAutoJoinRoom?: (code: string) => void;
|
||||
modeConverter?: {
|
||||
// 将URL模式转换为组件内部模式
|
||||
fromURL: (urlMode: 'send' | 'receive') => T;
|
||||
// 将组件内部模式转换为URL模式
|
||||
toURL: (componentMode: T) => 'send' | 'receive';
|
||||
};
|
||||
}
|
||||
|
||||
export const useURLHandler = <T = 'send' | 'receive'>({
|
||||
featureType,
|
||||
onModeChange,
|
||||
onAutoJoinRoom,
|
||||
modeConverter
|
||||
}: UseURLHandlerProps<T>) => {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const [hasProcessedInitialUrl, setHasProcessedInitialUrl] = useState(false);
|
||||
const urlProcessedRef = useRef(false);
|
||||
|
||||
// 从URL参数中获取初始模式(仅在首次加载时处理)
|
||||
useEffect(() => {
|
||||
// 使用 ref 确保只处理一次,避免严格模式的重复调用
|
||||
if (urlProcessedRef.current) {
|
||||
console.log('URL已处理过,跳过重复处理');
|
||||
return;
|
||||
}
|
||||
|
||||
const urlMode = searchParams.get('mode') as 'send' | 'receive';
|
||||
const type = searchParams.get('type') as FeatureType;
|
||||
const code = searchParams.get('code');
|
||||
|
||||
// 只在首次加载且URL中有对应功能类型时处理
|
||||
if (!hasProcessedInitialUrl && type === featureType && urlMode && ['send', 'receive'].includes(urlMode)) {
|
||||
console.log(`=== 处理初始URL参数 [${featureType}] ===`);
|
||||
console.log('URL模式:', urlMode, '类型:', type, '取件码:', code);
|
||||
|
||||
// 立即标记为已处理,防止重复
|
||||
urlProcessedRef.current = true;
|
||||
|
||||
// 转换模式(如果有转换器的话)
|
||||
const componentMode = modeConverter ? modeConverter.fromURL(urlMode) : urlMode as T;
|
||||
onModeChange(componentMode);
|
||||
setHasProcessedInitialUrl(true);
|
||||
|
||||
// 自动加入房间(只在receive模式且有code时)
|
||||
if (code && urlMode === 'receive' && onAutoJoinRoom) {
|
||||
console.log('URL中有取件码,自动加入房间');
|
||||
onAutoJoinRoom(code);
|
||||
}
|
||||
}
|
||||
}, [searchParams, hasProcessedInitialUrl, featureType, onModeChange, onAutoJoinRoom, modeConverter]);
|
||||
|
||||
// 更新URL参数
|
||||
const updateMode = useCallback((newMode: T) => {
|
||||
console.log(`=== 手动切换模式 [${featureType}] ===`);
|
||||
console.log('新模式:', newMode);
|
||||
|
||||
onModeChange(newMode);
|
||||
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('type', featureType);
|
||||
|
||||
// 转换模式(如果有转换器的话)
|
||||
const urlMode = modeConverter ? modeConverter.toURL(newMode) : newMode as string;
|
||||
params.set('mode', urlMode);
|
||||
|
||||
// 如果切换到发送模式,移除code参数
|
||||
if (urlMode === 'send') {
|
||||
params.delete('code');
|
||||
}
|
||||
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}, [searchParams, router, featureType, onModeChange, modeConverter]);
|
||||
|
||||
// 更新URL中的房间代码
|
||||
const updateRoomCode = useCallback((code: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.set('type', featureType);
|
||||
params.set('code', code);
|
||||
router.push(`?${params.toString()}`, { scroll: false });
|
||||
}, [searchParams, router, featureType]);
|
||||
|
||||
// 获取当前URL中的房间代码
|
||||
const getCurrentRoomCode = useCallback(() => {
|
||||
return searchParams.get('code') || '';
|
||||
}, [searchParams]);
|
||||
|
||||
// 清除URL参数
|
||||
const clearURLParams = useCallback(() => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete('type');
|
||||
params.delete('mode');
|
||||
params.delete('code');
|
||||
|
||||
const newURL = params.toString() ? `?${params.toString()}` : '/';
|
||||
router.push(newURL, { scroll: false });
|
||||
}, [searchParams, router]);
|
||||
|
||||
return {
|
||||
updateMode,
|
||||
updateRoomCode,
|
||||
getCurrentRoomCode,
|
||||
clearURLParams
|
||||
};
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
7
chuan-next/src/hooks/webrtc/index.ts
Normal file
7
chuan-next/src/hooks/webrtc/index.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// WebRTC核心功能
|
||||
export * from './DataChannelManager';
|
||||
export * from './MessageRouter';
|
||||
export * from './PeerConnectionManager';
|
||||
export * from './WebRTCManager';
|
||||
export * from './WebSocketManager';
|
||||
export * from './types';
|
||||
Reference in New Issue
Block a user