[mirotalk] - refine file share upload modal UI and interactions

This commit is contained in:
Miroslav Pejic
2026-04-30 17:02:30 +02:00
parent da8143f0c8
commit 6ef5db9f66
8 changed files with 382 additions and 128 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
# ====================================================
# MiroTalk P2P v.1.8.26 - Environment Configuration
# MiroTalk P2P v.1.8.27 - Environment Configuration
# ====================================================
# App environment
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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
*
*/
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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": {
+163
View File
@@ -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;
+1 -1
View File
@@ -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: `
<button
id="support-button"
+212 -121
View File
@@ -15,7 +15,7 @@
* @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
*
*/
@@ -14159,80 +14159,230 @@ function createStickyNote() {
}
/**
* Setup Canvas file selections
* @param {string} title
* Format accepted file types for UI helper text
* @param {string} accept
* @param {object} renderToCanvas
* @returns {string}
*/
function setupFileSelection(title, accept, renderToCanvas) {
Swal.fire({
function formatAcceptedFileTypes(accept = '*') {
if (!accept || accept === '*') return 'any file type';
return accept
.split(',')
.map((type) => type.trim())
.filter(Boolean)
.map((type) => {
if (type.startsWith('.')) return type.slice(1).toUpperCase();
if (type.endsWith('/*')) return `${type.slice(0, -2).toUpperCase()} files`;
if (type.includes('/')) return type.split('/')[1].toUpperCase();
return type.toUpperCase();
})
.join(', ');
}
/**
* Open a styled file picker modal with drag-and-drop support
* @param {object} config
* @returns {Promise<File|null>}
*/
async function openFilePickerModal(config) {
const {
title,
accept = '*',
confirmButtonText = 'OK',
emptyStateTitle = 'Drop file here',
emptyStateSubtitle = 'or browse from your device',
helperText = `Supports ${formatAcceptedFileTypes(accept)}`,
} = config;
let selectedFile = null;
const result = await Swal.fire({
allowOutsideClick: false,
background: swBg,
position: 'center',
title: title,
input: 'file',
html: `
<div id="dropArea">
<p>Drag and drop your file here</p>
<div class="mirotalk-file-picker">
<button type="button" id="mirotalkFileDropzone" class="mirotalk-file-dropzone">
<span class="mirotalk-file-dropzone-icon"><i class="fas fa-cloud-upload-alt"></i></span>
<span id="mirotalkFileDropzoneTitle" class="mirotalk-file-dropzone-title">${emptyStateTitle}</span>
<span id="mirotalkFileDropzoneSubtitle" class="mirotalk-file-dropzone-subtitle">${emptyStateSubtitle}</span>
<span class="mirotalk-file-dropzone-helper">${helperText}</span>
<span id="mirotalkFileBrowseBtn" class="mirotalk-file-dropzone-cta">Browse files</span>
</button>
<div id="mirotalkFilePreview" class="mirotalk-file-preview" hidden>
<div class="mirotalk-file-preview-icon"><i class="fas fa-file-alt"></i></div>
<div class="mirotalk-file-preview-meta">
<strong id="mirotalkFileName">No file selected</strong>
<span id="mirotalkFileDetails"></span>
</div>
<button type="button" id="mirotalkFileRemoveBtn" class="mirotalk-file-preview-remove">Remove</button>
</div>
</div>
`,
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: `
<div id="dropArea">
<p>Drag and drop your file here</p>
</div>
`,
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: `