unreleased rework

This commit is contained in:
EduardPrigoana 2026-02-01 14:48:01 +02:00
parent 9ef3d6573f
commit db777a7923
9 changed files with 1413 additions and 451 deletions

View file

@ -45,6 +45,8 @@
<li data-action="go-to-artist" data-type-filter="track,album">Go to artist</li>
<li data-action="go-to-album" data-type-filter="track">Go to album</li>
<li data-action="copy-link">Copy link</li>
<li data-action="track-info" data-type-filter="track">Track Info</li>
<li data-action="open-original-url" data-type-filter="track">Open Original URL</li>
<li data-action="download">Download</li>
</ul>
</div>
@ -795,6 +797,27 @@
<span>Recent</span>
</a>
</li>
<li class="nav-item">
<a href="/unreleased">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="7" height="7" />
<rect x="14" y="3" width="7" height="7" />
<rect x="14" y="14" width="7" height="7" />
<rect x="3" y="14" width="7" height="7" />
</svg>
<span>Unreleased</span>
</a>
</li>
<li class="nav-item">
<a href="/settings">
<svg
@ -1338,6 +1361,35 @@
<div class="track-list" id="recent-tracks-container"></div>
</div>
<div id="page-unreleased" class="page">
<div id="unreleased-content" style="padding: 1rem 0;"></div>
</div>
<div id="page-tracker-artist" class="page">
<header class="detail-header">
<img
id="tracker-artist-detail-image"
src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"
alt=""
class="detail-header-image artist"
/>
<div class="detail-header-info">
<div class="type">Unreleased Artist</div>
<h1 class="title" id="tracker-artist-detail-name"></h1>
<div class="meta" id="tracker-artist-detail-meta"></div>
<div class="detail-header-actions">
<button id="play-tracker-artist-btn" class="btn-primary">
<span>Shuffle Play</span>
</button>
<button id="download-tracker-artist-btn" class="btn-primary">
<span>Download All</span>
</button>
</div>
</div>
</header>
<div id="tracker-artist-projects-container"></div>
</div>
<div id="page-album" class="page">
<header class="detail-header">
<img
@ -1704,11 +1756,13 @@
<h2 class="section-title">EPs and Singles</h2>
<div class="card-grid" id="artist-detail-eps"></div>
</section>
<div
id="artist-tracker-section"
class="content-section"
style="display: none; margin-top: 2rem"
></div>
<section class="content-section" id="artist-section-load-unreleased" style="display: none; margin: 1.5rem 0;">
<button id="load-unreleased-btn" class="btn-primary">Load Unreleased Projects</button>
</section>
<section class="content-section" id="artist-section-unreleased" style="display: none">
<h2 class="section-title">Unreleased Music</h2>
<div class="card-grid" id="artist-detail-unreleased"></div>
</section>
<section class="content-section" id="artist-section-similar" style="display: none">
<h2 class="section-title">Similar Artists</h2>
<div class="card-grid" id="artist-detail-similar"></div>
@ -2429,7 +2483,7 @@
</a>
</div>
<div class="about-footer">
<p class="version">Version 2.1.0</p>
<p class="version">Version 2.2.0</p>
<p class="disclaimer">
This is an independent client and is not affiliated with or endorsed by TIDAL or any
music streaming service.

View file

