First commit with entire first rendition of the project

This commit is contained in:
Your Name
2026-06-25 21:18:07 -06:00
commit 551322a94b
42 changed files with 4346 additions and 0 deletions

View File

@@ -0,0 +1,292 @@
{% 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">&#x1f4c1;</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 %}