Files
f0bm/public/s/js/upload.js
2026-01-24 01:28:14 +01:00

349 lines
13 KiB
JavaScript

(() => {
const form = document.getElementById('upload-form');
if (!form) return;
const fileInput = document.getElementById('file-input');
const dropZone = document.getElementById('drop-zone');
const filePreview = document.getElementById('file-preview');
// Note: prompt is now a label, but accessible via class
const dropZonePrompt = dropZone.querySelector('.drop-zone-prompt');
const fileName = document.getElementById('file-name');
const fileSize = document.getElementById('file-size');
const removeFile = document.getElementById('remove-file');
const tagInput = document.getElementById('tag-input');
const tagsList = document.getElementById('tags-list');
const tagsHidden = document.getElementById('tags-hidden');
const tagCount = document.getElementById('tag-count');
const tagSuggestions = document.getElementById('tag-suggestions');
const submitBtn = document.getElementById('submit-btn');
const progressContainer = document.getElementById('upload-progress');
const progressFill = document.getElementById('progress-fill');
const progressText = document.getElementById('progress-text');
const statusDiv = document.getElementById('upload-status');
let tags = [];
let selectedFile = null;
// Flash Message Logic
const showFlash = (msg, type = 'success') => {
const existing = document.querySelector('.flash-message');
if (existing) existing.remove();
const flash = document.createElement('div');
flash.className = `flash-message ${type}`;
flash.textContent = msg;
Object.assign(flash.style, {
position: 'fixed',
top: '20px',
left: '50%',
transform: 'translateX(-50%)',
padding: '15px 30px',
borderRadius: '5px',
color: '#fff',
fontWeight: '600',
zIndex: '9999',
boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
background: type === 'success' ? '#51cf66' : '#ff6b6b',
opacity: '0',
transition: 'opacity 0.3s'
});
document.body.appendChild(flash);
// Fade in
requestAnimationFrame(() => flash.style.opacity = '1');
// Remove after 5s
setTimeout(() => {
flash.style.opacity = '0';
setTimeout(() => flash.remove(), 300);
}, 5000);
};
const formatSize = (bytes) => {
const units = ['B', 'KB', 'MB', 'GB'];
let i = 0;
while (bytes >= 1024 && i < units.length - 1) {
bytes /= 1024;
i++;
}
return bytes.toFixed(2) + ' ' + units[i];
};
const updateSubmitButton = () => {
const rating = document.querySelector('input[name="rating"]:checked');
const hasFile = selectedFile !== null;
const hasRating = rating !== null;
const hasTags = tags.length >= 3;
submitBtn.disabled = !(hasFile && hasRating && hasTags);
if (!hasTags) {
submitBtn.querySelector('.btn-text').textContent = (3 - tags.length) + ' more tag' + (3 - tags.length !== 1 ? 's' : '') + ' required';
} else if (!hasFile) {
submitBtn.querySelector('.btn-text').textContent = 'Select a file';
} else if (!hasRating) {
submitBtn.querySelector('.btn-text').textContent = 'Select SFW or NSFW';
} else {
submitBtn.querySelector('.btn-text').textContent = 'Upload';
}
tagCount.textContent = '(' + tags.length + '/3 minimum)';
tagCount.classList.toggle('valid', tags.length >= 3);
};
const handleFile = (file) => {
if (!file) return;
const validTypes = ['video/mp4', 'video/webm'];
// Check extensions as fallback
const ext = file.name.split('.').pop().toLowerCase();
const validExts = ['mp4', 'webm'];
if (!validTypes.includes(file.type) && !validExts.includes(ext)) {
statusDiv.textContent = 'Only mp4 and webm files are allowed';
statusDiv.className = 'upload-status error';
return;
}
selectedFile = file;
fileName.textContent = file.name;
fileSize.textContent = formatSize(file.size);
dropZonePrompt.style.display = 'none';
filePreview.style.display = 'flex';
statusDiv.textContent = '';
statusDiv.className = 'upload-status';
// Video Preview
const itemPreview = filePreview.querySelector('.item-preview') || document.createElement('div');
itemPreview.className = 'item-preview';
itemPreview.style.marginRight = '15px';
// Clear previous
const existingVid = filePreview.querySelector('video');
if (existingVid) existingVid.remove();
const vid = document.createElement('video');
vid.src = URL.createObjectURL(file);
vid.controls = false;
vid.autoplay = true;
vid.muted = true;
vid.loop = true;
vid.style.maxHeight = '100px';
vid.style.maxWidth = '150px';
vid.style.borderRadius = '4px';
filePreview.prepend(vid);
updateSubmitButton();
};
const preventDefaults = (e) => {
e.preventDefault();
e.stopPropagation();
};
// Attach drag events only to dropZone now (Input is hidden)
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, preventDefaults, false);
});
['dragenter', 'dragover'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.add('dragover'), false);
});
['dragleave', 'drop'].forEach(eventName => {
dropZone.addEventListener(eventName, () => dropZone.classList.remove('dragover'), false);
});
dropZone.addEventListener('drop', (e) => {
const dt = e.dataTransfer;
const files = dt.files;
handleFile(files[0]);
});
// Native change listener on hidden input
fileInput.addEventListener('change', (e) => handleFile(e.target.files[0]));
removeFile.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
selectedFile = null;
fileInput.value = '';
dropZonePrompt.style.display = 'block';
filePreview.style.display = 'none';
// Clear preview video
const vid = filePreview.querySelector('video');
if (vid) vid.remove();
updateSubmitButton();
});
const addTag = (tagName) => {
tagName = tagName.trim().toLowerCase();
if (!tagName || tags.includes(tagName)) return;
if (tagName === 'sfw' || tagName === 'nsfw') return;
tags.push(tagName);
const chip = document.createElement('span');
chip.className = 'tag-chip';
chip.innerHTML = tagName + '<button type="button">&times;</button>';
chip.querySelector('button').addEventListener('click', () => {
tags = tags.filter(t => t !== tagName);
chip.remove();
updateSubmitButton();
});
tagsList.appendChild(chip);
tagsHidden.value = tags.join(',');
tagInput.value = '';
tagSuggestions.innerHTML = '';
tagSuggestions.classList.remove('show');
updateSubmitButton();
};
tagInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
addTag(tagInput.value);
}
});
let debounceTimer;
tagInput.addEventListener('input', () => {
clearTimeout(debounceTimer);
const query = tagInput.value.trim();
if (query.length < 2) {
tagSuggestions.classList.remove('show');
return;
}
debounceTimer = setTimeout(async () => {
try {
const res = await fetch('/api/v2/admin/tags/suggest?q=' + encodeURIComponent(query));
const data = await res.json();
if (data.success && data.suggestions && data.suggestions.length > 0) {
const filtered = data.suggestions.filter(s => !tags.includes(s.tag.toLowerCase()));
let html = '';
for (let i = 0; i < Math.min(8, filtered.length); i++) {
html += '<div class="tag-suggestion">' + filtered[i].tag + '</div>';
}
tagSuggestions.innerHTML = html;
tagSuggestions.classList.add('show');
tagSuggestions.querySelectorAll('.tag-suggestion').forEach(el => {
el.addEventListener('click', () => addTag(el.textContent));
});
} else {
tagSuggestions.classList.remove('show');
}
} catch (err) {
console.error(err);
}
}, 200);
});
document.addEventListener('click', (e) => {
if (!tagInput.contains(e.target) && !tagSuggestions.contains(e.target)) {
tagSuggestions.classList.remove('show');
}
});
document.querySelectorAll('input[name="rating"]').forEach(radio => {
radio.addEventListener('change', updateSubmitButton);
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
if (!selectedFile || tags.length < 3) return;
const rating = document.querySelector('input[name="rating"]:checked');
if (!rating) return;
submitBtn.disabled = true;
submitBtn.querySelector('.btn-text').style.display = 'none';
submitBtn.querySelector('.btn-loading').style.display = 'inline';
progressContainer.style.display = 'flex';
statusDiv.textContent = '';
statusDiv.className = 'upload-status';
const formData = new FormData();
formData.append('file', selectedFile);
formData.append('rating', rating.value);
formData.append('tags', tags.join(','));
try {
const xhr = new XMLHttpRequest();
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percent = Math.round((e.loaded / e.total) * 100);
progressFill.style.width = percent + '%';
progressText.textContent = percent + '%';
}
});
xhr.onload = () => {
const res = JSON.parse(xhr.responseText);
if (res.success) {
statusDiv.innerHTML = '✓ ' + res.msg;
statusDiv.className = 'upload-status success';
// Flash Message
showFlash(res.msg, 'success');
form.reset();
tags = [];
tagsList.innerHTML = '';
selectedFile = null;
dropZonePrompt.style.display = 'block'; // label is actually flex/block via CSS
filePreview.style.display = 'none';
const vid = filePreview.querySelector('video');
if (vid) vid.remove();
} else {
statusDiv.textContent = '✕ ' + res.msg;
statusDiv.className = 'upload-status error';
if (res.repost) {
statusDiv.innerHTML += ' <a href="/' + res.repost + '">View existing</a>';
}
showFlash('Upload failed: ' + res.msg, 'error');
}
submitBtn.querySelector('.btn-text').style.display = 'inline';
submitBtn.querySelector('.btn-loading').style.display = 'none';
progressContainer.style.display = 'none';
progressFill.style.width = '0%';
updateSubmitButton();
};
xhr.onerror = () => {
statusDiv.textContent = '✕ Upload failed. Please try again.';
statusDiv.className = 'upload-status error';
showFlash('Upload failed network error', 'error');
submitBtn.querySelector('.btn-text').style.display = 'inline';
submitBtn.querySelector('.btn-loading').style.display = 'none';
progressContainer.style.display = 'none';
updateSubmitButton();
};
xhr.open('POST', '/api/v2/upload');
xhr.send(formData);
} catch (err) {
console.error(err);
statusDiv.textContent = '✕ Upload failed: ' + err.message;
statusDiv.className = 'upload-status error';
showFlash('Upload failed: ' + err.message, 'error');
submitBtn.querySelector('.btn-text').style.display = 'inline';
submitBtn.querySelector('.btn-loading').style.display = 'none';
updateSubmitButton();
}
});
updateSubmitButton();
})();