diff --git a/resources/js/app.js b/resources/js/app.js index 85c7ea5..b54b7f3 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,7 +1,7 @@ import './bootstrap'; import { registerLadillConfirmStore, registerLadillModalHelpers } from './ladill-modals'; -import { shouldUseChunkedUpload, uploadFileChunked } from './chunked-upload'; +import { uploadFileChunked } from './chunked-upload'; registerLadillModalHelpers(); @@ -316,7 +316,10 @@ Alpine.data('transferCreateForm', (config = {}) => ({ fileItems: [], submitting: false, prepared: false, - _uploadQueue: Promise.resolve(), + _ignoreInputChange: false, + _uploadQueue: [], + _draining: false, + _drainPromise: null, get uploading() { return this.fileItems.some((item) => item.status === 'uploading' || item.status === 'pending'); @@ -348,17 +351,21 @@ Alpine.data('transferCreateForm', (config = {}) => ({ }, onFilesChange(event) { - const newFiles = Array.from(event.target.files || []); + if (this._ignoreInputChange) { + return; + } + + const picked = Array.from(event.target.files || []); + if (!picked.length) { + return; + } + this.prepared = false; + const existingKeys = new Set(this.fileItems.map((item) => item.key)); - const prevByKey = new Map(this.fileItems.map((item) => [item.key, item])); - this.fileItems = []; - - for (const file of newFiles) { + for (const file of picked) { const key = `${file.name}:${file.size}:${file.lastModified}`; - const prev = prevByKey.get(key); - if (prev && (prev.status === 'uploading' || prev.status === 'ready')) { - this.fileItems.push(prev); + if (existingKeys.has(key)) { continue; } @@ -368,25 +375,51 @@ Alpine.data('transferCreateForm', (config = {}) => ({ file, name: file.name, size: file.size, - status: shouldUseChunkedUpload(file.size) ? 'pending' : 'ready', + status: 'pending', progress: 0, - uploadId: prev?.uploadId || null, + uploadId: null, error: null, }; this.fileItems.push(item); - - if (item.status === 'pending') { - this.queueUpload(item); - } + this._uploadQueue.push(item); } - this.syncFileInput(event.target); + this.resetFileInput(event.target); + void this.drainUploadQueue(); }, - queueUpload(item) { - this._uploadQueue = this._uploadQueue - .then(() => this.uploadFileItem(item)) - .catch(() => {}); + resetFileInput(input) { + this._ignoreInputChange = true; + input.value = ''; + this._ignoreInputChange = false; + }, + + drainUploadQueue() { + if (this._drainPromise) { + return this._drainPromise; + } + + this._drainPromise = (async () => { + this._draining = true; + + while (this._uploadQueue.length) { + const item = this._uploadQueue.shift(); + if (!item || item.status !== 'pending') { + continue; + } + + await this.uploadFileItem(item); + } + + this._draining = false; + this._drainPromise = null; + + if (this._uploadQueue.length) { + void this.drainUploadQueue(); + } + })(); + + return this._drainPromise; }, async uploadFileItem(item) { @@ -424,27 +457,7 @@ Alpine.data('transferCreateForm', (config = {}) => ({ } this.fileItems = this.fileItems.filter((entry) => entry.id !== id); - this.syncFileInput(document.querySelector('#files')); - }, - - syncFileInput(input) { - if (!input) { - return; - } - - const smallFiles = this.fileItems - .filter((item) => item.status === 'ready' && item.file && !shouldUseChunkedUpload(item.size)) - .map((item) => item.file); - - if (smallFiles.length) { - const dataTransfer = new DataTransfer(); - smallFiles.forEach((file) => dataTransfer.items.add(file)); - input.files = dataTransfer.files; - input.required = true; - } else { - input.removeAttribute('required'); - input.value = ''; - } + this._uploadQueue = this._uploadQueue.filter((entry) => entry.id !== id); }, async submit(event) { @@ -454,7 +467,6 @@ Alpine.data('transferCreateForm', (config = {}) => ({ event.preventDefault(); const form = event.target; - const fileInput = form.querySelector('#files'); if (!this.fileItems.length) { window.alert('Add at least one file to share.'); @@ -467,7 +479,7 @@ Alpine.data('transferCreateForm', (config = {}) => ({ } if (this.uploading) { - await this._uploadQueue; + await this.drainUploadQueue(); } if (this.fileItems.some((item) => item.status === 'uploading' || item.status === 'pending')) { @@ -475,18 +487,15 @@ Alpine.data('transferCreateForm', (config = {}) => ({ return; } - if (!this.fileItems.some((item) => item.status === 'ready')) { + const readyItems = this.fileItems.filter((item) => item.status === 'ready' && item.uploadId); + if (!readyItems.length) { window.alert('Add at least one file to share.'); return; } form.querySelectorAll('input[name="upload_ids[]"]').forEach((node) => node.remove()); - for (const item of this.fileItems) { - if (!item.uploadId) { - continue; - } - + for (const item of readyItems) { const hidden = document.createElement('input'); hidden.type = 'hidden'; hidden.name = 'upload_ids[]'; @@ -494,7 +503,6 @@ Alpine.data('transferCreateForm', (config = {}) => ({ form.appendChild(hidden); } - this.syncFileInput(fileInput); this.submitting = true; this.prepared = true; form.submit(); diff --git a/resources/views/transfer/transfers/create.blade.php b/resources/views/transfer/transfers/create.blade.php index 32c70ee..3c2b89b 100644 --- a/resources/views/transfer/transfers/create.blade.php +++ b/resources/views/transfer/transfers/create.blade.php @@ -36,9 +36,9 @@
Up to {{ $maxFiles }} files. Large files upload in the background with live progress.
+Up to {{ $maxFiles }} files. Uploads start immediately — pick more files anytime before creating the transfer.