list-editor: add per-track description button + modal
Each music list row now shows a 설명 button immediately to the left of the 별칭 button. Click opens a modal with a multi-line textarea; on close the value is persisted into MusicListEntry.description and saved to the same pack list JSON. The button gets a hasDesc visual indicator when filled. Description is stored but intentionally not consumed by datapack export or alias matching — purely informational metadata. - types.ts: add description: string to MusicListEntry - store.ts: normalize entry.description via sanitizeStr (defaults to '') - listEditor.ejs: new #descModal alongside aliasModal - listEditor.js: render descBtn left of aliasBtn, attach handlers, also set description: '' on playlist-fetched entries - styles.css: extend trackRow grid to 6 cols, reuse aliasBtn styling for descBtn, add descTextarea sizing - locale (ko-kr): descBtn / descModalTitle / descBack / descPlaceholder / descHint Backwards-compatible: existing list JSON files without description field normalize to ''. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -103,6 +103,7 @@
|
||||
var aliasLabel = aliasCount > 0
|
||||
? tt('aliasBtnWithCount', { count: aliasCount })
|
||||
: tt('aliasBtn')
|
||||
var hasDesc = typeof entry.description === 'string' && entry.description.trim().length > 0
|
||||
li.innerHTML =
|
||||
'<span class="rowNum">' + (idx + 1) + '</span>' +
|
||||
'<img class="rowThumb" src="' + thumbUrl(entry.url) + '" alt="" loading="lazy" draggable="false"/>' +
|
||||
@@ -114,12 +115,16 @@
|
||||
escapeHtml(entry.artist || '') +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<button type="button" class="descBtn' + (hasDesc ? ' hasDesc' : '') + '" data-desc-open="' + idx + '" draggable="false">' +
|
||||
escapeHtml(tt('descBtn')) +
|
||||
'</button>' +
|
||||
'<button type="button" class="aliasBtn' + (aliasCount > 0 ? ' hasAliases' : '') + '" data-alias-open="' + idx + '" draggable="false">' +
|
||||
escapeHtml(aliasLabel) +
|
||||
'</button>' +
|
||||
'<span class="rowDur">' + fmtTime(entry.durationSec) + '</span>'
|
||||
attachDraggable(li, 'music', idx)
|
||||
attachInlineEdit(li, idx)
|
||||
attachDescBtn(li, idx)
|
||||
attachAliasBtn(li, idx)
|
||||
ol.appendChild(li)
|
||||
})
|
||||
@@ -527,6 +532,57 @@
|
||||
if (e.target === aliasModal) closeAliasModalSaving()
|
||||
})
|
||||
|
||||
// ── 설명 모달 (음악) ─────────────────────────────────
|
||||
// 별칭 모달과 같은 패턴: 모달 닫힐 때 textarea 값을 state.music[idx].description 에 저장.
|
||||
var descModal = document.getElementById('descModal')
|
||||
var descTextarea = document.getElementById('desc-textarea')
|
||||
var descModalTitleEl = document.getElementById('desc-modal-title')
|
||||
var descBackBtn = document.getElementById('desc-back')
|
||||
var descEditingIdx = -1
|
||||
|
||||
function attachDescBtn(li, idx) {
|
||||
var btn = li.querySelector('[data-desc-open]')
|
||||
if (!btn) return
|
||||
btn.addEventListener('mousedown', function (e) { e.stopPropagation() })
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.stopPropagation()
|
||||
openDescModal(idx)
|
||||
})
|
||||
}
|
||||
|
||||
function openDescModal(idx) {
|
||||
if (!state.music[idx]) return
|
||||
descEditingIdx = idx
|
||||
var entry = state.music[idx]
|
||||
descModalTitleEl.textContent = tt('descModalTitle', { title: entry.title || tt('titleFallback') })
|
||||
descTextarea.value = typeof entry.description === 'string' ? entry.description : ''
|
||||
descModal.hidden = false
|
||||
setTimeout(function () { descTextarea.focus() }, 0)
|
||||
}
|
||||
|
||||
function closeDescModalSaving() {
|
||||
if (descEditingIdx < 0 || !state.music[descEditingIdx]) {
|
||||
descModal.hidden = true
|
||||
descEditingIdx = -1
|
||||
return
|
||||
}
|
||||
// textarea 값을 그대로 저장하되, 줄바꿈은 보존하고 양끝 공백만 다듬는다.
|
||||
var nextDesc = (descTextarea.value || '').replace(/\r\n/g, '\n').trim()
|
||||
var prev = state.music[descEditingIdx].description || ''
|
||||
if (nextDesc !== prev) {
|
||||
state.music[descEditingIdx].description = nextDesc
|
||||
markDirty()
|
||||
renderMusic()
|
||||
}
|
||||
descModal.hidden = true
|
||||
descEditingIdx = -1
|
||||
}
|
||||
|
||||
descBackBtn.addEventListener('click', closeDescModalSaving)
|
||||
descModal.addEventListener('click', function (e) {
|
||||
if (e.target === descModal) closeDescModalSaving()
|
||||
})
|
||||
|
||||
// ── 사진목록: 음악목록 그대로 복사 ─────────────────
|
||||
document.getElementById('image-from-music').addEventListener('click', function () {
|
||||
if (state.music.length === 0) {
|
||||
@@ -637,7 +693,7 @@
|
||||
var entries = result.body.entries || []
|
||||
if (target === 'music') {
|
||||
state.music = entries.map(function (e) {
|
||||
return { url: e.url, title: e.title || '', artist: e.channel || '', durationSec: e.durationSec || 0 }
|
||||
return { url: e.url, title: e.title || '', artist: e.channel || '', durationSec: e.durationSec || 0, aliases: [], description: '' }
|
||||
})
|
||||
renderMusic()
|
||||
} else {
|
||||
|
||||
@@ -407,19 +407,24 @@ body.siteBody.centerLayout {
|
||||
.trackList { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: 4px; }
|
||||
.trackRow {
|
||||
display: grid;
|
||||
grid-template-columns: 36px 80px 1fr auto auto;
|
||||
grid-template-columns: 36px 80px 1fr auto auto auto;
|
||||
gap: 12px; align-items: center;
|
||||
padding: 8px 12px; background: var(--bg-card);
|
||||
border: 1px solid var(--border); border-radius: 8px;
|
||||
cursor: grab; user-select: none;
|
||||
}
|
||||
.aliasBtn {
|
||||
.aliasBtn, .descBtn {
|
||||
background: var(--bg); border: 1px solid var(--border); color: var(--text);
|
||||
padding: 6px 10px; border-radius: 6px; cursor: pointer; font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.aliasBtn:hover { border-color: var(--accent); }
|
||||
.aliasBtn.hasAliases { border-color: var(--accent); color: var(--accent); }
|
||||
.aliasBtn:hover, .descBtn:hover { border-color: var(--accent); }
|
||||
.aliasBtn.hasAliases, .descBtn.hasDesc { border-color: var(--accent); color: var(--accent); }
|
||||
.descTextarea {
|
||||
width: 100%; min-height: 140px; resize: vertical;
|
||||
font-family: inherit; font-size: 13px; line-height: 1.5;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
/* 별칭 모달 */
|
||||
.aliasModalHeader {
|
||||
|
||||
Reference in New Issue
Block a user