Show character avatar images in casting pickers (v1.14.12)

The Characters Found sidebar, the "Assign to" popup's character list,
and the "merge with an already-found character" list now show each
character's saved portrait when one exists (same source as the profile
detail panel), falling back to the existing colour-keyed initial-letter
dot otherwise.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-07-09 02:09:20 +02:00
parent 2133fe25c7
commit 3db3616830
5 changed files with 32 additions and 8 deletions

View File

@ -9,6 +9,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
---
## [1.14.12] — 2026-07-09
### Added
- **Character avatar images in the casting pickers** — the Characters Found sidebar, the "Assign to" popup's character list, and the "merge with an already-found character" list now show the character's saved portrait (same source as the profile detail panel) when one exists, falling back to the colour-keyed initial-letter dot otherwise.
---
## [1.14.11] — 2026-07-09
### Fixed

View File

@ -1 +1 @@
1.14.11
1.14.12

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.14.11">
<meta name="app-version" content="1.14.12">
<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.14.11">
<link rel="stylesheet" href="/static/style.css?v=1.14.12">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
@ -365,7 +365,7 @@ window.toggleNavTree = function(treeId, chevronId) {
</script>
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
<script src="/static/loader.js?v=1.14.11"></script>
<script src="/static/loader.js?v=1.14.12"></script>
</body>
</html>

View File

@ -1566,6 +1566,16 @@ STRIKTE FORMAT- UND TEXTREGELN:
else roster.get(name).color = color;
return roster.get(name).color;
};
// Small round avatar for character pickers/lists — the character's saved
// portrait (same source as the profile detail panel) when one exists,
// otherwise the colour-keyed initial-letter dot used everywhere else.
const _abAvatarHtml = (name, color) => {
const img = recordForName(name)?.image;
const c = color || colorFor(name);
return img
? `<span class="ab-char-dot ab-char-dot-img"><img src="${escHtml(img)}" alt=""></span>`
: `<span class="ab-char-dot" style="background:${c}">${escHtml((name || '?')[0].toUpperCase())}</span>`;
};
const setCharacterColor = (name, color) => {
const safe = _abNormalizeColor(color, name);
const info = roster.get(name);
@ -1982,7 +1992,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
chars.innerHTML = items.map(([n, info]) => {
const color = info.color || colorFor(n);
return `<div class="ab-char-item${prev===n?' is-active':''}" data-name="${escHtml(n)}">
<span class="ab-char-dot" style="background:${color}">${escHtml((n||'?')[0].toUpperCase())}</span>
${_abAvatarHtml(n, color)}
<span class="ab-char-name">${escHtml(n)}</span>
<b class="ab-char-count">${roster.get(n)?.count||0}</b>
<button class="ab-char-alias-btn" data-name="${escHtml(n)}" title="Add alias / also known as"><span class="mdi mdi-tag-plus-outline"></span></button>
@ -2039,7 +2049,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
</datalist>
${otherNames.length ? `<div class="ab-alias-popup-hint">Or merge with an already-found character:</div>
<div class="ab-alias-popup-list">
${otherNames.slice(0, 8).map(([n, info]) => `<button type="button" class="ab-alias-merge-opt" data-name="${escHtml(n)}">${escHtml(n)} <span class="ab-alias-merge-count">${info.count}</span></button>`).join('')}
${otherNames.slice(0, 8).map(([n, info]) => `<button type="button" class="ab-alias-merge-opt" data-name="${escHtml(n)}"><span class="ab-alias-merge-name">${_abAvatarHtml(n, info.color)}${escHtml(n)}</span><span class="ab-alias-merge-count">${info.count}</span></button>`).join('')}
</div>` : ''}
<div class="ab-alias-popup-actions">
<button type="button" class="btn-secondary btn-sm ab-alias-cancel">Cancel</button>
@ -2700,7 +2710,10 @@ STRIKTE FORMAT- UND TEXTREGELN:
const btn = document.createElement('div');
btn.dataset.name = n;
btn.style.cssText = 'padding:6px 8px; font-size:12px; cursor:pointer; border-radius:4px; display:flex; align-items:center; gap:6px; transition:background 0.1s; font-weight:600; color:var(--text);';
btn.innerHTML = `<span style="width:8px;height:8px;border-radius:50%;background:${info.color};"></span> ${escHtml(n)}`;
const img = recordForName(n)?.image;
btn.innerHTML = (img
? `<span style="width:18px;height:18px;border-radius:50%;flex-shrink:0;overflow:hidden;"><img src="${escHtml(img)}" alt="" style="width:100%;height:100%;object-fit:cover;"></span>`
: `<span style="width:8px;height:8px;border-radius:50%;background:${info.color};flex-shrink:0;"></span>`) + ` ${escHtml(n)}`;
btn.onmouseover = () => btn.style.background = 'var(--panel)';
btn.onmouseout = () => btn.style.background = 'transparent';
btn.onclick = () => assignName(n);

View File

@ -5530,6 +5530,8 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
display:flex; align-items:center; justify-content:center;
font-size:11px; font-weight:700; color:#fff;
}
.ab-char-dot-img { overflow: hidden; background: var(--panel); }
.ab-char-dot-img img { width: 100%; height: 100%; object-fit: cover; }
.ab-char-name { flex:1; font-size:12px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width: 0; }
.ab-char-count { font-size:11px; color:var(--subtext); font-weight:700; flex-shrink:0; }
.ab-char-alias-btn {
@ -5563,7 +5565,9 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
background: none; color: var(--text); cursor: pointer; text-align: left;
}
.ab-alias-merge-opt:hover { background: var(--panel); border-color: var(--border); }
.ab-alias-merge-count { font-size: 10.5px; color: var(--subtext); font-weight: 700; }
.ab-alias-merge-name { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.ab-alias-merge-name .ab-char-dot { width: 18px; height: 18px; font-size: 9px; }
.ab-alias-merge-count { font-size: 10.5px; color: var(--subtext); font-weight: 700; flex-shrink: 0; }
.ab-alias-popup-actions { display: flex; justify-content: flex-end; gap: 6px; }
/* Character detail panel inside the casting feed area */
.ab-char-detail-panel {