Release v1.20.38
This commit is contained in:
parent
189ca51e47
commit
388fd601ab
@ -5,6 +5,15 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [1.20.38] — 2026-08-19
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Characters / Cast now has a Portainer-style column chooser.** Use **Show / Hide Columns** to control the table density; visibility preferences are saved locally per user.
|
||||||
|
- **Profile completeness is positioned beside moral alignment.** The sortable `filled / 32` profile indicator is now directly before the assigned voice column.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **The cast table’s horizontal scrollbar could disappear** after the column layout changed. The table now preserves a wide scrollable canvas and displays a dedicated horizontal scrollbar.
|
||||||
|
|
||||||
## [1.20.37] — 2026-08-19
|
## [1.20.37] — 2026-08-19
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
2
static/dist/main.min.js
vendored
2
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -10,7 +10,7 @@
|
|||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="theme-color" content="#2563EB">
|
<meta name="theme-color" content="#2563EB">
|
||||||
<meta name="app-version" content="1.20.37">
|
<meta name="app-version" content="1.20.38">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||||
@ -27,7 +27,7 @@
|
|||||||
|
|
||||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||||
<link rel="stylesheet" href="/static/style.css?v=1.20.37">
|
<link rel="stylesheet" href="/static/style.css?v=1.20.38">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
|
|||||||
@ -163,8 +163,9 @@ async function libraryRenderCharacters() {
|
|||||||
return ratingOf(b) - ratingOf(a) || (a.name || '').localeCompare(b.name || '');
|
return ratingOf(b) - ratingOf(a) || (a.name || '').localeCompare(b.name || '');
|
||||||
},
|
},
|
||||||
profile: function (a, b) {
|
profile: function (a, b) {
|
||||||
const ca = _charProfileCompleteness(a.sheet), cb = _charProfileCompleteness(b.sheet);
|
const filledA = _charProfileCompleteness(a.sheet).filled;
|
||||||
return cb.filled - ca.filled || (a.name || '').localeCompare(b.name || '');
|
const filledB = _charProfileCompleteness(b.sheet).filled;
|
||||||
|
return filledB - filledA || (a.name || '').localeCompare(b.name || '');
|
||||||
},
|
},
|
||||||
age: function (a, b) { return (_charAgeSortVal(a.sheet) - _charAgeSortVal(b.sheet)) || (a.name || '').localeCompare(b.name || ''); },
|
age: function (a, b) { return (_charAgeSortVal(a.sheet) - _charAgeSortVal(b.sheet)) || (a.name || '').localeCompare(b.name || ''); },
|
||||||
language: function (a, b) { return _charLangLabel(a).localeCompare(_charLangLabel(b)) || (a.name || '').localeCompare(b.name || ''); },
|
language: function (a, b) { return _charLangLabel(a).localeCompare(_charLangLabel(b)) || (a.name || '').localeCompare(b.name || ''); },
|
||||||
@ -283,6 +284,28 @@ async function libraryRenderCharacters() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const columnWrap = prod.querySelector('.lib-chars-column-chooser');
|
||||||
|
const columnButton = columnWrap?.querySelector('.lib-chars-column-button');
|
||||||
|
const columnMenu = columnWrap?.querySelector('.lib-chars-column-menu');
|
||||||
|
const table = prod.querySelector('.lib-chars-tbl');
|
||||||
|
if (columnWrap && columnButton && columnMenu && table) {
|
||||||
|
const hiddenColumns = _charTableHiddenColumns();
|
||||||
|
columnMenu.querySelectorAll('input[data-column-key]').forEach(function (input) {
|
||||||
|
input.checked = hiddenColumns[input.dataset.columnKey] !== false;
|
||||||
|
input.addEventListener('change', function () {
|
||||||
|
hiddenColumns[input.dataset.columnKey] = input.checked;
|
||||||
|
localStorage.setItem('ttsvc_libchars_columns', JSON.stringify(hiddenColumns));
|
||||||
|
_applyCharTableColumns(table, hiddenColumns);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
_applyCharTableColumns(table, hiddenColumns);
|
||||||
|
columnButton.addEventListener('click', function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
columnMenu.classList.toggle('is-open');
|
||||||
|
});
|
||||||
|
columnMenu.addEventListener('click', function (e) { e.stopPropagation(); });
|
||||||
|
}
|
||||||
|
|
||||||
const selectAllBtn = prod.querySelector('.lib-chars-select-all-btn');
|
const selectAllBtn = prod.querySelector('.lib-chars-select-all-btn');
|
||||||
const bulkBtn = prod.querySelector('.lib-chars-bulk-voice-btn');
|
const bulkBtn = prod.querySelector('.lib-chars-bulk-voice-btn');
|
||||||
const bulkCount = prod.querySelector('.lib-chars-bulk-count');
|
const bulkCount = prod.querySelector('.lib-chars-bulk-count');
|
||||||
@ -886,6 +909,30 @@ const _CHAR_PROFILE_COMPLETENESS_FIELDS = [
|
|||||||
'mannerisms', 'communication_style', 'voice_pattern', 'secret', 'conflict_style',
|
'mannerisms', 'communication_style', 'voice_pattern', 'secret', 'conflict_style',
|
||||||
'win_condition', 'reputation', 'religious_beliefs', 'notes', 'inventory',
|
'win_condition', 'reputation', 'religious_beliefs', 'notes', 'inventory',
|
||||||
];
|
];
|
||||||
|
const _CHAR_TABLE_COLUMN_DEFS = [
|
||||||
|
['select', 'Select'], ['avatar', 'Avatar'], ['name', 'Name'], ['gender', 'Gender'],
|
||||||
|
['age', 'Age'], ['lines', 'Lines'], ['language', 'Language'], ['alignment', 'Good / Evil'],
|
||||||
|
['profile', 'Profile'], ['voice', 'Voice'], ['rating', 'Rating'], ['prompt', 'Voice Design Prompt'],
|
||||||
|
['notes', 'Notes'], ['tags', 'Tags'], ['book', 'Book / Script'], ['export', 'Export'],
|
||||||
|
];
|
||||||
|
function _charTableHiddenColumns() {
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem('ttsvc_libchars_columns') || '{}');
|
||||||
|
return saved && typeof saved === 'object' ? saved : {};
|
||||||
|
} catch (_) { return {}; }
|
||||||
|
}
|
||||||
|
function _applyCharTableColumns(table, hidden) {
|
||||||
|
if (!table) return;
|
||||||
|
_CHAR_TABLE_COLUMN_DEFS.forEach(function (def, index) {
|
||||||
|
const isHidden = hidden[def[0]] === false;
|
||||||
|
table.querySelectorAll('tr').forEach(function (row) {
|
||||||
|
const cell = row.children[index];
|
||||||
|
if (cell) cell.classList.toggle('lib-chars-col-hidden', isHidden);
|
||||||
|
});
|
||||||
|
const col = table.querySelector('colgroup')?.children[index];
|
||||||
|
if (col) col.classList.toggle('lib-chars-col-hidden', isHidden);
|
||||||
|
});
|
||||||
|
}
|
||||||
function _charProfileCompleteness(sheet) {
|
function _charProfileCompleteness(sheet) {
|
||||||
const sh = sheet || {};
|
const sh = sheet || {};
|
||||||
const filled = _CHAR_PROFILE_COMPLETENESS_FIELDS.filter(function (key) {
|
const filled = _CHAR_PROFILE_COMPLETENESS_FIELDS.filter(function (key) {
|
||||||
@ -957,6 +1004,7 @@ function _charsTableHtml(chars, sortMode, sortDir) {
|
|||||||
+ '<td>' + (sh.line_count != null ? sh.line_count : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
+ '<td>' + (sh.line_count != null ? sh.line_count : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
||||||
+ '<td>' + (voiceLang ? escHtml(voiceLang) : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
+ '<td>' + (voiceLang ? escHtml(voiceLang) : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
||||||
+ '<td>' + (pct != null ? '<div class="lib-char-align-bar" title="' + pct + '/100"><div class="lib-char-align-dot" style="left:' + pct + '%"></div></div>' : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
+ '<td>' + (pct != null ? '<div class="lib-char-align-bar" title="' + pct + '/100"><div class="lib-char-align-dot" style="left:' + pct + '%"></div></div>' : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
||||||
|
+ '<td class="lib-chars-tbl-profile"><span class="lib-chars-profile-count ' + profileClass + '" title="' + profile.filled + ' von ' + profile.total + ' Profilfeldern ausgefüllt">' + profile.filled + ' / ' + profile.total + '</span></td>'
|
||||||
+ '<td class="lib-chars-tbl-voice"><div class="lib-chars-tbl-voice-wrap">'
|
+ '<td class="lib-chars-tbl-voice"><div class="lib-chars-tbl-voice-wrap">'
|
||||||
+ '<div class="lib-chars-tbl-voice-row">' + (voiceId ? '<span class="' + (voiceMissing ? 'lib-chars-tbl-voice-missing' : '') + '"' + (voiceMissing ? ' title="Stimme wurde aus der Voice Library gelöscht — bitte neu zuweisen"' : '') + '>' + (voiceMissing ? '<span class="mdi mdi-alert-circle-outline"></span> ' : '') + escHtml(voiceId) + '</span>' : '<span class="lib-chars-tbl-dash">Keine Stimme</span>') + '</div>'
|
+ '<div class="lib-chars-tbl-voice-row">' + (voiceId ? '<span class="' + (voiceMissing ? 'lib-chars-tbl-voice-missing' : '') + '"' + (voiceMissing ? ' title="Stimme wurde aus der Voice Library gelöscht — bitte neu zuweisen"' : '') + '>' + (voiceMissing ? '<span class="mdi mdi-alert-circle-outline"></span> ' : '') + escHtml(voiceId) + '</span>' : '<span class="lib-chars-tbl-dash">Keine Stimme</span>') + '</div>'
|
||||||
+ '<div class="lib-chars-tbl-voice-row">'
|
+ '<div class="lib-chars-tbl-voice-row">'
|
||||||
@ -974,11 +1022,17 @@ function _charsTableHtml(chars, sortMode, sortDir) {
|
|||||||
+ '</td>'
|
+ '</td>'
|
||||||
+ '<td class="lib-chars-tbl-tags"><div class="lib-chars-tbl-tags-wrap">' + tagList.map(function (t) { return '<span class="cl-tag-chip"><span class="mdi mdi-tag-outline"></span>' + escHtml(t) + '</span>'; }).join('') + '</div></td>'
|
+ '<td class="lib-chars-tbl-tags"><div class="lib-chars-tbl-tags-wrap">' + tagList.map(function (t) { return '<span class="cl-tag-chip"><span class="mdi mdi-tag-outline"></span>' + escHtml(t) + '</span>'; }).join('') + '</div></td>'
|
||||||
+ '<td class="lib-chars-tbl-book-cell">' + (bookLabel ? escHtml(bookLabel) : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
+ '<td class="lib-chars-tbl-book-cell">' + (bookLabel ? escHtml(bookLabel) : '<span class="lib-chars-tbl-dash">—</span>') + '</td>'
|
||||||
+ '<td class="lib-chars-tbl-profile"><span class="lib-chars-profile-count ' + profileClass + '" title="' + profile.filled + ' von ' + profile.total + ' Profilfeldern ausgefüllt">' + profile.filled + ' / ' + profile.total + '</span></td>'
|
|
||||||
+ '<td><button class="lib-char-export btn-sm" title="Als SillyTavern-Karte exportieren"><span class="mdi mdi-export-variant"></span></button></td>'
|
+ '<td><button class="lib-char-export btn-sm" title="Als SillyTavern-Karte exportieren"><span class="mdi mdi-export-variant"></span></button></td>'
|
||||||
+ '</tr>';
|
+ '</tr>';
|
||||||
}).join('');
|
}).join('');
|
||||||
return '<div class="lib-chars-tbl-wrap"><table class="lib-chars-tbl">'
|
const columnChooser = '<div class="lib-chars-column-chooser">'
|
||||||
|
+ '<button type="button" class="btn-secondary btn-sm lib-chars-column-button"><span class="mdi mdi-table-eye"></span> Show / Hide Columns</button>'
|
||||||
|
+ '<div class="lib-chars-column-menu">'
|
||||||
|
+ _CHAR_TABLE_COLUMN_DEFS.map(function (def) {
|
||||||
|
return '<label><input type="checkbox" data-column-key="' + def[0] + '" checked> <span>' + def[1] + '</span></label>';
|
||||||
|
}).join('')
|
||||||
|
+ '</div></div>';
|
||||||
|
return columnChooser + '<div class="lib-chars-tbl-wrap"><table class="lib-chars-tbl">'
|
||||||
// table-layout:auto put a max-width'd wrapping cell (Tags) at the wrong
|
// table-layout:auto put a max-width'd wrapping cell (Tags) at the wrong
|
||||||
// physical position — its own header stayed put but the cell rendered
|
// physical position — its own header stayed put but the cell rendered
|
||||||
// stacked under the previous column instead, a Chromium auto-layout
|
// stacked under the previous column instead, a Chromium auto-layout
|
||||||
@ -986,13 +1040,13 @@ function _charsTableHtml(chars, sortMode, sortDir) {
|
|||||||
// the same row. Fixed explicit widths sidestep the whole class of bug.
|
// the same row. Fixed explicit widths sidestep the whole class of bug.
|
||||||
+ '<colgroup><col style="width:28px"><col style="width:60px"><col style="width:300px">'
|
+ '<colgroup><col style="width:28px"><col style="width:60px"><col style="width:300px">'
|
||||||
+ '<col style="width:88px"><col style="width:80px"><col style="width:72px"><col style="width:100px">'
|
+ '<col style="width:88px"><col style="width:80px"><col style="width:72px"><col style="width:100px">'
|
||||||
+ '<col style="width:140px"><col style="width:230px"><col style="width:126px"><col style="width:440px"><col style="width:260px"><col style="width:220px"><col style="width:200px"><col style="width:90px">'
|
+ '<col style="width:140px"><col style="width:90px"><col style="width:230px"><col style="width:126px"><col style="width:440px"><col style="width:260px"><col style="width:220px"><col style="width:200px">'
|
||||||
+ '<col style="width:36px"></colgroup>'
|
+ '<col style="width:36px"></colgroup>'
|
||||||
+ '<thead><tr>'
|
+ '<thead><tr>'
|
||||||
+ '<th class="lib-chars-tbl-cb"><input type="checkbox" class="lib-chars-tbl-select-all-cb" title="Select all"></th><th></th>' + th('alpha', 'Name') + th('gender', 'Geschlecht') + th('age', 'Alter', 'Estimated age')
|
+ '<th class="lib-chars-tbl-cb"><input type="checkbox" class="lib-chars-tbl-select-all-cb" title="Select all"></th><th></th>' + th('alpha', 'Name') + th('gender', 'Geschlecht') + th('age', 'Alter', 'Estimated age')
|
||||||
+ th('lines', 'Zeilen', 'Anzahl Zeilen') + th('language', 'Sprache') + th('align', 'Gut/Böse', 'Moralische Gesinnung') + th('voice', 'Stimme') + th('rating', 'Rating', 'Rate the assigned voice')
|
+ th('lines', 'Zeilen', 'Anzahl Zeilen') + th('language', 'Sprache') + th('align', 'Gut/Böse', 'Moralische Gesinnung') + th('profile', 'Profil (gefüllt)', 'Sort by filled profile fields; the second number is always 32') + th('voice', 'Stimme') + th('rating', 'Rating', 'Rate the assigned voice')
|
||||||
+ '<th title="The exact prompt used to create the assigned voice">Voice Design Prompt</th><th>Notes</th>'
|
+ '<th title="The exact prompt used to create the assigned voice">Voice Design Prompt</th><th>Notes</th>'
|
||||||
+ '<th>Tags</th><th>Book / Script</th><th class="lib-chars-tbl-sortable" data-sort-key="profile" title="Filled profile fields">Profil</th><th></th>'
|
+ '<th>Tags</th><th>Book / Script</th><th></th>'
|
||||||
+ '</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
+ '</tr></thead><tbody>' + rows + '</tbody></table></div>';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -609,11 +609,26 @@ audio { width: 100%; }
|
|||||||
|
|
||||||
/* Table view — dense alternative to the card grid for scanning a large cast */
|
/* Table view — dense alternative to the card grid for scanning a large cast */
|
||||||
.lib-chars-tbl-wrap {
|
.lib-chars-tbl-wrap {
|
||||||
width:100%; max-width:100%; min-width:0; overflow-x:auto; overflow-y:hidden;
|
width:100%; max-width:100%; min-width:0; overflow-x:scroll; overflow-y:hidden;
|
||||||
border:1px solid var(--border); border-radius:12px; background:var(--surface);
|
border:1px solid var(--border); border-radius:12px; background:var(--surface);
|
||||||
position:relative; scrollbar-gutter:stable;
|
position:relative; scrollbar-gutter:stable; scrollbar-width:auto;
|
||||||
}
|
}
|
||||||
.lib-chars-tbl { width:100%; min-width:2410px; table-layout:fixed; border-collapse:collapse; font-size:12.9px; white-space:nowrap; }
|
.lib-chars-tbl-wrap::-webkit-scrollbar { height:12px; }
|
||||||
|
.lib-chars-tbl-wrap::-webkit-scrollbar-track { background:var(--panel); }
|
||||||
|
.lib-chars-tbl-wrap::-webkit-scrollbar-thumb { background:var(--subtext); border-radius:8px; border:3px solid var(--panel); }
|
||||||
|
.lib-chars-column-chooser { position:relative; display:flex; justify-content:flex-end; margin:0 0 6px; }
|
||||||
|
.lib-chars-column-button { display:inline-flex; align-items:center; gap:5px; }
|
||||||
|
.lib-chars-column-menu {
|
||||||
|
display:none; position:absolute; top:calc(100% + 4px); right:0; z-index:20;
|
||||||
|
min-width:190px; padding:8px; border:1px solid var(--border); border-radius:8px;
|
||||||
|
background:var(--surface); box-shadow:0 8px 24px rgba(0,0,0,.18);
|
||||||
|
}
|
||||||
|
.lib-chars-column-menu.is-open { display:block; }
|
||||||
|
.lib-chars-column-menu label { display:flex; align-items:center; gap:5px; padding:3px 2px; color:var(--text); font-size:12px; white-space:nowrap; cursor:pointer; }
|
||||||
|
.lib-chars-column-menu label:hover { background:var(--panel); border-radius:4px; }
|
||||||
|
.lib-chars-column-menu input { accent-color:var(--accent); margin:0; }
|
||||||
|
.lib-chars-tbl .lib-chars-col-hidden { display:none !important; }
|
||||||
|
.lib-chars-tbl { width:100%; min-width:2470px; table-layout:fixed; border-collapse:collapse; font-size:12.9px; white-space:nowrap; }
|
||||||
.lib-chars-tbl td { overflow: hidden; text-overflow: ellipsis; }
|
.lib-chars-tbl td { overflow: hidden; text-overflow: ellipsis; }
|
||||||
.lib-chars-tbl thead th {
|
.lib-chars-tbl thead th {
|
||||||
text-align:left; padding:8px 8px; font-size:10.5px; font-weight:800; text-transform:uppercase;
|
text-align:left; padding:8px 8px; font-size:10.5px; font-weight:800; text-transform:uppercase;
|
||||||
@ -1002,7 +1017,7 @@ tr.lib-char-card.lib-chars-tbl-row:hover { transform: none; box-shadow: none; ba
|
|||||||
.lib-char-card-banner { aspect-ratio: auto; height: auto; min-height: 120px; }
|
.lib-char-card-banner { aspect-ratio: auto; height: auto; min-height: 120px; }
|
||||||
.lib-char-avatar { width:82px; height:82px; font-size:30px; }
|
.lib-char-avatar { width:82px; height:82px; font-size:30px; }
|
||||||
.lib-char-name { font-size:18px; }
|
.lib-char-name { font-size:18px; }
|
||||||
.lib-chars-tbl { min-width: 2410px; }
|
.lib-chars-tbl { min-width: 2470px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Character detail modal (D&D / LARP card) ───────────────────────────────── */
|
/* ── Character detail modal (D&D / LARP card) ───────────────────────────────── */
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user