Bugfix + Feat Besseres Laden + Bildervorschau und Uploadbalken + Popup wirklich bild löschen?

This commit is contained in:
2026-09-20 14:53:49 +00:00
parent 77b549c960
commit 2ae0d254a3
7 changed files with 385 additions and 36 deletions
+124 -13
View File
@@ -7,12 +7,27 @@
<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 />
<input id="photo-input" class="sr-only" type="file" name="photo" accept="image/jpeg,image/png,image/jpg,image/gif,image/heic,image/heif,.gif,.heic,.heif" multiple />
</label>
<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>
<div id="upload-progress" class="upload-progress" hidden>
<div
id="upload-progress-track"
class="upload-progress-track"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
aria-valuenow="0"
>
<span id="upload-progress-bar"></span>
</div>
<p id="upload-progress-count" class="upload-progress-count" aria-live="polite"></p>
<p id="upload-progress-status" class="upload-progress-status">{{ t('upload_in_progress') }}</p>
</div>
<button id="upload-submit-btn" class="btn" type="submit" disabled>{{ t('upload_submit') }}</button>
<button id="upload-retry-btn" class="btn upload-retry-btn" type="button" hidden>{{ t('upload_retry') }}</button>
</form>
</section>
@@ -24,17 +39,27 @@
const readyEl = document.getElementById("upload-ready-hint");
const listEl = document.getElementById("upload-file-list");
const submitBtn = document.getElementById("upload-submit-btn");
const retryBtn = document.getElementById("upload-retry-btn");
const progressEl = document.getElementById("upload-progress");
const progressTrack = document.getElementById("upload-progress-track");
const progressBar = document.getElementById("upload-progress-bar");
const progressCount = document.getElementById("upload-progress-count");
const progressStatus = document.getElementById("upload-progress-status");
const countTpl = {{ t('upload_selected_count')|tojson }};
const progressTpl = {{ t('upload_progress')|tojson }};
const inProgressText = {{ t('upload_in_progress')|tojson }};
const batchFailedText = {{ t('upload_batch_failed')|tojson }};
const galleryUrl = {{ url_for('gallery')|tojson }};
const maxBatchFiles = 10;
const maxBatchBytes = 40 * 1024 * 1024;
const selectedFiles = [];
let remainingFiles = [];
let totalFiles = 0;
let uploadedFiles = 0;
let isUploading = false;
const fileKey = (file) => `${file.name}__${file.size}__${file.lastModified}`;
const syncInputFiles = () => {
const transfer = new DataTransfer();
selectedFiles.forEach((file) => transfer.items.add(file));
fileInput.files = transfer.files;
};
const renderSelection = () => {
if (!selectedFiles.length) {
countEl.textContent = "";
@@ -54,7 +79,7 @@
readyEl.classList.add("is-visible");
}
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.disabled = isUploading;
}
selectedFiles.slice(0, 20).forEach((file, index) => {
@@ -69,7 +94,6 @@
removeBtn.textContent = "×";
removeBtn.addEventListener("click", () => {
selectedFiles.splice(index, 1);
syncInputFiles();
renderSelection();
});
@@ -93,18 +117,105 @@
existingKeys.add(key);
}
});
syncInputFiles();
fileInput.value = "";
renderSelection();
});
const updateProgress = () => {
const percent = totalFiles ? Math.round((uploadedFiles / totalFiles) * 100) : 0;
progressBar.style.width = `${percent}%`;
progressTrack.setAttribute("aria-valuenow", String(percent));
progressCount.textContent = progressTpl
.replace("{uploaded}", String(uploadedFiles))
.replace("{total}", String(totalFiles));
};
const nextBatch = () => {
let batchSize = 0;
let batchLength = 0;
while (batchLength < remainingFiles.length && batchLength < maxBatchFiles) {
const nextSize = remainingFiles[batchLength].size;
if (batchLength > 0 && batchSize + nextSize > maxBatchBytes) {
break;
}
batchSize += nextSize;
batchLength += 1;
}
return remainingFiles.slice(0, batchLength);
};
const setUploadLocked = (locked) => {
fileInput.disabled = locked;
submitBtn.disabled = locked;
document.querySelectorAll(".upload-file-remove").forEach((button) => {
button.disabled = locked;
});
form.classList.toggle("is-uploading", locked);
};
const uploadRemainingFiles = async () => {
if (isUploading || !remainingFiles.length) return;
isUploading = true;
setUploadLocked(true);
retryBtn.hidden = true;
progressEl.hidden = false;
progressStatus.textContent = inProgressText;
progressStatus.classList.remove("is-error");
updateProgress();
try {
while (remainingFiles.length) {
const batch = nextBatch();
const body = new FormData();
batch.forEach((file) => body.append("photo", file, file.name));
const response = await fetch(form.action || window.location.href, {
method: "POST",
body,
credentials: "same-origin",
headers: { "X-Upload-Batch": "1" }
});
const contentType = response.headers.get("content-type") || "";
const payload = contentType.includes("application/json") ? await response.json() : null;
if (!response.ok || !payload || !payload.ok) {
throw new Error(payload?.error || batchFailedText);
}
remainingFiles.splice(0, batch.length);
uploadedFiles += batch.length;
updateProgress();
}
window.location.assign(galleryUrl);
} catch (error) {
progressStatus.textContent = `${batchFailedText} ${error.message || ""}`.trim();
progressStatus.classList.add("is-error");
retryBtn.hidden = false;
} finally {
isUploading = false;
form.classList.remove("is-uploading");
if (remainingFiles.length) {
fileInput.disabled = true;
submitBtn.disabled = true;
}
}
};
form.addEventListener("submit", (event) => {
if (!selectedFiles.length) {
event.preventDefault();
event.preventDefault();
if (!selectedFiles.length || isUploading) {
renderSelection();
return;
}
syncInputFiles();
totalFiles = selectedFiles.length;
uploadedFiles = 0;
remainingFiles = [...selectedFiles];
uploadRemainingFiles();
});
retryBtn.addEventListener("click", uploadRemainingFiles);
window.addEventListener("beforeunload", (event) => {
if (!isUploading) return;
event.preventDefault();
event.returnValue = "";
});
renderSelection();