feat: Seiten-Thumbnails mit echter Vorschau + Hell/Dunkel-Toggle
All checks were successful
Deploy bericht / deploy (push) Successful in 1s

- Jeder Thumb hat jetzt einen mini canvas, der das Bild oder die erste
  PDF-Seite gerendert anzeigt (max 200px)
- Papier-Look: A4 aspect-ratio (1:1.414), weißer Hintergrund per Default
- 🌓-Button im Pages-Header schaltet zwischen paper-light und paper-dark
- Toolbar-Inputs (select/number/checkbox): heller Text auf dunklem
  Hintergrund für Dark-Theme

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
[deploy]
This commit is contained in:
Eduard Wisch 2026-04-08 16:30:03 +02:00
parent 1d3315a0b5
commit 90d89130f3
3 changed files with 136 additions and 22 deletions

View file

@ -313,11 +313,17 @@ if (!$bericht) {
// RECHTS: Seiten-Thumbnails
print '<aside class="bericht-pages">';
print '<div class="bericht-pages-header">';
print '<h4>'.$langs->trans("BerichtPages").' (<span id="page-count">'.count($pages).'</span>)</h4>';
print '<div id="bericht-page-list" class="page-list">';
print '<button type="button" id="btn-toggle-thumb-bg" title="Hell/Dunkel">🌓</button>';
print '</div>';
print '<div id="bericht-page-list" class="page-list paper-light">';
foreach ($pages as $idx => $p) {
print '<div class="page-thumb" data-pageid="'.$p->id.'" data-order="'.$p->page_order.'">';
print '<div class="page-thumb-inner"><span class="page-num">'.($idx + 1).'</span></div>';
print '<div class="page-thumb-paper">';
print '<canvas class="thumb-canvas"></canvas>';
print '</div>';
print '<div class="page-thumb-label"><span class="page-num">'.($idx + 1).'</span></div>';
print '<div class="page-thumb-actions">';
print '<button type="button" class="thumb-del" title="'.$langs->trans("BerichtDeletePage").'">🗑️</button>';
print '</div>';

View file

@ -102,16 +102,19 @@
.bericht-toolbar select,
.bericht-toolbar input[type="number"],
.bericht-toolbar input[type="text"] {
background: var(--inputbackgroundcolor, #fff);
color: var(--inputtextcolor, #000);
border: 1px solid var(--colorboxbordertitle1, #ccc);
background: var(--colorbackbody, #2a2a30);
color: var(--colortext, #ddd);
border: 1px solid var(--colorboxbordertitle1, #555);
border-radius: 3px;
padding: 2px 4px;
font-size: 12px;
}
.bericht-toolbar #zoom-label {
color: var(--colortext, inherit);
.bericht-toolbar select option {
background: var(--colorbackbody, #2a2a30);
color: var(--colortext, #ddd);
}
.bericht-toolbar #zoom-label { color: var(--colortext, inherit); }
.bericht-toolbar input[type="checkbox"] { accent-color: var(--colorbackhmenu1, #337ab7); }
.bericht-canvas-wrap {
position: relative;
@ -139,34 +142,70 @@
border: 1px solid var(--colorboxbordertitle1, #ccc);
}
.page-list { display: flex; flex-direction: column; gap: 6px; }
.page-thumb {
background: var(--colorbacktitle1, #fff);
.bericht-pages-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
.bericht-pages-header h4 { margin: 0; }
#btn-toggle-thumb-bg {
background: transparent;
border: 1px solid var(--colorboxbordertitle1, #555);
color: var(--colortext, inherit);
border: 2px solid var(--colorboxbordertitle1, #ddd);
border-radius: 4px;
padding: 6px; cursor: pointer; position: relative;
transition: border-color 0.15s;
border-radius: 3px;
cursor: pointer;
padding: 2px 6px;
}
.page-list { display: flex; flex-direction: column; gap: 8px; }
.page-thumb {
border: 2px solid var(--colorboxbordertitle1, #444);
border-radius: 3px;
cursor: pointer;
position: relative;
transition: border-color 0.15s, box-shadow 0.15s;
overflow: hidden;
}
.page-thumb:hover { border-color: var(--colortextlink, #888); }
.page-thumb.active {
border-color: var(--colortextlink, #337ab7);
box-shadow: 0 0 0 2px var(--colorbackhmenu1, rgba(51,122,183,0.3));
box-shadow: 0 0 0 2px rgba(51,122,183,0.4);
}
.page-thumb-inner {
height: 100px;
background: var(--colorbackvmenu1, #f0f0f0);
/* "Papier"-Look — heller Default, mit Papier-Schatten */
.page-list.paper-light .page-thumb-paper { background: #ffffff; }
.page-list.paper-dark .page-thumb-paper { background: #1e1e22; }
.page-thumb-paper {
width: 100%;
aspect-ratio: 1 / 1.414; /* DIN A4 Verhältnis (Hochformat) */
display: flex; align-items: center; justify-content: center;
font-size: 24px; opacity: 0.6;
box-shadow: inset 0 0 0 1px rgba(0,0,0,0.1);
overflow: hidden;
}
.page-thumb-paper canvas.thumb-canvas {
max-width: 100%;
max-height: 100%;
display: block;
}
.page-thumb-label {
text-align: center;
font-size: 11px;
padding: 3px 0;
background: var(--colorbacktitle1, #2a2a30);
color: var(--colortext, #ddd);
border-top: 1px solid var(--colorboxbordertitle1, #444);
}
.page-thumb-actions { position: absolute; top: 4px; right: 4px; }
.thumb-del {
background: var(--colorbacktitle1, rgba(255,255,255,0.9));
color: var(--colortext, inherit);
border: 1px solid var(--colorboxbordertitle1, #ccc);
background: rgba(0,0,0,0.6);
color: #fff;
border: 1px solid rgba(255,255,255,0.3);
border-radius: 3px;
cursor: pointer;
padding: 2px 6px;
font-size: 12px;
}
.thumb-del:hover { background: rgba(217,83,79,0.9); }
.bericht-actions {
margin-top: 16px;

View file

@ -55,6 +55,9 @@
const firstThumb = document.querySelector('#bericht-page-list .page-thumb');
if (firstThumb) loadPage(firstThumb);
// Alle Thumbnails parallel rendern
renderAllThumbs();
bindThumbs();
bindToolbar();
bindAttachments();
@ -590,6 +593,72 @@
location.reload();
});
});
// Hell/Dunkel-Toggle
const tg = document.getElementById('btn-toggle-thumb-bg');
if (tg) tg.addEventListener('click', () => {
const list = document.getElementById('bericht-page-list');
list.classList.toggle('paper-light');
list.classList.toggle('paper-dark');
});
}
/**
* Rendert alle Thumbnails in der rechten Seitenleiste.
* Holt jedes Bild über page_image.php und zeichnet es klein in das Thumb-Canvas.
* PDFs werden mit PDF.js gerendert.
*/
async function renderAllThumbs() {
const thumbs = document.querySelectorAll('.page-thumb');
for (const t of thumbs) {
const pageid = t.dataset.pageid;
const canvas = t.querySelector('.thumb-canvas');
if (!canvas || !pageid) continue;
try {
const r = await fetch(cfg.urls.page_image + '?pageid=' + pageid);
const ct = r.headers.get('Content-Type') || '';
const buf = await r.arrayBuffer();
if (ct.includes('pdf')) {
await renderThumbPdf(canvas, buf);
} else if (ct.includes('image')) {
await renderThumbImage(canvas, buf, ct);
}
} catch (e) { /* skip */ }
}
}
async function renderThumbImage(canvas, buf, mime) {
return new Promise(res => {
const blob = new Blob([buf], { type: mime });
const url = URL.createObjectURL(blob);
const img = new Image();
img.onload = () => {
const maxSide = 200;
const ratio = Math.min(maxSide / img.width, maxSide / img.height);
canvas.width = Math.round(img.width * ratio);
canvas.height = Math.round(img.height * ratio);
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(url);
res();
};
img.onerror = () => { URL.revokeObjectURL(url); res(); };
img.src = url;
});
}
async function renderThumbPdf(canvas, buf) {
if (!window.pdfjsLib) return;
try {
const doc = await pdfjsLib.getDocument({ data: buf.slice(0) }).promise;
const page = await doc.getPage(1);
const base = page.getViewport({ scale: 1 });
const scale = 200 / base.width;
const vp = page.getViewport({ scale: scale });
canvas.width = vp.width;
canvas.height = vp.height;
await page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise;
} catch (e) { /* skip */ }
}
function bindSortable() {