Bildergalerie
Hubzilla bietet mit den Apps “Fotos” und “Galerie” zwei Möglichkeiten, Bilder anzuzeigen. Allerdings sind beide Apps von der Funktion und der Gestaltung her eher simpel und nicht wirklich gut nutzbar.
Besser verwendbar ist das Widget “Album”.
Für eine wirklich gut nutzbare Möglichkeit, Bilder aus einem bestimmten Verzeichnis anzuzeigen, empfiehlt es sich, einen entsprechenden Block selbst zu erstellen.
Um die Handhabung so einfach wie möglich zu machen und ihn flexibel für verschiedene Bilderordner einsetzen zu können, sollte der Block selbst alle Bild-Dateien aus einem Verzeichnis, dessen URL man angibt, auslesen und für die Galerie verwenden.
In der Regel werden wir Bilder aus der eigenen Cloud des Kanals verwenden. Es ist aber auch möglich, Bilde zu nutzen, die an anderer Stelle vorliegen. Sollten sie auf einem anderen Server liegen, bei welchem CORS greift und die Bilder so nicht abgefragt werden können, soll es die Möglichkeit geben, die URLS der einzelnen Bilder einzeln in ein Array einzutragen.
Außerdem soll ein Statusanzeige existieren, die anzeigt, dass die Bilder geladen werden, falls dieser Vorgang aufgrund einer größeren Zahl von Bildern länger dauert. Außerdem sollen in der Statusanzeige Fehlermeldungen erscheinen, falls der Block falsch genutzt wurde (Fehler bei der URL etc.). Dem <div> Container geben wir die Id “status”.
Die Bilder sollen dann in einen <div> Container der Bootstrap-Klasse grid eingefügt werden, welchem wir die Id “galleryRow” geben.
Einfache Galerie
<script>
const baseUrl = '<BASIS_URL_MIT_DEN_BILDERN>'; // HIER die URL eintragen
/* Falls CORS greift, in fallbackImages[] sämtliche Bild-URLs manuell eintragen */
const fallbackImages = [
// '<URL_ZUM_EINZELBILD>',
];
const galleryRow = document.getElementById('galleryRow');
const status = document.getElementById('status');
function looksLikeImage(u){
return /\.(jpe?g|png|gif|webp|bmp|svg)(\?.*)?$/i.test(u);
}
function joinUrl(base, path){
try{ return new URL(path, base).href; }catch(e){ return base + path; }
}
function clearGallery(){ galleryRow.innerHTML = ''; }
function addCard(src){
const col = document.createElement('div');
col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
const a = document.createElement('a');
a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.className = 'thumb-link';
const img = document.createElement('img');
img.src = src; img.alt = src.split('/').pop().split('?')[0]; img.className = 'thumb';
a.appendChild(img); col.appendChild(a); galleryRow.appendChild(col);
}
async function fetchImagesFromUrl(url){
const res = await fetch(url, { mode:'cors' });
if(!res.ok) throw new Error('Abruf fehlgeschlagen: ' + res.status);
const text = await res.text();
const found = new Set();
// <img src="...">
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
let m;
while((m = imgRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
// <a href="..."> (typisch für Verzeichnis-Listings)
const aRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/gi;
while((m = aRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
return Array.from(found);
}
(async function init(){
clearGallery();
if(!baseUrl || !baseUrl.trim()){ status.textContent = 'Keine Basis-URL gefunden. Bitte im Script eintragen.'; return; }
status.textContent = 'Lade Bilder von ' + baseUrl;
try{
const imgs = await fetchImagesFromUrl(baseUrl);
if(imgs.length){
imgs.forEach(u => addCard(u));
status.textContent = `Bilder in der Galerie: ${imgs.length}`;
return;
} else {
status.textContent = 'Keine Bilder auf der Seite gefunden. Prüfe URL oder nutze fallbackImages.';
}
}catch(err){
console.warn('Ladefehler:', err);
status.textContent = 'Automatisches Laden fehlgeschlagen (möglicher CORS-/Netzwerkfehler). Nutze fallbackImages.';
}
if(fallbackImages.length === 0){
status.textContent = 'Keine Bilder verfügbar. Entweder CORS auf Zielseite aktivieren oder fallbackImages befüllen.';
return;
}
fallbackImages.forEach(u => addCard(joinUrl(baseUrl || location.href, u)));
status.textContent = `Galerie geladen (Fallback): ${fallbackImages.length}`;
})();
</script>
Der eigentlich HTML-Code zur Darstellung sieht so aus:
<div class="container gallery-container py-3">
<div id="status" class="mb-2 text-muted">Lade…</div>
<div id="galleryRow" class="row g-3" aria-live="polite"></div>
</div>
Und eine passende Inline-CSS-Definition könnte so aussehen:
<style>
:root{ --thumb-size:180px; }
body, .gallery-container { margin:0; background: inherit; }
.thumb{ display:block; width:100%; height:var(--thumb-size); object-fit:cover; border-radius:.375rem; }
.thumb-link{ display:block; overflow:hidden; height:var(--thumb-size); }
@media (max-width:480px){ :root{ --thumb-size:120px; } }
</style>
Der gesamte Blockinhalt vom Typ HTML wäre also dieser:
<style>
:root{ --thumb-size:180px; }
body, .gallery-container { margin:0; background: inherit; }
.thumb{ display:block; width:100%; height:var(--thumb-size); object-fit:cover; border-radius:.375rem; }
.thumb-link{ display:block; overflow:hidden; height:var(--thumb-size); }
@media (max-width:480px){ :root{ --thumb-size:120px; } }
</style>
<div class="container gallery-container py-3">
<div id="status" class="mb-2 text-muted">Lade…</div>
<div id="galleryRow" class="row g-3" aria-live="polite"></div>
</div>
<script>
const baseUrl = '<BASIS_URL_MIT_DEN_BILDERN>'; // HIER die URL eintragen
/* Falls CORS greift, in fallbackImages[] sämtliche Bild-URLs manuell eintragen */
const fallbackImages = [
// '<URL_ZUM_EINZELBILD>',
];
const galleryRow = document.getElementById('galleryRow');
const status = document.getElementById('status');
function looksLikeImage(u){
return /\.(jpe?g|png|gif|webp|bmp|svg)(\?.*)?$/i.test(u);
}
function joinUrl(base, path){
try{ return new URL(path, base).href; }catch(e){ return base + path; }
}
function clearGallery(){ galleryRow.innerHTML = ''; }
function addCard(src){
const col = document.createElement('div');
col.className = 'col-6 col-sm-4 col-md-3 col-lg-2';
const a = document.createElement('a');
a.href = src; a.target = '_blank'; a.rel = 'noopener'; a.className = 'thumb-link';
const img = document.createElement('img');
img.src = src; img.alt = src.split('/').pop().split('?')[0]; img.className = 'thumb';
a.appendChild(img); col.appendChild(a); galleryRow.appendChild(col);
}
async function fetchImagesFromUrl(url){
const res = await fetch(url, { mode:'cors' });
if(!res.ok) throw new Error('Abruf fehlgeschlagen: ' + res.status);
const text = await res.text();
const found = new Set();
// <img src="...">
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
let m;
while((m = imgRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
// <a href="..."> (typisch für Verzeichnis-Listings)
const aRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/gi;
while((m = aRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
return Array.from(found);
}
(async function init(){
clearGallery();
if(!baseUrl || !baseUrl.trim()){ status.textContent = 'Keine Basis-URL gefunden. Bitte im Script eintragen.'; return; }
status.textContent = 'Lade Bilder von ' + baseUrl;
try{
const imgs = await fetchImagesFromUrl(baseUrl);
if(imgs.length){
imgs.forEach(u => addCard(u));
status.textContent = `Bilder in der Galerie: ${imgs.length}`;
return;
} else {
status.textContent = 'Keine Bilder auf der Seite gefunden. Prüfe URL oder nutze fallbackImages.';
}
}catch(err){
console.warn('Ladefehler:', err);
status.textContent = 'Automatisches Laden fehlgeschlagen (möglicher CORS-/Netzwerkfehler). Nutze fallbackImages.';
}
if(fallbackImages.length === 0){
status.textContent = 'Keine Bilder verfügbar. Entweder CORS auf Zielseite aktivieren oder fallbackImages befüllen.';
return;
}
fallbackImages.forEach(u => addCard(joinUrl(baseUrl || location.href, u)));
status.textContent = `Galerie geladen (Fallback): ${fallbackImages.length}`;
})();
</script>
Um den Block nun in der Praxis zu testen, habe ich für ein Beispiel den Bilderordner “Die Alten” mit Bildern aktueller und ehemaliger Bewohner unseres Hunde-Altersruhesitzes gewählt und die URL in den Block eingetragen:
const baseUrl = 'https://klacker.org/cloud/tutorial01/Die%20Alten'; // HIER die URL eintragen

Die Galerie sieht nun so aus, wenn der Block dargestellt wird (ein Klick auf ein Bild öffnet dieses im Original in einem neuen Tab):

Erweiterte Galerie mit Zoom-Effekt
Das ist schon sehr schön. Aber weil wir ohnehin schon dabei sind, peppen wir den Block noch etwas weiter auf. Die Bilder werden quadratisch beschnitten,ihre Ecken leicht abgerundet und sie werden ein Stück weit gezoomt, sobald sich der Mauszeiger über ihnen befindet. Auf jedem Bild wird als Overlay der Dateiname angezeigt. Ein Klick auf ein Bild öffnet dieses wiederum in einem neuen Browser-Tab.
Der Code für einen solchen Block sieht dann so aus:
<style>
:root{
--gap: 0.75rem;
--thumb-size: 200px;
--zoom-scale: 1.8;
--transition: 300ms;
}
body, .gallery-container {
margin: 0;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
color: #ddd;
background: inherit;
}
.page-title {
color: inherit;
margin-bottom: 0.75rem;
}
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--thumb-size), 1fr));
gap: var(--gap);
}
.card-thumb {
position: relative;
overflow: hidden;
border-radius: 0.5rem;
height: var(--thumb-size);
display: block;
text-decoration: none;
background: rgba(0,0,0,0.06);
}
.card-thumb img{
width:100%;
height:100%;
object-fit:cover;
transition: transform var(--transition) ease, filter var(--transition) ease;
transform-origin: center center;
will-change: transform;
display:block;
user-select:none;
pointer-events:none;
}
.card-thumb:hover img{
filter: brightness(1.05);
}
.card-thumb:hover img.zoomed{
transform: scale(var(--zoom-scale));
}
.meta {
position:absolute;
left:0.5rem;
bottom:0.5rem;
background:rgba(0,0,0,0.4);
color:#fff;
padding:0.25rem 0.5rem;
border-radius:0.375rem;
font-size:0.8rem;
max-width:90%;
white-space:nowrap;
overflow:hidden;
text-overflow:ellipsis;
}
@media (max-width:480px){
:root{ --thumb-size:120px; }
}
</style>
<div class="container gallery-container py-3">
<div id="status" class="mb-2 text-muted">Lade Bilder…</div>
<div id="gallery" class="gallery" aria-live="polite"></div>
</div>
<script>
const baseUrl = "<BASIS_URL_DER_BILDER>"; // <- HIER die URL mit den Bildern einfügen
const gallery = document.getElementById('gallery');
const status = document.getElementById('status');
/* Manuelle Liste als Fallback */
let images = [];
function looksLikeImage(url){
return /\.(jpe?g|png|gif|webp|bmp|svg)(\?.*)?$/i.test(url);
}
function joinUrl(base, path){
try { return new URL(path, base).href; } catch(e){ return base + path; }
}
function addImageCard(src, filename){
const a = document.createElement('a');
a.href = src;
a.className = 'card-thumb';
a.target = '_blank';
a.rel = 'noopener';
const img = document.createElement('img');
img.src = src;
img.alt = filename || '';
img.classList.add('zoomed');
const meta = document.createElement('div');
meta.className = 'meta';
meta.textContent = filename || '';
a.appendChild(img);
a.appendChild(meta);
gallery.appendChild(a);
a.addEventListener('mousemove', (e) => {
const rect = img.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
img.style.transformOrigin = `${x}% ${y}%`;
});
a.addEventListener('mouseleave', () => {
img.style.transformOrigin = 'center center';
});
}
async function fetchImagesFromUrl(url){
try{
const res = await fetch(url, { mode:'cors' });
if(!res.ok) throw new Error('Abrufen fehlgeschlagen: ' + res.status);
const text = await res.text();
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
let match;
while((match = imgRegex.exec(text)) !== null){
const src = match[1];
if(looksLikeImage(src)) images.push(joinUrl(url, src));
}
const aRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/gi;
while((match = aRegex.exec(text)) !== null){
const href = match[1];
if(looksLikeImage(href)) images.push(joinUrl(url, href));
}
images = Array.from(new Set(images));
return images;
}catch(err){
console.warn('Fehler beim Laden:', err);
throw err;
}
}
(async function init(){
gallery.innerHTML = '';
status.textContent = 'Lade Bilder von ' + baseUrl;
try{
if(baseUrl && baseUrl.trim() !== ""){
const found = await fetchImagesFromUrl(baseUrl);
if(found.length){
found.forEach(u => addImageCard(u, u.split('/').pop().split('?')[0]));
status.textContent = 'Bilder in der Galerie: ' + found.length;
return;
} else {
status.textContent = 'Keine Bilder auf der Seite gefunden. Fallback zur manuellen Liste.';
}
} else {
status.textContent = 'Keine baseUrl angegeben. Fallback zur manuellen Liste.';
}
}catch(e){
status.textContent = 'Automatisches Laden fehlgeschlagen (CORS oder kein Directory-Listing). Verwende manuelle image-Liste.';
}
// Fallback manuell befüllen:
if(images.length === 0){
images = [
// Beispiel:
// 'https://example.com/images/beispiel1.jpg',
// 'https://example.com/images/beispiel2.png'
];
}
if(images.length === 0){
status.textContent = 'Keine Bilder verfügbar. Bitte baseUrl setzen oder images[] manuell befüllen.';
return;
}
images.forEach(u => addImageCard(joinUrl(baseUrl || location.href, u), (u.split('/').pop().split('?')[0])));
status.textContent = 'Galerie geladen (manuelle Liste).';
})();
</script>
Nun zum Testen wieder das Verzeichnis mit den Hundesenioren eingebaut:
const baseUrl = "https://klacker.org/cloud/tutorial01/Die%20Alten"; // <- HIER die URL mit den Bildern einfügen
Und der Galerie-Block sieht dann so aus:

Galerie als Carousel
Als dritte Variante wäre auch ein Block mit einem Bilder-Carousel denkbar. Der Code dafür ist von der Basis her ähnlich:
<style>
body, .gallery-container { margin:0; background: inherit; }
.carousel-img {
width:100%;
height:60vh;
object-fit:contain;
background: #111;
}
@media (max-width:576px){
.carousel-img { height:40vh; }
}
</style>
<div class="container gallery-container py-3">
<div id="status" class="mb-2 text-muted">Lade…</div>
<!-- Carousel-Wrapper: wird dynamisch befüllt -->
<div id="carouselWrapper" style="display:none;">
<div id="imageCarousel" class="carousel slide" data-bs-ride="carousel">
<div class="carousel-indicators" id="carouselIndicators"></div>
<div class="carousel-inner" id="carouselInner"></div>
<button class="carousel-control-prev" type="button" data-bs-target="#imageCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Vorheriges</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#imageCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Nächstes</span>
</button>
</div>
</div>
</div>
<script>
const baseUrl = '<BASIS_URL_MIT_DEN_BILDERN>'; // <- Hier die URL mit den Bildern eintragen
/* Fallback-Liste falls CORS greift */
const fallbackImages = [
// '<URL_DES_BILDES>>',
];
const status = document.getElementById('status');
const carouselWrapper = document.getElementById('carouselWrapper');
const carouselIndicators = document.getElementById('carouselIndicators');
const carouselInner = document.getElementById('carouselInner');
function looksLikeImage(u){
return /\.(jpe?g|png|gif|webp|bmp|svg)(\?.*)?$/i.test(u);
}
function joinUrl(base, path){
try{ return new URL(path, base).href; }catch(e){ return base + path; }
}
async function fetchImagesFromUrl(url){
const res = await fetch(url, { mode:'cors' });
if(!res.ok) throw new Error('Abrufen fehlgeschlagen: ' + res.status);
const text = await res.text();
const found = new Set();
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
let m;
while((m = imgRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
const aRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/gi;
while((m = aRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
return Array.from(found);
}
function buildCarousel(images){
carouselIndicators.innerHTML = '';
carouselInner.innerHTML = '';
images.forEach((src, idx) => {
// Indicator
const btn = document.createElement('button');
btn.type = 'button';
btn.setAttribute('data-bs-target', '#imageCarousel');
btn.setAttribute('data-bs-slide-to', String(idx));
btn.ariaLabel = 'Slide ' + (idx + 1);
if(idx === 0) { btn.className = 'active'; btn.setAttribute('aria-current','true'); }
carouselIndicators.appendChild(btn);
// Slide
const slide = document.createElement('div');
slide.className = 'carousel-item' + (idx === 0 ? ' active' : '');
const link = document.createElement('a');
link.href = src;
link.target = '_blank';
link.rel = 'noopener';
const img = document.createElement('img');
img.src = src;
img.alt = src.split('/').pop().split('?')[0];
img.className = 'd-block w-100 carousel-img';
link.appendChild(img);
slide.appendChild(link);
carouselInner.appendChild(slide);
});
carouselWrapper.style.display = images.length ? '' : 'none';
}
(async function init(){
status.textContent = 'Lade Bilder von ' + baseUrl;
if(!baseUrl || !baseUrl.trim()){ status.textContent = 'Keine baseUrl gesetzt. Bitte im Script eintragen.'; return; }
try{
const imgs = await fetchImagesFromUrl(baseUrl);
if(imgs.length){
buildCarousel(imgs);
status.textContent = `Bilder in der Galerie: ${imgs.length}`;
return;
} else {
status.textContent = 'Keine Bilder auf der Seite gefunden. Nutze fallbackImages.';
}
}catch(err){
console.warn('Ladefehler:', err);
status.textContent = 'Automatisches Laden fehlgeschlagen (möglicher CORS-/Netzwerkfehler). Nutze fallbackImages.';
}
if(fallbackImages.length === 0){
status.textContent = 'Keine Bilder verfügbar. Entweder CORS aktivieren oder fallbackImages befüllen.';
return;
}
buildCarousel(fallbackImages.map(u => joinUrl(baseUrl || location.href, u)));
status.textContent = `Carousel geladen (Fallback): ${fallbackImages.length} Bild(er).`;
})();
</script>
Im Ergebnis werden die Bilder dann so dargestellt:

Galerie mit Zoom und Overlay-Slider
Zur “Krönung” hier noch der Quelltext für eine Galerie mit Zoom, die beim Klicken auf ein Bild auf eine Slider-Ansicht also Overlay wechselt.
<style>
:root{
--gap:10px;
--thumb-size:200px;
--zoom-scale:1.8;
--transition:300ms;
}
body, .gallery-container{ margin:0; font-family:system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial; background:inherit; color:#222; }
.gallery{ display:grid; grid-template-columns: repeat(auto-fill, minmax(var(--thumb-size), 1fr)); gap:var(--gap); }
.card{ position:relative; overflow:hidden; border-radius:8px; height:var(--thumb-size); background:#f3f4f6; box-shadow:0 6px 18px rgba(0,0,0,0.06); cursor:pointer; }
.card img{ width:100%; height:100%; object-fit:cover; transition: transform var(--transition) ease, filter var(--transition) ease; transform-origin:center center; will-change:transform; display:block; user-select:none; pointer-events:none; }
.card:hover img{ filter:brightness(1.03); }
.card:hover img.zoomed{ transform:scale(var(--zoom-scale)); }
.meta{ position:absolute; left:8px; bottom:8px; background:rgba(0,0,0,0.45); color:#fff; padding:6px 8px; border-radius:6px; font-size:12px; max-width:90%; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
/* Overlay / Viewer */
.viewer{
position:fixed; inset:0; display:none; align-items:center; justify-content:center;
background:rgba(0,0,0,0.85); z-index:1050;
}
.viewer.visible{ display:flex; }
.viewer-content{ position:relative; max-width:95%; max-height:95%; width:calc(80vw); height:calc(80vh); display:flex; align-items:center; justify-content:center; }
.viewer-img{ max-width:100%; max-height:100%; object-fit:contain; border-radius:6px; box-shadow:0 10px 40px rgba(0,0,0,0.6); background:#000; }
/* Controls */
.viewer-btn{
position:absolute; top:50%; transform:translateY(-50%);
color:#fff; background:rgba(0,0,0,0.35); border:0; width:56px; height:56px; border-radius:50%;
display:flex; align-items:center; justify-content:center; font-size:28px; cursor:pointer;
transition:background .15s;
}
.viewer-btn:hover{ background:rgba(0,0,0,0.6); }
.viewer-prev{ left:12px; }
.viewer-next{ right:12px; }
.viewer-close{
position:absolute; top:12px; right:12px; color:#fff; background:rgba(0,0,0,0.35); border:0; width:44px; height:44px; border-radius:6px;
display:flex; align-items:center; justify-content:center; font-size:20px; cursor:pointer;
}
.viewer-caption{ position:absolute; bottom:12px; left:12px; right:12px; color:#fff; text-align:center; font-size:14px; opacity:0.9; }
@media (max-width:480px){ :root{ --thumb-size:120px; } .viewer-btn{ width:44px;height:44px;font-size:22px; } .viewer-close{ width:36px;height:36px;font-size:18px; } .viewer-content{ width:92vw; height:60vh; } }
</style>
<div class="container gallery-container py-3">
<div id="status" class="mb-2 text-muted">Lade Bilder…</div>
<div class="gallery" id="gallery" aria-live="polite"></div>
</div>
<!-- Viewer Overlay -->
<div id="viewer" class="viewer" role="dialog" aria-modal="true" aria-hidden="true">
<div class="viewer-content">
<button class="viewer-btn viewer-prev" id="prevBtn" aria-label="Vorheriges">‹</button>
<img id="viewerImg" class="viewer-img" src="" alt="">
<button class="viewer-btn viewer-next" id="nextBtn" aria-label="Nächstes">›</button>
<button class="viewer-close" id="closeBtn" aria-label="Schließen">✕</button>
<div class="viewer-caption" id="viewerCaption"></div>
</div>
</div>
<script>
/* === URL hier eintragen (oder leer lassen und fallbackImages nutzen) === */
const baseUrl = 'https://<IHR_HUB>/cloud/<IHR_KANAL>/<BILDERVERZEICHNIS>/'; // <- anpassen: https://<IHR_HUB>/cloud/<IHR_KANAL>/<BILDERVERZEICHNIS>
const fallbackImages = [
// 'https://<IHR_HUB>/cloud/<IHR_KANAL>/<BILDERVERZEICHNIS>/<BILDDATEI>>',
];
const gallery = document.getElementById('gallery');
const status = document.getElementById('status');
const viewer = document.getElementById('viewer');
const viewerImg = document.getElementById('viewerImg');
const viewerCaption = document.getElementById('viewerCaption');
const prevBtn = document.getElementById('prevBtn');
const nextBtn = document.getElementById('nextBtn');
const closeBtn = document.getElementById('closeBtn');
let images = [];
let currentIndex = -1;
function looksLikeImage(u){ return /\.(jpe?g|png|gif|webp|bmp|svg)(\?.*)?$/i.test(u); }
function joinUrl(base, path){ try{ return new URL(path, base).href; }catch(e){ return base + path; } }
function addImageCard(src, filename, idx){
const a = document.createElement('a');
a.href = '#';
a.className = 'card';
a.dataset.index = String(idx);
a.title = filename || src;
a.addEventListener('click', (ev) => { ev.preventDefault(); openViewer(Number(a.dataset.index)); });
const img = document.createElement('img');
img.src = src; img.alt = filename || '';
img.className = 'zoomed';
const meta = document.createElement('div');
meta.className = 'meta';
meta.textContent = filename || '';
a.appendChild(img);
a.appendChild(meta);
gallery.appendChild(a);
a.addEventListener('mousemove', (e) => {
const rect = img.getBoundingClientRect();
const x = ((e.clientX - rect.left) / rect.width) * 100;
const y = ((e.clientY - rect.top) / rect.height) * 100;
img.style.transformOrigin = `${x}% ${y}%`;
});
a.addEventListener('mouseleave', () => { img.style.transformOrigin = 'center center'; });
}
function clearGallery(){ gallery.innerHTML = ''; }
async function fetchImagesFromUrl(url){
const res = await fetch(url, { mode:'cors' });
if(!res.ok) throw new Error('Abrufen fehlgeschlagen: ' + res.status);
const text = await res.text();
const found = new Set();
const imgRegex = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
let m;
while((m = imgRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
const aRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/gi;
while((m = aRegex.exec(text)) !== null) if(looksLikeImage(m[1])) found.add(joinUrl(url, m[1]));
return Array.from(found);
}
/* Viewer controls */
function showViewer(){ viewer.classList.add('visible'); viewer.setAttribute('aria-hidden','false'); document.body.style.overflow='hidden'; }
function hideViewer(){ viewer.classList.remove('visible'); viewer.setAttribute('aria-hidden','true'); document.body.style.overflow=''; currentIndex = -1; }
function updateViewer(){
if(currentIndex < 0 || currentIndex >= images.length) return;
viewerImg.src = images[currentIndex];
viewerImg.alt = images[currentIndex].split('/').pop().split('?')[0];
viewerCaption.textContent = viewerImg.alt;
}
function openViewer(idx){
currentIndex = idx;
updateViewer();
showViewer();
}
function showPrev(){ if(images.length===0) return; currentIndex = (currentIndex -1 + images.length) % images.length; updateViewer(); }
function showNext(){ if(images.length===0) return; currentIndex = (currentIndex +1) % images.length; updateViewer(); }
prevBtn.addEventListener('click', (e)=>{ e.stopPropagation(); showPrev(); });
nextBtn.addEventListener('click', (e)=>{ e.stopPropagation(); showNext(); });
closeBtn.addEventListener('click', (e)=>{ e.stopPropagation(); hideViewer(); });
viewer.addEventListener('click', (e)=>{
if(e.target === viewer) hideViewer();
});
document.addEventListener('keydown', (e)=>{
if(viewer.classList.contains('visible')){
if(e.key === 'Escape') hideViewer();
if(e.key === 'ArrowLeft') showPrev();
if(e.key === 'ArrowRight') showNext();
}
});
(async function init(){
clearGallery();
status.textContent = 'Lade Bilder von ' + baseUrl;
try{
if(baseUrl && baseUrl.trim()){
const found = await fetchImagesFromUrl(baseUrl);
if(found.length){
images = found;
found.forEach((u,i)=> addImageCard(u, u.split('/').pop().split('?')[0], i));
status.textContent = 'Bilder in der Galerie: ' + found.length;
return;
} else {
status.textContent = 'Keine Bilder gefunden. Nutze Fallback-Liste.';
}
} else {
status.textContent = 'Keine baseUrl gesetzt. Nutze Fallback-Liste.';
}
}catch(err){
console.warn('Fehler beim Laden:', err);
status.textContent = 'Automatisches Laden fehlgeschlagen (mögliche CORS-Einschränkung). Nutze Fallback-Liste.';
}
if(fallbackImages.length === 0){
status.textContent = 'Keine Bilder verfügbar. Bitte baseUrl setzen oder fallbackImages befüllen.';
return;
}
images = fallbackImages.map(u => joinUrl(baseUrl || location.href, u));
images.forEach((u,i)=> addImageCard(u, u.split('/').pop().split('?')[0], i));
status.textContent = 'Galerie geladen (Fallback): ' + images.length;
})();
</script>
Die Quelle zu den Bildern wird wie bei den vorhergehenden Blöcken eingetragen. Im Ergebnis sieht es dann so aus:
