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%}