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:
HoneyandClaude Sonnet 4.6 committed 2026-04-26 01:24:11 +02:00
1 parent dad2b0b730
commit 23d7c62045
4 files changed
+150 -2

No files matched your search

+78 -1
View File
@@ -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(){