293 lines
8.6 KiB
HTML
293 lines
8.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}Upload{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
#dropzone {
|
|
border: 2px dashed var(--pico-primary);
|
|
border-radius: var(--pico-border-radius);
|
|
padding: 3rem 1rem;
|
|
text-align: center;
|
|
cursor: pointer;
|
|
transition: background 0.2s, border-color 0.2s;
|
|
margin-bottom: 1rem;
|
|
}
|
|
#dropzone.drag-over {
|
|
background: var(--pico-primary-background);
|
|
border-color: var(--pico-primary-hover);
|
|
color: var(--pico-primary-inverse);
|
|
}
|
|
#dropzone.has-files {
|
|
padding: 1.5rem 1rem;
|
|
}
|
|
#dropzone-icon {
|
|
font-size: 2.5rem;
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
opacity: 0.5;
|
|
}
|
|
#dropzone.drag-over #dropzone-icon {
|
|
opacity: 1;
|
|
}
|
|
|
|
#file-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
.file-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.5rem;
|
|
border: 1px solid var(--pico-card-border-color);
|
|
border-radius: var(--pico-border-radius);
|
|
background: var(--pico-card-background-color);
|
|
}
|
|
.file-item .thumb {
|
|
width: 64px;
|
|
height: 48px;
|
|
object-fit: cover;
|
|
border-radius: 4px;
|
|
flex-shrink: 0;
|
|
}
|
|
.file-item .info {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.file-item .filename {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
font-size: 0.875rem;
|
|
}
|
|
.file-item .status {
|
|
font-size: 0.75rem;
|
|
opacity: 0.7;
|
|
}
|
|
.file-item .progress-wrap {
|
|
width: 120px;
|
|
flex-shrink: 0;
|
|
}
|
|
.file-item progress {
|
|
width: 100%;
|
|
height: 6px;
|
|
}
|
|
.file-item .badge {
|
|
font-size: 0.75rem;
|
|
padding: 0.15rem 0.5rem;
|
|
border-radius: 4px;
|
|
flex-shrink: 0;
|
|
}
|
|
.badge-ok { background: var(--pico-color-green); color: #fff; }
|
|
.badge-err { background: var(--pico-color-red); color: #fff; }
|
|
.badge-pending { background: var(--pico-muted-color); color: var(--pico-muted-color); }
|
|
.badge-uploading { background: var(--pico-primary); color: #fff; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<h1>Upload Images</h1>
|
|
|
|
<article>
|
|
<div id="dropzone">
|
|
<span id="dropzone-icon">📁</span>
|
|
<p><strong>Drag & drop images here</strong></p>
|
|
<p>or <a href="#" onclick="document.getElementById('file-input').click(); return false;">browse</a> to select files</p>
|
|
<input type="file" id="file-input" name="files" multiple
|
|
accept="image/png,image/jpeg,image/gif,image/bmp,image/webp"
|
|
style="display:none">
|
|
</div>
|
|
|
|
<div id="file-list"></div>
|
|
|
|
<div id="upload-options" style="display:none; margin-top:1rem;">
|
|
<label>
|
|
<input type="checkbox" id="show-now" checked>
|
|
Show on display after upload
|
|
</label>
|
|
</div>
|
|
|
|
<div id="upload-summary" style="display:none; margin-top:1rem;">
|
|
<article id="summary-box">
|
|
<header>Upload Complete</header>
|
|
<p id="summary-text"></p>
|
|
<div class="grid">
|
|
<a href="/gallery" role="button" class="contrast">Open Gallery</a>
|
|
<a href="/upload" role="button" class="secondary">Upload More</a>
|
|
</div>
|
|
</article>
|
|
</div>
|
|
</article>
|
|
|
|
<script>
|
|
var dropzone = document.getElementById('dropzone');
|
|
var fileInput = document.getElementById('file-input');
|
|
var fileList = document.getElementById('file-list');
|
|
var options = document.getElementById('upload-options');
|
|
var showNow = document.getElementById('show-now');
|
|
var summary = document.getElementById('upload-summary');
|
|
var summaryText = document.getElementById('summary-text');
|
|
|
|
var queue = [];
|
|
var uploading = false;
|
|
var results = { ok: 0, err: 0 };
|
|
var lastUploadedId = null;
|
|
|
|
function addFiles(files) {
|
|
for (var i = 0; i < files.length; i++) {
|
|
if (!files[i].type.match(/^image\//)) continue;
|
|
queue.push(files[i]);
|
|
renderFile(files[i]);
|
|
}
|
|
options.style.display = 'block';
|
|
dropzone.classList.add('has-files');
|
|
if (!uploading) processQueue();
|
|
}
|
|
|
|
function renderFile(file) {
|
|
var id = file.name + '-' + file.lastModified;
|
|
var div = document.createElement('div');
|
|
div.className = 'file-item';
|
|
div.id = 'file-' + id.replace(/[^a-zA-Z0-9]/g, '-');
|
|
|
|
var img = '';
|
|
if (file.type.match(/^image\//)) {
|
|
var url = URL.createObjectURL(file);
|
|
div.dataset.blobUrl = url;
|
|
img = '<img class="thumb" src="' + url + '" alt="">';
|
|
}
|
|
|
|
div.innerHTML = img +
|
|
'<div class="info">' +
|
|
'<div class="filename">' + escapeHtml(file.name) + '</div>' +
|
|
'<div class="status" id="status-' + id.replace(/[^a-zA-Z0-9]/g, '-') + '">Queued</div>' +
|
|
'</div>' +
|
|
'<div class="progress-wrap"><progress id="prog-' + id.replace(/[^a-zA-Z0-9]/g, '-') + '" value="0" max="100"></progress></div>' +
|
|
'<span class="badge badge-pending" id="badge-' + id.replace(/[^a-zA-Z0-9]/g, '-') + '">Pending</span>';
|
|
fileList.appendChild(div);
|
|
}
|
|
|
|
function updateFileStatus(file, status, pct, badgeClass, badgeText) {
|
|
var id = 'file-' + (file.name + '-' + file.lastModified).replace(/[^a-zA-Z0-9]/g, '-');
|
|
var el = document.getElementById(id);
|
|
if (!el) return;
|
|
el.querySelector('.status').textContent = status;
|
|
el.querySelector('progress').value = pct;
|
|
var badge = el.querySelector('.badge');
|
|
badge.className = 'badge ' + (badgeClass || 'badge-pending');
|
|
badge.textContent = badgeText || status;
|
|
if (badgeClass === 'badge-ok' || badgeClass === 'badge-err') {
|
|
var blobUrl = el.dataset.blobUrl;
|
|
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
}
|
|
}
|
|
|
|
async function processQueue() {
|
|
if (queue.length === 0) {
|
|
uploading = false;
|
|
showSummary();
|
|
return;
|
|
}
|
|
uploading = true;
|
|
var file = queue.shift();
|
|
|
|
updateFileStatus(file, 'Uploading...', 0, 'badge-uploading', 'Uploading');
|
|
|
|
try {
|
|
var formData = new FormData();
|
|
formData.append('file', file);
|
|
formData.append('title', file.name.replace(/\.[^.]+$/, ''));
|
|
formData.append('show_now', '0');
|
|
|
|
var xhr = new XMLHttpRequest();
|
|
xhr.upload.onprogress = function(e) {
|
|
if (e.lengthComputable) {
|
|
var pct = Math.round((e.loaded / e.total) * 100);
|
|
updateFileStatus(file, 'Uploading... ' + pct + '%', pct, 'badge-uploading', pct + '%');
|
|
}
|
|
};
|
|
|
|
var data = await new Promise(function(resolve, reject) {
|
|
xhr.onload = function() {
|
|
if (xhr.status >= 200 && xhr.status < 300) {
|
|
try { resolve(JSON.parse(xhr.responseText)); }
|
|
catch { resolve(null); }
|
|
} else {
|
|
try {
|
|
var err = JSON.parse(xhr.responseText);
|
|
reject(new Error(err.error || 'HTTP ' + xhr.status));
|
|
} catch { reject(new Error('HTTP ' + xhr.status)); }
|
|
}
|
|
};
|
|
xhr.onerror = function() { reject(new Error('Network error')); };
|
|
xhr.open('POST', '/api/images');
|
|
xhr.send(formData);
|
|
});
|
|
|
|
results.ok++;
|
|
lastUploadedId = data ? data.id : null;
|
|
updateFileStatus(file, 'Uploaded', 100, 'badge-ok', 'OK');
|
|
|
|
} catch (err) {
|
|
results.err++;
|
|
updateFileStatus(file, 'Failed: ' + err.message, 0, 'badge-err', 'Error');
|
|
}
|
|
|
|
processQueue();
|
|
}
|
|
|
|
function showSummary() {
|
|
if (results.ok + results.err === 0) return;
|
|
var parts = [];
|
|
if (results.ok > 0) parts.push(results.ok + ' uploaded');
|
|
if (results.err > 0) parts.push(results.err + ' failed');
|
|
summaryText.textContent = parts.join(', ') + '.';
|
|
|
|
if (showNow.checked && results.ok > 0 && lastUploadedId) {
|
|
fetch('/api/images/' + lastUploadedId + '/show', { method: 'POST' })
|
|
.then(function(r) {
|
|
if (r.ok) {
|
|
var extra = document.createElement('small');
|
|
extra.style.display = 'block';
|
|
extra.textContent = 'Last image sent to display.';
|
|
summaryText.appendChild(document.createTextNode(' '));
|
|
summaryText.appendChild(extra);
|
|
}
|
|
})
|
|
.catch(function() {});
|
|
}
|
|
|
|
summary.style.display = 'block';
|
|
}
|
|
|
|
// Drag events
|
|
dropzone.addEventListener('dragover', function(e) {
|
|
e.preventDefault();
|
|
dropzone.classList.add('drag-over');
|
|
});
|
|
dropzone.addEventListener('dragleave', function(e) {
|
|
e.preventDefault();
|
|
dropzone.classList.remove('drag-over');
|
|
});
|
|
dropzone.addEventListener('drop', function(e) {
|
|
e.preventDefault();
|
|
dropzone.classList.remove('drag-over');
|
|
addFiles(e.dataTransfer.files);
|
|
});
|
|
dropzone.addEventListener('click', function() {
|
|
fileInput.click();
|
|
});
|
|
fileInput.addEventListener('change', function() {
|
|
addFiles(this.files);
|
|
this.value = '';
|
|
});
|
|
|
|
function escapeHtml(str) {
|
|
var div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(str));
|
|
return div.innerHTML;
|
|
}
|
|
</script>
|
|
{% endblock %}
|