//ui.js
import { formatTime, createPlaceholder, trackDataStore, hasExplicitContent, getTrackTitle } from './utils.js';
import { recentActivityManager } from './storage.js';
export class UIRenderer {
constructor(api) {
this.api = api;
}
createExplicitBadge() {
return 'E';
}
createTrackMenuButton() {
return `
`;
}
createTrackItemHTML(track, index, showCover = false) {
const playIconSmall = '';
const trackNumberHTML = `
${showCover ? playIconSmall : index + 1}
`;
const explicitBadge = !hasExplicitContent(track) ? this.createExplicitBadge() : '';
const trackTitle = getTrackTitle(track);
return `
${trackNumberHTML}
${showCover ? `
})
` : ''}
${trackTitle}
${explicitBadge}
${track.artist?.name ?? 'Unknown Artist'}
${formatTime(track.duration)}
`;
}
createAlbumCardHTML(album) {
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
return `
${album.title} ${explicitBadge}
Album • ${album.artist?.name ?? ''}
`;
}
createArtistCardHTML(artist) {
return `
${artist.name}
Artist
`;
}
createSkeletonTrack(showCover = false) {
return `
`;
}
createSkeletonCard(isArtist = false) {
return `
`;
}
createSkeletonTracks(count = 5, showCover = false) {
return `${Array(count).fill(0).map(() => this.createSkeletonTrack(showCover)).join('')}
`;
}
createSkeletonCards(count = 6, isArtist = false) {
return `${Array(count).fill(0).map(() => this.createSkeletonCard(isArtist)).join('')}
`;
}
renderListWithTracks(container, tracks, showCover) {
const fragment = document.createDocumentFragment();
const tempDiv = document.createElement('div');
tempDiv.innerHTML = tracks.map((track, i) =>
this.createTrackItemHTML(track, i, showCover)
).join('');
while (tempDiv.firstChild) {
fragment.appendChild(tempDiv.firstChild);
}
container.innerHTML = '';
container.appendChild(fragment);
tracks.forEach(track => {
const element = container.querySelector(`[data-track-id="${track.id}"]`);
if (element) trackDataStore.set(element, track);
});
}
showPage(pageId) {
document.querySelectorAll('.page').forEach(page => {
page.classList.toggle('active', page.id === `page-${pageId}`);
});
document.querySelectorAll('.sidebar-nav a').forEach(link => {
link.classList.toggle('active', link.hash === `#${pageId}`);
});
document.querySelector('.main-content').scrollTop = 0;
if (pageId === 'settings') {
this.renderApiSettings();
}
}
async renderHomePage() {
this.showPage('home');
const recents = recentActivityManager.getRecents();
const albumsContainer = document.getElementById('home-recent-albums');
const artistsContainer = document.getElementById('home-recent-artists');
if (recents.albums.length > 0 || recents.artists.length > 0) {
albumsContainer.innerHTML = recents.albums.length
? recents.albums.map(album => this.createAlbumCardHTML(album)).join('')
: createPlaceholder("You haven't viewed any albums yet.");
artistsContainer.innerHTML = recents.artists.length
? recents.artists.map(artist => this.createArtistCardHTML(artist)).join('')
: createPlaceholder("You haven't viewed any artists yet.");
} else {
// Load from API
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
artistsContainer.innerHTML = this.createSkeletonCards(6, true);
const homeData = await window.loadHomeFeed(this.api, this);
if (homeData && homeData.rows) {
let albums = [];
let playlists = [];
homeData.rows.forEach(row => {
row.modules?.forEach(module => {
if (module.type === 'ALBUM_LIST' && module.pagedList?.items) {
albums.push(...module.pagedList.items);
} else if (module.type === 'PLAYLIST_LIST' && module.pagedList?.items) {
playlists.push(...module.pagedList.items);
}
});
});
if (albums.length > 0) {
albumsContainer.innerHTML = albums.slice(0, 10).map(album =>
this.createAlbumCardHTML(album)
).join('');
} else {
albumsContainer.innerHTML = createPlaceholder("No albums available.");
}
if (playlists.length > 0) {
document.querySelector('#home-recent-artists').parentElement.querySelector('.section-title').textContent = 'Featured Playlists';
artistsContainer.innerHTML = playlists.slice(0, 10).map(playlist => `
${playlist.title}
${playlist.numberOfTracks} tracks
`).join('');
} else {
artistsContainer.innerHTML = createPlaceholder("No playlists available.");
}
} else {
albumsContainer.innerHTML = createPlaceholder("Unable to load content.");
artistsContainer.innerHTML = createPlaceholder("Unable to load content.");
}
}
}
async renderSearchPage(query) {
this.showPage('search');
document.getElementById('search-results-title').textContent = `Search Results for "${query}"`;
const tracksContainer = document.getElementById('search-tracks-container');
const artistsContainer = document.getElementById('search-artists-container');
const albumsContainer = document.getElementById('search-albums-container');
tracksContainer.innerHTML = this.createSkeletonTracks(8, false);
artistsContainer.innerHTML = this.createSkeletonCards(6, true);
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
try {
const [tracksResult, artistsResult, albumsResult] = await Promise.all([
this.api.searchTracks(query),
this.api.searchArtists(query),
this.api.searchAlbums(query)
]);
let finalTracks = tracksResult.items;
let finalArtists = artistsResult.items;
let finalAlbums = albumsResult.items;
if (finalArtists.length === 0 && finalTracks.length > 0) {
const artistMap = new Map();
finalTracks.forEach(track => {
if (track.artist && !artistMap.has(track.artist.id)) {
artistMap.set(track.artist.id, track.artist);
}
if (track.artists) {
track.artists.forEach(artist => {
if (!artistMap.has(artist.id)) {
artistMap.set(artist.id, artist);
}
});
}
});
finalArtists = Array.from(artistMap.values());
}
if (finalAlbums.length === 0 && finalTracks.length > 0) {
const albumMap = new Map();
finalTracks.forEach(track => {
if (track.album && !albumMap.has(track.album.id)) {
albumMap.set(track.album.id, track.album);
}
});
finalAlbums = Array.from(albumMap.values());
}
if (finalTracks.length) {
this.renderListWithTracks(tracksContainer, finalTracks, false);
} else {
tracksContainer.innerHTML = createPlaceholder('No tracks found.');
}
artistsContainer.innerHTML = finalArtists.length
? finalArtists.map(artist => this.createArtistCardHTML(artist)).join('')
: createPlaceholder('No artists found.');
albumsContainer.innerHTML = finalAlbums.length
? finalAlbums.map(album => this.createAlbumCardHTML(album)).join('')
: createPlaceholder('No albums found.');
} catch (error) {
console.error("Search failed:", error);
const errorMsg = createPlaceholder(`Error during search. ${error.message}`);
tracksContainer.innerHTML = errorMsg;
artistsContainer.innerHTML = errorMsg;
albumsContainer.innerHTML = errorMsg;
}
}
async renderAlbumPage(albumId) {
this.showPage('album');
const imageEl = document.getElementById('album-detail-image');
const titleEl = document.getElementById('album-detail-title');
const metaEl = document.getElementById('album-detail-meta');
const tracklistContainer = document.getElementById('album-detail-tracklist');
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
titleEl.innerHTML = '';
metaEl.innerHTML = '';
tracklistContainer.innerHTML = `
${this.createSkeletonTracks(10, false)}
`;
try {
const { album, tracks } = await this.api.getAlbum(albumId);
imageEl.src = this.api.getCoverUrl(album.cover, '1280');
imageEl.style.backgroundColor = '';
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
titleEl.innerHTML = `${album.title} ${explicitBadge}`;
metaEl.innerHTML =
`By ${album.artist.name} • ${new Date(album.releaseDate).getFullYear()}`;
tracklistContainer.innerHTML = `
`;
tracks.sort((a, b) => a.trackNumber - b.trackNumber);
this.renderListWithTracks(tracklistContainer, tracks, false);
recentActivityManager.addAlbum(album);
} catch (error) {
console.error("Failed to load album:", error);
tracklistContainer.innerHTML = createPlaceholder(`Could not load album details. ${error.message}`);
}
}
async renderArtistPage(artistId) {
this.showPage('artist');
const imageEl = document.getElementById('artist-detail-image');
const nameEl = document.getElementById('artist-detail-name');
const metaEl = document.getElementById('artist-detail-meta');
const tracksContainer = document.getElementById('artist-detail-tracks');
const albumsContainer = document.getElementById('artist-detail-albums');
imageEl.src = '';
imageEl.style.backgroundColor = 'var(--muted)';
nameEl.innerHTML = '';
metaEl.innerHTML = '';
tracksContainer.innerHTML = this.createSkeletonTracks(5, true);
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
try {
const artist = await this.api.getArtist(artistId);
imageEl.src = this.api.getArtistPictureUrl(artist.picture, '750');
imageEl.style.backgroundColor = '';
nameEl.textContent = artist.name;
metaEl.textContent = `${artist.popularity} popularity`;
this.renderListWithTracks(tracksContainer, artist.tracks, true);
albumsContainer.innerHTML = artist.albums.map(album =>
this.createAlbumCardHTML(album)
).join('');
recentActivityManager.addArtist(artist);
} catch (error) {
console.error("Failed to load artist:", error);
tracksContainer.innerHTML = albumsContainer.innerHTML =
createPlaceholder(`Could not load artist details. ${error.message}`);
}
}
renderApiSettings() {
const container = document.getElementById('api-instance-list');
this.api.settings.getInstances().then(instances => {
const cachedData = this.api.settings.getCachedSpeedTests();
const speeds = cachedData?.speeds || {};
container.innerHTML = instances.map((url, index) => {
const speedInfo = speeds[url];
const speedText = speedInfo
? (speedInfo.speed === Infinity
? `Failed`
: `${speedInfo.speed.toFixed(0)}ms`)
: '';
return `
`;
}).join('');
const stats = this.api.getCacheStats();
const cacheInfo = document.getElementById('cache-info');
if (cacheInfo) {
cacheInfo.textContent = `Cache: ${stats.memoryEntries}/${stats.maxSize} entries`;
}
});
}
}