Add cover art thumbnail column to playlist

This commit is contained in:
Armin 2026-07-08 17:56:01 +02:00
commit 75986cf0ef

View file

@ -72,14 +72,14 @@ canvas { width: 100%; height: 100px; border-radius: 6px; display: block; }
.playlist-toolbar select { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #272c42, #1e2132); border: 1px solid #6b7089; color: #c6c8d1; padding: 4px 20px 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; outline: none; box-shadow: 0 2px 0 #161821, 0 3px 4px rgba(0,0,0,0.3); transition: transform .1s, box-shadow .1s; -webkit-appearance: none; appearance: none; } .playlist-toolbar select { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #272c42, #1e2132); border: 1px solid #6b7089; color: #c6c8d1; padding: 4px 20px 4px 8px; border-radius: 4px; font-size: 11px; cursor: pointer; outline: none; box-shadow: 0 2px 0 #161821, 0 3px 4px rgba(0,0,0,0.3); transition: transform .1s, box-shadow .1s; -webkit-appearance: none; appearance: none; }
.playlist-toolbar select:hover { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #2e3348, #272c42); } .playlist-toolbar select:hover { background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='10' viewBox='0 0 10 10'%3E%3Cpath d='M2 3l3 3 3-3' stroke='%23c6c8d1' stroke-width='1.5' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") right 6px center / 8px no-repeat, linear-gradient(to bottom, #2e3348, #272c42); }
select option { background: #1e2132; color: #c6c8d1; } select option { background: #1e2132; color: #c6c8d1; }
.playlist-header { display: grid; grid-template-columns: 36px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 8px 24px; font-size: 10px; text-transform: uppercase; letter-spacing: .6px; color: #6b7089; border-bottom: 1px solid #272c42; flex-shrink: 0; background: linear-gradient(to bottom, #1e2132, #161821); } .playlist-header { display: grid; grid-template-columns: 36px 28px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 8px 24px; font-size: 10px; text-transform: uppercase; letter-spacing: .6px; color: #6b7089; border-bottom: 1px solid #272c42; flex-shrink: 0; background: linear-gradient(to bottom, #1e2132, #161821); }
.playlist-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 2px 0; position: relative; background: #101219; } .playlist-body { flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden; padding: 2px 0; position: relative; background: #101219; }
#scanning { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(22,24,33,0.85); z-index: 10; font-size: 14px; color: #c6c8d1; flex-direction: column; gap: 10px; } #scanning { position: absolute; inset: 0; display: none; align-items: center; justify-content: center; background: rgba(22,24,33,0.85); z-index: 10; font-size: 14px; color: #c6c8d1; flex-direction: column; gap: 10px; }
#scanning .spinner { width: 20px; height: 20px; border: 2px solid #6b7089; border-top-color: #84a0c6; border-radius: 50%; animation: spin .8s linear infinite; flex-shrink: 0; } #scanning .spinner { width: 20px; height: 20px; border: 2px solid #6b7089; border-top-color: #84a0c6; border-radius: 50%; animation: spin .8s linear infinite; flex-shrink: 0; }
.playlist-body::-webkit-scrollbar { width: 6px; } .playlist-body::-webkit-scrollbar { width: 6px; }
.playlist-body::-webkit-scrollbar-track { background: transparent; } .playlist-body::-webkit-scrollbar-track { background: transparent; }
.playlist-body::-webkit-scrollbar-thumb { background: #272c42; border-radius: 3px; } .playlist-body::-webkit-scrollbar-thumb { background: #272c42; border-radius: 3px; }
.track-item { display: grid; grid-template-columns: 36px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 6px 24px; cursor: default; font-size: 12px; border-left: 3px solid transparent; transition: background .08s; align-items: center; user-select: none; } .track-item { display: grid; grid-template-columns: 36px 28px minmax(0,1fr) minmax(0,1fr) minmax(0,1fr) 60px 60px; gap: 8px; padding: 6px 24px; cursor: default; font-size: 12px; border-left: 3px solid transparent; transition: background .08s; align-items: center; user-select: none; }
.track-item:nth-child(odd) { background: rgba(0,0,0,0.12); } .track-item:nth-child(odd) { background: rgba(0,0,0,0.12); }
.track-item:hover { background: #1e2132; } .track-item:hover { background: #1e2132; }
.track-item.active { background: linear-gradient(to bottom, rgba(135,190,130,0.12), rgba(135,190,130,0.06)); border-left-color: #87be82; } .track-item.active { background: linear-gradient(to bottom, rgba(135,190,130,0.12), rgba(135,190,130,0.06)); border-left-color: #87be82; }
@ -99,7 +99,9 @@ select option { background: #1e2132; color: #c6c8d1; }
.track-item.active.selected .col-title { color: #9bbe8e; } .track-item.active.selected .col-title { color: #9bbe8e; }
.track-item.dragging { opacity: 0.4; } .track-item.dragging { opacity: 0.4; }
.track-item.drag-over { border-top: 2px solid #87be82; } .track-item.drag-over { border-top: 2px solid #87be82; }
.track-item .col-idx { color: #6b7089; text-align: right; font-size: 10px; } .track-item .col-idx { color: #6b7089; text-align: right; font-size: 10px; }
.track-item .col-cover { width: 24px; height: 24px; border-radius: 3px; overflow: hidden; display: flex; align-items: center; justify-content: center; background: #272c42; justify-self: center; }
.track-item .col-cover img { width: 100%; height: 100%; object-fit: cover; display: block; }
.track-item .col-title { color: #c6c8d1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .track-item .col-title { color: #c6c8d1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track-item .col-artist { color: #8a8fa8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .track-item .col-artist { color: #8a8fa8; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.track-item .col-file { color: #787d95; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .track-item .col-file { color: #787d95; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
@ -119,7 +121,7 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
#vuMeter { width: 100%; height: 20px; } #vuMeter { width: 100%; height: 20px; }
.pinned { padding: 12px; } .pinned { padding: 12px; }
.playlist-toolbar { padding: 6px 12px; } .playlist-toolbar { padding: 6px 12px; }
.playlist-header, .track-item { padding-left: 12px; padding-right: 12px; grid-template-columns: 28px minmax(0,1fr) minmax(0,1fr) 50px 40px; } .playlist-header, .track-item { padding-left: 12px; padding-right: 12px; grid-template-columns: 28px 24px minmax(0,1fr) minmax(0,1fr) 50px 40px; }
.track-item .col-file, .playlist-header .hdr-file { display: none; } .track-item .col-file, .playlist-header .hdr-file { display: none; }
.track-item .col-genre, .playlist-header .hdr-genre { display: none; } .track-item .col-genre, .playlist-header .hdr-genre { display: none; }
} }
@ -178,6 +180,7 @@ footer .footer-track { flex: 1; min-width: 0; white-space: nowrap; overflow: hid
<div class="playlist" id="playlist"> <div class="playlist" id="playlist">
<div class="playlist-toolbar"> <div class="playlist-toolbar">
<div class="spacer"></div> <div class="spacer"></div>
<button class="btn btn-sm" id="clearBtn">Clear</button>
<span style="font-size:11px;color:#6b7089">Sort:</span> <span style="font-size:11px;color:#6b7089">Sort:</span>
<select id="sortSelect"> <select id="sortSelect">
<option value="idx">#</option> <option value="idx">#</option>
@ -671,6 +674,7 @@ const addFilesBtn = document.getElementById('addFilesBtn');
const folderPicker = document.getElementById('folderPicker'); const folderPicker = document.getElementById('folderPicker');
const isolateBtn = document.getElementById('isolateBtn'); const isolateBtn = document.getElementById('isolateBtn');
const deleteBtn = document.getElementById('deleteBtn'); const deleteBtn = document.getElementById('deleteBtn');
const clearBtn = document.getElementById('clearBtn');
const sortSelect = document.getElementById('sortSelect'); const sortSelect = document.getElementById('sortSelect');
const scanning = document.getElementById('scanning'); const scanning = document.getElementById('scanning');
const scanningText = scanning.querySelector('span'); const scanningText = scanning.querySelector('span');
@ -698,6 +702,8 @@ const footerStatus = document.getElementById('footerStatus');
const volumeSlider = document.getElementById('volumeSlider'); const volumeSlider = document.getElementById('volumeSlider');
const volDisplay = document.getElementById('volDisplay'); const volDisplay = document.getElementById('volDisplay');
const audioInfo = document.getElementById('audioInfo'); const audioInfo = document.getElementById('audioInfo');
const coverImage = document.getElementById('coverImage');
const coverPlaceholder = document.getElementById('coverPlaceholder');
const errorEl = document.getElementById('error'); const errorEl = document.getElementById('error');
const windowSelect = document.getElementById('windowSelect'); const windowSelect = document.getElementById('windowSelect');
const decaySelect = document.getElementById('decaySelect'); const decaySelect = document.getElementById('decaySelect');
@ -1308,7 +1314,7 @@ function renderTracks() {
if (t === tracks[currentIndex]) classes.push('active'); if (t === tracks[currentIndex]) classes.push('active');
if (selectedIndices.has(realIdx)) classes.push('selected'); if (selectedIndices.has(realIdx)) classes.push('selected');
div.className = classes.join(' '); div.className = classes.join(' ');
div.innerHTML = `<span class="col-idx">${realIdx + 1}</span><span class="col-title">${escHtml(t.name)}</span><span class="col-artist">${escHtml(t.artist || '-')}</span><span class="col-file">${escHtml(t.filename)}</span><span class="col-time">${t.playtime ? fmtTime(t.playtime) : '-'}</span><span class="col-genre">${escHtml(t.genre || '-')}</span>`; div.innerHTML = `<span class="col-idx">${realIdx + 1}</span><span class="col-cover">${t.picture ? `<img src="${t.picture}" alt="">` : ''}</span><span class="col-title">${escHtml(t.name)}</span><span class="col-artist">${escHtml(t.artist || '-')}</span><span class="col-file">${escHtml(t.filename)}</span><span class="col-time">${t.playtime ? fmtTime(t.playtime) : '-'}</span><span class="col-genre">${escHtml(t.genre || '-')}</span>`;
div.addEventListener('mousedown', (e) => { div.addEventListener('mousedown', (e) => {
if (e.detail > 1) return; if (e.detail > 1) return;
const ctrl = e.ctrlKey || e.metaKey; const ctrl = e.ctrlKey || e.metaKey;