/**
* Item upload flows: audio file upload and version file upload.
*
* Loaded once in dashboard-item.html. Re-initializes on HTMX tab swap.
* Reads item ID from data-item-id on the container element.
* Depends on: upload.js (S3Uploader, initDropzone), mnw.js (csrfHeaders, showToast).
*/
(function() {
function init() {
initAudioUpload();
initVersionUpload();
}
// ── Audio Upload ──
function initAudioUpload() {
var container = document.getElementById('audio-upload');
if (!container) return;
var itemId = container.dataset.itemId;
if (!itemId) return;
var uploader = new S3Uploader({
filenameEl: document.getElementById('upload-filename'),
percentEl: document.getElementById('upload-percent'),
progressBar: document.getElementById('progress-bar'),
speedEl: document.getElementById('upload-speed'),
});
initDropzone(
document.getElementById('audio-dropzone'),
document.getElementById('audio-file-input'),
function(file) {
if (file.type.startsWith('audio/') || file.name.match(/\.(mp3|wav|flac|m4a|ogg)$/i)) {
uploadAudio(file);
}
}
);
var replaceBtn = document.getElementById('replace-audio-btn');
if (replaceBtn) {
replaceBtn.addEventListener('click', function() {
var cur = document.getElementById('current-audio');
if (cur) cur.classList.add('hidden');
document.getElementById('upload-area').classList.remove('hidden');
resetUpload();
});
}
var lastFile = null;
document.getElementById('cancel-upload-btn').addEventListener('click', function() {
uploader.cancel();
resetUpload();
});
document.getElementById('retry-upload-btn').addEventListener('click', function() {
if (lastFile) {
document.getElementById('upload-error').classList.add('hidden');
uploadAudio(lastFile);
} else {
resetUpload();
}
});
function resetUpload() {
lastFile = null;
document.getElementById('audio-dropzone').classList.remove('hidden');
document.getElementById('upload-progress').classList.add('hidden');
document.getElementById('upload-success').classList.add('hidden');
document.getElementById('upload-error').classList.add('hidden');
document.getElementById('audio-file-input').value = '';
}
function uploadAudio(file) {
lastFile = file;
document.getElementById('audio-dropzone').classList.add('hidden');
document.getElementById('upload-progress').classList.remove('hidden');
fetch('/api/upload/presign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({
item_id: itemId,
file_type: 'audio',
file_name: file.name,
content_type: file.type || 'audio/mpeg',
file_size_bytes: file.size
})
})
.then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
throw new Error(d.error || 'Failed to get upload URL');
});
return res.json();
})
.then(function(data) {
if (data.max_file_bytes && file.size > data.max_file_bytes) {
var limitMB = (data.max_file_bytes / (1024 * 1024)).toFixed(0);
var fileMB = (file.size / (1024 * 1024)).toFixed(1);
throw new Error('File is ' + fileMB + ' MB but your plan allows up to ' + limitMB + ' MB per file. Upgrade your tier or use a smaller file.');
}
return uploader.upload(data.upload_url, file, data.s3_key, 'audio/mpeg', data.cache_control);
})
.then(function(s3Key) {
return fetch('/api/upload/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({
item_id: itemId,
file_type: 'audio',
s3_key: s3Key
})
});
})
.then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
throw new Error(d.error || 'Failed to confirm upload');
});
return res.json().catch(function() { return {}; });
})
.then(function(result) {
document.getElementById('upload-progress').classList.add('hidden');
document.getElementById('upload-success').classList.remove('hidden');
// Scan flagged the file for manual review — surface it as a
// toast so the creator knows their content isn't public yet.
if (result && result.pending_review) {
showToast(
'Upload accepted but held for review — our scanner flagged it. ' +
'You’ll get an email once it’s cleared.',
'warning'
);
}
setTimeout(function() { window.location.href = '/dashboard/item/' + itemId + '#tab-files'; }, 1500);
})
.catch(function(err) {
document.getElementById('upload-progress').classList.add('hidden');
document.getElementById('upload-error').classList.remove('hidden');
document.getElementById('error-message').textContent = err.message || 'Upload failed';
});
}
}
// ── Version Upload ──
function initVersionUpload() {
var container = document.getElementById('version-upload');
if (!container) return;
var itemId = container.dataset.itemId;
if (!itemId) return;
var fileQueue = [];
var targetVersionId = null;
var uploader = new S3Uploader({
filenameEl: document.getElementById('version-upload-filename'),
percentEl: document.getElementById('version-upload-percent'),
progressBar: document.getElementById('version-progress-bar'),
speedEl: document.getElementById('version-upload-speed'),
});
var versionFileInput = document.getElementById('version-file-input');
var fileRows = document.getElementById('version-file-rows');
// Add files button
var addBtn = document.getElementById('add-version-file-btn');
if (addBtn) {
addBtn.addEventListener('click', function() { versionFileInput.click(); });
}
if (versionFileInput) {
versionFileInput.addEventListener('change', function() {
for (var i = 0; i < this.files.length; i++) addFileRow(this.files[i]);
this.value = '';
});
}
function guessLabel(name) {
var n = name.toLowerCase();
if (n.indexOf('aarch64') !== -1 || n.indexOf('arm64') !== -1) return n.indexOf('appimage') !== -1 || n.indexOf('.deb') !== -1 ? 'Linux (aarch64)' : 'macOS (arm)';
if (n.indexOf('x86_64') !== -1 || n.indexOf('amd64') !== -1 || n.indexOf('x64') !== -1) return n.indexOf('.exe') !== -1 || n.indexOf('.msi') !== -1 ? 'Windows (x64)' : 'Linux (x86_64)';
if (n.indexOf('.dmg') !== -1) return 'macOS';
if (n.indexOf('.msi') !== -1 || n.indexOf('.exe') !== -1) return 'Windows';
if (n.indexOf('.appimage') !== -1 || n.indexOf('.deb') !== -1) return 'Linux';
return '';
}
function addFileRow(file) {
var idx = fileQueue.length;
fileQueue.push({ file: file, idx: idx });
var tr = document.createElement('tr');
tr.dataset.idx = idx;
tr.style.borderBottom = '1px solid var(--border)';
tr.innerHTML =
'
' + escapeHtml(file.name) + ' | ' +
' | ' +
' | ';
fileRows.appendChild(tr);
tr.querySelector('.version-remove-file').addEventListener('click', function() {
fileQueue[idx] = null;
tr.remove();
});
}
function escapeHtml(s) { var d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
function escapeAttr(s) { return s.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>'); }
// Upload all button
document.getElementById('create-version-btn').addEventListener('click', function() {
var versionNumber = document.getElementById('new-version-number').value.trim();
var changelog = document.getElementById('version-changelog').value.trim();
var entries = fileQueue.filter(function(e) { return e !== null; });
if (!versionNumber) { showToast('Please enter a version number.'); return; }
if (entries.length === 0) { showToast('Please add at least one file.'); return; }
this.disabled = true;
this.textContent = 'Uploading...';
document.getElementById('new-version-form').classList.add('hidden');
document.getElementById('version-upload-progress').classList.remove('hidden');
// Build queue display
var queueEl = document.getElementById('version-upload-queue');
queueEl.innerHTML = '';
for (var q = 0; q < entries.length; q++) {
var li = document.createElement('div');
li.id = 'queue-item-' + entries[q].idx;
li.style.cssText = 'display: flex; align-items: center; gap: 0.5rem; padding: 0.3rem 0; font-size: 0.85rem;';
var labelInput = document.querySelector('.version-label-input[data-idx="' + entries[q].idx + '"]');
var labelText = labelInput ? labelInput.value.trim() : '';
var displayName = entries[q].file.name + (labelText ? ' (' + escapeHtml(labelText) + ')' : '');
li.innerHTML = '-' + displayName + '' + formatSize(entries[q].file.size) + '';
queueEl.appendChild(li);
}
uploadSequentially(entries, 0, versionNumber, changelog);
});
function formatSize(bytes) {
if (bytes > 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
if (bytes > 1024) return (bytes / 1024).toFixed(0) + ' KB';
return bytes + ' B';
}
function updateQueueStatus(idx, status) {
var el = document.getElementById('queue-item-' + idx);
if (!el) return;
var s = el.querySelector('.queue-status');
if (status === 'uploading') { s.textContent = '...'; s.style.opacity = '1'; }
else if (status === 'done') { s.textContent = 'OK'; s.style.opacity = '0.7'; el.style.opacity = '0.6'; }
else if (status === 'error') { s.textContent = '!'; s.style.color = 'var(--error, #c0392b)'; s.style.opacity = '1'; }
}
function uploadSequentially(entries, i, versionNumber, changelog) {
if (i >= entries.length) {
document.getElementById('version-upload-progress').classList.add('hidden');
document.getElementById('version-upload-success').classList.remove('hidden');
setTimeout(function() {
var filesBtn = document.getElementById('tab-files');
if (filesBtn) filesBtn.click();
}, 1500);
return;
}
var entry = entries[i];
var labelInput = document.querySelector('.version-label-input[data-idx="' + entry.idx + '"]');
var label = labelInput ? labelInput.value.trim() : '';
updateQueueStatus(entry.idx, 'uploading');
uploader.filenameEl.textContent = entry.file.name + (entries.length > 1 ? ' (' + (i + 1) + '/' + entries.length + ')' : '');
fetch('/api/items/' + itemId + '/versions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({
version_number: versionNumber,
changelog: changelog || null,
label: label || null
})
})
.then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
throw new Error(d.error || 'Failed to create version');
});
return res.json();
})
.then(function(data) {
return fetch('/api/versions/' + data.id + '/upload/presign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({
file_name: entry.file.name,
content_type: entry.file.type || 'application/octet-stream'
})
}).then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
throw new Error(d.error || 'Failed to get upload URL');
});
return res.json();
}).then(function(presign) {
if (presign.max_file_bytes && entry.file.size > presign.max_file_bytes) {
var limitMB = (presign.max_file_bytes / (1024 * 1024)).toFixed(0);
var fileMB = (entry.file.size / (1024 * 1024)).toFixed(1);
throw new Error(entry.file.name + ': ' + fileMB + ' MB exceeds ' + limitMB + ' MB limit');
}
return uploader.upload(presign.upload_url, entry.file, presign.s3_key, 'application/octet-stream', presign.cache_control)
.then(function(s3Key) { return { s3Key: s3Key, versionId: data.id }; });
});
})
.then(function(result) {
return fetch('/api/versions/' + result.versionId + '/upload/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({ s3_key: result.s3Key, file_size_bytes: entry.file.size })
});
})
.then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
throw new Error(d.error || 'Failed to confirm upload');
});
return res.json().catch(function() { return {}; });
})
.then(function(confirmData) {
if (confirmData && confirmData.pending_review) {
showToast('Version upload held for review — our scanner flagged it.', 'warning');
}
updateQueueStatus(entry.idx, 'done');
uploadSequentially(entries, i + 1, versionNumber, changelog);
})
.catch(function(err) {
updateQueueStatus(entry.idx, 'error');
showVersionError(err.message || 'Upload failed');
});
}
// Existing version upload (single file)
var existingDropzone = document.getElementById('existing-version-dropzone');
var existingFileInput = document.getElementById('existing-version-file-input');
if (existingDropzone && existingFileInput) {
initDropzone(existingDropzone, existingFileInput, function(file) {
if (targetVersionId) uploadSingleFile(targetVersionId, file);
});
}
function uploadSingleFile(versionId, file) {
document.getElementById('new-version-form').classList.add('hidden');
document.getElementById('existing-version-upload').classList.add('hidden');
document.getElementById('version-upload-progress').classList.remove('hidden');
fetch('/api/versions/' + versionId + '/upload/presign', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({ file_name: file.name, content_type: file.type || 'application/octet-stream' })
})
.then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) { throw new Error(d.error || 'Presign failed'); });
return res.json();
})
.then(function(data) {
return uploader.upload(data.upload_url, file, data.s3_key, 'application/octet-stream', data.cache_control);
})
.then(function(s3Key) {
return fetch('/api/versions/' + versionId + '/upload/confirm', {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...csrfHeaders() },
body: JSON.stringify({ s3_key: s3Key, file_size_bytes: file.size })
});
})
.then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) { throw new Error(d.error || 'Confirm failed'); });
return res.json().catch(function() { return {}; });
})
.then(function(confirmData) {
document.getElementById('version-upload-progress').classList.add('hidden');
document.getElementById('version-upload-success').classList.remove('hidden');
if (confirmData && confirmData.pending_review) {
showToast('Version upload held for review — our scanner flagged it.', 'warning');
}
setTimeout(function() {
var filesBtn = document.getElementById('tab-files');
if (filesBtn) filesBtn.click();
}, 1500);
})
.catch(function(err) { showVersionError(err.message || 'Upload failed'); });
}
document.querySelectorAll('.upload-to-version-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
targetVersionId = btn.dataset.versionId;
document.getElementById('new-version-form').classList.add('hidden');
document.getElementById('existing-version-upload').classList.remove('hidden');
});
});
document.getElementById('cancel-existing-upload-btn').addEventListener('click', function() {
targetVersionId = null;
document.getElementById('existing-version-upload').classList.add('hidden');
document.getElementById('new-version-form').classList.remove('hidden');
});
document.querySelectorAll('.download-version-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
fetch('/api/versions/' + btn.dataset.versionId + '/download')
.then(function(res) {
if (!res.ok) throw new Error('Failed to get download URL');
return res.json();
})
.then(function(data) { window.location.href = data.download_url; })
.catch(function(err) { showToast(err.message); });
});
});
document.querySelectorAll('.delete-version-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
if (!confirm('Delete this version?')) return;
var versionId = btn.dataset.versionId;
fetch('/api/items/' + itemId + '/versions/' + versionId, {
method: 'DELETE',
headers: csrfHeaders()
})
.then(function(res) {
if (!res.ok) return res.json().catch(function() { return {}; }).then(function(d) {
throw new Error(d.error || 'Failed to delete version');
});
var row = btn.closest('tr');
if (row) row.remove();
})
.catch(function(err) { showToast(err.message || 'Failed to delete version'); });
});
});
document.getElementById('cancel-version-upload-btn').addEventListener('click', function() {
uploader.cancel();
resetVersionUpload();
});
document.getElementById('retry-version-upload-btn').addEventListener('click', resetVersionUpload);
function showVersionError(message) {
document.getElementById('version-upload-progress').classList.add('hidden');
document.getElementById('version-upload-error').classList.remove('hidden');
document.getElementById('version-error-message').textContent = message;
}
function resetVersionUpload() {
document.getElementById('new-version-form').classList.remove('hidden');
document.getElementById('existing-version-upload').classList.add('hidden');
document.getElementById('version-upload-progress').classList.add('hidden');
document.getElementById('version-upload-success').classList.add('hidden');
document.getElementById('version-upload-error').classList.add('hidden');
fileQueue = [];
fileRows.innerHTML = '';
targetVersionId = null;
var btn = document.getElementById('create-version-btn');
btn.disabled = false;
btn.textContent = 'Upload All';
}
}
// Run on initial load
init();
// Re-run when HTMX swaps in upload tabs
document.body.addEventListener('htmx:afterSwap', function(e) {
if (e.detail.target.id === 'tab-content') {
init();
}
});
})();