feat: Edit Playlist modal — name, description, public toggle, cover upload
- 'Bearbeiten' button in playlist-detail header opens modal
- Subsonic updatePlaylist API: name, comment, public synced
- Cover upload via POST /api/playlist/{id}/image (Navidrome REST, Basic Auth)
- Cover delete via DELETE /api/playlist/{id}/image
- Graceful toast if Navidrome version doesn't support cover upload
- Mobile-responsive layout (stacks cover + fields on small screens)
- Bump SW cache to ns-v31
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
dad2b0b730
commit
23d7c62045
4 files changed
+150
-2
No files matched your search
@@ -422,15 +422,21 @@ async function quickPlayPlaylist(pl){
|
||||
try{const r=await api('getPlaylist',`&id=${pl.id}`);const songs=r.playlist?.entry||[];playQueue(songs,0)}
|
||||
catch(e){toast('Fehler: '+e.message)}
|
||||
}
|
||||
let _currentPlaylist=null;
|
||||
|
||||
async function loadPlaylistDetail(pl){
|
||||
_currentPlaylist={...pl};
|
||||
document.getElementById('playlist-detail-name').textContent=pl.name;
|
||||
const cw=document.getElementById('playlist-cover');
|
||||
const cv=coverUrl(pl.coverArt,300);
|
||||
cw.innerHTML=cv?`<img src="${cv}" onerror="this.parentNode.innerHTML='<div class=\'no-art\'>🎶</div>'">`:'<div class="no-art">🎶</div>';
|
||||
document.getElementById('btn-edit-playlist').onclick=()=>openEditPlModal(_currentPlaylist);
|
||||
const sl=document.getElementById('playlist-songs');sl.innerHTML='<div class="spinner"></div>';
|
||||
try{
|
||||
const r=await api('getPlaylist',`&id=${pl.id}`);
|
||||
const songs=r.playlist?.entry||[];
|
||||
const full=r.playlist||{};
|
||||
_currentPlaylist={...pl,...full,id:pl.id};
|
||||
const songs=full.entry||[];
|
||||
const totalMin=Math.round(songs.reduce((s,t)=>s+(t.duration||0),0)/60);
|
||||
document.getElementById('playlist-detail-stats').textContent=`${songs.length} Titel · ${totalMin} Min.`;
|
||||
document.getElementById('btn-play-playlist').onclick=()=>playQueue(songs,0);
|
||||
@@ -442,6 +448,77 @@ async function loadPlaylistDetail(pl){
|
||||
}catch(e){sl.innerHTML=`<p style="color:var(--danger)">${e.message}</p>`}
|
||||
}
|
||||
|
||||
// ─── EDIT PLAYLIST ───
|
||||
function openEditPlModal(pl){
|
||||
const overlay=document.getElementById('edit-pl-overlay');
|
||||
document.getElementById('edit-pl-name').value=pl.name||'';
|
||||
document.getElementById('edit-pl-desc').value=pl.comment||'';
|
||||
const tog=document.getElementById('edit-pl-public-toggle');
|
||||
tog.classList.toggle('on',!!pl.public);
|
||||
const cv=coverUrl(pl.coverArt,300);
|
||||
document.getElementById('edit-pl-cover').innerHTML=cv
|
||||
?`<img src="${cv}" style="width:100%;height:100%;object-fit:cover">`
|
||||
:'<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px">🎶</div>';
|
||||
overlay.classList.add('open');
|
||||
}
|
||||
function closeEditPlModal(){document.getElementById('edit-pl-overlay').classList.remove('open')}
|
||||
|
||||
async function saveEditPlaylist(){
|
||||
const pl=_currentPlaylist;if(!pl)return;
|
||||
const name=document.getElementById('edit-pl-name').value.trim();
|
||||
if(!name){toast('Name darf nicht leer sein');return;}
|
||||
const comment=document.getElementById('edit-pl-desc').value.trim();
|
||||
const pub=document.getElementById('edit-pl-public-toggle').classList.contains('on');
|
||||
const btn=document.querySelector('.edit-pl-btn-save');
|
||||
btn.disabled=true;btn.textContent='Speichern…';
|
||||
try{
|
||||
await api('updatePlaylist',`&playlistId=${pl.id}&name=${encodeURIComponent(name)}&comment=${encodeURIComponent(comment)}&public=${pub}`);
|
||||
_currentPlaylist={..._currentPlaylist,name,comment,public:pub};
|
||||
document.getElementById('playlist-detail-name').textContent=name;
|
||||
closeEditPlModal();
|
||||
toast('Playlist aktualisiert');
|
||||
}catch(e){toast('Fehler: '+e.message)}
|
||||
finally{btn.disabled=false;btn.textContent='Speichern';}
|
||||
}
|
||||
|
||||
function editPlUploadCover(){document.getElementById('edit-pl-file').click()}
|
||||
|
||||
async function editPlCoverSelected(input){
|
||||
const file=input.files?.[0];if(!file||!_currentPlaylist)return;
|
||||
input.value='';
|
||||
const preview=URL.createObjectURL(file);
|
||||
document.getElementById('edit-pl-cover').innerHTML=`<img src="${preview}" style="width:100%;height:100%;object-fit:cover">`;
|
||||
try{
|
||||
const form=new FormData();form.append('image',file);
|
||||
const auth='Basic '+btoa(`${cfg.user}:${cfg.pass}`);
|
||||
const res=await fetch(`${cfg.url}/api/playlist/${_currentPlaylist.id}/image`,{method:'POST',headers:{Authorization:auth},body:form});
|
||||
if(!res.ok)throw new Error(res.status);
|
||||
toast('Cover aktualisiert');
|
||||
// Refresh cover in detail view
|
||||
setTimeout(()=>{
|
||||
const cv=document.getElementById('playlist-cover');
|
||||
const img=cv?.querySelector('img');if(img)img.src=img.src.split('?')[0]+'?t='+Date.now();
|
||||
},500);
|
||||
}catch{
|
||||
toast('Cover-Upload nicht unterstützt (Navidrome-Version)');
|
||||
document.getElementById('edit-pl-cover').innerHTML=
|
||||
(coverUrl(_currentPlaylist.coverArt,300)
|
||||
?`<img src="${coverUrl(_currentPlaylist.coverArt,300)}" style="width:100%;height:100%;object-fit:cover">`
|
||||
:'<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px">🎶</div>');
|
||||
}
|
||||
}
|
||||
|
||||
async function editPlRemoveCover(){
|
||||
if(!_currentPlaylist)return;
|
||||
try{
|
||||
const auth='Basic '+btoa(`${cfg.user}:${cfg.pass}`);
|
||||
const res=await fetch(`${cfg.url}/api/playlist/${_currentPlaylist.id}/image`,{method:'DELETE',headers:{Authorization:auth}});
|
||||
if(!res.ok)throw new Error(res.status);
|
||||
toast('Cover entfernt');
|
||||
document.getElementById('edit-pl-cover').innerHTML='<div class="no-art" style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:36px">🎶</div>';
|
||||
}catch{toast('Entfernen nicht unterstützt (Navidrome-Version)')}
|
||||
}
|
||||
|
||||
// ─── RADIO MODE (endloser Shuffle aus der eigenen Bibliothek) ───
|
||||
let radioMode=false;
|
||||
function _applyRadioBtn(){
|
||||
|
||||
+41
@@ -234,6 +234,7 @@
|
||||
<div class="album-actions">
|
||||
<button class="btn-play-all" id="btn-play-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg>Abspielen</button>
|
||||
<button class="btn-outline" id="btn-shuffle-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="16 3 21 3 21 8"/><line x1="4" y1="20" x2="21" y2="3"/><polyline points="21 16 21 21 16 21"/><line x1="15" y1="15" x2="21" y2="21"/></svg>Shuffle</button>
|
||||
<button class="btn-outline" id="btn-edit-playlist"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>Bearbeiten</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -491,6 +492,46 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- EDIT PLAYLIST MODAL -->
|
||||
<div class="edit-pl-overlay" id="edit-pl-overlay" onclick="if(event.target===this)closeEditPlModal()">
|
||||
<div class="edit-pl-box">
|
||||
<div class="edit-pl-header">
|
||||
<span>Playlist bearbeiten</span>
|
||||
<button class="edit-pl-close" onclick="closeEditPlModal()">
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="edit-pl-body">
|
||||
<div class="edit-pl-cover-wrap">
|
||||
<div class="edit-pl-cover" id="edit-pl-cover"><div class="no-art">🎶</div></div>
|
||||
<div class="edit-pl-cover-actions">
|
||||
<button class="edit-pl-cover-btn" onclick="document.getElementById('edit-pl-file').click()" title="Bild hochladen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/></svg>
|
||||
</button>
|
||||
<button class="edit-pl-cover-btn" onclick="editPlRemoveCover()" title="Bild entfernen">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<input type="file" id="edit-pl-file" accept="image/*" style="display:none" onchange="editPlCoverSelected(this)">
|
||||
</div>
|
||||
<div class="edit-pl-fields">
|
||||
<label class="edit-pl-label">Name <span style="color:var(--accent)">*</span></label>
|
||||
<input class="edit-pl-input" id="edit-pl-name" type="text" placeholder="Playlist-Name">
|
||||
<label class="edit-pl-label">Beschreibung</label>
|
||||
<textarea class="edit-pl-input edit-pl-textarea" id="edit-pl-desc" placeholder="Optionale Beschreibung…" rows="3"></textarea>
|
||||
<div class="edit-pl-toggle-row">
|
||||
<span>Öffentlich</span>
|
||||
<div class="toggle" id="edit-pl-public-toggle" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="edit-pl-footer">
|
||||
<button class="edit-pl-btn-cancel" onclick="closeEditPlModal()">Abbrechen</button>
|
||||
<button class="edit-pl-btn-save" onclick="saveEditPlaylist()">Speichern</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SONG INFO MODAL -->
|
||||
<div class="info-modal" id="info-modal">
|
||||
<div class="inner">
|
||||
|
||||
+30
@@ -338,6 +338,36 @@ nav{flex:1;overflow-y:auto;padding:4px 10px}
|
||||
.pl-btns{display:flex;gap:8px}
|
||||
.pl-btns button{flex:1;padding:10px;border-radius:8px;font-family:var(--font);font-size:13px;font-weight:600;cursor:pointer;border:none}
|
||||
|
||||
/* ── EDIT PLAYLIST MODAL ── */
|
||||
.edit-pl-overlay{position:fixed;inset:0;background:rgba(0,0,0,.72);backdrop-filter:blur(5px);z-index:500;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
|
||||
.edit-pl-overlay.open{opacity:1;pointer-events:all}
|
||||
.edit-pl-box{width:min(580px,calc(100vw - 24px));background:var(--surface);border:1px solid var(--border2);border-radius:18px;box-shadow:0 40px 80px rgba(0,0,0,.8);overflow:hidden}
|
||||
.edit-pl-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px 16px;font-size:18px;font-weight:700;border-bottom:1px solid var(--border)}
|
||||
.edit-pl-close{background:none;border:none;color:var(--text2);cursor:pointer;padding:4px;display:flex;border-radius:6px;transition:color .15s}
|
||||
.edit-pl-close:hover{color:var(--text)}
|
||||
.edit-pl-body{display:flex;gap:24px;padding:24px;align-items:flex-start}
|
||||
.edit-pl-cover-wrap{position:relative;flex-shrink:0;width:180px;height:180px;border-radius:12px;overflow:hidden;background:var(--surface2)}
|
||||
.edit-pl-cover{width:100%;height:100%}
|
||||
.edit-pl-cover img{width:100%;height:100%;object-fit:cover;display:block}
|
||||
.edit-pl-cover-actions{position:absolute;bottom:8px;right:8px;display:flex;gap:6px;opacity:0;transition:opacity .2s}
|
||||
.edit-pl-cover-wrap:hover .edit-pl-cover-actions{opacity:1}
|
||||
.edit-pl-cover-actions{opacity:1}
|
||||
.edit-pl-cover-btn{width:34px;height:34px;background:rgba(0,0,0,.65);border:none;border-radius:50%;color:#fff;cursor:pointer;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(4px);transition:background .15s}
|
||||
.edit-pl-cover-btn:hover{background:rgba(0,0,0,.85)}
|
||||
.edit-pl-fields{flex:1;min-width:0;display:flex;flex-direction:column;gap:14px}
|
||||
.edit-pl-label{font-size:12px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:.05em;margin-bottom:-8px}
|
||||
.edit-pl-input{width:100%;padding:10px 13px;background:var(--bg2);border:1px solid var(--border2);border-radius:9px;color:var(--text);font-family:var(--font);font-size:14px;outline:none;transition:border-color .2s;box-sizing:border-box}
|
||||
.edit-pl-input:focus{border-color:var(--accent)}
|
||||
.edit-pl-textarea{resize:vertical;min-height:72px;line-height:1.5}
|
||||
.edit-pl-toggle-row{display:flex;align-items:center;justify-content:space-between;padding:4px 0}
|
||||
.edit-pl-toggle-row span{font-size:14px;font-weight:500}
|
||||
.edit-pl-footer{display:flex;justify-content:flex-end;gap:10px;padding:16px 24px;border-top:1px solid var(--border)}
|
||||
.edit-pl-btn-cancel{padding:10px 20px;border-radius:9px;border:none;background:none;color:var(--text2);font-family:var(--font);font-size:14px;font-weight:600;cursor:pointer;transition:color .15s}
|
||||
.edit-pl-btn-cancel:hover{color:var(--text)}
|
||||
.edit-pl-btn-save{padding:10px 22px;border-radius:9px;border:none;background:var(--accent);color:#000;font-family:var(--font);font-size:14px;font-weight:700;cursor:pointer;transition:opacity .15s}
|
||||
.edit-pl-btn-save:hover{opacity:.85}
|
||||
@media(max-width:480px){.edit-pl-body{flex-direction:column;align-items:center}.edit-pl-cover-wrap{width:140px;height:140px}}
|
||||
|
||||
/* ── SONG INFO MODAL ── */
|
||||
.info-modal{position:fixed;inset:0;background:rgba(0,0,0,.65);backdrop-filter:blur(4px);z-index:400;display:flex;align-items:center;justify-content:center;opacity:0;pointer-events:none;transition:opacity .2s}
|
||||
.info-modal.open{opacity:1;pointer-events:all}
|
||||
|
||||
Reference in new issue
Block a user