351 lines
9.3 KiB
JavaScript
351 lines
9.3 KiB
JavaScript
// Import necessary modules
|
|
// 导入必要的模块
|
|
import {
|
|
$,
|
|
$id,
|
|
createElement,
|
|
on,
|
|
addClass,
|
|
removeClass
|
|
} from './util.dom.js';
|
|
import { deflate } from 'fflate';
|
|
|
|
// File transfer state management
|
|
// 文件传输状态管理
|
|
window.fileTransfers = new Map();
|
|
|
|
// Generate unique file ID
|
|
// 生成唯一文件ID
|
|
function generateFileId() {
|
|
return 'file_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
|
}
|
|
|
|
// Compress file into volumes
|
|
// 将文件压缩为分卷
|
|
async function compressFileToVolumes(file, volumeSize = 128 * 1024) {
|
|
return new Promise((resolve, reject) => {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
const arrayBuffer = new Uint8Array(e.target.result);
|
|
|
|
// Compress the file data
|
|
deflate(arrayBuffer, { level: 6 }, (err, compressed) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
// Split into volumes
|
|
const volumes = [];
|
|
for (let i = 0; i < compressed.length; i += volumeSize) {
|
|
const volume = compressed.slice(i, i + volumeSize);
|
|
// Convert to base64 without using spread operator to avoid stack overflow
|
|
let binaryString = '';
|
|
for (let j = 0; j < volume.length; j++) {
|
|
binaryString += String.fromCharCode(volume[j]);
|
|
}
|
|
volumes.push(btoa(binaryString));
|
|
}
|
|
|
|
resolve({
|
|
volumes,
|
|
originalSize: file.size,
|
|
compressedSize: compressed.length
|
|
});
|
|
});
|
|
};
|
|
reader.onerror = () => reject(reader.error);
|
|
reader.readAsArrayBuffer(file);
|
|
});
|
|
}
|
|
|
|
// Decompress volumes back to file
|
|
// 将分卷解压回文件
|
|
async function decompressVolumesToFile(volumes, fileName) { try {
|
|
// Combine all volumes
|
|
const combinedData = volumes.map(volume => {
|
|
const binaryString = atob(volume);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
return bytes;
|
|
});
|
|
|
|
const totalLength = combinedData.reduce((sum, arr) => sum + arr.length, 0);
|
|
const compressed = new Uint8Array(totalLength);
|
|
let offset = 0;
|
|
|
|
for (const data of combinedData) {
|
|
compressed.set(data, offset);
|
|
offset += data.length;
|
|
}
|
|
|
|
// Decompress
|
|
return new Promise((resolve, reject) => {
|
|
import('fflate').then(({ inflate }) => {
|
|
inflate(compressed, (err, decompressed) => {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
// Create blob and download
|
|
const blob = new Blob([decompressed]);
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = fileName;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
} catch (error) {
|
|
console.error('Decompression error:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
// Setup file sending functionality
|
|
// 设置文件发送功能
|
|
export function setupFileSend({
|
|
inputSelector,
|
|
attachBtnSelector,
|
|
fileInputSelector,
|
|
onSend
|
|
}) {
|
|
const attachBtn = document.querySelector(attachBtnSelector);
|
|
const fileInput = document.querySelector(fileInputSelector);
|
|
|
|
if (attachBtn && fileInput) {
|
|
// 移除文件类型限制,接受所有文件
|
|
// Remove file type restriction, accept all files
|
|
fileInput.accept = '*/*';
|
|
|
|
// 点击附件按钮触发文件选择
|
|
// Click attach button to trigger file selection
|
|
attachBtn.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
fileInput.click();
|
|
});
|
|
|
|
// 处理文件选择
|
|
// Handle file selection
|
|
fileInput.addEventListener('change', async (e) => {
|
|
const files = e.target.files;
|
|
if (files && files.length > 0) {
|
|
const file = files[0];
|
|
const fileId = generateFileId();
|
|
|
|
try {
|
|
// 显示压缩进度
|
|
window.addSystemMsg(`Compressing ${file.name}...`);
|
|
|
|
// 压缩文件为分卷
|
|
const { volumes, originalSize, compressedSize } = await compressFileToVolumes(file);
|
|
|
|
// 创建文件传输状态
|
|
const fileTransfer = {
|
|
fileId,
|
|
fileName: file.name,
|
|
originalSize,
|
|
compressedSize,
|
|
totalVolumes: volumes.length,
|
|
sentVolumes: 0,
|
|
status: 'sending'
|
|
};
|
|
|
|
window.fileTransfers.set(fileId, fileTransfer);
|
|
|
|
// 发送文件开始消息
|
|
onSend({
|
|
type: 'file_start',
|
|
fileId,
|
|
fileName: file.name,
|
|
originalSize,
|
|
compressedSize,
|
|
totalVolumes: volumes.length
|
|
});
|
|
|
|
// 逐个发送分卷
|
|
for (let i = 0; i < volumes.length; i++) {
|
|
onSend({
|
|
type: 'file_volume',
|
|
fileId,
|
|
volumeIndex: i,
|
|
volumeData: volumes[i],
|
|
isLast: i === volumes.length - 1
|
|
});
|
|
|
|
// 更新发送进度
|
|
fileTransfer.sentVolumes = i + 1;
|
|
updateFileProgress(fileId);
|
|
|
|
// 添加小延迟避免消息过快
|
|
await new Promise(resolve => setTimeout(resolve, 50));
|
|
}
|
|
|
|
// 发送完成消息
|
|
onSend({
|
|
type: 'file_complete',
|
|
fileId
|
|
});
|
|
|
|
fileTransfer.status = 'completed';
|
|
updateFileProgress(fileId);
|
|
|
|
} catch (error) {
|
|
console.error('File compression error:', error);
|
|
window.addSystemMsg(`Failed to compress ${file.name}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// 清空文件输入
|
|
e.target.value = '';
|
|
});
|
|
}
|
|
}
|
|
|
|
// Update file progress in chat
|
|
// 更新聊天中的文件进度
|
|
function updateFileProgress(fileId) {
|
|
const transfer = window.fileTransfers.get(fileId);
|
|
if (!transfer) return;
|
|
|
|
const elements = document.querySelectorAll(`[data-file-id="${fileId}"]`);
|
|
elements.forEach(element => {
|
|
const progressBar = element.querySelector('.file-progress');
|
|
const statusText = element.querySelector('.file-status');
|
|
const downloadBtn = element.querySelector('.file-download-btn');
|
|
|
|
if (transfer.status === 'sending') {
|
|
const progress = (transfer.sentVolumes / transfer.totalVolumes) * 100;
|
|
if (progressBar) progressBar.style.width = `${progress}%`;
|
|
if (statusText) statusText.textContent = `Sending ${transfer.sentVolumes}/${transfer.totalVolumes}`;
|
|
} else if (transfer.status === 'receiving') {
|
|
const progress = (transfer.receivedVolumes.size / transfer.totalVolumes) * 100;
|
|
if (progressBar) progressBar.style.width = `${progress}%`;
|
|
if (statusText) statusText.textContent = `Receiving ${transfer.receivedVolumes.size}/${transfer.totalVolumes}`;
|
|
} else if (transfer.status === 'completed') {
|
|
if (progressBar) progressBar.style.width = '100%';
|
|
if (statusText) statusText.textContent = '✓ Completed';
|
|
if (downloadBtn) {
|
|
downloadBtn.style.display = 'inline-block';
|
|
downloadBtn.disabled = false;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handle incoming file messages
|
|
// 处理接收到的文件消息
|
|
export function handleFileMessage(message, isPrivate = false) {
|
|
const { type, fileId } = message;
|
|
|
|
switch (type) {
|
|
case 'file_start':
|
|
handleFileStart(message, isPrivate);
|
|
break;
|
|
case 'file_volume':
|
|
handleFileVolume(message);
|
|
break;
|
|
case 'file_complete':
|
|
handleFileComplete(message);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Handle file start message
|
|
// 处理文件开始消息
|
|
function handleFileStart(message, isPrivate) {
|
|
const { fileId, fileName, originalSize, compressedSize, totalVolumes } = message;
|
|
|
|
const fileTransfer = {
|
|
fileId,
|
|
fileName,
|
|
originalSize,
|
|
compressedSize,
|
|
totalVolumes,
|
|
receivedVolumes: new Set(),
|
|
volumeData: new Array(totalVolumes),
|
|
status: 'receiving'
|
|
};
|
|
|
|
window.fileTransfers.set(fileId, fileTransfer);
|
|
|
|
// 添加文件消息到聊天
|
|
if (window.addOtherMsg) {
|
|
window.addOtherMsg({
|
|
type: 'file',
|
|
fileId,
|
|
fileName,
|
|
originalSize,
|
|
totalVolumes
|
|
}, '', '', false, isPrivate ? 'file_private' : 'file');
|
|
}
|
|
}
|
|
|
|
// Handle file volume message
|
|
// 处理文件分卷消息
|
|
function handleFileVolume(message) {
|
|
const { fileId, volumeIndex, volumeData } = message;
|
|
const transfer = window.fileTransfers.get(fileId);
|
|
|
|
if (!transfer) return;
|
|
|
|
transfer.receivedVolumes.add(volumeIndex);
|
|
transfer.volumeData[volumeIndex] = volumeData;
|
|
|
|
updateFileProgress(fileId);
|
|
}
|
|
|
|
// Handle file complete message
|
|
// 处理文件完成消息
|
|
function handleFileComplete(message) {
|
|
const { fileId } = message;
|
|
const transfer = window.fileTransfers.get(fileId);
|
|
|
|
if (!transfer) return;
|
|
|
|
// 检查是否所有分卷都已接收
|
|
if (transfer.receivedVolumes.size === transfer.totalVolumes) {
|
|
transfer.status = 'completed';
|
|
updateFileProgress(fileId);
|
|
}
|
|
}
|
|
|
|
// Download file from volumes
|
|
// 从分卷下载文件
|
|
export async function downloadFile(fileId) {
|
|
const transfer = window.fileTransfers.get(fileId);
|
|
if (!transfer || transfer.status !== 'completed') return;
|
|
|
|
try {
|
|
await decompressVolumesToFile(transfer.volumeData, transfer.fileName);
|
|
window.addSystemMsg(`Downloaded ${transfer.fileName}`);
|
|
} catch (error) {
|
|
console.error('Download error:', error);
|
|
window.addSystemMsg(`Failed to download ${transfer.fileName}: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
// Format file size
|
|
// 格式化文件大小
|
|
export function formatFileSize(bytes) {
|
|
if (bytes === 0) return '0 Bytes';
|
|
const k = 1024;
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
}
|
|
|
|
// Legacy image send function for backward compatibility
|
|
// 向后兼容的图片发送函数
|
|
export function setupImageSend(config) {
|
|
setupFileSend(config);
|
|
} |