From 6ef5db9f661556c10ba8820a1bf45108d9083d6b Mon Sep 17 00:00:00 2001 From: Miroslav Pejic Date: Thu, 30 Apr 2026 17:02:30 +0200 Subject: [PATCH] [mirotalk] - refine file share upload modal UI and interactions --- .env.template | 2 +- app/src/config.template.js | 2 +- app/src/server.js | 2 +- package-lock.json | 4 +- package.json | 2 +- public/css/client.css | 163 ++++++++++++++++++ public/js/brand.js | 2 +- public/js/client.js | 333 +++++++++++++++++++++++-------------- 8 files changed, 382 insertions(+), 128 deletions(-) diff --git a/.env.template b/.env.template index 3f79e6fc..f67af008 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,5 @@ # ==================================================== -# MiroTalk P2P v.1.8.26 - Environment Configuration +# MiroTalk P2P v.1.8.27 - Environment Configuration # ==================================================== # App environment diff --git a/app/src/config.template.js b/app/src/config.template.js index 3aa4d8aa..6a6c1237 100644 --- a/app/src/config.template.js +++ b/app/src/config.template.js @@ -2,7 +2,7 @@ /** * ============================================== - * MiroTalk P2P v.1.8.26 - Configuration File + * MiroTalk P2P v.1.8.27 - Configuration File * ============================================== * * This file is the central configuration source. diff --git a/app/src/server.js b/app/src/server.js index 893fed68..beaf4133 100755 --- a/app/src/server.js +++ b/app/src/server.js @@ -45,7 +45,7 @@ dependencies: { * @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon * @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661 * @author Miroslav Pejic - miroslav.pejic.85@gmail.com - * @version 1.8.26 + * @version 1.8.27 * */ diff --git a/package-lock.json b/package-lock.json index 503c4d1f..98f927e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "mirotalk", - "version": "1.8.26", + "version": "1.8.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mirotalk", - "version": "1.8.26", + "version": "1.8.27", "license": "AGPL-3.0", "dependencies": { "@mattermost/client": "11.6.0", diff --git a/package.json b/package.json index 03675f5a..ccd21fd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "mirotalk", - "version": "1.8.26", + "version": "1.8.27", "description": "A free WebRTC browser-based video call", "main": "server.js", "scripts": { diff --git a/public/css/client.css b/public/css/client.css index e76caeab..f42c2d50 100755 --- a/public/css/client.css +++ b/public/css/client.css @@ -3829,11 +3829,174 @@ input:checked + .slider:before { border-radius: 12px !important; } +.mirotalk-hidden-file-input { + display: none !important; +} + .swal2-html-container { color: rgb(195, 195, 195) !important; background-color: transparent !important; } +.mirotalk-file-picker-html { + margin-top: 1rem !important; +} + +.mirotalk-file-picker { + display: flex; + flex-direction: column; + gap: 14px; +} + +.mirotalk-file-dropzone { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + width: 100%; + padding: 24px 20px; + border: 1px dashed rgba(255, 255, 255, 0.28); + border-radius: 20px; + background: radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 55%), rgba(255, 255, 255, 0.04); + color: #fff; + cursor: pointer; + transition: + border-color 0.18s ease, + background 0.18s ease, + box-shadow 0.18s ease; +} + +.mirotalk-file-dropzone:hover, +.mirotalk-file-dropzone.is-dragover { + border-color: var(--dd-color, #e8e8ec); + background: + radial-gradient(circle at top, rgba(255, 255, 255, 0.08), transparent 52%), + color-mix(in srgb, var(--dd-color, #e8e8ec) 14%, transparent); + box-shadow: + inset 0 0 0 1px rgba(255, 255, 255, 0.2), + 0 18px 36px rgba(0, 0, 0, 0.2); +} + +.mirotalk-file-dropzone.has-file { + border-style: solid; + border-color: rgba(102, 190, 255, 0.5); + background: radial-gradient(circle at top, rgba(102, 190, 255, 0.14), transparent 55%), rgba(255, 255, 255, 0.05); +} + +.mirotalk-file-dropzone-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 64px; + height: 64px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.08); + font-size: 1.5rem; +} + +.mirotalk-file-dropzone-title { + font-size: 1.2rem; + font-weight: 700; +} + +.mirotalk-file-dropzone-subtitle, +.mirotalk-file-dropzone-helper { + color: rgba(255, 255, 255, 0.72); +} + +.mirotalk-file-dropzone-subtitle { + font-size: 0.95rem; +} + +.mirotalk-file-dropzone-helper { + font-size: 0.82rem; +} + +.mirotalk-file-dropzone-cta { + margin-top: 4px; + padding: 10px 16px; + border-radius: 999px; + background: rgba(255, 255, 255, 0.1); + color: #fff; + font-size: 0.9rem; + font-weight: 600; +} + +.mirotalk-file-preview { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 18px; + background: rgba(255, 255, 255, 0.05); + text-align: left; +} + +.mirotalk-file-preview-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 12px; + background: color-mix(in srgb, var(--dd-color, #e8e8ec) 20%, transparent); + color: #fff; + flex: 0 0 auto; +} + +.mirotalk-file-preview-meta { + flex: 1; + min-width: 0; +} + +.mirotalk-file-preview-meta strong, +.mirotalk-file-preview-meta span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.mirotalk-file-preview-meta strong { + color: #fff; + font-size: 0.95rem; +} + +.mirotalk-file-preview-meta span { + margin-top: 4px; + color: rgba(255, 255, 255, 0.68); + font-size: 0.82rem; +} + +.mirotalk-file-preview-remove { + border: none; + border-radius: 12px; + padding: 10px 12px; + background: rgba(255, 255, 255, 0.08); + color: #fff; + cursor: pointer; + transition: background 0.18s ease; +} + +.mirotalk-file-preview-remove:hover { + background: rgba(255, 255, 255, 0.16); +} + +@media (max-width: 640px) { + .mirotalk-file-dropzone { + padding: 20px 16px; + } + + .mirotalk-file-preview { + flex-wrap: wrap; + } + + .mirotalk-file-preview-remove { + width: 100%; + } +} + .swal2-select { background-color: var(--select-bg) !important; color: white !important; diff --git a/public/js/brand.js b/public/js/brand.js index 92c4b483..064a106c 100644 --- a/public/js/brand.js +++ b/public/js/brand.js @@ -109,7 +109,7 @@ let brand = { }, about: { imageUrl: '../images/mirotalk-logo.gif', - title: 'WebRTC P2P v1.8.26', + title: 'WebRTC P2P v1.8.27', html: ` + `, inputAttributes: { accept: accept, 'aria-label': title, }, + customClass: { + htmlContainer: 'mirotalk-file-picker-html', + }, didOpen: () => { - const dropArea = document.getElementById('dropArea'); - dropArea.addEventListener('dragenter', handleDragEnter); - dropArea.addEventListener('dragover', handleDragOver); - dropArea.addEventListener('dragleave', handleDragLeave); - dropArea.addEventListener('drop', handleDrop); + const input = Swal.getInput(); + const confirmButton = Swal.getConfirmButton(); + const dropzone = getId('mirotalkFileDropzone'); + const dropzoneTitle = getId('mirotalkFileDropzoneTitle'); + const dropzoneSubtitle = getId('mirotalkFileDropzoneSubtitle'); + const preview = getId('mirotalkFilePreview'); + const fileName = getId('mirotalkFileName'); + const fileDetails = getId('mirotalkFileDetails'); + const browseBtn = getId('mirotalkFileBrowseBtn'); + const removeBtn = getId('mirotalkFileRemoveBtn'); + + if ( + !input || + !confirmButton || + !dropzone || + !preview || + !fileName || + !fileDetails || + !browseBtn || + !removeBtn + ) + return; + + input.classList.add('mirotalk-hidden-file-input'); + input.setAttribute('tabindex', '-1'); + confirmButton.disabled = true; + + const resetSelection = () => { + selectedFile = null; + input.value = ''; + preview.hidden = true; + dropzone.classList.remove('has-file', 'is-dragover'); + dropzoneTitle.textContent = emptyStateTitle; + dropzoneSubtitle.textContent = emptyStateSubtitle; + browseBtn.textContent = 'Browse files'; + confirmButton.disabled = true; + Swal.resetValidationMessage(); + }; + + const applySelection = (file) => { + if (!file) return resetSelection(); + if (file.size <= 0) { + resetSelection(); + return Swal.showValidationMessage('The selected file is empty.'); + } + + selectedFile = file; + fileName.textContent = file.name; + fileDetails.textContent = `${bytesToSize(file.size)}${file.type ? ` • ${file.type}` : ''}`; + preview.hidden = false; + dropzone.classList.add('has-file'); + dropzone.classList.remove('is-dragover'); + dropzoneTitle.textContent = 'File ready'; + dropzoneSubtitle.textContent = 'Drop another file here or browse to replace it'; + browseBtn.textContent = 'Browse another file'; + Swal.resetValidationMessage(); + confirmButton.disabled = false; + }; + + const openSystemPicker = (event) => { + if (event) { + event.preventDefault(); + event.stopPropagation(); + } + input.click(); + }; + + const handleDragState = (event, isActive) => { + event.preventDefault(); + event.stopPropagation(); + dropzone.classList.toggle('is-dragover', isActive); + if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; + }; + + browseBtn.addEventListener('click', openSystemPicker); + dropzone.addEventListener('click', openSystemPicker); + removeBtn.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + resetSelection(); + }); + + input.addEventListener('change', () => { + applySelection(input.files && input.files.length ? input.files[0] : null); + }); + + dropzone.addEventListener('dragenter', (event) => handleDragState(event, true)); + dropzone.addEventListener('dragover', (event) => handleDragState(event, true)); + dropzone.addEventListener('dragleave', (event) => handleDragState(event, false)); + dropzone.addEventListener('drop', (event) => { + handleDragState(event, false); + + const transfer = event.dataTransfer; + if (!transfer) return; + + if (transfer.items && transfer.items.length > 1) { + resetSelection(); + return Swal.showValidationMessage('Please choose a single file.'); + } + + const item = transfer.items && transfer.items.length ? transfer.items[0] : null; + const entry = item && typeof item.webkitGetAsEntry === 'function' ? item.webkitGetAsEntry() : null; + + if (entry && entry.isDirectory) { + resetSelection(); + return Swal.showValidationMessage('Folders are not supported.'); + } + + if (item && item.kind && item.kind !== 'file') { + resetSelection(); + return Swal.showValidationMessage('Only files can be uploaded here.'); + } + + const file = item && typeof item.getAsFile === 'function' ? item.getAsFile() : transfer.files[0]; + + if (!file) { + resetSelection(); + return Swal.showValidationMessage('Could not read the selected file.'); + } + + applySelection(file); + }); }, showDenyButton: true, - confirmButtonText: `OK`, - denyButtonText: `Cancel`, + confirmButtonText: confirmButtonText, + denyButtonText: 'Cancel', + preConfirm: () => { + if (!selectedFile) { + Swal.showValidationMessage('Choose a file to continue.'); + return false; + } + return selectedFile; + }, showClass: { popup: 'animate__animated animate__fadeInDown' }, hideClass: { popup: 'animate__animated animate__fadeOutUp' }, - }).then((result) => { - if (result.isConfirmed) { - renderToCanvas(result.value); - } }); - function handleDragEnter(e) { - e.preventDefault(); - e.stopPropagation(); - e.target.style.background = 'var(--body-bg)'; - } + return result.isConfirmed ? result.value || selectedFile : null; +} - function handleDragOver(e) { - e.preventDefault(); - e.stopPropagation(); - e.dataTransfer.dropEffect = 'copy'; - } +/** + * Setup Canvas file selections + * @param {string} title + * @param {string} accept + * @param {object} renderToCanvas + */ +async function setupFileSelection(title, accept, renderToCanvas) { + const file = await openFilePickerModal({ + title, + accept, + confirmButtonText: 'OK', + }); - function handleDragLeave(e) { - e.preventDefault(); - e.stopPropagation(); - e.target.style.background = ''; - } - - function handleDrop(e) { - e.preventDefault(); - e.stopPropagation(); - const dt = e.dataTransfer; - const files = dt.files; - handleFiles(files); - e.target.style.background = ''; - } - - function handleFiles(files) { - if (files.length > 0) { - const file = files[0]; - console.log('Selected file:', file); - Swal.close(); - renderToCanvas(file); - } - } + if (file) renderToCanvas(file); } /** @@ -15037,81 +15187,22 @@ function hideFileTransfer() { * @param {string} peer_id * @param {boolean} broadcast send to all (default false) */ -function selectFileToShare(peer_id, broadcast = false, peerName = '') { +async function selectFileToShare(peer_id, broadcast = false, peerName = '') { playSound('newMessage'); const targetLabel = !broadcast && peerName ? ` with ${peerName}` : ''; - Swal.fire({ - allowOutsideClick: false, - background: swBg, - imageAlt: 'mirotalk-file-sharing', - imageUrl: images.share, - position: 'center', + const file = await openFilePickerModal({ title: `Share file${targetLabel}`, - input: 'file', - html: ` -
-

