diff --git a/app.js b/app.js index 6361ba7..274d2d5 100644 --- a/app.js +++ b/app.js @@ -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?``:'
🎶
'; + document.getElementById('btn-edit-playlist').onclick=()=>openEditPlModal(_currentPlaylist); const sl=document.getElementById('playlist-songs');sl.innerHTML='
'; 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=`

${e.message}

`} } +// ─── 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 + ?`` + :'
🎶
'; + 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=``; + 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) + ?`` + :'
🎶
'); + } +} + +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='
🎶
'; + }catch{toast('Entfernen nicht unterstützt (Navidrome-Version)')} +} + // ─── RADIO MODE (endloser Shuffle aus der eigenen Bibliothek) ─── let radioMode=false; function _applyRadioBtn(){ diff --git a/index.html b/index.html index 7cf0d65..d40d2f4 100644 --- a/index.html +++ b/index.html @@ -234,6 +234,7 @@
+
@@ -491,6 +492,46 @@ + +
+
+
+ Playlist bearbeiten + +
+
+
+
🎶
+
+ + +
+ +
+
+ + + + +
+ Öffentlich +
+
+
+
+ +
+
+
diff --git a/styles.css b/styles.css index c1920c5..5277425 100644 --- a/styles.css +++ b/styles.css @@ -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} diff --git a/sw.js b/sw.js index 06c55be..93ca1a0 100644 --- a/sw.js +++ b/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'ns-v30'; +const CACHE = 'ns-v31'; const PRECACHE = ['/', 'index.html', 'styles.css', 'app.js', 'manifest.json', 'icon.svg']; const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];