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-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="go-to-album" data-type-filter="track">Go to album</li>
<li data-action="copy-link">Copy link</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> <li data-action="download">Download</li>
</ul> </ul>
</div> </div>
@ -795,6 +797,27 @@
<span>Recent</span> <span>Recent</span>
</a> </a>
</li> </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"> <li class="nav-item">
<a href="/settings"> <a href="/settings">
<svg <svg
@ -1338,6 +1361,35 @@
<div class="track-list" id="recent-tracks-container"></div> <div class="track-list" id="recent-tracks-container"></div>
</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"> <div id="page-album" class="page">
<header class="detail-header"> <header class="detail-header">
<img <img
@ -1704,11 +1756,13 @@
<h2 class="section-title">EPs and Singles</h2> <h2 class="section-title">EPs and Singles</h2>
<div class="card-grid" id="artist-detail-eps"></div> <div class="card-grid" id="artist-detail-eps"></div>
</section> </section>
<div <section class="content-section" id="artist-section-load-unreleased" style="display: none; margin: 1.5rem 0;">
id="artist-tracker-section" <button id="load-unreleased-btn" class="btn-primary">Load Unreleased Projects</button>
class="content-section" </section>
style="display: none; margin-top: 2rem" <section class="content-section" id="artist-section-unreleased" style="display: none">
></div> <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"> <section class="content-section" id="artist-section-similar" style="display: none">
<h2 class="section-title">Similar Artists</h2> <h2 class="section-title">Similar Artists</h2>
<div class="card-grid" id="artist-detail-similar"></div> <div class="card-grid" id="artist-detail-similar"></div>
@ -2429,7 +2483,7 @@
</a> </a>
</div> </div>
<div class="about-footer"> <div class="about-footer">
<p class="version">Version 2.1.0</p> <p class="version">Version 2.2.0</p>
<p class="disclaimer"> <p class="disclaimer">
This is an independent client and is not affiliated with or endorsed by TIDAL or any This is an independent client and is not affiliated with or endorsed by TIDAL or any
music streaming service. music streaming service.

View file

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

View file

@ -8,6 +8,7 @@ import {
trackDataStore, trackDataStore,
formatTime, formatTime,
SVG_BIN, SVG_BIN,
getTrackArtists,
} from './utils.js'; } from './utils.js';
import { lastFMStorage, waveformSettings } from './storage.js'; import { lastFMStorage, waveformSettings } from './storage.js';
import { showNotification, downloadTrackWithMetadata, downloadAlbumAsZip, downloadPlaylistAsZip } from './downloads.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( export async function handleTrackAction(
action, action,
item, item,
@ -942,6 +1049,143 @@ export async function handleTrackAction(
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
showNotification('Link copied to clipboard!'); 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'; 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 // Filter items based on type
const type = contextMenu._contextType || 'track'; const type = contextMenu._contextType || 'track';
contextMenu.querySelectorAll('li[data-action]').forEach((item) => { contextMenu.querySelectorAll('li[data-action]').forEach((item) => {
@ -1229,7 +1480,11 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
if (link) { if (link) {
e.stopPropagation(); e.stopPropagation();
const artistId = link.dataset.artistId; 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}`); navigate(`/artist/${artistId}`);
} }
return; return;
@ -1237,9 +1492,15 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
// Fallback for non-link clicks (e.g. separators) or single artist legacy // Fallback for non-link clicks (e.g. separators) or single artist legacy
const track = player.currentTrack; const track = player.currentTrack;
if (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}`); navigate(`/artist/${track.artist.id}`);
} }
}
}); });
const nowPlayingLikeBtn = document.getElementById('now-playing-like-btn'); const nowPlayingLikeBtn = document.getElementById('now-playing-like-btn');

View file

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

View file

@ -356,6 +356,32 @@ export class Player {
this.applyReplayGain(); this.applyReplayGain();
this.audio.src = streamUrl; 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) { if (startTime > 0) {
this.audio.currentTime = startTime; this.audio.currentTime = startTime;
} }

View file

