Feat: Fotofilter + Fotobox/graf user
This commit is contained in:
@@ -3,9 +3,44 @@
|
||||
<section class="card">
|
||||
<h1>{{ t('gallery') }}</h1>
|
||||
{% if images %}
|
||||
<div class="gallery-filter-controls">
|
||||
<div class="gallery-uploader-filter" id="gallery-uploader-filter">
|
||||
<button
|
||||
class="gallery-uploader-trigger"
|
||||
id="gallery-uploader-trigger"
|
||||
type="button"
|
||||
aria-expanded="false"
|
||||
aria-controls="gallery-uploader-menu"
|
||||
aria-label="{{ t('gallery_filter_label') }}"
|
||||
>
|
||||
<svg class="gallery-uploader-search" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="m21 20-4.35-4.35a8 8 0 1 0-1.41 1.41L19.59 21 21 20ZM5 11a6 6 0 1 1 12 0 6 6 0 0 1-12 0Z" />
|
||||
</svg>
|
||||
<span>{{ t('gallery_filter_trigger') }}</span>
|
||||
<strong id="gallery-uploader-summary">{{ t('gallery_uploader_all') }}</strong>
|
||||
<svg class="gallery-uploader-chevron" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="m7 9.5 5 5 5-5 1.4 1.4-6.4 6.4-6.4-6.4L7 9.5Z" />
|
||||
</svg>
|
||||
</button>
|
||||
<div class="gallery-uploader-menu" id="gallery-uploader-menu" aria-label="{{ t('gallery_uploader_filter_label') }}" hidden>
|
||||
<label class="gallery-uploader-option gallery-uploader-option-all">
|
||||
<input id="gallery-uploader-all" type="checkbox" checked />
|
||||
<span>{{ t('gallery_uploader_all') }}</span>
|
||||
</label>
|
||||
<div class="gallery-uploader-options">
|
||||
{% for uploader in uploaders %}
|
||||
<label class="gallery-uploader-option">
|
||||
<input type="checkbox" value="{{ uploader }}" data-uploader-option checked />
|
||||
<span>{{ uploader }}</span>
|
||||
</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery-grid">
|
||||
{% for image in images %}
|
||||
<figure class="gallery-item gallery-card">
|
||||
<figure class="gallery-item gallery-card" data-uploader="{{ image['uploaded_by_name'] }}">
|
||||
<div class="gallery-media">
|
||||
<a
|
||||
href="{{ url_for('serve_upload', filename=image['filename']) }}"
|
||||
@@ -24,10 +59,14 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<figcaption>{{ t('gallery_uploaded_by').format(name=image['uploaded_by_name']) }}</figcaption>
|
||||
<figcaption>
|
||||
<span>{{ t('gallery_uploaded_by_prefix') }}</span>
|
||||
<strong>{{ image['uploaded_by_name'] }}</strong>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p class="gallery-filter-empty" id="gallery-filter-empty" hidden>{{ t('gallery_filter_no_results') }}</p>
|
||||
|
||||
<div class="lightbox" id="gallery-lightbox" aria-hidden="true">
|
||||
<button class="lightbox-close" type="button" aria-label="Close">×</button>
|
||||
@@ -54,9 +93,22 @@
|
||||
const prevBtn = lightbox.querySelector(".lightbox-prev");
|
||||
const nextBtn = lightbox.querySelector(".lightbox-next");
|
||||
const openButtons = Array.from(document.querySelectorAll(".gallery-open"));
|
||||
const galleryItems = Array.from(document.querySelectorAll(".gallery-item"));
|
||||
const filterEmpty = document.getElementById("gallery-filter-empty");
|
||||
const uploaderFilter = document.getElementById("gallery-uploader-filter");
|
||||
const uploaderTrigger = document.getElementById("gallery-uploader-trigger");
|
||||
const uploaderMenu = document.getElementById("gallery-uploader-menu");
|
||||
const uploaderSummary = document.getElementById("gallery-uploader-summary");
|
||||
const uploaderAll = document.getElementById("gallery-uploader-all");
|
||||
const uploaderOptions = Array.from(document.querySelectorAll("[data-uploader-option]"));
|
||||
const uploaderLabelAll = {{ t('gallery_uploader_all')|tojson }};
|
||||
const uploaderLabelNone = {{ t('gallery_uploader_none')|tojson }};
|
||||
const uploaderLabelSelected = {{ t('gallery_uploader_selected')|tojson }};
|
||||
const images = [
|
||||
{% for image in images %}
|
||||
{
|
||||
index: {{ loop.index0 }},
|
||||
uploader: {{ image['uploaded_by_name']|tojson }},
|
||||
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 }}
|
||||
@@ -64,6 +116,7 @@
|
||||
{% endfor %}
|
||||
];
|
||||
|
||||
let visibleImages = [...images];
|
||||
let currentIndex = 0;
|
||||
let touchStartX = 0;
|
||||
let touchStartY = 0;
|
||||
@@ -85,18 +138,18 @@
|
||||
|
||||
const setImage = (index, options = {}) => {
|
||||
const { revealControls = true } = options;
|
||||
if (!images.length) {
|
||||
if (!visibleImages.length) {
|
||||
return;
|
||||
}
|
||||
currentIndex = (index + images.length) % images.length;
|
||||
const item = images[currentIndex];
|
||||
currentIndex = (index + visibleImages.length) % visibleImages.length;
|
||||
const item = visibleImages[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}`;
|
||||
lightboxCounter.textContent = `${currentIndex + 1} / ${visibleImages.length}`;
|
||||
if (revealControls) {
|
||||
showControlsTemporarily();
|
||||
}
|
||||
@@ -126,10 +179,71 @@
|
||||
openButtons.forEach((link) => {
|
||||
link.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
openLightbox(Number(link.dataset.imageIndex || 0));
|
||||
const imageIndex = Number(link.dataset.imageIndex || 0);
|
||||
const visibleIndex = visibleImages.findIndex((item) => item.index === imageIndex);
|
||||
if (visibleIndex >= 0) {
|
||||
openLightbox(visibleIndex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const applyFilters = () => {
|
||||
const selectedUploaders = new Set(
|
||||
uploaderOptions.filter((option) => option.checked).map((option) => option.value)
|
||||
);
|
||||
galleryItems.forEach((item) => {
|
||||
const matchesUploader = selectedUploaders.has(item.dataset.uploader || "");
|
||||
item.classList.toggle("is-filtered-out", !matchesUploader);
|
||||
});
|
||||
visibleImages = images.filter((item) => selectedUploaders.has(item.uploader));
|
||||
filterEmpty.hidden = visibleImages.length > 0;
|
||||
};
|
||||
|
||||
const syncUploaderSummary = () => {
|
||||
const selectedCount = uploaderOptions.filter((option) => option.checked).length;
|
||||
uploaderAll.checked = selectedCount === uploaderOptions.length;
|
||||
uploaderAll.indeterminate = selectedCount > 0 && selectedCount < uploaderOptions.length;
|
||||
if (selectedCount === uploaderOptions.length) {
|
||||
uploaderSummary.textContent = uploaderLabelAll;
|
||||
} else if (selectedCount === 0) {
|
||||
uploaderSummary.textContent = uploaderLabelNone;
|
||||
} else {
|
||||
uploaderSummary.textContent = uploaderLabelSelected.replace("{count}", String(selectedCount));
|
||||
}
|
||||
};
|
||||
|
||||
const closeUploaderMenu = () => {
|
||||
uploaderMenu.hidden = true;
|
||||
uploaderTrigger.setAttribute("aria-expanded", "false");
|
||||
};
|
||||
|
||||
uploaderTrigger.addEventListener("click", () => {
|
||||
const shouldOpen = uploaderMenu.hidden;
|
||||
uploaderMenu.hidden = !shouldOpen;
|
||||
uploaderTrigger.setAttribute("aria-expanded", String(shouldOpen));
|
||||
});
|
||||
|
||||
uploaderAll.addEventListener("change", () => {
|
||||
uploaderOptions.forEach((option) => {
|
||||
option.checked = uploaderAll.checked;
|
||||
});
|
||||
syncUploaderSummary();
|
||||
applyFilters();
|
||||
});
|
||||
|
||||
uploaderOptions.forEach((option) => {
|
||||
option.addEventListener("change", () => {
|
||||
syncUploaderSummary();
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!uploaderFilter.contains(event.target)) {
|
||||
closeUploaderMenu();
|
||||
}
|
||||
});
|
||||
|
||||
prevBtn.addEventListener("click", () => setImage(currentIndex - 1, { revealControls: true }));
|
||||
nextBtn.addEventListener("click", () => setImage(currentIndex + 1, { revealControls: true }));
|
||||
closeBtn.addEventListener("click", closeLightbox);
|
||||
@@ -184,6 +298,11 @@
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape" && !uploaderMenu.hidden) {
|
||||
closeUploaderMenu();
|
||||
uploaderTrigger.focus();
|
||||
return;
|
||||
}
|
||||
if (!lightbox.classList.contains("is-open")) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user