This commit is contained in:
2026-03-01 20:51:26 +00:00
parent a0bdcda7bf
commit 3cd7b78995
15 changed files with 859 additions and 258 deletions

View File

@@ -1,77 +1,113 @@
{% extends 'base.html' %}
{% block content %}
<section class="card form-card">
<section class="card upload-card">
<h1>{{ t('upload') }}</h1>
<form method="post" enctype="multipart/form-data" class="form-grid">
<label>
{{ t('file') }}
<input id="photo-input" type="file" name="photo" accept="image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif" multiple />
<p class="upload-intro">{{ t('upload_intro') }}</p>
<form id="upload-form" method="post" enctype="multipart/form-data" class="form-grid">
<label class="upload-picker" for="photo-input">
<span class="upload-picker-title">{{ t('file') }}</span>
<span class="upload-picker-subtitle">{{ t('upload_picker_hint') }}</span>
<input id="photo-input" class="sr-only" type="file" name="photo" accept="image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif" multiple />
</label>
<p class="upload-hint">{{ t('upload_multi_hint') }}</p>
<div id="extra-file-inputs"></div>
<button id="add-file-input" class="btn btn-ghost" type="button">{{ t('add_more_files') }}</button>
<p id="upload-selected-count" class="upload-count"></p>
<p id="upload-ready-hint" class="upload-ready">{{ t('upload_ready') }}</p>
<ul id="upload-file-list" class="upload-file-list"></ul>
<button class="btn" type="submit">{{ t('upload_submit') }}</button>
<button id="upload-submit-btn" class="btn" type="submit" disabled>{{ t('upload_submit') }}</button>
</form>
</section>
<script>
(() => {
const addBtn = document.getElementById("add-file-input");
const extraInputs = document.getElementById("extra-file-inputs");
const form = document.getElementById("upload-form");
const fileInput = document.getElementById("photo-input");
const countEl = document.getElementById("upload-selected-count");
const readyEl = document.getElementById("upload-ready-hint");
const listEl = document.getElementById("upload-file-list");
const submitBtn = document.getElementById("upload-submit-btn");
const countTpl = {{ t('upload_selected_count')|tojson }};
const selectedFiles = [];
const allInputs = () => Array.from(document.querySelectorAll('input[name="photo"]'));
const fileKey = (file) => `${file.name}__${file.size}__${file.lastModified}`;
const createExtraInput = () => {
const wrapper = document.createElement("label");
wrapper.className = "extra-file-input";
wrapper.textContent = {{ t('file')|tojson }};
const input = document.createElement("input");
input.type = "file";
input.name = "photo";
input.accept = "image/jpeg,image/png,image/jpg,image/heic,image/heif,.heic,.heif";
input.required = false;
input.addEventListener("change", renderSelection);
wrapper.appendChild(input);
extraInputs.appendChild(wrapper);
const syncInputFiles = () => {
const transfer = new DataTransfer();
selectedFiles.forEach((file) => transfer.items.add(file));
fileInput.files = transfer.files;
};
const renderSelection = () => {
const names = [];
allInputs().forEach((input) => {
Array.from(input.files || []).forEach((file) => names.push(file.name));
});
if (!names.length) {
if (!selectedFiles.length) {
countEl.textContent = "";
listEl.innerHTML = "";
if (readyEl) {
readyEl.classList.remove("is-visible");
}
if (submitBtn) {
submitBtn.disabled = true;
}
return;
}
countEl.textContent = countTpl.replace("{count}", String(names.length));
countEl.textContent = countTpl.replace("{count}", String(selectedFiles.length));
listEl.innerHTML = "";
names.slice(0, 20).forEach((name) => {
if (readyEl) {
readyEl.classList.add("is-visible");
}
if (submitBtn) {
submitBtn.disabled = false;
}
selectedFiles.slice(0, 20).forEach((file, index) => {
const item = document.createElement("li");
item.textContent = name;
const label = document.createElement("span");
label.textContent = file.name;
const removeBtn = document.createElement("button");
removeBtn.type = "button";
removeBtn.className = "upload-file-remove";
removeBtn.setAttribute("aria-label", "remove file");
removeBtn.textContent = "×";
removeBtn.addEventListener("click", () => {
selectedFiles.splice(index, 1);
syncInputFiles();
renderSelection();
});
item.appendChild(label);
item.appendChild(removeBtn);
listEl.appendChild(item);
});
if (names.length > 20) {
if (selectedFiles.length > 20) {
const more = document.createElement("li");
more.textContent = `+ ${names.length - 20} weitere`;
more.textContent = `+ ${selectedFiles.length - 20} weitere`;
listEl.appendChild(more);
}
};
allInputs().forEach((input) => input.addEventListener("change", renderSelection));
addBtn.addEventListener("click", () => {
createExtraInput();
fileInput.addEventListener("change", () => {
const existingKeys = new Set(selectedFiles.map(fileKey));
Array.from(fileInput.files || []).forEach((file) => {
const key = fileKey(file);
if (!existingKeys.has(key)) {
selectedFiles.push(file);
existingKeys.add(key);
}
});
syncInputFiles();
fileInput.value = "";
renderSelection();
});
form.addEventListener("submit", (event) => {
if (!selectedFiles.length) {
event.preventDefault();
renderSelection();
return;
}
syncInputFiles();
});
renderSelection();
})();
</script>
{% endblock %}