349 lines
13 KiB
JavaScript
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">×</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();
|
|
})();
|