11 Commits
v0.2.0 ... dev

Author SHA1 Message Date
9fb248dff0 wip: v0.2.8 dialog 美化 2025-06-18 23:01:08 +08:00
1b4ba1cb61 wip: v0.2.7(发送消息) 2025-06-17 22:45:22 +08:00
e5f7b5e6dc wip: 0.2.8
发送成功
2025-05-30 16:24:42 +08:00
21287e0874 wip: 0.2.7
实现了 rtc 握手和打开数据通道
2025-05-26 17:40:06 +08:00
5bc695bde3 wip: v0.2.6 2025-05-26 09:37:23 +08:00
b8645a68ed wip: 0.2.5
还未实现 rtc 握手
2025-05-23 18:01:16 +08:00
013670b78f wip: 0.2.4
还未实现 rtc 握手
2025-05-22 17:57:36 +08:00
16e9d663f4 wip: 0.2.3
1. websocket hook
  2. rtc init ok
2025-05-20 18:04:37 +08:00
becbc137c5 Merge tag 'v0.1.4' 2025-05-16 16:52:17 +08:00
d41d516f19 fix: 0.1.4
All checks were successful
/ build ushare (push) Successful in 36s
/ clean (push) Successful in 0s
1. meta clean goroutine walk error
  2. clean interval to args(--clean)
2025-05-16 16:50:22 +08:00
0a24393dcb fix: 0.1.4
All checks were successful
/ build ushare (push) Successful in 51s
/ clean (push) Successful in 0s
1. meta clean goroutine walk error
  2. clean interval to args(--clean)
2025-05-16 16:48:28 +08:00
14 changed files with 125 additions and 208 deletions

View File

