refactor: split views/views.js into 6 focused modules
navigation.js — navigate(), history, closeHeaderSearch, _navGoBack home.js — home page, genres, album rows, loadArtists/loadAlbums, getArtistImage detail.js — artist/album/iTunes detail pages, quickPlay helpers, expandArtistBio playlists.js — playlists, playlist detail, reorder, starred, random search.js — local/header/global search, iTunes helpers, s_goArtist, playerBarGoX discover.js — new releases, recommendations (Last.fm + iTunes fallback) views.js — barrel re-export; all external imports unchanged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
308a27d293
commit
2a2e5ea495
7 files changed
+1364
-1346
No files matched your search
@@ -0,0 +1,252 @@
|
||||
import { S } from '../state.js';
|
||||
import { esc, toast } from '../utils.js';
|
||||
import { api, itunesSearch, itunesLookupAlbum, fetchRomajiIndex, dispArtistById } from '../api.js';
|
||||
import { makeCard, makeAlbumCard, makeGlobalTrackItem, makeSongItem, mkSection } from '../ui/components.js';
|
||||
import { playPreview } from '../player/player.js';
|
||||
import { openDlModal } from '../ui/ui.js';
|
||||
import { closeNowPlaying } from '../player/nowplaying.js';
|
||||
// Circular — safe (called inside function bodies)
|
||||
import { navigate } from './navigation.js';
|
||||
import { getArtistImage } from './home.js';
|
||||
|
||||
// ─── ROMAJI → HIRAGANA ───
|
||||
let _wanakana = null;
|
||||
async function _romajiToHiragana(q) {
|
||||
if (!/^[a-z\s'-]+$/i.test(q.trim())) return null;
|
||||
try {
|
||||
if (!_wanakana) _wanakana = await import('https://cdn.jsdelivr.net/npm/wanakana@5/esm/wanakana.js');
|
||||
const h = _wanakana.toHiragana(q.trim(), { passRomaji: false });
|
||||
return /[-ゟ]/.test(h) ? h : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function _mergeUniq(a, b) {
|
||||
const seen = new Set(a.map(x => x.id));
|
||||
return [...a, ...(b || []).filter(x => !seen.has(x.id))];
|
||||
}
|
||||
|
||||
// ─── LOCAL SEARCH ───
|
||||
export function onLocalSearch(val) {
|
||||
clearTimeout(S.searchTimer);
|
||||
if(!val.trim()) { navigate('home'); return; }
|
||||
navigate('search');
|
||||
S.searchTimer = setTimeout(() => doLocalSearch(val), 350);
|
||||
}
|
||||
|
||||
export async function doLocalSearch(q, el) {
|
||||
if (!el) el = document.getElementById('search-results');
|
||||
el.innerHTML = '<div class="spinner"></div>';
|
||||
try {
|
||||
const hiragana = await _romajiToHiragana(q);
|
||||
const isRomaji = /^[a-z\s'-]+$/i.test(q.trim());
|
||||
const [r, r2, romajiIdx] = await Promise.all([
|
||||
api('search3', `&query=${encodeURIComponent(q)}&songCount=30&albumCount=10&artistCount=6`),
|
||||
hiragana ? api('search3', `&query=${encodeURIComponent(hiragana)}&songCount=20&albumCount=6&artistCount=4`) : Promise.resolve(null),
|
||||
isRomaji ? fetchRomajiIndex() : Promise.resolve(null),
|
||||
]);
|
||||
const res = r.searchResult3 || {};
|
||||
const res2 = r2?.searchResult3 || {};
|
||||
el.innerHTML = '';
|
||||
const artists = _mergeUniq(res.artist||[], res2.artist);
|
||||
const albums = _mergeUniq(res.album ||[], res2.album);
|
||||
const qLow = q.trim().toLowerCase();
|
||||
const romajiHits = romajiIdx ? romajiIdx.filter(e => e.r.includes(qLow)) : [];
|
||||
const songs = _mergeUniq(_mergeUniq(res.song||[], res2.song), romajiHits);
|
||||
|
||||
if(!artists.length && !albums.length && !songs.length) {
|
||||
el.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><p>Keine Ergebnisse</p></div>'; return;
|
||||
}
|
||||
if(artists.length) {
|
||||
const s = mkSection('Künstler');
|
||||
const g = document.createElement('div'); g.className = 'scroll-row';
|
||||
artists.forEach(a => {
|
||||
const c = makeCard(dispArtistById(a.id, a.name), a.albumCount+' Alben', null, '🎤');
|
||||
c.classList.add('artist-card');
|
||||
c.onclick = () => navigate('artist-detail', a);
|
||||
g.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>'">`;
|
||||
});
|
||||
});
|
||||
s.appendChild(g); el.appendChild(s);
|
||||
}
|
||||
if(albums.length) {
|
||||
const s = mkSection('Alben');
|
||||
const g = document.createElement('div'); g.className = 'scroll-row';
|
||||
albums.forEach(a => g.appendChild(makeAlbumCard(a)));
|
||||
s.appendChild(g); el.appendChild(s);
|
||||
}
|
||||
if(songs.length) {
|
||||
const sec = mkSection('Titel');
|
||||
const list = document.createElement('div'); list.className = 'song-list';
|
||||
songs.forEach((so,i) => list.appendChild(makeSongItem(so, i, songs)));
|
||||
sec.appendChild(list); el.appendChild(sec);
|
||||
}
|
||||
} catch(e) { el.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; }
|
||||
}
|
||||
|
||||
// ─── HEADER SEARCH ───
|
||||
export function onHeaderSearch(val) {
|
||||
const overlay = document.getElementById('header-search-overlay');
|
||||
const clearBtn = document.getElementById('header-search-clear');
|
||||
if (clearBtn) clearBtn.style.display = val ? 'block' : 'none';
|
||||
clearTimeout(S._headerSearchTimer);
|
||||
if (!val.trim()) { if (overlay) overlay.style.display = 'none'; return; }
|
||||
if (overlay) {
|
||||
const header = document.getElementById('mobile-header');
|
||||
if (header) overlay.style.top = header.offsetHeight + 'px';
|
||||
overlay.style.display = 'block';
|
||||
}
|
||||
const el = document.getElementById('header-search-results');
|
||||
S._headerSearchTimer = setTimeout(() => doLocalSearch(val, el), 300);
|
||||
}
|
||||
|
||||
export function onMobileSearch(val) { onHeaderSearch(val); }
|
||||
|
||||
// ─── GLOBAL (ITUNES) SEARCH ───
|
||||
export function onGlobalSearch(val) {
|
||||
clearTimeout(S.globalSearchTimer);
|
||||
const el = document.getElementById('global-search-results');
|
||||
if(!val.trim()) { el.innerHTML = '<div class="empty-state"><div class="icon">🔍</div><p>Gib einen Suchbegriff ein</p></div>'; return; }
|
||||
el.innerHTML = '<div class="spinner"></div>';
|
||||
S.globalSearchTimer = setTimeout(() => doGlobalSearch(val), 400);
|
||||
}
|
||||
|
||||
export async function doGlobalSearch(q) {
|
||||
const el = document.getElementById('global-search-results');
|
||||
try {
|
||||
const [tracks, albums, artists] = await Promise.all([
|
||||
itunesSearch(q,'musicTrack',25),
|
||||
itunesSearch(q,'album',10),
|
||||
itunesSearch(q,'musicArtist',6),
|
||||
]);
|
||||
el.innerHTML = '';
|
||||
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';
|
||||
artists.forEach(a => {
|
||||
const c = makeCard(a.artistName, 'iTunes Künstler', null, '🎤');
|
||||
c.onclick = () => loadItunesArtistResults(a.artistId, a.artistName);
|
||||
row.appendChild(c);
|
||||
itunesSearch(a.artistName,'album',1).then(res => {
|
||||
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 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');
|
||||
artEl.innerHTML = ''; artEl.appendChild(img); if(overlay) artEl.appendChild(overlay);
|
||||
img.onerror = () => { artEl.innerHTML = '<div class="no-art">🎤</div>'; if(overlay) artEl.appendChild(overlay); };
|
||||
c.addEventListener('mouseenter', () => img.style.transform = 'scale(1.05)');
|
||||
c.addEventListener('mouseleave', () => img.style.transform = 'scale(1)');
|
||||
}).catch(() => {});
|
||||
});
|
||||
s.appendChild(row); el.appendChild(s);
|
||||
}
|
||||
if(albums.length) {
|
||||
const s = mkSection('Alben');
|
||||
const row = document.createElement('div'); row.className = 'scroll-row';
|
||||
albums.forEach(a => {
|
||||
const art = a.artworkUrl100?.replace('100x100','300x300');
|
||||
const c = makeCard(a.collectionName, a.artistName, art, '💿');
|
||||
c.querySelector('.play-btn').onclick = (e) => { e.stopPropagation(); loadItunesAlbum(a); };
|
||||
c.querySelector('.dl-btn-overlay')?.remove();
|
||||
const dlOverlay = document.createElement('button');
|
||||
dlOverlay.className = 'dl-btn-overlay';
|
||||
dlOverlay.title = 'Auf Server laden';
|
||||
dlOverlay.innerHTML = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||
dlOverlay.onclick = (e) => { e.stopPropagation(); openDlModal({artistName:a.artistName,trackName:a.collectionName,artworkUrl100:a.artworkUrl100,collectionName:a.collectionName,isAlbum:true,itunesData:a}); };
|
||||
c.querySelector('.play-overlay').appendChild(dlOverlay);
|
||||
c.onclick = () => loadItunesAlbum(a);
|
||||
row.appendChild(c);
|
||||
});
|
||||
s.appendChild(row); el.appendChild(s);
|
||||
}
|
||||
if(trackResults.length) {
|
||||
const s = mkSection('Titel');
|
||||
const list = document.createElement('div'); list.className = 'song-list';
|
||||
trackResults.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, trackResults)));
|
||||
s.appendChild(list); el.appendChild(s);
|
||||
}
|
||||
if(!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>`; }
|
||||
}
|
||||
|
||||
export async function loadItunesAlbum(album) {
|
||||
try {
|
||||
const tracks = await itunesLookupAlbum(album.collectionId);
|
||||
if(!tracks.length) { toast('Keine Tracks gefunden'); return; }
|
||||
const el = document.getElementById('global-search-results');
|
||||
const s = document.createElement('div'); s.className = 'search-section';
|
||||
s.innerHTML = `<div style="display:flex;align-items:center;gap:10px;margin-bottom:12px">
|
||||
<button class="back-btn" style="margin:0" onclick="document.getElementById('global-search-input').dispatchEvent(new Event('input'))"><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="flex:1">
|
||||
<div style="font-size:15px;font-weight:700">${esc(album.collectionName)}</div>
|
||||
<div style="font-size:12px;color:var(--text2)">${esc(album.artistName)}</div>
|
||||
</div>
|
||||
<button class="btn-outline blue" style="font-size:12px;padding:7px 12px" onclick="openDlModal({artistName:'${esc(album.artistName)}',trackName:'${esc(album.collectionName)}',artworkUrl100:'${album.artworkUrl100||''}',collectionName:'${esc(album.collectionName)}',isAlbum:true,itunesData:${JSON.stringify(album).replace(/"/g,'"')}})">⬇ Album laden</button>
|
||||
</div>
|
||||
<div class="search-section-title">Tracks</div>`;
|
||||
const list = document.createElement('div'); list.className = 'song-list';
|
||||
tracks.forEach((t,i) => list.appendChild(makeGlobalTrackItem(t, i, tracks)));
|
||||
s.appendChild(list);
|
||||
el.innerHTML = ''; el.appendChild(s);
|
||||
} catch(e) { toast('Fehler: ' + e.message); }
|
||||
}
|
||||
|
||||
export function loadItunesArtistResults(artistId, artistName) {
|
||||
navigate('artist-detail', { id: null, name: artistName, itunesArtistId: artistId });
|
||||
}
|
||||
|
||||
// ─── SEARCH HELPERS ───
|
||||
export function searchSimilarArtist(artistName) {
|
||||
navigate('discover');
|
||||
document.getElementById('global-search-input').value = artistName;
|
||||
onGlobalSearch(artistName);
|
||||
}
|
||||
|
||||
export async function searchAndPreview(trackName, artistName) {
|
||||
try {
|
||||
const res = await itunesSearch(trackName+' '+artistName, 'musicTrack', 1);
|
||||
if(res[0]?.previewUrl) playPreview(res[0]); else toast('Kein Preview verfügbar');
|
||||
} catch { toast('Suche fehlgeschlagen'); }
|
||||
}
|
||||
|
||||
// ─── NAVIGATION SHORTCUTS ───
|
||||
export async function s_goArtist(e, name, id) {
|
||||
if(!name) return;
|
||||
if(id) { navigate('artist-detail', {id, name}); return; }
|
||||
try {
|
||||
const r = await api('search3', `&query=${encodeURIComponent(name)}&artistCount=5&albumCount=0&songCount=0`);
|
||||
const artists = r.searchResult3?.artist || [];
|
||||
const match = artists.find(a => a.name.toLowerCase() === name.toLowerCase()) || artists[0];
|
||||
if(match) navigate('artist-detail', {id: match.id, name: match.name});
|
||||
else toast('Künstler nicht gefunden');
|
||||
} catch { toast('Suche fehlgeschlagen'); }
|
||||
}
|
||||
|
||||
export function s_goAlbum(e, albumId, albumName, artist, artistId) {
|
||||
if(albumId) navigate('album-detail', {id:albumId, name:albumName, artist, artistId:artistId||'', coverArt:''});
|
||||
}
|
||||
|
||||
export function playerBarGoAlbum() {
|
||||
const s = S.queue[S.qIdx];
|
||||
if(!s) return;
|
||||
if(S.npOpen) closeNowPlaying();
|
||||
if(s.albumId) navigate('album-detail', {id:s.albumId, name:s.album||'', artist:s.artist||'', artistId:s.artistId||'', coverArt:s.coverArt||''});
|
||||
}
|
||||
|
||||
export function playerBarGoArtist() {
|
||||
const s = S.queue[S.qIdx];
|
||||
if(!s || !s.artist) return;
|
||||
if(S.npOpen) closeNowPlaying();
|
||||
navigate('artist-detail', {id: s.artistId||s.artist, name: s.artist});
|
||||
}
|
||||
Reference in new issue
Block a user