feat: Add Inline Table Edit Mode

This commit is contained in:
mARTin-B78 2026-06-22 15:15:13 +02:00
parent 6f0b278bd9
commit cada45fb15
4 changed files with 102 additions and 10 deletions

View File

@ -26,7 +26,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.8.0-4">
<link rel="stylesheet" href="/static/style.css?v=1.8.0-5">
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->

View File

@ -134,19 +134,35 @@ document.addEventListener('click', e => {
const icon = btn?.querySelector('.mdi');
if (icon) icon.className = 'mdi mdi-folder' + (window._voiceGroupByTag ? '-open' : '') + '-outline';
renderVoiceList();
}
if (e.target.closest('#voice-table-view-btn')) {
} else if (e.target.closest('#voice-table-view-btn')) {
window._voiceTableView = !window._voiceTableView;
if (!window._voiceTableView) window._voiceTableEditMode = false;
try { localStorage.setItem('vl-table-view', window._voiceTableView ? '1' : '0'); } catch (_) {}
const btn = document.getElementById('voice-table-view-btn');
btn?.classList.toggle('active', window._voiceTableView);
const editBtn = document.getElementById('voice-table-edit-btn');
if (editBtn) {
editBtn.style.display = window._voiceTableView ? 'inline-flex' : 'none';
editBtn.classList.toggle('active', !!window._voiceTableEditMode);
}
document.querySelector('.voices-workbench')?.classList.toggle('table-view', window._voiceTableView);
document.querySelector('.voices-workbench')?.classList.toggle('table-edit-mode', !!window._voiceTableEditMode);
// Auto-close any open inspector row when toggling table view
if (window._voiceTableView) {
document.querySelectorAll('.edit-open').forEach(r => r.classList.remove('edit-open'));
const inspector = document.getElementById('voices-inspector');
if (inspector) inspector.innerHTML = '<div class="inspector-placeholder"><span><span class="mdi mdi-table"></span></span><p>Table View Mode<br>Click a row to exit table view and edit.</p></div>';
}
renderVoiceList();
} else if (e.target.closest('#voice-table-edit-btn')) {
window._voiceTableEditMode = !window._voiceTableEditMode;
const editBtn = document.getElementById('voice-table-edit-btn');
editBtn?.classList.toggle('active', window._voiceTableEditMode);
document.querySelector('.voices-workbench')?.classList.toggle('table-edit-mode', window._voiceTableEditMode);
renderVoiceList();
}
});
// Restore the preferences on load
@ -169,6 +185,30 @@ document.addEventListener('change', e => {
if (e.target.id === 'voice-sort-field') setSort(e.target.value);
});
document.addEventListener('change', async e => {
if (e.target.classList.contains('vl-inline-edit')) {
const row = e.target.closest('.vl-row');
if (!row) return;
const voiceId = row.dataset.id;
const field = e.target.dataset.field;
let value = e.target.type === 'checkbox' ? e.target.checked : e.target.value;
if (field === 'rating') value = parseInt(value) || 0;
const payload = {};
payload[field] = value;
try {
await saveMeta(voiceId, payload);
const v = _voices.find(vv => vv.id === voiceId);
if (v) v[field] = value;
if (typeof toast === 'function') toast('Saved ' + field, 'success');
} catch (err) {
if (typeof toast === 'function') toast('Failed to save ' + field, 'error');
}
}
});
const FLAG_LANGUAGE_CANDIDATES = {
GB:['EN'], US:['EN'], AU:['EN'], NZ:['EN'], IE:['EN'], ZA:['EN'], NG:['EN'], KE:['EN'], GH:['EN'], JM:['EN'], TT:['EN'],
CA:['EN','FR'], IN:['EN','HI'], SG:['EN','ZH'], PH:['EN','FIL'], MT:['EN','MT'],
@ -1824,7 +1864,9 @@ function makeVoiceRow(v) {
<div class="vl-compact">
<div class="${avatarClass}" ${avatarStyle}>${avatarContent}</div>
<div class="vl-info">
<span class="vl-name" title="${escHtml(v.id)}">${escHtml(dispName)}</span>
${window._voiceTableEditMode
? `<input type="text" class="vl-inline-edit vl-name-input" data-field="name" value="${escHtml(dispName)}" title="Display Name" placeholder="Name">`
: `<span class="vl-name" title="${escHtml(v.id)}">${escHtml(dispName)}</span>`}
<div class="vl-meta">
<span class="vl-type-label ${isClone ? 'vl-type-clone' : 'vl-type-design'}">${isClone ? 'Clone' : 'Design'}</span>
${gender ? `<span class="vl-gender-label ${genderClass[gender]||'g-n'}" title="${genderLabel[gender]||''}">${genderMap[gender]||'?'} ${genderLabel[gender]||''}</span>` : ''}
@ -1835,18 +1877,42 @@ function makeVoiceRow(v) {
<div class="vr-play vr-play-original"><button class="play-btn-orig" title="Play original recording" aria-label="Play original recording"><span class="mdi mdi-play"></span></button></div>
<div class="vr-play vr-play-synth"><button class="play-btn-synth" title="Generate and play TTS preview" aria-label="Generate and play TTS preview"><span class="mdi mdi-play"></span></button></div>
</div>
<div class="vl-tbl-lang" title="${escHtml(currentFlag || langCode)}">${flagEmoji} ${escHtml(langCode)}</div>
<div class="vl-tbl-gender" title="${escHtml(genderLabel[gender]||'')}">${genderMap[gender]||'?'} ${escHtml(genderLabel[gender]||'—')}</div>
<div class="vl-tbl-lang" title="${escHtml(currentFlag || langCode)}">
${window._voiceTableEditMode
? `<input type="text" class="vl-inline-edit" data-field="lang" value="${escHtml(langCode)}" style="width:100%">`
: `${flagEmoji} ${escHtml(langCode)}`}
</div>
<div class="vl-tbl-gender" title="${escHtml(genderLabel[gender]||'')}">
${window._voiceTableEditMode
? `<select class="vl-inline-edit" data-field="gender" style="width:100%"><option value="N" ${gender==='N'?'selected':''}>N</option><option value="M" ${gender==='M'?'selected':''}>M</option><option value="F" ${gender==='F'?'selected':''}>F</option></select>`
: `${genderMap[gender]||'?'} ${escHtml(genderLabel[gender]||'—')}`}
</div>
<div class="vl-tbl-type"><span class="vl-type-label ${isClone ? 'vl-type-clone' : 'vl-type-design'}">${isClone ? 'Clone' : 'Design'}</span></div>
<div class="vl-tbl-bench">${benchText !== '-' ? `<span class="vl-bench-chip ${benchCls}" title="${escHtml(benchTitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(benchText)}</span>` : '-'}</div>
<div class="vl-tbl-dbfs" title="${dbTitle}">${dbfs} dB</div>
<div class="vl-tbl-len">${fmtDuration(v.duration)}</div>
<div class="vl-tbl-rating">${starsHtml}</div>
<div class="vl-tbl-rating">
${window._voiceTableEditMode
? `<input type="number" min="0" max="5" class="vl-inline-edit" data-field="rating" value="${v.rating||0}" style="width:40px">`
: starsHtml}
</div>
<div class="vl-tbl-source">${escHtml(v.origin || '-')}</div>
<div class="vl-tbl-seed">${v.seed != null ? '#' + v.seed : 'Auto'}</div>
<div class="vl-tbl-note" title="${escHtml(v.note||'')}">${escHtml(v.note || '')}</div>
<div class="vl-tbl-tag" title="${escHtml(v.tag||'')}">${escHtml(v.tag || '')}</div>
<div class="vl-tbl-active"><span class="mdi ${v.enabled !== false ? 'mdi-check-circle' : 'mdi-close-circle'}" style="color:${v.enabled !== false ? 'var(--green)' : 'var(--subtext)'}"></span></div>
<div class="vl-tbl-note" title="${escHtml(v.note||'')}">
${window._voiceTableEditMode
? `<input type="text" class="vl-inline-edit" data-field="note" value="${escHtml(v.note||'')}" style="width:100%">`
: escHtml(v.note || '')}
</div>
<div class="vl-tbl-tag" title="${escHtml(v.tag||'')}">
${window._voiceTableEditMode
? `<input type="text" class="vl-inline-edit" data-field="tag" value="${escHtml(v.tag||'')}" style="width:100%">`
: escHtml(v.tag || '')}
</div>
<div class="vl-tbl-active">
${window._voiceTableEditMode
? `<input type="checkbox" class="vl-inline-edit" data-field="enabled" ${v.enabled!==false?'checked':''} style="width:16px;height:16px">`
: `<span class="mdi ${v.enabled !== false ? 'mdi-check-circle' : 'mdi-close-circle'}" style="color:${v.enabled !== false ? 'var(--green)' : 'var(--subtext)'}"></span>`}
</div>
</div>
<div class="vr-main-row">

View File

@ -74,6 +74,7 @@
<button id="voice-sort-dir" class="vl-sort-dir-btn" title="Toggle sort direction"><span class="mdi mdi-arrow-up"></span></button>
<button id="voice-group-tag-btn" class="vl-sort-dir-btn" title="Group into virtual folders by tag"><span class="mdi mdi-folder-outline"></span></button>
<button id="voice-table-view-btn" class="vl-sort-dir-btn" title="Toggle full-width table view"><span class="mdi mdi-table"></span></button>
<button id="voice-table-edit-btn" class="vl-sort-dir-btn" title="Toggle table edit mode" style="display:none"><span class="mdi mdi-pencil-box-outline"></span></button>
<button id="vl-select-all-visible" class="vl-sort-dir-btn" title="Select all visible voices (toggle)"><span class="mdi mdi-checkbox-multiple-marked-outline"></span></button>
</div>

View File

@ -1411,6 +1411,31 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami
.voices-workbench.table-view .vl-tbl-tag { display: block; order: 14; font-size: 12px; color: var(--accent); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; }
.voices-workbench.table-view .vl-tbl-active { display: block; order: 15; font-size: 16px; text-align: center; }
.voices-workbench.table-edit-mode .vl-compact {
background: rgba(37,99,235,0.03);
border-bottom: 1px solid var(--border);
}
.vl-inline-edit {
background: var(--surface-light);
border: 1px solid var(--border);
color: var(--text);
font-family: inherit;
font-size: 12px;
padding: 2px 4px;
border-radius: 4px;
outline: none;
transition: border-color 0.15s;
}
.vl-inline-edit:focus {
border-color: var(--accent);
}
.vl-inline-edit.vl-name-input {
font-size: 13px;
font-weight: 600;
width: 100%;
}
/* Synth mode panel (segmented control + preview input) */
.vl-synth-panel { background: var(--surface); }
.vl-synth-mode-row { display: flex; align-items: center; gap: 7px; padding: 5px 8px 5px; }