Lazy load character artwork

This commit is contained in:
mARTin-B78 2026-08-19 17:12:45 +02:00
parent 2bb01de94a
commit 08a6544661
4 changed files with 13 additions and 8 deletions

View File

@ -5,6 +5,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
## [1.20.41] — 2026-08-19
### Changed
- **Character portraits and concept art now lazy-load.** Off-screen images no longer load all at once when large cast lists are opened.
## [1.20.40] — 2026-08-19
### Added

View File

@ -1 +1 @@
1.20.40
1.20.41

View File

@ -10,7 +10,7 @@
<meta name="format-detection" content="telephone=no">
<meta name="color-scheme" content="light dark">
<meta name="theme-color" content="#2563EB">
<meta name="app-version" content="1.20.40">
<meta name="app-version" content="1.20.41">
<link rel="manifest" href="/manifest.webmanifest">
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/static/icon.svg">
@ -27,7 +27,7 @@
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
<link rel="stylesheet" href="/static/style.css?v=1.20.40">
<link rel="stylesheet" href="/static/style.css?v=1.20.41">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.20.40"></script>
<script src="/static/loader.js?v=1.20.41"></script>
</body>
</html>

View File

@ -951,7 +951,7 @@ function _charsTableHtml(chars, sortMode, sortDir) {
const pct = score != null ? Math.max(0, Math.min(100, score)) : null;
const tagList = String(rec.tags || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
const avatarInner = rec.image
? '<img src="' + rec.image + '" alt="' + escHtml(rec.name) + '" style="width:100%;height:100%;object-fit:cover;border-radius:50%">'
? '<img loading="lazy" decoding="async" src="' + rec.image + '" alt="' + escHtml(rec.name) + '" style="width:100%;height:100%;object-fit:cover;border-radius:50%">'
: escHtml((rec.name || '?')[0].toUpperCase());
const occupation = _libStr(sh.profession);
const ageLabel = _charAgeLabel(sh);
@ -1178,7 +1178,7 @@ async function _charDetailPage(rec, allChars, opts) {
const avatarHtml = '<div class="lcd-avatar-wrap">'
+ (rec.image
? '<div class="lcd-avatar lcd-avatar-upload" title="Profilbild öffnen"><img src="' + rec.image + '" alt="' + escHtml(rec.name) + '"></div>'
? '<div class="lcd-avatar lcd-avatar-upload" title="Profilbild öffnen"><img loading="lazy" decoding="async" src="' + rec.image + '" alt="' + escHtml(rec.name) + '"></div>'
: '<div class="lcd-avatar lcd-avatar-upload" style="background:hsl(' + hue + ',55%,38%)" title="Profilbild öffnen">' + escHtml((rec.name || '?')[0].toUpperCase()) + '</div>')
+ '</div>';
@ -1197,7 +1197,7 @@ async function _charDetailPage(rec, allChars, opts) {
+ '</button>'
+ '</div>'
+ (sh.concept_art_image
? '<div class="lcd-conceptart-img" title="Konzeptbild ansehen"><img src="' + sh.concept_art_image + '" alt="Concept art — ' + escHtml(rec.name) + '"></div>'
? '<div class="lcd-conceptart-img" title="Konzeptbild ansehen"><img loading="lazy" decoding="async" src="' + sh.concept_art_image + '" alt="Concept art — ' + escHtml(rec.name) + '"></div>'
: '<div class="lcd-conceptart-empty' + (_libStr(sh.concept_art_prompt).trim() ? '' : ' is-noprompt') + '">'
+ '<span class="mdi mdi-image-frame"></span>'
+ '<span>' + (_libStr(sh.concept_art_prompt).trim() ? 'Kein Konzeptbild' : 'Kein Konzeptbild-Prompt — erst unten bei Generation Prompts erzeugen') + '</span>'
@ -1594,7 +1594,7 @@ function _charDetailModal(rec, allChars) {
const voiceId = rec.voice ? (typeof rec.voice === 'object' ? (rec.voice.id || '') : String(rec.voice)) : '';
const avatarHtml = rec.image
? '<div class="lcd-avatar"><img src="' + rec.image + '" alt="' + escHtml(rec.name) + '"></div>'
? '<div class="lcd-avatar"><img loading="lazy" decoding="async" src="' + rec.image + '" alt="' + escHtml(rec.name) + '"></div>'
: '<div class="lcd-avatar" style="background:hsl(' + hue + ',55%,38%)">' + escHtml((rec.name || '?')[0].toUpperCase()) + '</div>';
const alignHtml = pct != null ? (