Ship FrankenPHP Compose services with Reverb WebSockets for real-time transcription status, skip re-uploading identical audio via content hash, and format disk usage without requiring intl.
160 lines
4.4 KiB
JavaScript
160 lines
4.4 KiB
JavaScript
/**
|
|
* Upload dropzone: discard duplicate files in the selection (and known server fingerprints)
|
|
* before submitting the form.
|
|
*/
|
|
|
|
export function uploadDropzone({ existingFingerprints = [] } = {}) {
|
|
const acceptExt = ['.mp3', '.wav', '.ogg', '.oga', '.flac', '.m4a', '.mp4', '.aac', '.webm', '.wma', '.aiff', '.aif'];
|
|
const known = new Set(existingFingerprints);
|
|
|
|
return {
|
|
files: [],
|
|
dragging: false,
|
|
uploading: false,
|
|
error: null,
|
|
notice: null,
|
|
|
|
get uploadLabel() {
|
|
if (this.uploading) {
|
|
return 'Uploading…';
|
|
}
|
|
|
|
if (this.files.length <= 1) {
|
|
return 'Upload';
|
|
}
|
|
|
|
return 'Upload ' + this.files.length + ' files';
|
|
},
|
|
|
|
onBrowse(event) {
|
|
this.addFiles(Array.from(event.target.files || []));
|
|
},
|
|
|
|
onDrop(event) {
|
|
this.dragging = false;
|
|
this.addFiles(Array.from(event.dataTransfer?.files || []));
|
|
},
|
|
|
|
addFiles(incoming) {
|
|
this.error = null;
|
|
this.notice = null;
|
|
|
|
const accepted = [];
|
|
let skippedUnsupported = 0;
|
|
let skippedTooLarge = 0;
|
|
let skippedDuplicates = 0;
|
|
|
|
for (const file of incoming) {
|
|
if (! this.isAccepted(file)) {
|
|
skippedUnsupported++;
|
|
continue;
|
|
}
|
|
|
|
if (file.size > 100 * 1024 * 1024) {
|
|
skippedTooLarge++;
|
|
continue;
|
|
}
|
|
|
|
const fingerprint = this.fingerprint(file);
|
|
|
|
if (known.has(fingerprint) || this.files.some((existing) => this.fingerprint(existing) === fingerprint)) {
|
|
skippedDuplicates++;
|
|
continue;
|
|
}
|
|
|
|
if (accepted.some((existing) => this.fingerprint(existing) === fingerprint)) {
|
|
skippedDuplicates++;
|
|
continue;
|
|
}
|
|
|
|
accepted.push(file);
|
|
}
|
|
|
|
this.files = [...this.files, ...accepted];
|
|
|
|
if (this.files.length > 50) {
|
|
this.error = 'You can upload at most 50 files at once.';
|
|
this.files = this.files.slice(0, 50);
|
|
}
|
|
|
|
if (skippedUnsupported > 0) {
|
|
this.error = 'Skipped unsupported file type. Use common audio formats only.';
|
|
} else if (skippedTooLarge > 0) {
|
|
this.error = 'Skipped a file larger than 100 MB.';
|
|
}
|
|
|
|
if (skippedDuplicates > 0) {
|
|
this.notice = skippedDuplicates === 1
|
|
? 'Skipped 1 duplicate file.'
|
|
: `Skipped ${skippedDuplicates} duplicate files.`;
|
|
}
|
|
|
|
this.syncInput();
|
|
},
|
|
|
|
isAccepted(file) {
|
|
const name = (file.name || '').toLowerCase();
|
|
|
|
if (acceptExt.some((ext) => name.endsWith(ext))) {
|
|
return true;
|
|
}
|
|
|
|
return (file.type || '').startsWith('audio/');
|
|
},
|
|
|
|
fingerprint(file) {
|
|
return `${String(file.name || '').toLowerCase()}:${Number(file.size) || 0}`;
|
|
},
|
|
|
|
fileListKey(file, index = 0) {
|
|
return `${this.fingerprint(file)}:${index}`;
|
|
},
|
|
|
|
removeFile(index) {
|
|
this.files.splice(index, 1);
|
|
this.syncInput();
|
|
},
|
|
|
|
clearFiles() {
|
|
this.files = [];
|
|
this.notice = null;
|
|
this.syncInput();
|
|
},
|
|
|
|
syncInput() {
|
|
const input = this.$refs.fileInput;
|
|
|
|
if (! input) {
|
|
return;
|
|
}
|
|
|
|
const transfer = new DataTransfer();
|
|
this.files.forEach((file) => transfer.items.add(file));
|
|
input.files = transfer.files;
|
|
},
|
|
|
|
ensureFilesSelected(event) {
|
|
if (this.files.length === 0) {
|
|
event.preventDefault();
|
|
this.error = 'Drop or choose at least one audio file.';
|
|
return;
|
|
}
|
|
|
|
this.uploading = true;
|
|
this.error = null;
|
|
},
|
|
|
|
formatSize(bytes) {
|
|
if (bytes < 1024) {
|
|
return bytes + ' B';
|
|
}
|
|
|
|
if (bytes < 1024 * 1024) {
|
|
return (bytes / 1024).toFixed(1) + ' KB';
|
|
}
|
|
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
},
|
|
};
|
|
}
|