feat: partial album split view + artist page full-screen + Bibliothek iTunes integration

- Partial album detail: tapping incomplete albums now shows downloaded songs
  (normal playback) above and missing iTunes tracks (30s preview + download)
  below — on both Android and web PWA
- Android: ItunesArtistScreen now renders full-screen above the tab/search bar
- Android: ArtistDetailScreen (Bibliothek) now loads iTunes data in parallel —
  adds Weltweit populär section and In Library/Unvollständig/Nicht heruntergeladen
  album sections identical to the Erkunden artist page
- Fix: artist no longer appears in its own Similar Artists row
- Fix: duplicate album entries deduplicated by collectionId in categorizeAlbums

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-06-14 20:54:32 +02:00
1 parent b25e162806
commit afae5d94df
104 files changed
+8839 -9420

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
const THEME_KEY = 'ns_theme';
const DEFAULT_ID = 'dark-amber';
const DEFAULT_ID = 'dark-ocean';
export const THEMES = [
{
+70 -1
View File
@@ -291,7 +291,7 @@ function _renderArtistAlbums(el, ndAlbums, iAlbums, artist) {
const art = itunes?.artworkUrl100?.replace('100x100','512x512') || coverUrl(nd.coverArt, 512);
const c = makeCard(nd.name, `${have} / ${total} Tracks`, art, '💿');
c.querySelector('.card-art').onclick = (e) => { e.stopPropagation(); quickPlayAlbum(nd); };
c.querySelector('.card-info').onclick = () => itunes ? navigate('itunes-album-detail', itunes) : navigate('album-detail', { ...nd, _fromArtist: artist });
c.querySelector('.card-info').onclick = () => itunes ? navigate('partial-album-detail', { nd, itunes }) : navigate('album-detail', { ...nd, _fromArtist: artist });
const artEl = c.querySelector('.card-art');
const badge = document.createElement('div');
badge.className = 'album-partial-badge'; badge.textContent = `${have}/${total}`;
@@ -376,6 +376,75 @@ export async function loadItunesAlbumDetail(album) {
} catch(e) { sl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
}
export async function loadPartialAlbumDetail({ nd, itunes }) {
document.getElementById('partial-album-back-btn').onclick = () => _navGoBack();
document.getElementById('partial-album-detail-name').textContent = nd.name || '—';
document.getElementById('partial-album-detail-artist').textContent = nd.artist || itunes?.artistName || '';
const art = (itunes?.artworkUrl100 || '').replace('100x100bb','600x600bb').replace('100x100','600x600')
|| (nd.coverArt ? coverUrl(nd.coverArt, 512) : '');
const cw = document.getElementById('partial-album-cover-wrap');
cw.innerHTML = art ? `<img src="${art}" onerror="_imgRetry(this,'💿')">` : '<div class="no-art">💿</div>';
const dlEl = document.getElementById('partial-album-downloaded');
const missEl = document.getElementById('partial-album-missing');
const statsEl = document.getElementById('partial-album-detail-stats');
dlEl.innerHTML = missEl.innerHTML = '<div class="spinner"></div>';
try {
const [ndRes, iTracks] = await Promise.all([
api('getAlbum', `&id=${nd.id}`),
itunes?.collectionId ? itunesLookupAlbum(itunes.collectionId) : Promise.resolve([]),
]);
const ndSongs = (ndRes.album?.song || []).sort((a,b) => (a.track||999)-(b.track||999));
// Cross-reference by normalized track name
const norm = s => (s||'').toLowerCase().replace(/[^a-z0-9]/g,'').trim();
const ndNames = new Set(ndSongs.map(s => norm(s.title)));
const missing = iTracks.filter(t => !ndNames.has(norm(t.trackName))).sort((a,b)=>(a.trackNumber||999)-(b.trackNumber||999));
statsEl.textContent = `${ndSongs.length} heruntergeladen · ${missing.length} fehlen`;
// Play button plays downloaded songs
document.getElementById('partial-album-play-btn').onclick = () => {
if(ndSongs.length) playQueue(ndSongs, 0);
};
// Download button offers missing via iTunes
document.getElementById('partial-album-dl-btn').onclick = () => {
if(itunes) openDlModal({ artistName: itunes.artistName, trackName: itunes.collectionName, collectionName: itunes.collectionName, artworkUrl100: itunes.artworkUrl100||'', isAlbum: true, itunesData: itunes, collectionId: itunes.collectionId });
};
// Section: downloaded
dlEl.innerHTML = '';
if(ndSongs.length) {
const h = document.createElement('div');
h.className = 'section-title';
h.style.cssText = 'margin:16px 0 8px';
h.innerHTML = `Heruntergeladen <span class="section-count">${ndSongs.length}</span>`;
dlEl.appendChild(h);
const list = document.createElement('div'); list.className = 'song-list'; dlEl.appendChild(list);
ndSongs.forEach((s,i) => list.appendChild(makeSongItem(s, i, ndSongs)));
}
// Section: missing
missEl.innerHTML = '';
if(missing.length) {
const h = document.createElement('div');
h.className = 'section-title';
h.style.cssText = 'margin:16px 0 8px;opacity:0.7';
h.innerHTML = `Nicht heruntergeladen <span class="section-count">${missing.length}</span>`;
missEl.appendChild(h);
const list = document.createElement('div'); list.className = 'song-list'; missEl.appendChild(list);
missing.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, missing)));
} else {
missEl.innerHTML = '';
}
} catch(e) {
dlEl.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`;
missEl.innerHTML = '';
}
}
// ─── ALBUM DETAIL ───
export async function loadAlbumDetail(album) {
if (S.cfg.showRomaji) await ensureArtistSortMap().catch(() => {});
+3 -2
View File
@@ -5,7 +5,7 @@ import { loadSpotifyImporter } from './spotify.js';
import { loadSoundCloudImporter } from './soundcloud.js';
// Circular — safe (all calls happen inside function bodies)
import { loadHomeData, loadAlbumRow, loadArtists, loadAlbums, loadGenreDetail } from './home.js';
import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadItunesAlbumDetail } from './detail.js';
import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadItunesAlbumDetail, loadPartialAlbumDetail } from './detail.js';
import { loadPlaylists, loadPlaylistDetail, loadStarred, loadRandom } from './playlists.js';
import { loadNewReleases, loadRecommendations } from './discover.js';
@@ -57,7 +57,8 @@ export function navigate(page, data=null) {
if(page==='artist-all-songs' && data) loadArtistAllSongs(data);
if(page==='album-detail' && data) loadAlbumDetail(data);
if(page==='genre-detail' && data) loadGenreDetail(data);
if(page==='itunes-album-detail'&& data) loadItunesAlbumDetail(data);
if(page==='itunes-album-detail' && data) loadItunesAlbumDetail(data);
if(page==='partial-album-detail' && data) loadPartialAlbumDetail(data);
if(page==='new-releases') loadNewReleases();
if(page==='playlists') loadPlaylists();
if(page==='playlist-detail' && data) loadPlaylistDetail(data);