@ -231,6 +231,7 @@ export class MusicDatabase {
// Keep mix info
mixes: item.mixes || null,
isTracker: item.isTracker || (item.id && String(item.id).startsWith('tracker-')),
trackerInfo: item.trackerInfo || null,
audioUrl: item.remoteUrl || item.audioUrl || null,
remoteUrl: item.remoteUrl || null,
audioQuality: item.audioQuality || null,

View file

@ -8,6 +8,7 @@ import {
trackDataStore,
formatTime,
SVG_BIN,
getTrackArtists,
} from './utils.js';
import { lastFMStorage, waveformSettings } from './storage.js';
import { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.js';
@ -579,6 +580,112 @@ function initializeSmoothSliders(audioPlayer, player) {
);
}
// Standalone function to show add to playlist modal
export async function showAddToPlaylistModal(track) {
const modal = document.getElementById('playlist-select-modal');
const list = document.getElementById('playlist-select-list');
const cancelBtn = document.getElementById('playlist-select-cancel');
const overlay = modal.querySelector('.modal-overlay');
const renderModal = async () => {
const playlists = await db.getPlaylists(true);
const trackId = track.id;
const playlistsWithTrack = new Set();
for (const playlist of playlists) {
if (playlist.tracks && playlist.tracks.some((t) => t.id == trackId)) {
playlistsWithTrack.add(playlist.id);
}
}
list.innerHTML =
`
<div class="modal-option create-new-option" style="border-bottom: 1px solid var(--border); margin-bottom: 0.5rem;">
<span style="font-weight: 600; color: var(--primary);">+ Create New Playlist</span>
</div>
` +
playlists
.map((p) => {
const alreadyContains = playlistsWithTrack.has(p.id);
return `
<div class="modal-option ${alreadyContains ? 'already-contains' : ''}" data-id="${p.id}">
<span>${p.name}</span>
${
alreadyContains
? `<button class="remove-from-playlist-btn-modal" title="Remove from playlist" style="background: transparent; border: none; color: inherit; cursor: pointer; padding: 4px; display: flex; align-items: center;">${SVG_BIN}</button>`
: ''
}
</div>
`;
})
.join('');
return true;
};
if (!(await renderModal())) return;
const closeModal = () => {
modal.classList.remove('active');
cleanup();
};
const handleOptionClick = async (e) => {
const removeBtn = e.target.closest('.remove-from-playlist-btn-modal');
const option = e.target.closest('.modal-option');
if (!option) return;
if (option.classList.contains('create-new-option')) {
closeModal();
const createModal = document.getElementById('playlist-modal');
document.getElementById('playlist-modal-title').textContent = 'Create Playlist';
document.getElementById('playlist-name-input').value = '';
document.getElementById('playlist-cover-input').value = '';
createModal.dataset.editingId = '';
document.getElementById('csv-import-section').style.display = 'none';
// Pass track
createModal._pendingTracks = [track];
createModal.classList.add('active');
document.getElementById('playlist-name-input').focus();
return;
}
const playlistId = option.dataset.id;
if (removeBtn) {
e.stopPropagation();
await db.removeTrackFromPlaylist(playlistId, track.id);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Removed from playlist: ${option.querySelector('span').textContent}`);
await renderModal();
} else {
if (option.classList.contains('already-contains')) return;
await db.addTrackToPlaylist(playlistId, track);
const updatedPlaylist = await db.getPlaylist(playlistId);
syncManager.syncUserPlaylist(updatedPlaylist, 'update');
showNotification(`Added to playlist: ${option.querySelector('span').textContent}`);
closeModal();
}
};
const cleanup = () => {
cancelBtn.removeEventListener('click', closeModal);
overlay.removeEventListener('click', closeModal);
list.removeEventListener('click', handleOptionClick);
};
cancelBtn.addEventListener('click', closeModal);
overlay.addEventListener('click', closeModal);
list.addEventListener('click', handleOptionClick);
modal.classList.add('active');
}
export async function handleTrackAction(
action,
item,
@ -942,6 +1049,143 @@ export async function handleTrackAction(
navigator.clipboard.writeText(url).then(() => {
showNotification('Link copied to clipboard!');
});
} else if (action === 'track-info') {
// Show detailed track info modal
const isTracker = item.isTracker;
let infoHTML = '';
if (isTracker && item.trackerInfo) {
// Detailed unreleased/tracker track info
const releaseDate = item.trackerInfo.releaseDate || item.streamStartDate;
const dateDisplay = releaseDate ? new Date(releaseDate).toLocaleDateString() : 'Unknown';
const addedDate = item.trackerInfo.addedDate ? new Date(item.trackerInfo.addedDate).toLocaleDateString() : 'Unknown';
infoHTML = `
<div style="padding: 1.5rem; max-width: 500px; max-height: 80vh; overflow-y: auto;">
<h3 style="margin-bottom: 1rem; font-size: 1.3rem; font-weight: 600;">${item.title}</h3>
<div style="color: var(--muted-foreground); font-size: 0.9rem; line-height: 1.8;">
<div style="margin-bottom: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
<p style="color: var(--primary); font-weight: 500;">Unreleased Track</p>
</div>
<div style="display: grid; gap: 0.5rem;">
${item.artists ? `<p><strong style="color: var(--foreground);">Artist:</strong> ${Array.isArray(item.artists) ? item.artists.map(a => a.name || a).join(', ') : item.artists}</p>` : ''}
${item.trackerInfo.artist ? `<p><strong style="color: var(--foreground);">Tracked Artist:</strong> ${item.trackerInfo.artist}</p>` : ''}
${item.trackerInfo.project ? `<p><strong style="color: var(--foreground);">Project:</strong> ${item.trackerInfo.project}</p>` : ''}
${item.trackerInfo.era ? `<p><strong style="color: var(--foreground);">Era:</strong> ${item.trackerInfo.era}</p>` : ''}
${item.trackerInfo.timeline ? `<p><strong style="color: var(--foreground);">Timeline:</strong> ${item.trackerInfo.timeline}</p>` : ''}
${item.trackerInfo.category ? `<p><strong style="color: var(--foreground);">Category:</strong> ${item.trackerInfo.category}</p>` : ''}
${item.trackerInfo.trackNumber ? `<p><strong style="color: var(--foreground);">Track Number:</strong> ${item.trackerInfo.trackNumber}</p>` : ''}
<p><strong style="color: var(--foreground);">Duration:</strong> ${formatTime(item.duration)}</p>
${releaseDate !== 'Unknown' ? `<p><strong style="color: var(--foreground);">Release Date:</strong> ${dateDisplay}</p>` : ''}
${item.trackerInfo.addedDate ? `<p><strong style="color: var(--foreground);">Added to Tracker:</strong> ${addedDate}</p>` : ''}
${item.trackerInfo.leakedDate ? `<p><strong style="color: var(--foreground);">Leak Date:</strong> ${new Date(item.trackerInfo.leakedDate).toLocaleDateString()}</p>` : ''}
${item.trackerInfo.recordingDate ? `<p><strong style="color: var(--foreground);">Recording Date:</strong> ${new Date(item.trackerInfo.recordingDate).toLocaleDateString()}</p>` : ''}
</div>
${item.trackerInfo.description ? `
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Description</p>
<p style="font-size: 0.85rem; line-height: 1.6;">${item.trackerInfo.description}</p>
</div>
` : ''}
${item.trackerInfo.notes ? `
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Notes</p>
<p style="font-size: 0.85rem; line-height: 1.6;">${item.trackerInfo.notes}</p>
</div>
` : ''}
${item.trackerInfo.sourceUrl ? `
<div style="margin-top: 1rem;">
<p style="margin-bottom: 0.5rem;"><strong style="color: var(--foreground);">Source URL:</strong></p>
<a href="${item.trackerInfo.sourceUrl}" target="_blank" style="color: var(--primary); word-break: break-all; font-size: 0.85rem; display: block; padding: 0.5rem; background: var(--accent); border-radius: 6px; text-decoration: none;">
${item.trackerInfo.sourceUrl}
</a>
</div>
` : ''}
${item.id ? `<p style="margin-top: 1rem; font-size: 0.8rem; color: var(--muted);"><strong>Track ID:</strong> ${item.id}</p>` : ''}
</div>
<button onclick="this.closest('.modal-overlay').remove()" class="btn-primary" style="margin-top: 1.5rem; width: 100%;">Close</button>
</div>
`;
} else {
// Detailed normal track info
const releaseDate = item.album?.releaseDate || item.streamStartDate;
const dateDisplay = releaseDate ? new Date(releaseDate).toLocaleDateString() : 'Unknown';
const quality = item.audioQuality || 'Unknown';
const bitrate = item.bitrate ? `${item.bitrate} kbps` : '';
infoHTML = `
<div style="padding: 1.5rem; max-width: 500px; max-height: 80vh; overflow-y: auto;">
<h3 style="margin-bottom: 1rem; font-size: 1.3rem; font-weight: 600;">${item.title}</h3>
<div style="color: var(--muted-foreground); font-size: 0.9rem; line-height: 1.8;">
<div style="display: grid; gap: 0.5rem;">
<p><strong style="color: var(--foreground);">Artist:</strong> ${getTrackArtists(item)}</p>
<p><strong style="color: var(--foreground);">Album:</strong> ${item.album?.title || 'Unknown'}</p>
${item.album?.artist?.name ? `<p><strong style="color: var(--foreground);">Album Artist:</strong> ${item.album.artist.name}</p>` : ''}
<p><strong style="color: var(--foreground);">Release Date:</strong> ${dateDisplay}</p>
<p><strong style="color: var(--foreground);">Duration:</strong> ${formatTime(item.duration)}</p>
${item.trackNumber ? `<p><strong style="color: var(--foreground);">Track Number:</strong> ${item.trackNumber}</p>` : ''}
${item.discNumber ? `<p><strong style="color: var(--foreground);">Disc Number:</strong> ${item.discNumber}</p>` : ''}
${item.version ? `<p><strong style="color: var(--foreground);">Version:</strong> ${item.version}</p>` : ''}
${item.explicit ? `<p><strong style="color: var(--foreground);">Explicit:</strong> Yes</p>` : ''}
<p><strong style="color: var(--foreground);">Quality:</strong> ${quality} ${bitrate ? `(${bitrate})` : ''}</p>
</div>
${item.credits && item.credits.length > 0 ? `
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Credits</p>
<div style="font-size: 0.85rem; line-height: 1.6;">
${item.credits.map(c => `<p>${c.type}: ${c.name}</p>`).join('')}
</div>
</div>
` : ''}
${item.composers && item.composers.length > 0 ? `
<p style="margin-top: 0.5rem;"><strong style="color: var(--foreground);">Composers:</strong> ${item.composers.map(c => c.name).join(', ')}</p>
` : ''}
${item.lyrics?.text ? `
<div style="margin-top: 1rem; padding: 0.75rem; background: var(--accent); border-radius: 8px;">
<p style="color: var(--foreground); font-weight: 500; margin-bottom: 0.5rem;">Has Lyrics</p>
</div>
` : ''}
${item.id ? `<p style="margin-top: 1rem; font-size: 0.8rem; color: var(--muted);"><strong>Track ID:</strong> ${item.id}</p>` : ''}
${item.album?.id ? `<p style="font-size: 0.8rem; color: var(--muted);"><strong>Album ID:</strong> ${item.album.id}</p>` : ''}
</div>
<button onclick="this.closest('.modal-overlay').remove()" class="btn-primary" style="margin-top: 1.5rem; width: 100%;">Close</button>
</div>
`;
}
// Create and show modal
const modal = document.createElement('div');
modal.className = 'modal-overlay';
modal.style.cssText = 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 10000;';
modal.innerHTML = infoHTML;
modal.onclick = (e) => {
if (e.target === modal) modal.remove();
};
document.body.appendChild(modal);
} else if (action === 'open-original-url') {
// Open the original source URL for the track
let url = null;
if (item.isTracker && item.trackerInfo && item.trackerInfo.sourceUrl) {
url = item.trackerInfo.sourceUrl;
} else if (item.remoteUrl) {
url = item.remoteUrl;
}
if (url) {
window.open(url, '_blank');
} else {
showNotification('No original URL available for this track.');
}
}
}
@ -961,6 +1205,13 @@ async function updateContextMenuLikeState(contextMenu, contextTrack) {
trackMixItem.style.display = hasMix ? 'block' : 'none';
}
// Show/hide "Open Original URL" only for unreleased/tracker tracks
const openOriginalUrlItem = contextMenu.querySelector('li[data-action="open-original-url"]');
if (openOriginalUrlItem) {
const isUnreleased = contextTrack.isTracker || (contextTrack.trackerInfo && contextTrack.trackerInfo.sourceUrl);
openOriginalUrlItem.style.display = isUnreleased ? 'block' : 'none';
}
// Filter items based on type
const type = contextMenu._contextType || 'track';
contextMenu.querySelectorAll('li[data-action]').forEach((item) => {
@ -1229,7 +1480,11 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
if (link) {
e.stopPropagation();
const artistId = link.dataset.artistId;
if (artistId) {
const trackerSheetId = link.dataset.trackerSheetId;
if (trackerSheetId) {
// Navigate to tracker artist page
navigate(`/unreleased/${trackerSheetId}`);
} else if (artistId) {
navigate(`/artist/${artistId}`);
}
return;
@ -1237,8 +1492,14 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
// Fallback for non-link clicks (e.g. separators) or single artist legacy
const track = player.currentTrack;
if (track?.artist?.id) {
navigate(`/artist/${track.artist.id}`);
if (track) {
// Check if this is a tracker track
const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-'));
if (isTracker && track.trackerInfo?.sheetId) {
navigate(`/unreleased/${track.trackerInfo.sheetId}`);
} else if (track.artist?.id) {
navigate(`/artist/${track.artist.id}`);
}
}
});

View file

@ -180,9 +180,10 @@ export class LastFMScrobbler {
this.clearScrobbleTimer();
try {
const scrobbleTitle = track.cleanTitle || track.title;
const params = {
artist: this._getScrobbleArtist(track),
track: track.title,
track: scrobbleTitle,
};
if (track.album?.title) {
@ -199,7 +200,7 @@ export class LastFMScrobbler {
await this.makeRequest('track.updateNowPlaying', params, true);
console.log('Now playing updated:', track.title);
console.log('Now playing updated:', scrobbleTitle);
this.scrobbleThreshold = Math.min(track.duration / 2, 240);
this.scheduleScrobble(this.scrobbleThreshold * 1000);
@ -228,10 +229,11 @@ export class LastFMScrobbler {
try {
const timestamp = Math.floor(Date.now() / 1000);
const scrobbleTitle = this.currentTrack.cleanTitle || this.currentTrack.title;
const params = {
artist: this._getScrobbleArtist(this.currentTrack),
track: this.currentTrack.title,
track: scrobbleTitle,
timestamp: timestamp,
};
@ -250,7 +252,7 @@ export class LastFMScrobbler {
await this.makeRequest('track.scrobble', params, true);
this.hasScrobbled = true;
console.log('Scrobbled:', this.currentTrack.title);
console.log('Scrobbled:', this.currentTrack.cleanTitle || this.currentTrack.title);
} catch (error) {
console.error('Failed to scrobble:', error);
}

View file

@ -356,6 +356,32 @@ export class Player {
this.applyReplayGain();
this.audio.src = streamUrl;
// Wait for audio to be ready before playing (prevents restart issues with blob URLs)
if (isTracker) {
await new Promise((resolve, reject) => {
const onCanPlay = () => {
this.audio.removeEventListener('canplay', onCanPlay);
this.audio.removeEventListener('error', onError);
resolve();
};
const onError = (e) => {
this.audio.removeEventListener('canplay', onCanPlay);
this.audio.removeEventListener('error', onError);
reject(e);
};
this.audio.addEventListener('canplay', onCanPlay);
this.audio.addEventListener('error', onError);
// Timeout after 10 seconds
setTimeout(() => {
this.audio.removeEventListener('canplay', onCanPlay);
this.audio.removeEventListener('error', onError);
reject(new Error('Timeout waiting for audio to load'));
}, 10000);
});
}
if (startTime > 0) {
this.audio.currentTime = startTime;
}

View file

@ -52,7 +52,11 @@ export function createRouter(ui) {
await ui.renderMixPage(param);
break;
case 'track':
await ui.renderTrackPage(param);
if (param.startsWith('tracker-')) {
await ui.renderTrackerTrackPage(param);
} else {
await ui.renderTrackPage(param);
}
break;
case 'library':
await ui.renderLibraryPage();
@ -60,6 +64,20 @@ export function createRouter(ui) {
case 'recent':
await ui.renderRecentPage();
break;
case 'unreleased':
if (param) {
const parts = param.split('/');
const sheetId = parts[0];
const projectName = parts[1] ? decodeURIComponent(parts[1]) : null;
if (projectName) {
await ui.renderTrackerProjectPage(sheetId, projectName);
} else {
await ui.renderTrackerArtistPage(sheetId);
}
} else {
await ui.renderUnreleasedPage();
}
break;
case 'home':
await ui.renderHomePage();
break;

File diff suppressed because it is too large Load diff

139
js/ui.js
View file

@ -23,6 +23,7 @@ import { getVibrantColorFromImage } from './vibrant-color.js';
import { syncManager } from './accounts/pocketbase.js';
import { Visualizer } from './visualizer.js';
import { navigate } from './router.js';
import { renderUnreleasedPage as renderUnreleasedTrackerPage, renderTrackerArtistPage as renderTrackerArtistContent, renderTrackerProjectPage as renderTrackerProjectContent, renderTrackerTrackPage as renderTrackerTrackContent, findTrackerArtistByName, getArtistUnreleasedProjects, createProjectCardHTML } from './tracker.js';
export class UIRenderer {
constructor(api, player) {
@ -109,31 +110,42 @@ export class UIRenderer {
if (track) {
const isLocal = track.isLocal;
const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-'));
const shouldHide = isLocal || isTracker;
const shouldHideLikes = isLocal || isTracker;
if (likeBtn) {
if (shouldHide) {
if (shouldHideLikes) {
likeBtn.style.display = 'none';
} else {
likeBtn.style.display = 'flex';
this.updateLikeState(likeBtn.parentElement, 'track', track.id);
}
}
// For tracker tracks: show add playlist, hide lyrics
// For normal tracks: hide add playlist, show lyrics (unless local)
if (addPlaylistBtn) {
if (shouldHide) addPlaylistBtn.style.setProperty('display', 'none', 'important');
else addPlaylistBtn.style.removeProperty('display');
if (isTracker) {
addPlaylistBtn.style.removeProperty('display');
addPlaylistBtn.style.display = 'flex';
} else {
addPlaylistBtn.style.setProperty('display', 'none', 'important');
}
}
if (mobileAddPlaylistBtn) {
if (shouldHide) mobileAddPlaylistBtn.style.setProperty('display', 'none', 'important');
else mobileAddPlaylistBtn.style.removeProperty('display');
if (isTracker) {
mobileAddPlaylistBtn.style.removeProperty('display');
mobileAddPlaylistBtn.style.display = 'flex';
} else {
mobileAddPlaylistBtn.style.setProperty('display', 'none', 'important');
}
}
if (lyricsBtn) {
if (isLocal) lyricsBtn.style.display = 'none';
if (isLocal || isTracker) lyricsBtn.style.display = 'none';
else lyricsBtn.style.removeProperty('display');
}
if (fsLikeBtn) {
if (shouldHide) {
if (shouldHideLikes) {
fsLikeBtn.style.display = 'none';
} else {
fsLikeBtn.style.display = 'flex';
@ -141,7 +153,7 @@ export class UIRenderer {
}
}
if (fsAddPlaylistBtn) {
if (shouldHide) fsAddPlaylistBtn.style.display = 'none';
if (shouldHideLikes) fsAddPlaylistBtn.style.display = 'none';
else fsAddPlaylistBtn.style.display = 'flex';
}
} else {
@ -2192,6 +2204,8 @@ export class UIRenderer {
albumsContainer.innerHTML = this.createSkeletonCards(6, false);
if (epsContainer) epsContainer.innerHTML = this.createSkeletonCards(6, false);
if (epsSection) epsSection.style.display = 'none';
const loadUnreleasedSection = document.getElementById('artist-section-load-unreleased');
if (loadUnreleasedSection) loadUnreleasedSection.style.display = 'none';
if (similarContainer) similarContainer.innerHTML = this.createSkeletonCards(6, true);
if (similarSection) similarSection.style.display = 'block';
@ -2299,6 +2313,89 @@ export class UIRenderer {
}
});
// Check for unreleased projects
const unreleasedSection = document.getElementById('artist-section-unreleased');
const unreleasedContainer = document.getElementById('artist-detail-unreleased');
const loadUnreleasedBtn = document.getElementById('load-unreleased-btn');
const loadUnreleasedSection = document.getElementById('artist-section-load-unreleased');
if (unreleasedSection && unreleasedContainer && loadUnreleasedBtn && loadUnreleasedSection) {
// Initially hide the unreleased section
unreleasedSection.style.display = 'none';
loadUnreleasedSection.style.display = 'none';
// Check if artist has unreleased projects
const trackerArtist = findTrackerArtistByName(artist.name);
if (trackerArtist) {
// Show the load button section
loadUnreleasedSection.style.display = 'block';
// Add click handler to load and display unreleased projects
loadUnreleasedBtn.onclick = async () => {
loadUnreleasedBtn.disabled = true;
loadUnreleasedBtn.textContent = 'Loading...';
try {
const unreleasedData = await getArtistUnreleasedProjects(artist.name);
if (unreleasedData && unreleasedData.eras.length > 0) {
const { artist: trackerArtistData, sheetId, eras } = unreleasedData;
unreleasedContainer.innerHTML = eras.map(e => {
let trackCount = 0;
if (e.data) {
Object.values(e.data).forEach((songs) => {
if (songs && songs.length) trackCount += songs.length;
});
}
return createProjectCardHTML(e, trackerArtistData, sheetId, trackCount);
}).join('');
unreleasedSection.style.display = 'block';
loadUnreleasedBtn.style.display = 'none';
// Add click handlers
unreleasedContainer.querySelectorAll('.card').forEach((card) => {
const eraName = decodeURIComponent(card.dataset.trackerProjectId);
const era = eras.find(e => e.name === eraName);
if (!era) return;
card.onclick = (e) => {
if (e.target.closest('.card-play-btn')) {
e.stopPropagation();
let eraTracks = [];
if (era.data) {
Object.values(era.data).forEach((songs) => {
if (songs && songs.length) {
songs.forEach((song, index) => {
const track = createTrackFromSong(song, era, trackerArtistData.name, eraTracks.length, sheetId);
eraTracks.push(track);
});
}
});
}
const availableTracks = eraTracks.filter(t => !t.unavailable);
if (availableTracks.length > 0) {
globalPlayer.setQueue(availableTracks, 0);
globalPlayer.playTrackFromQueue();
}
} else if (e.target.closest('.card-menu-btn')) {
e.stopPropagation();
} else {
navigate(`/unreleased/${sheetId}/${encodeURIComponent(era.name)}`);
}
};
});
} else {
loadUnreleasedBtn.textContent = 'No unreleased projects';
}
} catch (error) {
console.error('Failed to load unreleased projects:', error);
loadUnreleasedBtn.textContent = 'Failed to load';
loadUnreleasedBtn.disabled = false;
}
};
}
}
recentActivityManager.addArtist(artist);
document.title = artist.name;
@ -2376,6 +2473,30 @@ export class UIRenderer {
}
}
async renderUnreleasedPage() {
this.showPage('unreleased');
const container = document.getElementById('unreleased-content');
await renderUnreleasedTrackerPage(container);
}
async renderTrackerArtistPage(sheetId) {
this.showPage('tracker-artist');
const container = document.getElementById('tracker-artist-projects-container');
await renderTrackerArtistContent(sheetId, container);
}
async renderTrackerProjectPage(sheetId, projectName) {
this.showPage('album'); // Use album page template
const container = document.getElementById('album-detail-tracklist');
await renderTrackerProjectContent(sheetId, projectName, container, this);
}
async renderTrackerTrackPage(trackId) {
this.showPage('album'); // Use album page template
const container = document.getElementById('album-detail-tracklist');
await renderTrackerTrackContent(trackId, container, this);
}
updatePlaylistHeaderActions(playlist, isOwned, tracks, showShare = false, onSort = null) {
const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions');

View file

@ -274,7 +274,16 @@ export const getTrackArtists = (track = {}, { fallback = 'Unknown Artist' } = {}
export const getTrackArtistsHTML = (track = {}, { fallback = 'Unknown Artist' } = {}) => {
if (track?.artists?.length) {
return track.artists
.map((artist) => `<span class="artist-link" data-artist-id="${artist.id}">${artist.name}</span>`)
.map((artist) => {
// Check if this is a tracker/unreleased track
const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-'));
if (isTracker && track.trackerInfo?.sheetId) {
// For tracker tracks, link to the tracker artist page
return `<span class="artist-link tracker-artist-link" data-tracker-sheet-id="${track.trackerInfo.sheetId}">${artist.name}</span>`;
}
// For normal tracks, use the artist ID
return `<span class="artist-link" data-artist-id="${artist.id}">${artist.name}</span>`;
})
.join(', ');
}