Drag and drop your file here

-
- `, - inputAttributes: { - accept: fileSharingInput, - 'aria-label': 'Select file', - }, - didOpen: () => { - const dropArea = getId('dropArea'); - dropArea.addEventListener('dragenter', handleDragEnter); - dropArea.addEventListener('dragover', handleDragOver); - dropArea.addEventListener('dragleave', handleDragLeave); - dropArea.addEventListener('drop', handleDrop); - }, - showDenyButton: true, - confirmButtonText: `Send`, - denyButtonText: `Cancel`, - showClass: { popup: 'animate__animated animate__fadeInDown' }, - hideClass: { popup: 'animate__animated animate__fadeOutUp' }, - }).then((result) => { - if (result.isConfirmed) { - sendFileInformations(result.value, peer_id, broadcast, peerName); - } + accept: fileSharingInput, + confirmButtonText: 'Send', + helperText: + fileSharingInput === '*' + ? 'Any file type supported' + : `Supports ${formatAcceptedFileTypes(fileSharingInput)}`, }); - function handleDragEnter(e) { - e.preventDefault(); - e.stopPropagation(); - e.target.style.background = 'var(--body-bg)'; - } - - function handleDragOver(e) { - e.preventDefault(); - e.stopPropagation(); - e.dataTransfer.dropEffect = 'copy'; - } - - function handleDragLeave(e) { - e.preventDefault(); - e.stopPropagation(); - e.target.style.background = ''; - } - - function handleDrop(e) { - e.preventDefault(); - e.stopPropagation(); - const dt = e.dataTransfer; - const files = dt.files; - handleFiles(files); - e.target.style.background = ''; - } - - function handleFiles(files) { - if (files.length > 0) { - const file = files[0]; - console.log('Selected file:', file); - Swal.close(); - sendFileInformations(file, peer_id, broadcast, peerName); - } - } + if (file) sendFileInformations(file, peer_id, broadcast, peerName); } /** @@ -15692,7 +15783,7 @@ function showAbout() { Swal.fire({ background: swBg, position: 'center', - title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.26', + title: brand.about?.title && brand.about.title.trim() !== '' ? brand.about.title : 'WebRTC P2P v1.8.27', imageUrl: brand.about?.imageUrl && brand.about.imageUrl.trim() !== '' ? brand.about.imageUrl : images.about, customClass: { image: 'img-about' }, html: `