@ -16,6 +16,7 @@ export const useFileUpload = () => {
setProgress(0); setProgress(0);
try { try {
console.log(`[D] api.Upload: upload file = ${file.name}, size = ${file.size}`, file);
const url = `/api/ushare/${file.name}`; const url = `/api/ushare/${file.name}`;
// 1. 初始化上传 // 1. 初始化上传
@ -25,7 +26,7 @@ export const useFileUpload = () => {
}); });
if (!res1.ok) { if (!res1.ok) {
console.log(`[W] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`) console.log(`[D] upload: put file not ok, status = ${res1.status}, res = ${await res1.text()}`)
if (res1.status === 401) { if (res1.status === 401) {
window.location.href = "/login?next=/share" window.location.href = "/login?next=/share"
return "" return ""

View File

@ -17,8 +17,7 @@ const useClass = createUseStyles({
background: "rgba(0, 0, 0, 0.5)", background: "rgba(0, 0, 0, 0.5)",
backdropFilter: "blur(2px)" backdropFilter: "blur(2px)"
}, },
background: "rgba(212,212,212,0.85)", background: "rgba(255, 255, 255, 0.4)",
backdropFilter: "blur(8px)",
}, },
dialog_content: { dialog_content: {
padding: "1.5rem", padding: "1.5rem",

View File

@ -50,6 +50,7 @@ export const Drawer: React.FC<DrawerProps> = ({
useEffect(() => { useEffect(() => {
const handleEscape = (e: KeyboardEvent) => { const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
console.log('[D] escape close:', close)
close() close()
} }
}; };

View File

@ -1,87 +1,6 @@
import React, {useState} from 'react'; import React, {useState} from 'react';
import {Dialog} from "../../../component/dialog/dialog.tsx"; import {Dialog} from "../../../component/dialog/dialog.tsx";
import {ReceivedMessage} from "./types.ts"; import {ReceivedMessage} from "./types.ts";
import {createUseStyles} from "react-jss";
const useStyles = createUseStyles({
root: {
marginBottom: '1rem',
},
sender: {
margin: '0 0 0.5rem 0',
fontSize: '0.9rem',
color: '#666',
},
msgBox: {
background: 'rgba(255,255,255,0.96)',
color: '#222',
padding: '1rem',
borderRadius: '4px',
border: '1px solid #ddd',
maxHeight: '400px',
minHeight: '80px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
},
downloadLink: {
color: '#007aff',
textDecoration: 'underline',
},
btnRow: {
display: 'flex',
flexDirection: 'row',
gap: '0.5rem',
justifyContent: 'flex-end',
alignItems: 'center',
marginTop: '8px',
},
copySuccess: {
color: '#28a745',
fontSize: '0.9rem',
marginRight: '0.5rem',
animation: 'fadeIn 0.3s ease-in',
},
copyBtn: {
padding: '8px 16px',
background: '#007aff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s',
'&:hover': {
background: '#0056b3',
},
},
copyBtnSuccess: {
background: '#28a745',
},
closeBtn: {
padding: '8px 16px',
background: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s',
},
progressBar: {
width: '100%',
height: 8,
background: '#eee',
borderRadius: 4,
margin: '12px 0',
overflow: 'hidden',
},
progressInner: {
height: '100%',
background: '#4dabf7',
transition: 'width 0.3s',
},
});
interface MessageDialogProps { interface MessageDialogProps {
open: boolean; open: boolean;
@ -91,14 +10,16 @@ interface MessageDialogProps {
export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onClose}) => { export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onClose}) => {
const [copySuccess, setCopySuccess] = useState(false); const [copySuccess, setCopySuccess] = useState(false);
const classes = useStyles();
const handleCopyMessage = () => { const handleCopyMessage = () => {
if (message) { if (message) {
navigator.clipboard.writeText(message.text || '').then(() => { navigator.clipboard.writeText(message.text || '').then(() => {
console.log('消息已复制到剪贴板');
setCopySuccess(true); setCopySuccess(true);
// 2秒后隐藏成功提示
setTimeout(() => setCopySuccess(false), 2000); setTimeout(() => setCopySuccess(false), 2000);
}).catch(() => { }).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动复制'); alert('复制失败,请手动复制');
}); });
} }
@ -111,49 +32,82 @@ export const MessageDialog: React.FC<MessageDialogProps> = ({open, message, onCl
onClose={onClose} onClose={onClose}
footer={false} footer={false}
> >
<div className={classes.root}> <div style={{ marginBottom: '1rem' }}>
<p className={classes.sender}> <p style={{ margin: '0 0 0.5rem 0', fontSize: '0.9rem', color: '#666' }}>
: {message?.sender} : {message?.sender}
</p> </p>
<div className={classes.msgBox}> <div style={{
background: '#f5f5f5',
padding: '1rem',
borderRadius: '4px',
border: '1px solid #ddd',
maxHeight: '200px',
overflowY: 'auto',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word'
}}>
{message?.isFile ? ( {message?.isFile ? (
<> <>
<div>📎 : {message.fileName} ({message.fileSize ? (message.fileSize/1024).toFixed(1) : ''} KB)</div> <div>📎 : {message.fileName} ({message.fileSize ? (message.fileSize/1024).toFixed(1) : ''} KB)</div>
{message.receiving ? ( <a href={message.fileBlobUrl} download={message.fileName} style={{color:'#007aff',textDecoration:'underline'}}>
<>
<div style={{fontSize: '0.95em', color: '#888'}}>... {Math.round((message.progress||0)*100)}%</div> </a>
<div className={classes.progressBar}>
<div className={classes.progressInner} style={{width: `${Math.round((message.progress||0)*100)}%`}} />
</div>
</>
) : (
<a href={message.fileBlobUrl} download={message.fileName} className={classes.downloadLink}>
</a>
)}
</> </>
) : ( ) : (
message?.text message?.text
)} )}
</div> </div>
</div> </div>
<div className={classes.btnRow}> <div style={{ display: 'flex', flexDirection: 'row', gap: '0.5rem', justifyContent: 'flex-end', alignItems: 'center', marginTop: '8px' }}>
{copySuccess && ( {copySuccess && (
<span className={classes.copySuccess}> <span style={{
color: '#28a745',
fontSize: '0.9rem',
marginRight: '0.5rem',
animation: 'fadeIn 0.3s ease-in'
}}>
</span> </span>
)} )}
{!message?.isFile && ( {!message?.isFile && (
<button <button
onClick={handleCopyMessage} onClick={handleCopyMessage}
className={copySuccess ? `${classes.copyBtn} ${classes.copyBtnSuccess}` : classes.copyBtn} style={{
padding: '8px 16px',
background: copySuccess ? '#28a745' : '#007aff',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s'
}}
onMouseEnter={(e) => {
if (!copySuccess) {
e.currentTarget.style.backgroundColor = '#0056b3';
}
}}
onMouseLeave={(e) => {
if (!copySuccess) {
e.currentTarget.style.backgroundColor = '#007aff';
}
}}
> >
{copySuccess ? '已复制' : '复制消息'} {copySuccess ? '已复制' : '复制消息'}
</button> </button>
)} )}
<button <button
onClick={onClose} onClick={onClose}
className={classes.closeBtn} style={{
padding: '8px 16px',
background: '#6c757d',
color: 'white',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'all 0.2s'
}}
> >
</button> </button>

View File

@ -1,4 +1,5 @@
import { WSMessage, ReceivedMessage} from "./types.ts"; import {Resp} from "../../../interface/response.ts";
import {Client, WSMessage, ReceivedMessage} from "./types.ts";
import {useLocalStore} from "../../../store/local.ts"; import {useLocalStore} from "../../../store/local.ts";
// 文件接收缓存 // 文件接收缓存
@ -8,7 +9,7 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
if (chunk.type === 'file') { if (chunk.type === 'file') {
// 文件元信息,初始化缓存 // 文件元信息,初始化缓存
fileReceiveCache[chunk.name + '_' + chunk.timestamp] = { fileReceiveCache[chunk.name + '_' + chunk.timestamp] = {
chunks: new Array(chunk.totalChunks), chunks: [],
total: chunk.totalChunks, total: chunk.totalChunks,
received: 0, received: 0,
name: chunk.name, name: chunk.name,
@ -16,16 +17,6 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
sender: chunk.sender, sender: chunk.sender,
timestamp: chunk.timestamp timestamp: chunk.timestamp
}; };
// 首次弹出进度
onFileReceived({
sender: chunk.sender,
timestamp: chunk.timestamp,
fileName: chunk.name,
fileSize: chunk.size,
isFile: true,
progress: 0,
receiving: true
});
} else if (chunk.type === 'file-chunk') { } else if (chunk.type === 'file-chunk') {
const key = chunk.name + '_' + chunk.timestamp; const key = chunk.name + '_' + chunk.timestamp;
const cache = fileReceiveCache[key]; const cache = fileReceiveCache[key];
@ -34,49 +25,21 @@ export const handleFileChunk = (chunk: any, onFileReceived: (msg: import("./type
const uint8 = new Uint8Array(chunk.data); const uint8 = new Uint8Array(chunk.data);
cache.chunks[chunk.chunkIndex] = uint8.buffer; cache.chunks[chunk.chunkIndex] = uint8.buffer;
cache.received++; cache.received++;
// 实时回调进度 // 全部收到
if (cache.received < cache.total) { if (cache.received === cache.total) {
const blob = new Blob(cache.chunks);
const url = URL.createObjectURL(blob);
onFileReceived({ onFileReceived({
sender: cache.sender, sender: cache.sender,
timestamp: cache.timestamp, timestamp: cache.timestamp,
fileName: cache.name, fileName: cache.name,
fileSize: cache.size, fileSize: cache.size,
isFile: true, fileBlobUrl: url,
progress: cache.received / cache.total, isFile: true
receiving: true
}); });
delete fileReceiveCache[key];
} }
} }
} else if (chunk.type === 'file-end') {
const key = chunk.name + '_' + chunk.timestamp;
const cache = fileReceiveCache[key];
if (cache && cache.received === cache.total) {
const blob = new Blob(cache.chunks);
const url = URL.createObjectURL(blob);
onFileReceived({
sender: cache.sender,
timestamp: cache.timestamp,
fileName: cache.name,
fileSize: cache.size,
fileBlobUrl: url,
isFile: true,
progress: 1,
receiving: false
});
delete fileReceiveCache[key];
} else if (cache) {
// 分块未齐全,提示异常
onFileReceived({
sender: cache.sender,
timestamp: cache.timestamp,
fileName: cache.name,
fileSize: cache.size,
isFile: true,
progress: cache.received / cache.total,
receiving: true,
text: '文件分块未齐全,接收失败'
});
}
} }
}; };
@ -97,18 +60,21 @@ export class RTCHandler {
// 更新回调函数的方法 // 更新回调函数的方法
updateCallbacks = (newCallbacks: RTCHandlerCallbacks) => { updateCallbacks = (newCallbacks: RTCHandlerCallbacks) => {
console.log('[D] Updating RTC handler callbacks');
this.callbacks = newCallbacks; this.callbacks = newCallbacks;
}; };
setupDataChannel = async (ch: RTCDataChannel, type: 'sender' | 'receiver') => { setupDataChannel = async (ch: RTCDataChannel, type: 'sender' | 'receiver') => {
console.log(`[D] Setting up data channel for type: ${type}`);
ch.onopen = () => { ch.onopen = () => {
console.log(`[D] 通道已打开!类型: ${type}`); console.log(`[D] 通道已打开!类型: ${type}`);
console.log('[D] Calling onChannelOpen callback with type:', type);
this.callbacks.onChannelOpen(type); this.callbacks.onChannelOpen(type);
useLocalStore.getState().setChannel(ch); useLocalStore.getState().setChannel(ch);
}; };
ch.onmessage = (e) => { ch.onmessage = (e) => {
// console.log('[D] Received message:', e.data); console.log('[D] Received message:', e.data);
try { try {
const data = JSON.parse(e.data); const data = JSON.parse(e.data);
if (data.type === 'message') { if (data.type === 'message') {
@ -118,9 +84,10 @@ export class RTCHandler {
timestamp: Date.now(), timestamp: Date.now(),
sender: data.sender || '未知用户' sender: data.sender || '未知用户'
}; };
console.log('[D] Calling onMessageReceived callback with message:', message);
this.callbacks.onMessageReceived(message); this.callbacks.onMessageReceived(message);
} else if (data.type === 'file' || data.type === 'file-chunk' || data.type === 'file-end') { } else if (data.type === 'file') {
// 处理文件相关消息 // 处理文件消息
handleFileChunk(data, this.callbacks.onMessageReceived); handleFileChunk(data, this.callbacks.onMessageReceived);
} }
} catch (error) { } catch (error) {
@ -130,23 +97,27 @@ export class RTCHandler {
timestamp: Date.now(), timestamp: Date.now(),
sender: '未知用户' sender: '未知用户'
}; };
console.log('[D] Calling onMessageReceived callback with plain text message:', message);
this.callbacks.onMessageReceived(message); this.callbacks.onMessageReceived(message);
} }
}; };
ch.onclose = () => { ch.onclose = () => {
console.log('[D] 通道关闭'); console.log('[D] 通道关闭');
console.log('[D] Calling onChannelClose callback');
this.callbacks.onChannelClose(); this.callbacks.onChannelClose();
useLocalStore.getState().setChannel(); useLocalStore.getState().setChannel();
}; };
}; };
handleBubbleClick = async (bubbleId: string, currentUserId: string) => { handleBubbleClick = async (bubbleId: string, currentUserId: string) => {
console.log(`[D] click id = ${bubbleId}`);
const current_rtc = this.rtcRef.current; const current_rtc = this.rtcRef.current;
if (!current_rtc) return; if (!current_rtc) return;
current_rtc.onnegotiationneeded = async () => { current_rtc.onnegotiationneeded = async () => {
const offer = await current_rtc.createOffer(); const offer = await current_rtc.createOffer();
console.log('[D] offer created', offer);
await current_rtc.setLocalDescription(offer); await current_rtc.setLocalDescription(offer);
const data = { const data = {
id: bubbleId, id: bubbleId,
@ -161,6 +132,7 @@ export class RTCHandler {
}; };
current_rtc.onicecandidate = async (e) => { current_rtc.onicecandidate = async (e) => {
console.log('[D] on candidate ', e);
await fetch('/api/ulocal/candidate', { await fetch('/api/ulocal/candidate', {
method: 'POST', method: 'POST',
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
@ -176,7 +148,7 @@ export class RTCHandler {
let current_id: string; let current_id: string;
let current_rtc: RTCPeerConnection | null; let current_rtc: RTCPeerConnection | null;
const msg = JSON.parse(e.data) as WSMessage; const msg = JSON.parse(e.data) as WSMessage;
// console.log('[D] ws event msg =', msg); console.log('[D] ws event msg =', msg);
switch (msg.type) { switch (msg.type) {
case "enter": case "enter":
@ -241,6 +213,7 @@ export class RTCHandler {
return; return;
} }
if (!candidate_data.candidate) { if (!candidate_data.candidate) {
console.log('[W] candidate data null');
return; return;
} }
await current_rtc.addIceCandidate(candidate_data.candidate); await current_rtc.addIceCandidate(candidate_data.candidate);
@ -250,8 +223,10 @@ export class RTCHandler {
sendMessage = (msg: string, files: File[], senderName: string) => { sendMessage = (msg: string, files: File[], senderName: string) => {
const ch = useLocalStore.getState().channel; const ch = useLocalStore.getState().channel;
console.log('[D] ready to send:', msg, files, ch);
const CHUNK_SIZE = 64 * 1024; // 64KB const CHUNK_SIZE = 64 * 1024; // 64KB
const BUFFERED_AMOUNT_THRESHOLD = 1 * 1024 * 1024; // 1MB const BUFFERED_AMOUNT_THRESHOLD = 1 * 1024 * 1024; // 1MB
if (ch && ch.readyState === 'open') { if (ch && ch.readyState === 'open') {
if (msg.trim()) { if (msg.trim()) {
// 发送文本消息 // 发送文本消息
@ -279,28 +254,23 @@ export class RTCHandler {
}; };
ch.send(JSON.stringify(fileData)); ch.send(JSON.stringify(fileData));
ch.bufferedAmountLowThreshold = BUFFERED_AMOUNT_THRESHOLD;
let offset = 0; let offset = 0;
let chunkIndex = 0; let chunkIndex = 0;
const reader = new FileReader(); const reader = new FileReader();
const sendNextChunk = () => { function sendNextChunk() {
if (offset >= file.size) { if (offset >= file.size) return;
// 分块全部发送完毕,发送 file-end if (ch && ch.bufferedAmount && ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) {
ch.send(JSON.stringify({ // 等待缓冲区变低
type: 'file-end',
name: file.name,
timestamp,
}));
return;
}
if (ch.bufferedAmount > BUFFERED_AMOUNT_THRESHOLD) {
ch.addEventListener('bufferedamountlow', sendNextChunk, { once: true }); ch.addEventListener('bufferedamountlow', sendNextChunk, { once: true });
return; return;
} }
const slice = file.slice(offset, offset + CHUNK_SIZE); const slice = file.slice(offset, offset + CHUNK_SIZE);
reader.onload = (e) => { reader.onload = (e) => {
if (e.target?.result) { if (e.target?.result) {
ch.send(JSON.stringify({ ch?.send(JSON.stringify({
type: 'file-chunk', type: 'file-chunk',
name: file.name, name: file.name,
timestamp, timestamp,
@ -314,7 +284,7 @@ export class RTCHandler {
} }
}; };
reader.readAsArrayBuffer(slice); reader.readAsArrayBuffer(slice);
}; }
sendNextChunk(); sendNextChunk();
}); });
} }

View File

@ -6,12 +6,11 @@ interface SendDialogProps {
open: boolean; open: boolean;
onSend: (msg: string, files: File[]) => void; onSend: (msg: string, files: File[]) => void;
onClose: () => void; onClose: () => void;
name: string;
} }
export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose, name}) => { export const SendDialog: React.FC<SendDialogProps> = ({open, onSend, onClose}) => {
return ( return (
<Dialog open={open} title={`发送消息给${name}`} onClose={onClose}> <Dialog open={open} title="发送消息" onClose={onClose}>
<Sender onSend={onSend} /> <Sender onSend={onSend} />
</Dialog> </Dialog>
); );

View File

@ -150,10 +150,17 @@ export const Sender: React.FC<SenderProps> = ({onSend}) => {
</div> </div>
<div className={classes.buttons}> <div className={classes.buttons}>
<input type="file" ref={fileInputRef} hidden onChange={handleFileSelect}/> <input type="file" ref={fileInputRef} hidden onChange={handleFileSelect}/>
<button className={classes.action_btn} onClick={() => { <button
className={classes.action_btn}
style={{ opacity: 0.5, cursor: 'not-allowed' }}
onClick={() => { message.warning('暂不支持文件发送') }}
>
📁
</button>
{/* <button className={classes.action_btn} onClick={() => {
fileInputRef.current && fileInputRef.current.click() fileInputRef.current && fileInputRef.current.click()
}}>📁 选择文件 }}>📁 选择文件
</button> </button> */}
<button className={classes.action_btn} onClick={handleSubmit}> </button> <button className={classes.action_btn} onClick={handleSubmit}> </button>
</div> </div>
</div> </div>

View File

@ -33,6 +33,4 @@ export interface ReceivedMessage {
fileSize?: number; fileSize?: number;
fileBlobUrl?: string; fileBlobUrl?: string;
isFile?: boolean; isFile?: boolean;
progress?: number; // 0-1
receiving?: boolean; // 是否正在接收
} }

View File

@ -55,15 +55,14 @@ const useClass = createUseStyles({
export const LocalSharing: React.FC = () => { export const LocalSharing: React.FC = () => {
const classes = useClass(); const classes = useClass();
const {id, name, set, } = useLocalStore(); const {id, name, set, setChannel} = useLocalStore();
const [_rtc, setRTC] = useState<RTCPeerConnection>(); const [rtc, setRTC] = useState<RTCPeerConnection>();
const rtcRef = useRef<RTCPeerConnection | null>(null); const rtcRef = useRef<RTCPeerConnection | null>(null);
const [clients, setClients] = useState<Client[]>([]); const [clients, setClients] = useState<Client[]>([]);
const {connect, close} = useWebsocket({}); const {connect, close} = useWebsocket({});
const [open, setOpen] = useState<{ send: boolean; receive: boolean }>({send: false, receive: false}); const [open, setOpen] = useState<{ send: boolean; receive: boolean }>({send: false, receive: false});
const [receivedMessage, setReceivedMessage] = useState<ReceivedMessage | null>(null); const [receivedMessage, setReceivedMessage] = useState<ReceivedMessage | null>(null);
const [showMessageDialog, setShowMessageDialog] = useState(false); const [showMessageDialog, setShowMessageDialog] = useState(false);
const [receivingFile, setReceivingFile] = useState<ReceivedMessage | null>(null);
// RTC处理器的回调函数 - 使用useCallback确保稳定性 // RTC处理器的回调函数 - 使用useCallback确保稳定性
const onChannelOpen = useCallback((type: 'sender' | 'receiver') => { const onChannelOpen = useCallback((type: 'sender' | 'receiver') => {
@ -72,16 +71,9 @@ export const LocalSharing: React.FC = () => {
}, []); }, []);
const onMessageReceived = useCallback((message: ReceivedMessage) => { const onMessageReceived = useCallback((message: ReceivedMessage) => {
if (message.isFile && message.receiving) { console.log('[D] Message received:', message);
setReceivingFile(message); setReceivedMessage(message);
} else if (message.isFile && !message.receiving) { setShowMessageDialog(true);
setReceivingFile(null);
setReceivedMessage(message);
setShowMessageDialog(true);
} else {
setReceivedMessage(message);
setShowMessageDialog(true);
}
}, []); }, []);
const onChannelClose = useCallback(() => { const onChannelClose = useCallback(() => {
@ -101,6 +93,7 @@ export const LocalSharing: React.FC = () => {
// 更新RTC处理器的回调函数 // 更新RTC处理器的回调函数
useEffect(() => { useEffect(() => {
if (rtcHandlerRef.current) { if (rtcHandlerRef.current) {
console.log('[D] Updating RTC handler callbacks');
rtcHandlerRef.current.updateCallbacks(rtcCallbacks); rtcHandlerRef.current.updateCallbacks(rtcCallbacks);
} }
}, [rtcCallbacks]); }, [rtcCallbacks]);
@ -115,6 +108,7 @@ export const LocalSharing: React.FC = () => {
const handleWSEvent = async (e: MessageEvent) => { const handleWSEvent = async (e: MessageEvent) => {
const msgData = JSON.parse(e.data); const msgData = JSON.parse(e.data);
console.log('[D] ws event msg =', msgData);
if (msgData.type === "enter" || msgData.type === "leave") { if (msgData.type === "enter" || msgData.type === "leave") {
await updateClients(); await updateClients();
@ -135,6 +129,7 @@ export const LocalSharing: React.FC = () => {
const handleBubbleClick = async (bubble: any) => { const handleBubbleClick = async (bubble: any) => {
setOpen({send: true, receive: false}); setOpen({send: true, receive: false});
console.log('[D] Bubble clicked:', bubble.id, 'Current RTC handler:', rtcHandlerRef.current);
if (rtcHandlerRef.current) { if (rtcHandlerRef.current) {
try { try {
await rtcHandlerRef.current.handleBubbleClick(bubble.id, id); await rtcHandlerRef.current.handleBubbleClick(bubble.id, id);
@ -170,6 +165,7 @@ export const LocalSharing: React.FC = () => {
const response = await fetch('/api/ulocal/register', {method: 'POST'}); const response = await fetch('/api/ulocal/register', {method: 'POST'});
const data = ((await response.json()) as Resp<{ id: string; name: string }>).data; const data = ((await response.json()) as Resp<{ id: string; name: string }>).data;
set(data.id, data.name); set(data.id, data.name);
console.log(`[D] register id = ${data.id}`);
connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent}); connect(`/api/ulocal/ws?id=${data.id}`, {fn: handleWSEvent});
await updateClients(); await updateClients();
@ -178,6 +174,7 @@ export const LocalSharing: React.FC = () => {
setRTC(_rtc); setRTC(_rtc);
// 在RTC连接创建后立即创建处理器实例 // 在RTC连接创建后立即创建处理器实例
console.log('[D] Creating RTC handler after connection setup');
rtcHandlerRef.current = new RTCHandler(rtcRef, rtcCallbacks); rtcHandlerRef.current = new RTCHandler(rtcRef, rtcCallbacks);
return () => { return () => {
@ -192,14 +189,6 @@ export const LocalSharing: React.FC = () => {
const bubbles = generateBubbles(clients, id); const bubbles = generateBubbles(clients, id);
useEffect(() => {
if (receivingFile && receivingFile.isFile && !receivingFile.receiving) {
setReceivingFile(null);
setReceivedMessage(receivingFile);
setShowMessageDialog(true);
}
}, [receivingFile]);
return ( return (
<div className={classes.container}> <div className={classes.container}>
<CloudBackground/> <CloudBackground/>
@ -220,17 +209,8 @@ export const LocalSharing: React.FC = () => {
open={open.send} open={open.send}
onSend={handleSend} onSend={handleSend}
onClose={() => setOpen({send: false, receive: false})} onClose={() => setOpen({send: false, receive: false})}
name={name}
/> />
{/* 文件接收进度弹窗 */}
{receivingFile && (
<MessageDialog
open={true}
message={receivingFile}
onClose={() => setReceivingFile(null)}
/>
)}
<MessageDialog <MessageDialog
open={showMessageDialog} open={showMessageDialog}

View File

@ -136,6 +136,7 @@ const PanelLeftUpload: React.FC<{ set_code: (code:string) => void }> = ({set_cod
} }
function onFileChange(e: React.ChangeEvent<HTMLInputElement>) { function onFileChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log('[D] onFileChange: e =', e)
setFile(e.currentTarget.files ? e.currentTarget.files[0] : null) setFile(e.currentTarget.files ? e.currentTarget.files[0] : null)
} }

View File

@ -42,6 +42,7 @@ export const PanelRight = () => {
async function onFetchFile() { async function onFetchFile() {
const url = `/ushare/${code}` const url = `/ushare/${code}`
console.log('[D] onFetchFile: url =', url)
const link = document.createElement('a'); const link = document.createElement('a');
link.href = url; link.href = url;
document.body.appendChild(link); document.body.appendChild(link);

View File

@ -1,12 +1,13 @@
import {useState} from "react"; import {useState} from "react";
import {createUseStyles} from "react-jss"; import {createUseStyles} from "react-jss";
import {Dialog} from "../../component/dialog/dialog.tsx";
const useClass = createUseStyles({ const useClass = createUseStyles({
container: {} container: {}
}) })
export const TestPage = () => { export const TestPage = () => {
const classes = useClass() const classes = useClass()
const [_open, setOpen] = useState<boolean>(false) const [open, setOpen] = useState<boolean>(false)
const handleOpen = () => { const handleOpen = () => {
setOpen(true) setOpen(true)
@ -14,5 +15,8 @@ export const TestPage = () => {
return <div className={classes.container}> return <div className={classes.container}>
<button onClick={handleOpen}>open</button> <button onClick={handleOpen}>open</button>
<Dialog open={open} title="hello world" >
<input />
</Dialog>
</div> </div>
} }

View File

@ -8,7 +8,7 @@ export interface LocalStore {
setChannel: (chan?: RTCDataChannel) => void; setChannel: (chan?: RTCDataChannel) => void;
} }
export const useLocalStore = create<LocalStore>()((_set, _get) => ({ export const useLocalStore = create<LocalStore>()((_set, get) => ({
id: '', id: '',
name: '', name: '',
set: (id: string, name: string) => { set: (id: string, name: string) => {

View File

@ -167,6 +167,8 @@
const text = inputBox.value; const text = inputBox.value;
const files = fileInput.files; const files = fileInput.files;
console.log('文本内容:', text);
console.log('发送文件:', Array.from(files));
// 清空内容 // 清空内容
inputBox.value = ''; inputBox.value = '';