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
+43 -2
View File
@@ -47,10 +47,14 @@
class="gallery-open"
data-image-index="{{ loop.index0 }}"
>
<img src="{{ url_for('serve_upload', filename=image['filename']) }}" alt="{{ t('gallery_image_alt').format(name=image['uploaded_by_name']) }}" loading="lazy" />
<img src="{{ url_for('serve_thumbnail', filename=image['filename']) }}" alt="{{ t('gallery_image_alt').format(name=image['uploaded_by_name']) }}" loading="lazy" />
</a>
{% if is_host or guest_id == image['uploaded_by'] %}
<form class="gallery-delete-form" method="post" action="{{ url_for('delete_image', image_id=image['id']) }}">
<form
class="gallery-delete-form"
method="post"
action="{{ url_for('delete_image', image_id=image['id']) }}"
>
<button class="gallery-delete-btn" type="submit" aria-label="{{ t('delete') }}" title="{{ t('delete') }}">
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M9 3h6l1 2h4v2H4V5h4l1-2zm1 6h2v8h-2V9zm4 0h2v8h-2V9zM7 9h2v8H7V9z" />
@@ -82,8 +86,45 @@
<div class="lightbox-save-hint" id="lightbox-save-hint" role="status">{{ t('gallery_save_hint') }}</div>
</div>
<dialog class="gallery-delete-dialog" id="gallery-delete-dialog" aria-labelledby="gallery-delete-title" aria-describedby="gallery-delete-message">
<h2 id="gallery-delete-title">{{ t('gallery_delete_title') }}</h2>
<p id="gallery-delete-message">{{ t('gallery_delete_confirm') }}</p>
<div class="gallery-delete-dialog-actions">
<button class="btn btn-ghost" id="gallery-delete-cancel" type="button">{{ t('gallery_delete_cancel') }}</button>
<button class="btn btn-danger" id="gallery-delete-confirm" type="button">{{ t('delete') }}</button>
</div>
</dialog>
<script>
(() => {
const deleteDialog = document.getElementById("gallery-delete-dialog");
const deleteCancel = document.getElementById("gallery-delete-cancel");
const deleteConfirm = document.getElementById("gallery-delete-confirm");
let pendingDeleteForm = null;
document.querySelectorAll(".gallery-delete-form").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
pendingDeleteForm = form;
deleteDialog.showModal();
});
});
deleteCancel.addEventListener("click", () => deleteDialog.close());
deleteConfirm.addEventListener("click", () => {
if (pendingDeleteForm) {
HTMLFormElement.prototype.submit.call(pendingDeleteForm);
}
});
deleteDialog.addEventListener("close", () => {
pendingDeleteForm = null;
});
deleteDialog.addEventListener("click", (event) => {
if (event.target === deleteDialog) {
deleteDialog.close();
}
});
const lightbox = document.getElementById("gallery-lightbox");
const lightboxImage = document.getElementById("lightbox-image");
const lightboxDownload = document.getElementById("lightbox-download");
+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();