new: mix for artists, use new api
This commit is contained in:
parent
4ffac0ae0a
commit
57f3e42dbe
8 changed files with 271 additions and 117 deletions
60
index.html
60
index.html
|
|
@ -42,7 +42,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="fullscreen-cover-overlay" style="display: none;">
|
<div id="fullscreen-cover-overlay" style="display: none;">
|
||||||
<div class="fullscreen-cover-content">
|
<div class="fullscreen-cover-content">
|
||||||
<button id="close-fullscreen-cover-btn" title="Close">×</button>
|
<button id="close-fullscreen-cover-btn" title="Close">×</button>
|
||||||
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Toggle Lyrics">
|
<button id="toggle-fullscreen-lyrics-btn" class="fullscreen-lyrics-toggle" title="Toggle Lyrics">
|
||||||
|
|
@ -67,8 +67,7 @@
|
||||||
<div class="fullscreen-lyrics-container" id="fullscreen-lyrics-container" style="display: none;">
|
<div class="fullscreen-lyrics-container" id="fullscreen-lyrics-container" style="display: none;">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div id="playlist-modal" class="modal" style="display: none;">
|
<div id="playlist-modal" class="modal" style="display: none;">
|
||||||
<div class="modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
|
<div class="modal-overlay" style="position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
|
||||||
|
|
@ -89,11 +88,11 @@
|
||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div>
|
<div>
|
||||||
<div class="sidebar-logo">
|
<div class="sidebar-logo">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="14.75 14.75 70.5 70.5" >
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="14.75 14.75 70.5 70.5" >
|
||||||
<g fill="currentColor" >
|
<g fill="currentColor" >
|
||||||
<path d="M38.25 14.75H85.25V61.75H61.75V38.25H38.25ZM14.75 38.25H38.25V61.75H61.75V85.25H14.75Z" />
|
<path d="M38.25 14.75H85.25V61.75H61.75V38.25H38.25ZM14.75 38.25H38.25V61.75H61.75V85.25H14.75Z" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Monochrome</span>
|
<span>Monochrome</span>
|
||||||
</div>
|
</div>
|
||||||
<nav class="sidebar-nav">
|
<nav class="sidebar-nav">
|
||||||
|
|
@ -199,11 +198,13 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="page-library" class="page">
|
<div id="page-library" class="page">
|
||||||
<h2 class="section-title">Your Library</h2>
|
|
||||||
<section class="content-section">
|
<section class="content-section">
|
||||||
<h2 class="section-title">My Playlists <button id="create-playlist-btn" class="btn-secondary">Create Playlist</button></h2>
|
<h2 class="section-title">My Playlists <button id="create-playlist-btn" class="btn-secondary">Create Playlist</button></h2>
|
||||||
<div class="card-grid" id="my-playlists-container"></div>
|
<div class="card-grid" id="my-playlists-container"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="content-section">
|
||||||
|
<h2 class="section-title">Your Favorites</h2>
|
||||||
<div class="search-tabs">
|
<div class="search-tabs">
|
||||||
<button class="search-tab active" data-tab="tracks">Liked Tracks</button>
|
<button class="search-tab active" data-tab="tracks">Liked Tracks</button>
|
||||||
<button class="search-tab" data-tab="albums">Albums</button>
|
<button class="search-tab" data-tab="albums">Albums</button>
|
||||||
|
|
@ -222,6 +223,7 @@
|
||||||
<div class="search-tab-content" id="library-tab-playlists">
|
<div class="search-tab-content" id="library-tab-playlists">
|
||||||
<div class="card-grid" id="library-playlists-container"></div>
|
<div class="card-grid" id="library-playlists-container"></div>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="page-recent" class="page">
|
<div id="page-recent" class="page">
|
||||||
|
|
@ -240,10 +242,16 @@
|
||||||
<button id="play-album-btn" class="btn-primary">
|
<button id="play-album-btn" class="btn-primary">
|
||||||
<span>Play Album</span>
|
<span>Play Album</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="download-album-btn" class="btn-primary">
|
<button id="album-mix-btn" class="btn-primary" style="display: none;">
|
||||||
<span>Download Album</span>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Mix</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="like-album-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="album" title="Save to Library">
|
<button id="download-album-btn" class="btn-primary">
|
||||||
|
<span>Download</span>
|
||||||
|
</button>
|
||||||
|
<button id="like-album-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="album" title="Save to Favorites">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -252,7 +260,7 @@
|
||||||
<div class="track-list" id="album-detail-tracklist"></div>
|
<div class="track-list" id="album-detail-tracklist"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section id="page-playlist" class="page">
|
<section id="page-playlist" class="page">
|
||||||
<div class="detail-header">
|
<div class="detail-header">
|
||||||
<img id="playlist-detail-image" class="detail-cover" alt="Playlist Cover">
|
<img id="playlist-detail-image" class="detail-cover" alt="Playlist Cover">
|
||||||
<div class="detail-header-info">
|
<div class="detail-header-info">
|
||||||
|
|
@ -267,14 +275,31 @@
|
||||||
<button id="download-playlist-btn" class="btn-primary">
|
<button id="download-playlist-btn" class="btn-primary">
|
||||||
<span>Download</span>
|
<span>Download</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="like-playlist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="playlist" title="Save to Library">
|
<button id="like-playlist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="playlist" title="Save to Favorites">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="playlist-detail-tracklist" class="track-list"></div>
|
<div id="playlist-detail-tracklist" class="track-list"></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="page-mix" class="page">
|
||||||
|
<div class="detail-header">
|
||||||
|
<img id="mix-detail-image" class="detail-cover" alt="Mix Cover">
|
||||||
|
<div class="detail-header-info">
|
||||||
|
<h1 class="title" id="mix-detail-title"></h1>
|
||||||
|
<p class="meta" id="mix-detail-meta"></p>
|
||||||
|
<p id="mix-detail-description" class="detail-description"></p>
|
||||||
|
<div class="detail-actions">
|
||||||
|
<button id="play-mix-btn" class="btn-primary">
|
||||||
|
<span>Play</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="mix-detail-tracklist" class="track-list"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div id="page-artist" class="page">
|
<div id="page-artist" class="page">
|
||||||
<header class="detail-header">
|
<header class="detail-header">
|
||||||
|
|
@ -289,10 +314,16 @@
|
||||||
</svg>
|
</svg>
|
||||||
<span>Artist Radio</span>
|
<span>Artist Radio</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button id="artist-mix-btn" class="btn-primary" style="display: none;">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
|
||||||
|
</svg>
|
||||||
|
<span>Mix</span>
|
||||||
|
</button>
|
||||||
<button id="download-discography-btn" class="btn-primary">
|
<button id="download-discography-btn" class="btn-primary">
|
||||||
<span>Download Discography</span>
|
<span>Download Discography</span>
|
||||||
</button>
|
</button>
|
||||||
<button id="like-artist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="artist" title="Save to Library">
|
<button id="like-artist-btn" class="btn-secondary like-btn" data-action="toggle-like" data-type="artist" title="Save to Favorites">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="heart-icon"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -652,7 +683,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="volume-controls">
|
<div class="volume-controls">
|
||||||
<div class="player-actions-row">
|
<div class="player-actions-row">
|
||||||
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Library" style="display: none;">
|
<button id="now-playing-like-btn" class="like-btn" data-action="toggle-like" title="Save to Favorites" style="display: none;">
|
||||||
</button>
|
</button>
|
||||||
<button id="toggle-lyrics-btn" title="Lyrics">
|
<button id="toggle-lyrics-btn" title="Lyrics">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mic"><path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/><line x1="8" y1="22" x2="16" y2="22"/></svg>
|
||||||
|
|
@ -696,7 +727,6 @@ if ('serviceWorker' in navigator) {
|
||||||
location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"api": [
|
"api": [
|
||||||
"https://tidal-api.binimum.org"
|
"https://monochrome-api.samidy.com"
|
||||||
],
|
],
|
||||||
"streaming": [
|
"streaming": [
|
||||||
"https://triton.squid.wtf",
|
"https://triton.squid.wtf",
|
||||||
|
|
|
||||||
34
js/api.js
34
js/api.js
|
|
@ -480,6 +480,40 @@ export class LosslessAPI {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMix(id) {
|
||||||
|
const cached = await this.cache.get('mix', id);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const response = await this.fetchWithRetry(`/mix/?id=${id}`, { type: 'api' });
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
let mix = null;
|
||||||
|
let tracks = [];
|
||||||
|
|
||||||
|
// Mix response structure might vary, handle likely cases
|
||||||
|
const items = data.items || data.tracks || (Array.isArray(data) ? data : []);
|
||||||
|
|
||||||
|
// If data has mix info, use it. Otherwise, fabricate one or look for it.
|
||||||
|
if (data.mix) {
|
||||||
|
mix = data.mix;
|
||||||
|
} else if (!Array.isArray(data) && data.id) {
|
||||||
|
mix = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mix) {
|
||||||
|
// Basic placeholder if mix metadata isn't explicitly separated
|
||||||
|
mix = { id, title: 'Mix', description: 'Generated Mix' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length > 0) {
|
||||||
|
tracks = items.map(i => this.prepareTrack(i.item || i));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = { mix, tracks };
|
||||||
|
await this.cache.set('mix', id, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
async getArtist(artistId) {
|
async getArtist(artistId) {
|
||||||
const cached = await this.cache.get('artist', artistId);
|
const cached = await this.cache.get('artist', artistId);
|
||||||
if (cached) return cached;
|
if (cached) return cached;
|
||||||
|
|
|
||||||
|
|
@ -423,7 +423,7 @@ export async function handleTrackAction(action, item, player, api, lyricsManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btn.classList.toggle('active', added);
|
btn.classList.toggle('active', added);
|
||||||
btn.title = added ? 'Remove from Library' : 'Add to Library';
|
btn.title = added ? 'Remove from Favorites' : 'Add to Favorites';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle Library Page Update
|
// Handle Library Page Update
|
||||||
|
|
@ -674,7 +674,7 @@ async function updateContextMenuLikeState(menu, track) {
|
||||||
const likeItem = menu.querySelector('[data-action="toggle-like"]');
|
const likeItem = menu.querySelector('[data-action="toggle-like"]');
|
||||||
if (likeItem) {
|
if (likeItem) {
|
||||||
const isLiked = await db.isFavorite('track', track.id);
|
const isLiked = await db.isFavorite('track', track.id);
|
||||||
likeItem.textContent = isLiked ? 'Remove from Library' : 'Add to Library';
|
likeItem.textContent = isLiked ? 'Remove from Favorites' : 'Add to Favorites';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,9 @@ export function createRouter(ui) {
|
||||||
case 'userplaylist':
|
case 'userplaylist':
|
||||||
ui.renderPlaylistPage(param);
|
ui.renderPlaylistPage(param);
|
||||||
break;
|
break;
|
||||||
|
case 'mix':
|
||||||
|
ui.renderMixPage(param);
|
||||||
|
break;
|
||||||
case 'library':
|
case 'library':
|
||||||
ui.renderLibraryPage();
|
ui.renderLibraryPage();
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ export const apiSettings = {
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to load instances from GitHub:', error);
|
console.error('Failed to load instances from GitHub:', error);
|
||||||
this.defaultInstances = {
|
this.defaultInstances = {
|
||||||
api: ['https://tidal-api.binimum.org'],
|
api: ["https://monochrome-api.samidy.com"],
|
||||||
streaming: [
|
streaming: [
|
||||||
"https://triton.squid.wtf",
|
"https://triton.squid.wtf",
|
||||||
"https://wolf.qqdl.site",
|
"https://wolf.qqdl.site",
|
||||||
|
|
|
||||||
100
js/ui.js
100
js/ui.js
|
|
@ -797,6 +797,8 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
||||||
if (playBtn) playBtn.innerHTML = `${SVG_PLAY}<span>Play Album</span>`;
|
if (playBtn) playBtn.innerHTML = `${SVG_PLAY}<span>Play Album</span>`;
|
||||||
const dlBtn = document.getElementById('download-album-btn');
|
const dlBtn = document.getElementById('download-album-btn');
|
||||||
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download Album</span>`;
|
if (dlBtn) dlBtn.innerHTML = `${SVG_DOWNLOAD}<span>Download Album</span>`;
|
||||||
|
const mixBtn = document.getElementById('album-mix-btn');
|
||||||
|
if (mixBtn) mixBtn.style.display = 'none';
|
||||||
|
|
||||||
imageEl.src = '';
|
imageEl.src = '';
|
||||||
imageEl.style.backgroundColor = 'var(--muted)';
|
imageEl.style.backgroundColor = 'var(--muted)';
|
||||||
|
|
@ -902,6 +904,13 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
||||||
|
|
||||||
const artistData = await this.api.getArtist(album.artist.id);
|
const artistData = await this.api.getArtist(album.artist.id);
|
||||||
|
|
||||||
|
// Add Mix/Radio Button to header
|
||||||
|
const mixBtn = document.getElementById('album-mix-btn');
|
||||||
|
if (mixBtn && artistData.mixes && artistData.mixes.ARTIST_MIX) {
|
||||||
|
mixBtn.style.display = 'flex';
|
||||||
|
mixBtn.onclick = () => window.location.hash = `#mix/${artistData.mixes.ARTIST_MIX}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Remove placeholder
|
// Remove placeholder
|
||||||
placeholderSection.remove();
|
placeholderSection.remove();
|
||||||
|
|
||||||
|
|
@ -1105,6 +1114,86 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async renderMixPage(mixId) {
|
||||||
|
this.showPage('mix');
|
||||||
|
const imageEl = document.getElementById('mix-detail-image');
|
||||||
|
const titleEl = document.getElementById('mix-detail-title');
|
||||||
|
const metaEl = document.getElementById('mix-detail-meta');
|
||||||
|
const descEl = document.getElementById('mix-detail-description');
|
||||||
|
const tracklistContainer = document.getElementById('mix-detail-tracklist');
|
||||||
|
const playBtn = document.getElementById('play-mix-btn');
|
||||||
|
if (playBtn) playBtn.innerHTML = `${SVG_PLAY}<span>Play</span>`;
|
||||||
|
|
||||||
|
// Skeleton loading
|
||||||
|
imageEl.src = '';
|
||||||
|
imageEl.style.backgroundColor = 'var(--muted)';
|
||||||
|
titleEl.innerHTML = '<div class="skeleton" style="height: 48px; width: 300px; max-width: 90%;"></div>';
|
||||||
|
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 200px; max-width: 80%;"></div>';
|
||||||
|
descEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 100%;"></div>';
|
||||||
|
tracklistContainer.innerHTML = `
|
||||||
|
<div class="track-list-header">
|
||||||
|
<span style="width: 40px; text-align: center;">#</span>
|
||||||
|
<span>Title</span>
|
||||||
|
<span class="duration-header">Duration</span>
|
||||||
|
</div>
|
||||||
|
${this.createSkeletonTracks(10, true)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { mix, tracks } = await this.api.getMix(mixId);
|
||||||
|
|
||||||
|
// Mixes usually have covers from Tidal resources, similar to playlists
|
||||||
|
const imageId = mix.images?.medium?.source || mix.image || mix.id;
|
||||||
|
// Fallback for cover: if mix.id matches a pattern or we can just try generic mix cover
|
||||||
|
// Often mix ID isn't directly an image ID.
|
||||||
|
// If API returns explicit image URL/ID use it.
|
||||||
|
// For now assume standard playlist-like cover or placeholder.
|
||||||
|
if (imageId && imageId !== mix.id) {
|
||||||
|
imageEl.src = this.api.getCoverUrl(imageId, '1080');
|
||||||
|
} else {
|
||||||
|
// Try to get cover from first track album
|
||||||
|
if (tracks.length > 0 && tracks[0].album?.cover) {
|
||||||
|
imageEl.src = this.api.getCoverUrl(tracks[0].album.cover, '1080');
|
||||||
|
} else {
|
||||||
|
imageEl.src = 'assets/appicon.png';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageEl.style.backgroundColor = '';
|
||||||
|
|
||||||
|
const firstTrackArtist = tracks.length > 0 ? tracks[0].artist?.name : '';
|
||||||
|
const displayTitle = firstTrackArtist ? `${firstTrackArtist} Mix` : 'Mix';
|
||||||
|
|
||||||
|
titleEl.textContent = displayTitle;
|
||||||
|
this.adjustTitleFontSize(titleEl, displayTitle);
|
||||||
|
|
||||||
|
const totalDuration = calculateTotalDuration(tracks);
|
||||||
|
|
||||||
|
metaEl.textContent = `${tracks.length} tracks • ${formatDuration(totalDuration)}`;
|
||||||
|
descEl.textContent = mix.subTitle || mix.description || '';
|
||||||
|
|
||||||
|
tracklistContainer.innerHTML = `
|
||||||
|
<div class="track-list-header">
|
||||||
|
<span style="width: 40px; text-align: center;">#</span>
|
||||||
|
<span>Title</span>
|
||||||
|
<span class="duration-header">Duration</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.renderListWithTracks(tracklistContainer, tracks, true);
|
||||||
|
|
||||||
|
// Set play button action
|
||||||
|
playBtn.onclick = () => {
|
||||||
|
player.playTracks(tracks, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
document.title = `${displayTitle} - Monochrome`;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to load mix:", error);
|
||||||
|
tracklistContainer.innerHTML = createPlaceholder(`Could not load mix details. ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async renderArtistPage(artistId) {
|
async renderArtistPage(artistId) {
|
||||||
this.showPage('artist');
|
this.showPage('artist');
|
||||||
|
|
||||||
|
|
@ -1134,6 +1223,17 @@ async showFullscreenCover(track, nextTrack, lyricsManager, audioPlayer) {
|
||||||
try {
|
try {
|
||||||
const artist = await this.api.getArtist(artistId);
|
const artist = await this.api.getArtist(artistId);
|
||||||
|
|
||||||
|
// Handle Artist Mix Button
|
||||||
|
const mixBtn = document.getElementById('artist-mix-btn');
|
||||||
|
if (mixBtn) {
|
||||||
|
if (artist.mixes && artist.mixes.ARTIST_MIX) {
|
||||||
|
mixBtn.style.display = 'flex';
|
||||||
|
mixBtn.onclick = () => window.location.hash = `#mix/${artist.mixes.ARTIST_MIX}`;
|
||||||
|
} else {
|
||||||
|
mixBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Similar Artists
|
// Similar Artists
|
||||||
if (similarContainer && similarSection) {
|
if (similarContainer && similarSection) {
|
||||||
this.api.getSimilarArtists(artistId).then(similar => {
|
this.api.getSimilarArtists(artistId).then(similar => {
|
||||||
|
|
|
||||||
57
styles.css
57
styles.css
|
|
@ -3061,7 +3061,8 @@ input:checked + .slider::before {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-playlist .detail-header {
|
#page-playlist .detail-header,
|
||||||
|
#page-mix .detail-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-top;
|
align-items: flex-top;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
|
|
@ -3069,7 +3070,8 @@ input:checked + .slider::before {
|
||||||
padding-bottom: var(--spacing-md);
|
padding-bottom: var(--spacing-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-playlist .detail-cover {
|
#page-playlist .detail-cover,
|
||||||
|
#page-mix .detail-cover {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
height: 200px;
|
height: 200px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
@ -3080,12 +3082,14 @@ input:checked + .slider::before {
|
||||||
transition: opacity 0.3s ease-in-out;
|
transition: opacity 0.3s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlist-detail-image.loading {
|
#playlist-detail-image.loading,
|
||||||
|
#mix-detail-image.loading {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#playlist-detail-title {
|
#playlist-detail-title,
|
||||||
|
#mix-detail-title {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
|
|
@ -3096,7 +3100,8 @@ input:checked + .slider::before {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlist-detail-meta {
|
#playlist-detail-meta,
|
||||||
|
#mix-detail-meta {
|
||||||
color: var(--muted-foreground);
|
color: var(--muted-foreground);
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
|
|
@ -3106,7 +3111,8 @@ input:checked + .slider::before {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlist-detail-description {
|
#playlist-detail-description,
|
||||||
|
#mix-detail-description {
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
|
|
@ -3114,7 +3120,8 @@ input:checked + .slider::before {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#page-playlist .detail-actions {
|
#page-playlist .detail-actions,
|
||||||
|
#page-mix .detail-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
margin-top: 1.5rem;
|
margin-top: 1.5rem;
|
||||||
|
|
@ -3178,7 +3185,8 @@ input:checked + .slider::before {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
#page-playlist .detail-header {
|
#page-playlist .detail-header,
|
||||||
|
#page-mix .detail-header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: var(--spacing-lg);
|
gap: var(--spacing-lg);
|
||||||
|
|
@ -3186,42 +3194,21 @@ input:checked + .slider::before {
|
||||||
margin-bottom: var(--spacing-lg);
|
margin-bottom: var(--spacing-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlist-detail-image {
|
#playlist-detail-image,
|
||||||
|
#mix-detail-image {
|
||||||
width: 150px;
|
width: 150px;
|
||||||
height: 150px;
|
height: 150px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlist-detail-title {
|
#playlist-detail-title,
|
||||||
|
#mix-detail-title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
#playlist-detail-meta {
|
#playlist-detail-meta,
|
||||||
|
#mix-detail-meta {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#playlist-detail-description {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#page-playlist .detail-actions {
|
|
||||||
width: auto;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
|
|
||||||
#play-playlist-btn,
|
|
||||||
#download-playlist-btn {
|
|
||||||
width: auto;
|
|
||||||
padding: 0.875rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
aspect-ratio: 1/1;
|
|
||||||
}
|
|
||||||
|
|
||||||
#play-playlist-btn span,
|
|
||||||
#download-playlist-btn span {
|
|
||||||
display: none;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue