Bugfix + Feat Besseres Laden + Bildervorschau und Uploadbalken + Popup wirklich bild löschen?
This commit is contained in:
@@ -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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user