Viele neue Features
This commit is contained in:
@@ -30,6 +30,7 @@
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
|
||||
{% with messages = get_flashed_messages() %}
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
|
||||
@@ -5,14 +5,191 @@
|
||||
{% if images %}
|
||||
<div class="gallery-grid">
|
||||
{% for image in images %}
|
||||
<figure class="gallery-item">
|
||||
<a href="{{ url_for('serve_upload', filename=image['filename']) }}" target="_blank" rel="noopener">
|
||||
<img src="{{ url_for('serve_upload', filename=image['filename']) }}" alt="{{ t('gallery_image_alt').format(name=image['uploaded_by']) }}" loading="lazy" />
|
||||
</a>
|
||||
<figcaption>{{ t('gallery_uploaded_by').format(name=image['uploaded_by']) }}</figcaption>
|
||||
<figure class="gallery-item gallery-card">
|
||||
<div class="gallery-media">
|
||||
<a
|
||||
href="{{ url_for('serve_upload', filename=image['filename']) }}"
|
||||
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" />
|
||||
</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']) }}">
|
||||
<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" />
|
||||
</svg>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<figcaption>{{ t('gallery_uploaded_by').format(name=image['uploaded_by_name']) }}</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
|
||||
<button class="lightbox-close" type="button" aria-label="Close">×</button>
|
||||
<div class="lightbox-counter" id="lightbox-counter">1 / 1</div>
|
||||
<a class="lightbox-download" id="lightbox-download" href="#" aria-label="{{ t('download') }}" title="{{ t('download') }}">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M12 3a1 1 0 0 1 1 1v8.59l2.3-2.29a1 1 0 1 1 1.4 1.41l-4 4a1 1 0 0 1-1.4 0l-4-4a1 1 0 1 1 1.4-1.41L11 12.59V4a1 1 0 0 1 1-1zm-7 14a1 1 0 0 1 1 1v1h12v-1a1 1 0 1 1 2 0v2a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-2a1 1 0 0 1 1-1z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<button class="lightbox-nav lightbox-prev" type="button" aria-label="Previous image">‹</button>
|
||||
<img class="lightbox-image" id="lightbox-image" alt="" />
|
||||
<button class="lightbox-nav lightbox-next" type="button" aria-label="Next image">›</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const lightbox = document.getElementById("gallery-lightbox");
|
||||
const lightboxImage = document.getElementById("lightbox-image");
|
||||
const lightboxDownload = document.getElementById("lightbox-download");
|
||||
const lightboxCounter = document.getElementById("lightbox-counter");
|
||||
const closeBtn = lightbox.querySelector(".lightbox-close");
|
||||
const prevBtn = lightbox.querySelector(".lightbox-prev");
|
||||
const nextBtn = lightbox.querySelector(".lightbox-next");
|
||||
const openButtons = Array.from(document.querySelectorAll(".gallery-open"));
|
||||
const images = [
|
||||
{% for image in images %}
|
||||
{
|
||||
src: {{ url_for('serve_upload', filename=image['filename'])|tojson }},
|
||||
download: {{ url_for('serve_upload', filename=image['filename'], download=1)|tojson }},
|
||||
alt: {{ t('gallery_image_alt').format(name=image['uploaded_by_name'])|tojson }}
|
||||
}{% if not loop.last %},{% endif %}
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
let currentIndex = 0;
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
let didSwipe = false;
|
||||
let controlsTimer = null;
|
||||
|
||||
const scheduleHideControls = () => {
|
||||
clearTimeout(controlsTimer);
|
||||
controlsTimer = window.setTimeout(() => {
|
||||
lightbox.classList.add("lightbox-controls-hidden");
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
const showControlsTemporarily = () => {
|
||||
lightbox.classList.remove("lightbox-controls-hidden");
|
||||
scheduleHideControls();
|
||||
};
|
||||
|
||||
const setImage = (index, options = {}) => {
|
||||
const { revealControls = true } = options;
|
||||
if (!images.length) {
|
||||
return;
|
||||
}
|
||||
currentIndex = (index + images.length) % images.length;
|
||||
const item = images[currentIndex];
|
||||
lightboxImage.classList.remove("is-fading");
|
||||
void lightboxImage.offsetWidth;
|
||||
lightboxImage.classList.add("is-fading");
|
||||
lightboxImage.src = item.src;
|
||||
lightboxImage.alt = item.alt;
|
||||
lightboxDownload.href = item.download;
|
||||
lightboxCounter.textContent = `${currentIndex + 1} / ${images.length}`;
|
||||
if (revealControls) {
|
||||
showControlsTemporarily();
|
||||
}
|
||||
};
|
||||
|
||||
const openLightbox = (index) => {
|
||||
setImage(index);
|
||||
lightbox.classList.add("is-open");
|
||||
lightbox.setAttribute("aria-hidden", "false");
|
||||
document.body.classList.add("no-scroll");
|
||||
showControlsTemporarily();
|
||||
};
|
||||
|
||||
const closeLightbox = () => {
|
||||
lightbox.classList.remove("is-open");
|
||||
lightbox.setAttribute("aria-hidden", "true");
|
||||
document.body.classList.remove("no-scroll");
|
||||
lightbox.classList.remove("lightbox-controls-hidden");
|
||||
clearTimeout(controlsTimer);
|
||||
};
|
||||
|
||||
openButtons.forEach((link) => {
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
openLightbox(Number(link.dataset.imageIndex || 0));
|
||||
});
|
||||
});
|
||||
|
||||
prevBtn.addEventListener("click", () => setImage(currentIndex - 1, { revealControls: true }));
|
||||
nextBtn.addEventListener("click", () => setImage(currentIndex + 1, { revealControls: true }));
|
||||
closeBtn.addEventListener("click", closeLightbox);
|
||||
|
||||
lightbox.addEventListener("mousemove", () => {
|
||||
if (lightbox.classList.contains("is-open")) {
|
||||
showControlsTemporarily();
|
||||
}
|
||||
});
|
||||
|
||||
lightbox.addEventListener("click", (event) => {
|
||||
if (event.target === lightbox) {
|
||||
closeLightbox();
|
||||
} else if (
|
||||
lightbox.classList.contains("is-open") &&
|
||||
event.target.closest(".lightbox-close, .lightbox-download, .lightbox-nav")
|
||||
) {
|
||||
showControlsTemporarily();
|
||||
}
|
||||
});
|
||||
|
||||
lightboxImage.addEventListener("touchstart", (event) => {
|
||||
const point = event.changedTouches[0];
|
||||
touchStartX = point.clientX;
|
||||
touchStartY = point.clientY;
|
||||
didSwipe = false;
|
||||
}, { passive: true });
|
||||
|
||||
lightboxImage.addEventListener("touchend", (event) => {
|
||||
const point = event.changedTouches[0];
|
||||
const deltaX = point.clientX - touchStartX;
|
||||
const deltaY = point.clientY - touchStartY;
|
||||
const absX = Math.abs(deltaX);
|
||||
const absY = Math.abs(deltaY);
|
||||
if (absX < 40 || absX <= absY) {
|
||||
return;
|
||||
}
|
||||
didSwipe = true;
|
||||
if (deltaX < 0) {
|
||||
setImage(currentIndex + 1, { revealControls: false });
|
||||
} else {
|
||||
setImage(currentIndex - 1, { revealControls: false });
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
lightboxImage.addEventListener("click", (event) => {
|
||||
if (didSwipe) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
didSwipe = false;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (!lightbox.classList.contains("is-open")) {
|
||||
return;
|
||||
}
|
||||
showControlsTemporarily();
|
||||
if (event.key === "Escape") {
|
||||
closeLightbox();
|
||||
} else if (event.key === "ArrowLeft") {
|
||||
setImage(currentIndex - 1, { revealControls: true });
|
||||
} else if (event.key === "ArrowRight") {
|
||||
setImage(currentIndex + 1, { revealControls: true });
|
||||
}
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% else %}
|
||||
<p>{{ t('gallery_empty') }}</p>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
{% extends 'base.html' %}
|
||||
{% block content %}
|
||||
<section class="hero card">
|
||||
<h1>{{ t('subtitle') }}</h1>
|
||||
<p>{{ t('login_note') }}</p>
|
||||
</section>
|
||||
<div class="login-layout">
|
||||
<section class="hero card">
|
||||
<h1>{{ t('subtitle') }}</h1>
|
||||
<p>{{ t('login_note') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="card form-card">
|
||||
<h2>{{ t('login') }}</h2>
|
||||
<form method="post" action="{{ url_for('login') }}" class="form-grid">
|
||||
<label>
|
||||
{{ t('name') }}
|
||||
<input type="text" name="name" required />
|
||||
</label>
|
||||
<section class="card form-card">
|
||||
<h2>{{ t('login') }}</h2>
|
||||
<form method="post" action="{{ url_for('login') }}" class="form-grid">
|
||||
<label>
|
||||
{{ t('name') }}
|
||||
<input type="text" name="name" required />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
{{ t('event_password') }}
|
||||
<input type="password" name="event_password" required />
|
||||
</label>
|
||||
<label>
|
||||
{{ t('event_password') }}
|
||||
<input type="password" name="event_password" required />
|
||||
</label>
|
||||
|
||||
<button class="btn" type="submit">{{ t('login_submit') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
<button class="btn" type="submit">{{ t('login_submit') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -5,9 +5,73 @@
|
||||
<form method="post" enctype="multipart/form-data" class="form-grid">
|
||||
<label>
|
||||
{{ t('file') }}
|
||||
<input type="file" name="photo" accept=".jpg,.jpeg,.png" required />
|
||||
<input id="photo-input" 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>
|
||||
<ul id="upload-file-list" class="upload-file-list"></ul>
|
||||
<button class="btn" type="submit">{{ t('upload_submit') }}</button>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(() => {
|
||||
const addBtn = document.getElementById("add-file-input");
|
||||
const extraInputs = document.getElementById("extra-file-inputs");
|
||||
const countEl = document.getElementById("upload-selected-count");
|
||||
const listEl = document.getElementById("upload-file-list");
|
||||
const countTpl = {{ t('upload_selected_count')|tojson }};
|
||||
|
||||
const allInputs = () => Array.from(document.querySelectorAll('input[name="photo"]'));
|
||||
|
||||
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 renderSelection = () => {
|
||||
const names = [];
|
||||
allInputs().forEach((input) => {
|
||||
Array.from(input.files || []).forEach((file) => names.push(file.name));
|
||||
});
|
||||
|
||||
if (!names.length) {
|
||||
countEl.textContent = "";
|
||||
listEl.innerHTML = "";
|
||||
return;
|
||||
}
|
||||
|
||||
countEl.textContent = countTpl.replace("{count}", String(names.length));
|
||||
listEl.innerHTML = "";
|
||||
names.slice(0, 20).forEach((name) => {
|
||||
const item = document.createElement("li");
|
||||
item.textContent = name;
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
if (names.length > 20) {
|
||||
const more = document.createElement("li");
|
||||
more.textContent = `+ ${names.length - 20} weitere`;
|
||||
listEl.appendChild(more);
|
||||
}
|
||||
};
|
||||
|
||||
allInputs().forEach((input) => input.addEventListener("change", renderSelection));
|
||||
addBtn.addEventListener("click", () => {
|
||||
createExtraInput();
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user