From fa7e980f1acdeb108fae43b2bfc87116e89b440a Mon Sep 17 00:00:00 2001 From: Honey Date: Sun, 26 Apr 2026 17:26:50 +0200 Subject: [PATCH] feat: player drag feedback, global search fixes, iTunes artist discovery - Player bar: drag handle pill + live lift/peek animation when swiping up to NP - Global search: now includes Navidrome library results alongside iTunes; fix songs not showing (filter wrapperType=track) - iTunes artist page: hero banner with album art, top songs section - Artist detail: iTunes global top tracks + extra albums not on server Co-Authored-By: Claude Sonnet 4.6 --- index.html | 1 + src/main.js | 38 +++++++++++-- src/views.js | 158 +++++++++++++++++++++++++++++++++++++++++++-------- styles.css | 2 + 4 files changed, 168 insertions(+), 31 deletions(-) diff --git a/index.html b/index.html index 2fe3244..49ab15f 100644 --- a/index.html +++ b/index.html @@ -307,6 +307,7 @@
+
♪
diff --git a/src/main.js b/src/main.js index 1b81505..673ce98 100644 --- a/src/main.js +++ b/src/main.js @@ -112,20 +112,46 @@ window.addEventListener('load', () => { // Click player-art to open Now Playing document.getElementById('player-art')?.addEventListener('click', openNowPlaying); - // Swipe-up on player bar to open Now Playing + // Swipe-up on player bar to open Now Playing (with drag visual feedback) const playerBar = document.querySelector('.player'); + const npOverlay = document.getElementById('np-overlay'); if(playerBar) { - let _sy=0, _st=0; + let _sy=0, _dragging=false; + + function _resetDrag(open) { + playerBar.classList.remove('dragging'); + playerBar.style.transform = ''; + if(npOverlay) { npOverlay.style.transition = ''; npOverlay.style.transform = ''; } + if(open) openNowPlaying(); + _sy=0; _dragging=false; + } + playerBar.addEventListener('touchstart', e => { if(e.target.closest('button') || e.target.closest('.progress-bar') || e.target.closest('.volume-wrap')) return; - _sy=e.touches[0].clientY; _st=Date.now(); + _sy=e.touches[0].clientY; _dragging=false; }, {passive:true}); + + playerBar.addEventListener('touchmove', e => { + if(!_sy) return; + const dy = _sy - e.touches[0].clientY; + if(dy < 5) return; + _dragging = true; + playerBar.classList.add('dragging'); + const lift = Math.min(Math.max(dy, 0), 160); + playerBar.style.transform = `translateY(${-lift}px)`; + if(npOverlay) { + npOverlay.style.transition = 'none'; + npOverlay.style.transform = `translateY(${Math.max(0, window.innerHeight - lift * 1.6)}px)`; + } + }, {passive:true}); + playerBar.addEventListener('touchend', e => { if(!_sy) return; - const dy=_sy-e.changedTouches[0].clientY, dt=Date.now()-_st; - if(dy>40 || (dt<300&&dy>15)) openNowPlaying(); - _sy=0; + const dy = _sy - e.changedTouches[0].clientY; + _resetDrag(_dragging ? dy > 60 : dy > 15); }, {passive:true}); + + playerBar.addEventListener('touchcancel', () => _resetDrag(false), {passive:true}); } }); diff --git a/src/views.js b/src/views.js index 7162693..f0785a2 100644 --- a/src/views.js +++ b/src/views.js @@ -228,12 +228,15 @@ export async function loadArtistDetail(artist) { }); const g = document.getElementById('artist-albums-grid'); g.innerHTML = '
'; + document.getElementById('artist-itunes-extra')?.remove(); - const [imgRes, albumRes, infoRes, topRes] = await Promise.allSettled([ + const [imgRes, albumRes, infoRes, topRes, itunesTracksRes, itunesAlbumsRes] = await Promise.allSettled([ getArtistImage(artist.id), api('getArtist', `&id=${artist.id}`), api('getArtistInfo2', `&id=${artist.id}&count=8`), - api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`) + api('getTopSongs', `&artist=${encodeURIComponent(artist.name)}&count=10`), + itunesSearch(artist.name, 'musicTrack', 10), + itunesSearch(artist.name, 'album', 20), ]); const img = imgRes.status==='fulfilled' ? imgRes.value : null; @@ -299,6 +302,50 @@ export async function loadArtistDetail(artist) { tsEl.appendChild(list); } } + + // ── iTunes: global top tracks ── + const itunesTracks = itunesTracksRes.status==='fulfilled' + ? (itunesTracksRes.value||[]).filter(t => t.wrapperType==='track' && t.artistName?.toLowerCase()===artist.name.toLowerCase()) + : []; + if(itunesTracks.length) { + const tsEl = document.getElementById('artist-topsongs-section'); + if(tsEl) { + const heading = topSongs.length ? 'Weltweit populär (iTunes)' : 'Populäre Titel'; + const sec = document.createElement('div'); + sec.innerHTML = `
${heading}
`; + const list = document.createElement('div'); list.className = 'song-list'; list.style.marginBottom = '20px'; + itunesTracks.forEach((t, i) => list.appendChild(makeGlobalTrackItem(t, i, itunesTracks))); + sec.appendChild(list); + tsEl.appendChild(sec); + } + } + + // ── iTunes: more albums ── + const itunesAlbums = itunesAlbumsRes.status==='fulfilled' ? (itunesAlbumsRes.value||[]) : []; + const serverAlbumNames = new Set((albumRes.status==='fulfilled' ? (albumRes.value.artist?.album||[]) : []).map(a => a.name.toLowerCase())); + const extraAlbums = itunesAlbums.filter(a => + (a.artistId === undefined || a.artistName?.toLowerCase() === artist.name.toLowerCase()) && + !serverAlbumNames.has(a.collectionName?.toLowerCase()) + ); + if(extraAlbums.length) { + const g = document.getElementById('artist-albums-grid'); + if(g) { + const wrap = document.createElement('div'); wrap.id = 'artist-itunes-extra'; + const heading = document.createElement('div'); + heading.className = 'section-title'; + heading.style.cssText = 'margin-top:24px;margin-bottom:12px'; + heading.textContent = 'Weitere Alben (iTunes)'; + const extraGrid = document.createElement('div'); extraGrid.className = 'grid grid-4'; + extraAlbums.slice(0,16).forEach(a => { + const art = a.artworkUrl100?.replace('100x100','300x300'); + const c = makeCard(a.collectionName, String(a.releaseDate?.slice(0,4)||''), art, '💿'); + c.onclick = () => loadItunesAlbum(a); + extraGrid.appendChild(c); + }); + wrap.appendChild(heading); wrap.appendChild(extraGrid); + g.after(wrap); + } + } } // ─── ALBUM DETAIL ─── @@ -486,12 +533,48 @@ export function onGlobalSearch(val) { export async function doGlobalSearch(q) { const el = document.getElementById('global-search-results'); try { - const [tracks, albums, artists] = await Promise.all([ - itunesSearch(q,'musicTrack',25), + const [serverRes, tracks, albums, artists] = await Promise.all([ + api('search3', `&query=${encodeURIComponent(q)}&songCount=15&albumCount=5&artistCount=3`).catch(() => null), + itunesSearch(q,'musicTrack',20), itunesSearch(q,'album',10), itunesSearch(q,'musicArtist',6) ]); el.innerHTML = ''; + + // ── Library results (Navidrome) ── + const srvRes = serverRes?.searchResult3 || {}; + const srvSongs = srvRes.song || [], srvArtists = srvRes.artist || [], srvAlbums = srvRes.album || []; + if(srvSongs.length || srvArtists.length || srvAlbums.length) { + const s = mkSection('📚 In meiner Bibliothek'); + if(srvArtists.length) { + const row = document.createElement('div'); row.className = 'scroll-row'; + srvArtists.forEach(a => { + const c = makeCard(a.name, a.albumCount+' Alben', null, '🎤'); + c.onclick = () => navigate('artist-detail', a); + row.appendChild(c); + getArtistImage(a.id).then(img => { + if(!img) return; + const artEl = c.querySelector('.card-art'); if(!artEl) return; + artEl.innerHTML = `
`; + }); + }); + s.appendChild(row); + } + if(srvAlbums.length) { + const row = document.createElement('div'); row.className = 'scroll-row'; + srvAlbums.forEach(a => row.appendChild(makeAlbumCard(a))); + s.appendChild(row); + } + if(srvSongs.length) { + const list = document.createElement('div'); list.className = 'song-list'; + srvSongs.forEach((so, i) => list.appendChild(makeSongItem(so, i, srvSongs))); + s.appendChild(list); + } + el.appendChild(s); + } + + // ── iTunes results ── + const trackResults = tracks.filter(t => t.wrapperType === 'track'); if(artists.length) { const s = mkSection('Künstler'); const row = document.createElement('div'); row.className = 'scroll-row'; @@ -503,8 +586,7 @@ export async function doGlobalSearch(q) { if(!res[0]) return; const imgUrl = res[0].artworkUrl100?.replace('100x100','300x300'); if(!imgUrl) return; - const artEl = c.querySelector('.card-art'); - if(!artEl) return; + const artEl = c.querySelector('.card-art'); if(!artEl) return; const img = document.createElement('img'); img.src = imgUrl; img.loading = 'lazy'; img.style.cssText = 'width:100%;height:100%;object-fit:cover;transition:transform .4s'; const overlay = artEl.querySelector('.play-overlay'); @@ -535,13 +617,13 @@ export async function doGlobalSearch(q) { }); s.appendChild(row); el.appendChild(s); } - if(tracks.length) { + if(trackResults.length) { const s = mkSection('Titel'); const list = document.createElement('div'); list.className = 'song-list'; - tracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, tracks))); + trackResults.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, trackResults))); s.appendChild(list); el.appendChild(s); } - if(!artists.length && !albums.length && !tracks.length) { + if(!srvSongs.length && !srvArtists.length && !srvAlbums.length && !artists.length && !albums.length && !trackResults.length) { el.innerHTML = '
🔍

Keine Ergebnisse gefunden

'; } } catch(e) { el.innerHTML = `

Suche fehlgeschlagen: ${e.message}

`; } @@ -573,24 +655,50 @@ export async function loadItunesArtistResults(artistId, artistName) { const el = document.getElementById('global-search-results'); el.innerHTML = '
'; try { - const albums = await itunesSearch(artistName, 'album', 20); + const [albums, topTracks] = await Promise.all([ + itunesSearch(artistName, 'album', 20), + itunesSearch(artistName, 'musicTrack', 10), + ]); const artistAlbums = albums.filter(a => a.artistId===artistId || a.artistName===artistName); + const artistTracks = topTracks.filter(t => t.wrapperType==='track' && (t.artistId===artistId || t.artistName===artistName)); + const coverImg = artistAlbums[0]?.artworkUrl100?.replace('100x100','600x600') || ''; el.innerHTML = ''; - const header = document.createElement('div'); - header.innerHTML = `
- -
${esc(artistName)}
-
-
Alben
`; - el.appendChild(header); - const grid = document.createElement('div'); grid.className = 'grid grid-4'; - (artistAlbums.length ? artistAlbums : albums).forEach(a => { - const art = a.artworkUrl100?.replace('100x100','300x300'); - const c = makeCard(a.collectionName, a.artistName, art, '💿'); - c.onclick = () => loadItunesAlbum(a); - grid.appendChild(c); - }); - el.appendChild(grid); + + // Hero header + const hero = document.createElement('div'); + hero.style.cssText = 'position:relative;border-radius:16px;overflow:hidden;margin-bottom:20px;min-height:120px;background:var(--surface2)'; + if(coverImg) hero.style.background = `url('${coverImg}') center/cover no-repeat`; + hero.innerHTML = ` +
+
+ +
${esc(artistName)}
+
`; + el.appendChild(hero); + + // Top songs + if(artistTracks.length) { + const s = mkSection('Populäre Titel'); + const list = document.createElement('div'); list.className = 'song-list'; + artistTracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, artistTracks))); + s.appendChild(list); el.appendChild(s); + } + + // Albums + const finalAlbums = artistAlbums.length ? artistAlbums : albums; + if(finalAlbums.length) { + const s = mkSection('Alben'); + const grid = document.createElement('div'); grid.className = 'grid grid-4'; + finalAlbums.forEach(a => { + const art = a.artworkUrl100?.replace('100x100','300x300'); + const c = makeCard(a.collectionName, a.artistName, art, '💿'); + c.onclick = () => loadItunesAlbum(a); + grid.appendChild(c); + }); + s.appendChild(grid); el.appendChild(s); + } } catch(e) { el.innerHTML = `

${e.message}

`; } } diff --git a/styles.css b/styles.css index 7fdf5f1..78ea5b4 100644 --- a/styles.css +++ b/styles.css @@ -176,6 +176,8 @@ nav{flex:1;overflow-y:auto;padding:4px 10px} /* ── PLAYER ── */ .player{position:fixed;bottom:0;left:0;right:0;height:var(--player-h);background:rgba(8,8,20,.93);backdrop-filter:blur(28px);border-top:1px solid var(--border2);display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:0;padding:0 20px;z-index:100} +.player-drag-handle{position:absolute;top:5px;left:50%;transform:translateX(-50%);width:32px;height:3px;border-radius:2px;background:rgba(255,255,255,.18);transition:background .2s,width .2s;pointer-events:none} +.player.dragging .player-drag-handle{background:rgba(255,255,255,.45);width:44px} .player-track{min-width:0;display:flex;align-items:center;gap:10px;overflow:visible} .player-art{width:52px;height:52px;border-radius:50%;background:var(--surface2);flex-shrink:0;overflow:hidden;position:relative;box-shadow:0 0 0 2px var(--surface2),0 0 0 4px rgba(255,255,255,.08)} .player-art img{width:100%;height:100%;object-fit:cover;border-radius:50%}