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 <noreply@anthropic.com>
This commit is contained in:
1 parent
ca21b98a19
commit
fa7e980f1a
4 files changed
+168
-31
No files matched your search
@@ -307,6 +307,7 @@
|
||||
|
||||
<!-- PLAYER -->
|
||||
<div class="player">
|
||||
<div class="player-drag-handle"></div>
|
||||
<div class="player-track">
|
||||
<div class="player-art" id="player-art" style="cursor:pointer" title="Jetzt läuft öffnen"><div class="no-art">♪</div></div>
|
||||
<div class="player-info">
|
||||
|
||||
+32
-6
@@ -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});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
+133
-25
@@ -228,12 +228,15 @@ export async function loadArtistDetail(artist) {
|
||||
});
|
||||
const g = document.getElementById('artist-albums-grid');
|
||||
g.innerHTML = '<div class="spinner"></div>';
|
||||
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 = `<div class="section-title" style="margin-top:${topSongs.length?'8px':'0'};margin-bottom:10px">${heading}</div>`;
|
||||
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 = `<img src="${img}" loading="lazy" onerror="this.parentNode.innerHTML='<div class=\\'no-art\\'>🎤</div>'"><div class="play-overlay"><button class="play-btn"><svg width="13" height="13" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"/></svg></button></div>`;
|
||||
});
|
||||
});
|
||||
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 = '<div class="empty-state"><div class="icon">🔍</div><p>Keine Ergebnisse gefunden</p></div>';
|
||||
}
|
||||
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">Suche fehlgeschlagen: ${e.message}</p>`; }
|
||||
@@ -573,24 +655,50 @@ export async function loadItunesArtistResults(artistId, artistName) {
|
||||
const el = document.getElementById('global-search-results');
|
||||
el.innerHTML = '<div class="spinner"></div>';
|
||||
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 = `<div style="display:flex;align-items:center;gap:10px;margin-bottom:20px">
|
||||
<button class="back-btn" style="margin:0" onclick="onGlobalSearch(document.getElementById('global-search-input').value)"><svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Zurück</button>
|
||||
<div style="font-size:20px;font-weight:800">${esc(artistName)}</div>
|
||||
</div>
|
||||
<div class="section-title" style="margin-bottom:12px">Alben</div>`;
|
||||
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 = `
|
||||
<div style="position:absolute;inset:0;background:linear-gradient(to right,rgba(7,7,15,.92) 0%,rgba(7,7,15,.5) 70%,transparent 100%)"></div>
|
||||
<div style="position:relative;padding:24px 20px;display:flex;align-items:center;gap:12px">
|
||||
<button class="back-btn" style="margin:0;flex-shrink:0" onclick="onGlobalSearch(document.getElementById('global-search-input').value)">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>Zurück
|
||||
</button>
|
||||
<div style="font-size:22px;font-weight:800;text-shadow:0 2px 8px rgba(0,0,0,.6)">${esc(artistName)}</div>
|
||||
</div>`;
|
||||
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 = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||
}
|
||||
|
||||
|
||||
@@ -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%}
|
||||
|
||||
Reference in new issue
Block a user