|
|
|
|
|
let selectedFiles = []; |
|
|
let completedFiles = []; |
|
|
|
|
|
|
|
|
const MAX_AGE_DAYS = 2; |
|
|
const MAX_AGE_MS = MAX_AGE_DAYS * 24 * 60 * 60 * 1000; |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function () { |
|
|
cleanupOrphanedStorageEntries(); |
|
|
setupEventListeners(); |
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function cleanupOrphanedStorageEntries() { |
|
|
try { |
|
|
const storageData = JSON.parse(localStorage.getItem('uploadedFileData') || '{}'); |
|
|
const now = Date.now(); |
|
|
let cleaned = false; |
|
|
|
|
|
for (const id in storageData) { |
|
|
if (now - storageData[id].timestamp > MAX_AGE_MS) { |
|
|
delete storageData[id]; |
|
|
cleaned = true; |
|
|
} |
|
|
} |
|
|
|
|
|
if (cleaned) { |
|
|
localStorage.setItem('uploadedFileData', JSON.stringify(storageData)); |
|
|
console.log('Cleaned orphaned localStorage entries'); |
|
|
} |
|
|
|
|
|
|
|
|
localStorage.removeItem('uploadedFileIds'); |
|
|
} catch (e) { |
|
|
console.warn('Failed to cleanup localStorage:', e); |
|
|
} |
|
|
} |
|
|
|
|
|
function setupEventListeners() { |
|
|
const uploadSection = document.getElementById('upload-section'); |
|
|
const fileInput = document.getElementById('file-input'); |
|
|
const processButton = document.getElementById('process-button'); |
|
|
const clearButton = document.getElementById('clear-button'); |
|
|
const downloadButton = document.getElementById('download-button'); |
|
|
|
|
|
|
|
|
uploadSection.addEventListener('click', () => fileInput.click()); |
|
|
uploadSection.addEventListener('dragover', handleDragOver); |
|
|
uploadSection.addEventListener('dragleave', handleDragLeave); |
|
|
uploadSection.addEventListener('drop', handleDrop); |
|
|
fileInput.addEventListener('change', handleFileSelect); |
|
|
|
|
|
|
|
|
processButton.addEventListener('click', processButtonFn); |
|
|
clearButton.addEventListener('click', clearAllFiles); |
|
|
downloadButton.addEventListener('click', downloadAllFiles); |
|
|
} |
|
|
|
|
|
function showMessage(message, type = 'error') { |
|
|
const errorMsg = document.getElementById('error-message'); |
|
|
const successMsg = document.getElementById('success-message'); |
|
|
|
|
|
|
|
|
errorMsg.style.display = 'none'; |
|
|
successMsg.style.display = 'none'; |
|
|
|
|
|
if (type === 'error') { |
|
|
errorMsg.textContent = message; |
|
|
errorMsg.style.display = 'block'; |
|
|
} else { |
|
|
successMsg.textContent = message; |
|
|
successMsg.style.display = 'block'; |
|
|
} |
|
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
errorMsg.style.display = 'none'; |
|
|
successMsg.style.display = 'none'; |
|
|
}, 5000); |
|
|
} |
|
|
|
|
|
function handleDragOver(e) { |
|
|
e.preventDefault(); |
|
|
e.currentTarget.classList.add('dragover'); |
|
|
} |
|
|
|
|
|
function handleDragLeave(e) { |
|
|
e.preventDefault(); |
|
|
e.currentTarget.classList.remove('dragover'); |
|
|
} |
|
|
|
|
|
function handleDrop(e) { |
|
|
e.preventDefault(); |
|
|
const uploadSection = e.currentTarget; |
|
|
uploadSection.classList.remove('dragover'); |
|
|
|
|
|
const files = Array.from(e.dataTransfer.files); |
|
|
processFiles(files); |
|
|
} |
|
|
|
|
|
function handleFileSelect(e) { |
|
|
const files = Array.from(e.target.files); |
|
|
processFiles(files); |
|
|
} |
|
|
|
|
|
function generateUniqueId(file, length = 12) { |
|
|
let ext = "." + file.name.split(".")[file.name.split(".").length - 1] |
|
|
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; |
|
|
return Array.from({ length }, () => chars[Math.floor(Math.random() * chars.length)]).join('') + ext; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function saveFileIdToStorage(id) { |
|
|
const storageData = JSON.parse(localStorage.getItem('uploadedFileData') || '{}'); |
|
|
if (!storageData[id]) { |
|
|
storageData[id] = { |
|
|
timestamp: Date.now() |
|
|
}; |
|
|
localStorage.setItem('uploadedFileData', JSON.stringify(storageData)); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function removeFileIdFromStorage(id) { |
|
|
const storageData = JSON.parse(localStorage.getItem('uploadedFileData') || '{}'); |
|
|
delete storageData[id]; |
|
|
localStorage.setItem('uploadedFileData', JSON.stringify(storageData)); |
|
|
} |
|
|
|
|
|
function processFiles(files) { |
|
|
files.forEach((file, index) => { |
|
|
const id = generateUniqueId(file); |
|
|
const fileData = { |
|
|
id: id, |
|
|
file: file, |
|
|
name: file.name, |
|
|
size: formatFileSize(file.size), |
|
|
status: 'ready', |
|
|
preview: null |
|
|
}; |
|
|
|
|
|
|
|
|
saveFileIdToStorage(id); |
|
|
|
|
|
|
|
|
const reader = new FileReader(); |
|
|
reader.onload = (e) => { |
|
|
fileData.preview = e.target.result; |
|
|
updateFileCard(fileData.id); |
|
|
}; |
|
|
reader.readAsDataURL(file); |
|
|
|
|
|
selectedFiles.push(fileData); |
|
|
}); |
|
|
|
|
|
updateUI(); |
|
|
} |
|
|
|
|
|
function updateUI() { |
|
|
const filesPreview = document.getElementById('files-preview'); |
|
|
const processButton = document.getElementById('process-button'); |
|
|
|
|
|
if (selectedFiles.length > 0) { |
|
|
filesPreview.style.display = 'block'; |
|
|
processButton.disabled = false; |
|
|
renderFileCards(); |
|
|
} else { |
|
|
filesPreview.style.display = 'none'; |
|
|
processButton.disabled = true; |
|
|
} |
|
|
} |
|
|
|
|
|
function renderFileCards() { |
|
|
const filesGrid = document.getElementById('files-grid'); |
|
|
|
|
|
filesGrid.innerHTML = selectedFiles.map(file => { |
|
|
|
|
|
const completedFile = completedFiles.find(cf => cf.id === file.id); |
|
|
|
|
|
const hasMetadata = file.other_info && Object.keys(file.other_info).length > 0; |
|
|
|
|
|
const isMetadataPage = window.location.pathname == '/image/remove_metadata'; |
|
|
const downloadTop = isMetadataPage ? '70px' : '40px'; |
|
|
const viewTop = isMetadataPage ? '100px' : '70px'; |
|
|
|
|
|
return ` |
|
|
<div class="file-card" id="file-${file.id}"> |
|
|
<button class="remove-button" onclick="removeFile('${file.id}')">×</button> |
|
|
${isMetadataPage && file.status === 'completed' ? `<button class="remove-button" style="top: 40px; background: #17a2b8;" onclick="showMetadata('${file.id}')" title="View removed metadata">i</button>` : ``} |
|
|
${file.status === 'error' && file.errorMessage ? `<button class="remove-button" style="top: 40px; background: #dc3545;" onclick="showError('${file.id}')" title="View error">!</button>` : ``} |
|
|
${file.status === 'completed' && completedFile ? `<button class="remove-button" style="top: ${downloadTop}; background: #28a745;" onclick="downloadFile('${file.id}')" title="Download">⬇</button>` : ``} |
|
|
${file.status === 'completed' && completedFile ? `<button class="remove-button" style="top: ${viewTop}; background: #6f42c1;" onclick="viewImage('${file.id}')" title="View image">👁</button>` : ``} |
|
|
|
|
|
<div class="file-preview"> |
|
|
${file.preview ? `<img src="${file.preview}" alt="${file.name}">` : '<span class="file-icon">🖼️</span>'} |
|
|
</div> |
|
|
|
|
|
<div class="file-info"> |
|
|
<h4>${file.name}</h4> |
|
|
<p>${file.size}</p> |
|
|
|
|
|
<div class="file-status"> |
|
|
<div class="status-indicator ${file.status === 'processing' ? 'processing' : file.status === 'error' ? 'error' : file.status === 'completed' ? 'completed' : ''}"></div> |
|
|
<span>${getStatusText(file.status)}</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
`; |
|
|
}).join(''); |
|
|
} |
|
|
|
|
|
function updateFileCard(fileId) { |
|
|
const file = selectedFiles.find(f => f.id === fileId); |
|
|
if (!file) return; |
|
|
|
|
|
const card = document.getElementById(`file-${fileId}`); |
|
|
if (!card) return; |
|
|
|
|
|
const preview = card.querySelector('.file-preview'); |
|
|
if (file.preview && preview) { |
|
|
preview.innerHTML = `<img src="${file.preview}" alt="${file.name}">`; |
|
|
} |
|
|
} |
|
|
|
|
|
async function processButtonFn() { |
|
|
if (selectedFiles.length === 0) return; |
|
|
|
|
|
const progressSection = document.getElementById('progress-section'); |
|
|
const progressBar = document.getElementById('progress-bar'); |
|
|
const progressText = document.getElementById('progress-text'); |
|
|
const processButton = document.getElementById('process-button'); |
|
|
|
|
|
|
|
|
progressSection.style.display = 'block'; |
|
|
processButton.disabled = true; |
|
|
const old_message = processButton.innerHTML; |
|
|
processButton.innerHTML = '🔄 Processing...'; |
|
|
const settings = getSettingsData(); |
|
|
completedFiles = []; |
|
|
|
|
|
for (let i = 0; i < selectedFiles.length; i++) { |
|
|
const file = selectedFiles[i]; |
|
|
|
|
|
|
|
|
file.status = 'uploading'; |
|
|
renderFileCards(); |
|
|
|
|
|
try { |
|
|
|
|
|
await upload(file); |
|
|
file.status = 'processing'; |
|
|
renderFileCards(); |
|
|
const processedFile = await secret_sauce(file, settings); |
|
|
file.status = 'completed'; |
|
|
file.other_info = processedFile.other_info; |
|
|
renderFileCards(); |
|
|
completedFiles.push(processedFile); |
|
|
} catch (error) { |
|
|
console.error('Processing error:', error); |
|
|
file.status = 'error'; |
|
|
file.errorMessage = error.message; |
|
|
showMessage(`Error processing ${file.name}: ${error.message}`, 'error'); |
|
|
} |
|
|
|
|
|
|
|
|
const progress = Math.round(((i + 1) / selectedFiles.length) * 100); |
|
|
progressBar.style.width = progress + '%'; |
|
|
progressText.textContent = `${progress}% complete (${i + 1}/${selectedFiles.length})`; |
|
|
|
|
|
renderFileCards(); |
|
|
} |
|
|
|
|
|
|
|
|
processButton.disabled = false; |
|
|
processButton.innerHTML = old_message; |
|
|
|
|
|
const successfulProcessing = completedFiles.length; |
|
|
if (successfulProcessing > 0) { |
|
|
document.getElementById('download-button').style.display = 'inline-flex'; |
|
|
showMessage(`Successfully processed ${successfulProcessing} image(s)!`, 'success'); |
|
|
} |
|
|
|
|
|
setTimeout(() => { |
|
|
progressSection.style.display = 'none'; |
|
|
}, 1000); |
|
|
} |
|
|
|
|
|
async function secret_sauce(file, settings) { |
|
|
const formData = new FormData(); |
|
|
formData.append('id', file.id); |
|
|
for (let key of Object.keys(settings)) { |
|
|
if (settings[key]) { |
|
|
formData.append('to_format', settings[key]); |
|
|
} |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(secret_sauce_url(), { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
const errorData = await response.json().catch(() => ({})); |
|
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`); |
|
|
} |
|
|
|
|
|
const result = await response.json(); |
|
|
|
|
|
return { |
|
|
id: file.id, |
|
|
originalName: file.name, |
|
|
processedName: result.new_filename, |
|
|
url: "/image/download?id=" + result.new_filename, |
|
|
other_info: result.other_info |
|
|
}; |
|
|
} catch (error) { |
|
|
throw new Error(`Failed to process ${file.name}: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
function downloadAllFiles() { |
|
|
if (completedFiles.length === 0) return; |
|
|
|
|
|
|
|
|
completedFiles.forEach((file, index) => { |
|
|
setTimeout(() => { |
|
|
const link = document.createElement('a'); |
|
|
link.href = file.url; |
|
|
link.download = file.processedName; |
|
|
link.target = '_blank'; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
}, index * 100); |
|
|
}); |
|
|
} |
|
|
|
|
|
function formatFileSize(bytes) { |
|
|
if (bytes === 0) return '0 Bytes'; |
|
|
const k = 1024; |
|
|
const sizes = ['Bytes', 'KB', 'MB', 'GB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
|
} |
|
|
|
|
|
function removeFile(fileId) { |
|
|
selectedFiles = selectedFiles.filter(file => file.id !== fileId); |
|
|
completedFiles = completedFiles.filter(file => file.id !== fileId); |
|
|
removeFileIdFromStorage(fileId); |
|
|
updateUI(); |
|
|
|
|
|
if (selectedFiles.length === 0) { |
|
|
document.getElementById('download-button').style.display = 'none'; |
|
|
} |
|
|
} |
|
|
|
|
|
async function clearAllFiles() { |
|
|
const storageData = JSON.parse(localStorage.getItem('uploadedFileData') || '{}'); |
|
|
const fileIds = Object.keys(storageData); |
|
|
|
|
|
if (fileIds.length > 0) { |
|
|
try { |
|
|
await fetch('/image/delete', { |
|
|
method: 'POST', |
|
|
headers: { |
|
|
'Content-Type': 'application/json' |
|
|
}, |
|
|
body: JSON.stringify({ ids: fileIds }) |
|
|
}); |
|
|
} catch (err) { |
|
|
console.error('Error sending delete request:', err); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
selectedFiles = []; |
|
|
completedFiles = []; |
|
|
updateUI(); |
|
|
document.getElementById('download-button').style.display = 'none'; |
|
|
document.getElementById('progress-section').style.display = 'none'; |
|
|
localStorage.removeItem('uploadedFileData'); |
|
|
} |
|
|
|
|
|
async function upload(file) { |
|
|
const formData = new FormData(); |
|
|
formData.append('image', file.file); |
|
|
formData.append('id', file.id); |
|
|
|
|
|
try { |
|
|
const response = await fetch('/image/upload', { |
|
|
method: 'POST', |
|
|
body: formData |
|
|
}); |
|
|
|
|
|
if (!response.ok) { |
|
|
const errorData = await response.json().catch(() => ({})); |
|
|
throw new Error(errorData.message || `HTTP error! status: ${response.status}`); |
|
|
} |
|
|
return true; |
|
|
} catch (error) { |
|
|
throw new Error(`Failed to upload ${file.name}: ${error.message}`); |
|
|
} |
|
|
} |
|
|
|
|
|
function getSettingsData() { |
|
|
return { |
|
|
to_format: document.getElementById('output-format')?.value |
|
|
}; |
|
|
} |
|
|
|
|
|
function getStatusText(status) { |
|
|
switch (status) { |
|
|
case 'ready': return 'Ready'; |
|
|
case 'uploading': return 'Uploading...'; |
|
|
case 'processing': return window.location.pathname == '/image/remove_metadata' ? 'Removing metadata...' : 'Converting...'; |
|
|
case 'completed': return 'Processed'; |
|
|
case 'error': return 'Error'; |
|
|
default: return 'Unknown'; |
|
|
} |
|
|
} |
|
|
function showMetadata(fileId) { |
|
|
const file = selectedFiles.find(f => f.id === fileId); |
|
|
const contentEl = document.getElementById('metadata-content'); |
|
|
if (file && file.other_info) { |
|
|
contentEl.textContent = JSON.stringify(file.other_info, null, 2); |
|
|
document.getElementById('metadata-modal').style.display = 'block'; |
|
|
document.getElementById('modal-backdrop').style.display = 'block'; |
|
|
} else { |
|
|
contentEl.textContent = 'No metadata removed or unavailable.'; |
|
|
document.getElementById('metadata-modal').style.display = 'block'; |
|
|
document.getElementById('modal-backdrop').style.display = 'block'; |
|
|
} |
|
|
} |
|
|
|
|
|
function closeMetadataModal() { |
|
|
document.getElementById('metadata-modal').style.display = 'none'; |
|
|
document.getElementById('modal-backdrop').style.display = 'none'; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function showError(fileId) { |
|
|
const file = selectedFiles.find(f => f.id === fileId); |
|
|
if (file && file.errorMessage) { |
|
|
|
|
|
showMessage(`Error: ${file.errorMessage}`, 'error'); |
|
|
} else { |
|
|
showMessage('Unknown error occurred', 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function downloadFile(fileId) { |
|
|
const completedFile = completedFiles.find(f => f.id === fileId); |
|
|
if (completedFile) { |
|
|
const link = document.createElement('a'); |
|
|
link.href = completedFile.url; |
|
|
link.download = completedFile.processedName; |
|
|
link.target = '_blank'; |
|
|
document.body.appendChild(link); |
|
|
link.click(); |
|
|
document.body.removeChild(link); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function viewImage(fileId) { |
|
|
const completedFile = completedFiles.find(f => f.id === fileId); |
|
|
if (completedFile) { |
|
|
|
|
|
let modal = document.getElementById('image-view-modal'); |
|
|
let backdrop = document.getElementById('image-view-backdrop'); |
|
|
|
|
|
if (!modal) { |
|
|
|
|
|
backdrop = document.createElement('div'); |
|
|
backdrop.id = 'image-view-backdrop'; |
|
|
backdrop.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.8); z-index: 9998; display: none;'; |
|
|
backdrop.onclick = closeImageModal; |
|
|
document.body.appendChild(backdrop); |
|
|
|
|
|
|
|
|
modal = document.createElement('div'); |
|
|
modal.id = 'image-view-modal'; |
|
|
modal.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; display: none; max-width: 90%; max-height: 90%; background: white; border-radius: 10px; padding: 10px; box-shadow: 0 0 30px rgba(0,0,0,0.5);'; |
|
|
modal.innerHTML = ` |
|
|
<button onclick="closeImageModal()" style="position: absolute; top: -10px; right: -10px; width: 30px; height: 30px; border-radius: 50%; border: none; background: #dc3545; color: white; font-size: 18px; cursor: pointer; z-index: 10000;">×</button> |
|
|
<img id="image-view-content" style="max-width: 80vw; max-height: 80vh; display: block; border-radius: 5px;" /> |
|
|
`; |
|
|
document.body.appendChild(modal); |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('image-view-content').src = completedFile.url; |
|
|
modal.style.display = 'block'; |
|
|
backdrop.style.display = 'block'; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function closeImageModal() { |
|
|
const modal = document.getElementById('image-view-modal'); |
|
|
const backdrop = document.getElementById('image-view-backdrop'); |
|
|
if (modal) modal.style.display = 'none'; |
|
|
if (backdrop) backdrop.style.display = 'none'; |
|
|
} |
|
|
|