@ -52,7 +52,11 @@ export function createRouter(ui) {
await ui.renderMixPage(param); await ui.renderMixPage(param);
break; break;
case 'track': case 'track':
if (param.startsWith('tracker-')) {
await ui.renderTrackerTrackPage(param);
} else {
await ui.renderTrackPage(param); await ui.renderTrackPage(param);
}
break; break;
case 'library': case 'library':
await ui.renderLibraryPage(); await ui.renderLibraryPage();
@ -60,6 +64,20 @@ export function createRouter(ui) {
case 'recent': case 'recent':
await ui.renderRecentPage(); await ui.renderRecentPage();
break; 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': case 'home':
await ui.renderHomePage(); await ui.renderHomePage();
break; 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 { syncManager } from './accounts/pocketbase.js';
import { Visualizer } from './visualizer.js'; import { Visualizer } from './visualizer.js';
import { navigate } from './router.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 { export class UIRenderer {
constructor(api, player) { constructor(api, player) {
@ -109,31 +110,42 @@ export class UIRenderer {
if (track) { if (track) {
const isLocal = track.isLocal; const isLocal = track.isLocal;
const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-')); const isTracker = track.isTracker || (track.id && String(track.id).startsWith('tracker-'));
const shouldHide = isLocal || isTracker; const shouldHideLikes = isLocal || isTracker;
if (likeBtn) { if (likeBtn) {
if (shouldHide) { if (shouldHideLikes) {
likeBtn.style.display = 'none'; likeBtn.style.display = 'none';
} else { } else {
likeBtn.style.display = 'flex'; likeBtn.style.display = 'flex';
this.updateLikeState(likeBtn.parentElement, 'track', track.id); 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 (addPlaylistBtn) {
if (shouldHide) addPlaylistBtn.style.setProperty('display', 'none', 'important'); if (isTracker) {
else addPlaylistBtn.style.removeProperty('display'); addPlaylistBtn.style.removeProperty('display');
addPlaylistBtn.style.display = 'flex';
} else {
addPlaylistBtn.style.setProperty('display', 'none', 'important');
}
} }
if (mobileAddPlaylistBtn) { if (mobileAddPlaylistBtn) {
if (shouldHide) mobileAddPlaylistBtn.style.setProperty('display', 'none', 'important'); if (isTracker) {
else mobileAddPlaylistBtn.style.removeProperty('display'); mobileAddPlaylistBtn.style.removeProperty('display');
mobileAddPlaylistBtn.style.display = 'flex';
} else {
mobileAddPlaylistBtn.style.setProperty('display', 'none', 'important');
}
} }
if (lyricsBtn) { if (lyricsBtn) {
if (isLocal) lyricsBtn.style.display = 'none'; if (isLocal || isTracker) lyricsBtn.style.display = 'none';
else lyricsBtn.style.removeProperty('display'); else lyricsBtn.style.removeProperty('display');
} }
if (fsLikeBtn) { if (fsLikeBtn) {
if (shouldHide) { if (shouldHideLikes) {
fsLikeBtn.style.display = 'none'; fsLikeBtn.style.display = 'none';
} else { } else {
fsLikeBtn.style.display = 'flex'; fsLikeBtn.style.display = 'flex';
@ -141,7 +153,7 @@ export class UIRenderer {
} }
} }
if (fsAddPlaylistBtn) { if (fsAddPlaylistBtn) {
if (shouldHide) fsAddPlaylistBtn.style.display = 'none'; if (shouldHideLikes) fsAddPlaylistBtn.style.display = 'none';
else fsAddPlaylistBtn.style.display = 'flex'; else fsAddPlaylistBtn.style.display = 'flex';
} }
} else { } else {
@ -2192,6 +2204,8 @@ export class UIRenderer {
albumsContainer.innerHTML = this.createSkeletonCards(6, false); albumsContainer.innerHTML = this.createSkeletonCards(6, false);
if (epsContainer) epsContainer.innerHTML = this.createSkeletonCards(6, false); if (epsContainer) epsContainer.innerHTML = this.createSkeletonCards(6, false);
if (epsSection) epsSection.style.display = 'none'; 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 (similarContainer) similarContainer.innerHTML = this.createSkeletonCards(6, true);
if (similarSection) similarSection.style.display = 'block'; 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); recentActivityManager.addArtist(artist);
document.title = artist.name; 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) { updatePlaylistHeaderActions(playlist, isOwned, tracks, showShare = false, onSort = null) {
const actionsDiv = document.getElementById('page-playlist').querySelector('.detail-header-actions'); 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' } = {}) => { export const getTrackArtistsHTML = (track = {}, { fallback = 'Unknown Artist' } = {}) => {
if (track?.artists?.length) { if (track?.artists?.length) {
return track.artists 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(', '); .join(', ');
} }