diff --git a/client/css/style.css b/client/css/style.css index 1e7b8f5..358678b 100644 --- a/client/css/style.css +++ b/client/css/style.css @@ -897,3 +897,132 @@ body { border-radius: 4px; } +/* File message styles */ +/* 文件消息样式 */ +.file-message { + max-width: 300px; + background: #f8f9fa; + border: 1px solid #e0e0e0; + border-radius: 12px; + padding: 12px; + margin: 4px 0; + font-family: inherit; +} + +.file-info { + display: flex; + align-items: center; + margin-bottom: 8px; +} + +.file-icon { + font-size: 24px; + margin-right: 10px; + flex-shrink: 0; +} + +.file-details { + flex: 1; + min-width: 0; +} + +.file-name { + font-weight: 500; + font-size: 14px; + color: #333; + margin-bottom: 2px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.file-meta { + font-size: 12px; + color: #666; + display: flex; + gap: 8px; +} + +.file-size { + font-weight: 500; +} + +.file-volumes { + color: #888; +} + +.file-progress-container { + margin-bottom: 8px; +} + +.file-progress-bar { + width: 100%; + height: 6px; + background: #e0e0e0; + border-radius: 3px; + overflow: hidden; + margin-bottom: 4px; +} + +.file-progress { + height: 100%; + background: linear-gradient(90deg, #30a8f7, #4fc3f7); + border-radius: 3px; + transition: width 0.3s ease; + width: 0%; +} + +.file-status { + font-size: 11px; + color: #666; + text-align: center; +} + +.file-download-btn { + width: 100%; + padding: 8px 12px; + background: #30a8f7; + color: white; + border: none; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background-color 0.2s ease, transform 0.1s ease; + display: none; +} + +.file-download-btn:hover { + background: #2196f3; + transform: translateY(-1px); +} + +.file-download-btn:active { + transform: translateY(0); +} + +.file-download-btn:disabled { + background: #ccc; + cursor: not-allowed; + transform: none; +} + +/* File message in bubbles */ +.bubble .file-message { + background: rgba(255, 255, 255, 0.9); + border: 1px solid rgba(255, 255, 255, 0.3); +} + +.bubble.me .file-message { + background: rgba(255, 255, 255, 0.95); +} + +.bubble.other .file-message { + background: #fff; +} + +.bubble.private-message .file-message { + background: rgba(255, 248, 220, 0.9); + border-color: rgba(255, 193, 7, 0.3); +} + diff --git a/client/js/NodeCrypt.js b/client/js/NodeCrypt.js index 900e394..486d556 100644 --- a/client/js/NodeCrypt.js +++ b/client/js/NodeCrypt.js @@ -332,11 +332,10 @@ class NodeCrypt { } } return - } - if (!this.channel[serverDecrypted.c].username) { + } if (!this.channel[serverDecrypted.c].username) { return } - if (clientDecrypted.a === 'm' && this.isString(clientDecrypted.t) && this.isString(clientDecrypted.d)) { + if (clientDecrypted.a === 'm' && this.isString(clientDecrypted.t) && (this.isString(clientDecrypted.d) || this.isObject(clientDecrypted.d))) { if (this.callbacks.onClientMessage) { try { this.callbacks.onClientMessage({ diff --git a/client/js/chat.js b/client/js/chat.js index 632b1d8..bb1a2ea 100644 --- a/client/js/chat.js +++ b/client/js/chat.js @@ -19,6 +19,9 @@ import { addClass, removeClass } from './util.dom.js'; +import { + formatFileSize +} from './util.file.js'; // Render the chat area // 渲染聊天区域 @@ -54,11 +57,13 @@ export function addMsg(text, isHistory = false, msgType = 'text', timestamp = nu if (!chatArea) return; const className = 'bubble me' + (msgType.includes('_private') ? ' private-message' : ''); const date = new Date(ts); - const time = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0'); - let contentHtml = ''; + const time = date.getHours().toString().padStart(2, '0') + ':' + date.getMinutes().toString().padStart(2, '0'); let contentHtml = ''; if (msgType === 'image' || msgType === 'image_private') { const safeImgSrc = escapeHTML(text).replace(/javascript:/gi, ''); contentHtml = `image` + } else if (msgType === 'file' || msgType === 'file_private') { + // Handle file messages + contentHtml = renderFileMessage(text, true); } else { contentHtml = textToHTML(text) } @@ -95,11 +100,13 @@ export function addOtherMsg(msg, userName = '', avatar = '', isHistory = false, if (!chatArea) return; const bubbleWrap = createElement('div', { class: 'bubble-other-wrap' - }); - let contentHtml = ''; + }); let contentHtml = ''; if (msgType === 'image' || msgType === 'image_private') { const safeImgSrc = escapeHTML(msg).replace(/javascript:/gi, ''); contentHtml = `image` + } else if (msgType === 'file' || msgType === 'file_private') { + // Handle file messages + contentHtml = renderFileMessage(msg, false); } else { contentHtml = textToHTML(msg) } @@ -254,6 +261,133 @@ export function showImageModal(src) { updateTransform() } +// Render file message content +// 渲染文件消息内容 +function renderFileMessage(fileData, isSender) { + const { + fileId, + fileName, + originalSize, + totalVolumes + } = fileData; + const safeFileName = escapeHTML(fileName); + const formattedSize = formatFileSize(originalSize); + + // Determine file icon based on extension + // 根据扩展名确定文件图标 + const fileExtension = fileName.split('.').pop().toLowerCase(); + let fileIcon = '📄'; // Default document icon 默认文档图标 + + const iconMap = { + // Images + 'jpg': '🖼️', + 'jpeg': '🖼️', + 'png': '🖼️', + 'gif': '🖼️', + 'bmp': '🖼️', + 'svg': '🖼️', + 'webp': '🖼️', + // Videos + 'mp4': '🎬', + 'avi': '🎬', + 'mov': '🎬', + 'mkv': '🎬', + 'wmv': '🎬', + 'flv': '🎬', + 'webm': '🎬', + // Audio + 'mp3': '🎵', + 'wav': '🎵', + 'flac': '🎵', + 'aac': '🎵', + 'ogg': '🎵', + 'm4a': '🎵', + // Documents + 'pdf': '📕', + 'doc': '📄', + 'docx': '📄', + 'txt': '📄', + 'rtf': '📄', + 'xls': '📊', + 'xlsx': '📊', + 'csv': '📊', + 'ppt': '📈', + 'pptx': '📈', + // Archives + 'zip': '🗜️', + 'rar': '🗜️', + '7z': '🗜️', + 'tar': '🗜️', + 'gz': '🗜️', + // Code + 'js': '💻', + 'html': '💻', + 'css': '💻', + 'py': '💻', + 'java': '💻', + 'cpp': '💻', + 'c': '💻', + 'php': '💻', + 'rb': '💻', + 'go': '💻', + 'ts': '💻', + 'json': '💻', + 'xml': '💻' + }; + if (iconMap[fileExtension]) { + fileIcon = iconMap[fileExtension]; + } + + // Check actual file transfer status + const transfer = window.fileTransfers ? window.fileTransfers.get(fileId) : null; + let statusText = 'Waiting...'; + let progressWidth = '0%'; + let downloadBtnStyle = 'display: none;'; + + if (transfer) { + if (transfer.status === 'sending') { + const progress = (transfer.sentVolumes / transfer.totalVolumes) * 100; + progressWidth = `${progress}%`; + statusText = `Sending ${transfer.sentVolumes}/${transfer.totalVolumes}`; + } else if (transfer.status === 'receiving') { + const progress = (transfer.receivedVolumes.size / transfer.totalVolumes) * 100; + progressWidth = `${progress}%`; + statusText = `Receiving ${transfer.receivedVolumes.size}/${transfer.totalVolumes}`; + } else if (transfer.status === 'completed') { + progressWidth = '100%'; + statusText = '✓ Completed'; + downloadBtnStyle = isSender ? 'display: none;' : 'display: inline-block;'; + } + } else if (isSender) { + statusText = 'Sent'; + progressWidth = '100%'; + } + + return ` +
+
+
${fileIcon}
+
+
${safeFileName}
+
+ ${formattedSize} + ${totalVolumes} volumes +
+
+
+
+
+
+
+
${statusText}
+
+ +
+ `; +} + // Automatically adjust the height of the input area // 自动调整输入区域的高度 export function autoGrowInput() { diff --git a/client/js/main.js b/client/js/main.js index 1d5c0a2..ca7e412 100644 --- a/client/js/main.js +++ b/client/js/main.js @@ -2,11 +2,13 @@ // Import the NodeCrypt module (used for encryption) import './NodeCrypt.js'; -// 从 util.image.js 中导入设置图片发送的函数 -// Import setupImageSend function from util.image.js +// 从 util.file.js 中导入设置文件发送的函数 +// Import setupFileSend function from util.file.js import { - setupImageSend -} from './util.image.js'; + setupFileSend, + handleFileMessage, + downloadFile +} from './util.file.js'; // 从 util.emoji.js 中导入设置表情选择器的函数 // Import setupEmojiPicker function from util.emoji.js @@ -75,6 +77,8 @@ window.addSystemMsg = addSystemMsg; window.joinRoom = joinRoom; window.notifyMessage = notifyMessage; window.setupEmojiPicker = setupEmojiPicker; +window.handleFileMessage = handleFileMessage; +window.downloadFile = downloadFile; // 当 DOM 内容加载完成后执行初始化逻辑 // Run initialization logic when the DOM content is fully loaded @@ -176,25 +180,24 @@ window.addEventListener('DOMContentLoaded', () => { } }); } - - // 设置发送图片功能 - // Setup image sending functionality - setupImageSend({ + // 设置发送文件功能 + // Setup file sending functionality + setupFileSend({ inputSelector: '.input-message-input', // 消息输入框选择器 / Message input selector attachBtnSelector: '.chat-attach-btn', // 附件按钮选择器 / Attach button selector fileInputSelector: '.new-message-wrapper input[type="file"]', // 文件输入框选择器 / File input selector - onSend: (dataUrl) => { + onSend: (message) => { const rd = roomsData[activeRoomIndex]; if (rd && rd.chat) { if (rd.privateChatTargetId) { - // 私聊图片加密并发送 - // Encrypt and send private image + // 私聊文件加密并发送 + // Encrypt and send private file message const targetClient = rd.chat.channel[rd.privateChatTargetId]; if (targetClient && targetClient.shared) { const clientMessagePayload = { a: 'm', - t: 'image_private', - d: dataUrl + t: message.type + '_private', + d: message }; const encryptedClientMessage = rd.chat.encryptClientMessage(clientMessagePayload, targetClient.shared); const serverRelayPayload = { @@ -204,15 +207,23 @@ window.addEventListener('DOMContentLoaded', () => { }; const encryptedMessageForServer = rd.chat.encryptServerMessage(serverRelayPayload, rd.chat.serverShared); rd.chat.sendMessage(encryptedMessageForServer); - addMsg(dataUrl, false, 'image_private'); + + // 添加到自己的聊天记录 + if (message.type === 'file_start') { + addMsg(message, false, 'file_private'); + } } else { - addSystemMsg(`Cannot send private image to ${rd.privateChatTargetName}.User might not be fully connected.`) + addSystemMsg(`Cannot send private file to ${rd.privateChatTargetName}. User might not be fully connected.`) } } else { - // 公共频道发送图片 - // Send image to public channel - rd.chat.sendChannelMessage('image', dataUrl); - addMsg(dataUrl, false, 'image'); + // 公共频道文件发送 + // Send file to public channel + rd.chat.sendChannelMessage(message.type, message); + + // 添加到自己的聊天记录 + if (message.type === 'file_start') { + addMsg(message, false, 'file'); + } } } } diff --git a/client/js/room.js b/client/js/room.js index ac7ecd2..3926ae5 100644 --- a/client/js/room.js +++ b/client/js/room.js @@ -242,10 +242,52 @@ export function handleClientMessage(idx, msg) { return } } + let msgType = msg.type || 'text'; + + // Handle file messages + if (msgType.startsWith('file_')) { + // Import handleFileMessage function if needed + if (window.handleFileMessage) { + window.handleFileMessage(msg.data, msgType.includes('_private')); + } + + // Only add file_start messages to chat history + if (msgType === 'file_start' || msgType === 'file_start_private') { + let realUserName = msg.userName; + if (!realUserName && msg.clientId && newRd.userMap[msg.clientId]) { + realUserName = newRd.userMap[msg.clientId].userName || newRd.userMap[msg.clientId].username || newRd.userMap[msg.clientId].name + } + + roomsData[idx].messages.push({ + type: 'other', + text: msg.data, + userName: realUserName, + avatar: realUserName, + msgType: msgType.includes('_private') ? 'file_private' : 'file', + timestamp: Date.now() + }); + + const notificationMsgType = msgType.includes('_private') ? 'private file' : 'file'; + if (window.notifyMessage) { + window.notifyMessage(newRd.roomName, notificationMsgType, `${msg.data.fileName}`, realUserName) + } + + if (activeRoomIndex !== idx) { + roomsData[idx].unreadCount = (roomsData[idx].unreadCount || 0) + 1; + renderRooms(activeRoomIndex) + } else { + renderChatArea() + } + } + return; + } + + // Handle legacy image detection if (!msgType.includes('_private') && msg.data && msg.data.startsWith('data:image/')) { msgType = 'image' } + let realUserName = msg.userName; if (!realUserName && msg.clientId && newRd.userMap[msg.clientId]) { realUserName = newRd.userMap[msg.clientId].userName || newRd.userMap[msg.clientId].username || newRd.userMap[msg.clientId].name diff --git a/client/js/util.file.js b/client/js/util.file.js new file mode 100644 index 0000000..eaabc63 --- /dev/null +++ b/client/js/util.file.js @@ -0,0 +1,351 @@ +// 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); +} \ No newline at end of file diff --git a/client/js/util.image.js b/client/js/util.image.js deleted file mode 100644 index 5091ec6..0000000 --- a/client/js/util.image.js +++ /dev/null @@ -1,117 +0,0 @@ -// Import DOM helpers -// 导入 DOM 辅助函数 -import { - $, - on, - createElement -} from './util.dom.js'; -// Process and compress image file -// 处理并压缩图片文件 -export async function processImage(file, callback) { - // Create image element for loading file - // 创建图片元素用于加载文件 - const img = new Image(); - img.onload = function() { - const maxW = 1280, // Max width - maxH = 1280; // 最大宽度/高度 - let w = img.naturalWidth, - h = img.naturalHeight; - // Resize if too large - // 如果图片过大则缩放 - if (w > maxW || h > maxH) { - const scale = Math.min(maxW / w, maxH / h); - w = Math.round(w * scale); - h = Math.round(h * scale) - } - // Create canvas for drawing image - // 创建画布用于绘制图片 - const canvas = createElement('canvas'); - canvas.width = w; - canvas.height = h; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, w, h); - let dataUrl; - // Export as webp - // 导出为 webp - dataUrl = canvas.toDataURL('image/webp', 0.80); - callback(dataUrl) - }; - // Read file as data URL - // 以 dataURL 方式读取文件 - const reader = new FileReader(); - reader.onload = function(e) { - img.src = e.target.result - }; - reader.readAsDataURL(file) -} -// Translate message key -// 翻译消息 key -function t(key) { - const messages = { - tooLarge: 'Image is too large (over 5MB)', // 图片过大(超过5MB) - }; - return messages[key] || key -} -// Setup image send logic for UI -// 设置图片发送相关 UI 逻辑 -export function setupImageSend({ - inputSelector, - attachBtnSelector, - fileInputSelector, - onSend -}) { - const input = $(inputSelector); - const attachBtn = $(attachBtnSelector); - const fileInput = $(fileInputSelector); - if (fileInput) fileInput.setAttribute('accept', 'image/*'); - if (attachBtn && fileInput) { - // Click attach triggers file input - // 点击附件按钮触发文件选择 - on(attachBtn, 'click', () => fileInput.click()); - // Handle file input change - // 处理文件选择变化 - on(fileInput, 'change', async function() { - if (!fileInput.files || !fileInput.files.length) return; - const file = fileInput.files[0]; - // Only allow image files - // 只允许图片文件 - if (!file.type.startsWith('image/')) return; - // Check file size - // 检查文件大小 - if (file.size > 5 * 1024 * 1024) { - alert(t('tooLarge')); - return - } - processImage(file, (dataUrl) => { - if (typeof onSend === 'function') onSend(dataUrl) - }); - fileInput.value = '' - }) - } - if (input) { - // Paste image from clipboard - // 从剪贴板粘贴图片 - on(input, 'paste', function(e) { - if (!e.clipboardData) return; - for (const item of e.clipboardData.items) { - // Only handle image type - // 只处理图片类型 - if (item.type.startsWith('image/')) { - const file = item.getAsFile(); - if (!file) continue; - // Check file size - // 检查文件大小 - if (file.size > 5 * 1024 * 1024) { - alert(t('tooLarge')); - continue - } - processImage(file, (dataUrl) => { - if (typeof onSend === 'function') onSend(dataUrl) - }); - e.preventDefault(); - break - } - } - }) - } -} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 52e6a6d..f4f8334 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "dompurify": "^3.2.6", "elliptic": "^6.6.1", "emoji-picker-element": "^1.26.3", + "fflate": "^0.8.2", "js-chacha20": "^1.1.0", "js-sha256": "^0.11.0" }, @@ -2083,6 +2084,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmmirror.com/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "license": "MIT" + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-4.3.7.tgz", diff --git a/package.json b/package.json index e1709d2..5c557d3 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "dompurify": "^3.2.6", "elliptic": "^6.6.1", "emoji-picker-element": "^1.26.3", + "fflate": "^0.8.2", "js-chacha20": "^1.1.0", "js-sha256": "^0.11.0" }