Merge pull request #5 from JulienMaille/main
UI Enhancements, Light Theme Support, and Miscellaneous Fixes
This commit is contained in:
commit
70c942e39b
7 changed files with 225 additions and 36 deletions
|
|
@ -47,7 +47,7 @@
|
||||||
<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="white" >
|
<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>
|
||||||
|
|
@ -138,9 +138,9 @@
|
||||||
<header class="detail-header">
|
<header class="detail-header">
|
||||||
<img id="album-detail-image" src="" alt="" class="detail-header-image">
|
<img id="album-detail-image" src="" alt="" class="detail-header-image">
|
||||||
<div class="detail-header-info">
|
<div class="detail-header-info">
|
||||||
<div class="type">Album</div>
|
|
||||||
<h1 class="title" id="album-detail-title"></h1>
|
<h1 class="title" id="album-detail-title"></h1>
|
||||||
<div class="meta" id="album-detail-meta"></div>
|
<div class="meta" id="album-detail-meta"></div>
|
||||||
|
<div class="meta" id="album-detail-producer"></div>
|
||||||
<div class="detail-header-actions">
|
<div class="detail-header-actions">
|
||||||
<button id="play-album-btn" class="btn-primary">
|
<button id="play-album-btn" class="btn-primary">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
|
@ -236,8 +236,10 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="theme-picker" id="theme-picker">
|
<div class="theme-picker" id="theme-picker">
|
||||||
<div class="theme-option" data-theme="monochrome">Black</div>
|
<div class="theme-option" data-theme="system">System</div>
|
||||||
|
<div class="theme-option" data-theme="light">Light</div>
|
||||||
<div class="theme-option" data-theme="dark">Dark</div>
|
<div class="theme-option" data-theme="dark">Dark</div>
|
||||||
|
<div class="theme-option" data-theme="monochrome">Black</div>
|
||||||
<div class="theme-option" data-theme="ocean">Ocean</div>
|
<div class="theme-option" data-theme="ocean">Ocean</div>
|
||||||
<div class="theme-option" data-theme="purple">Purple</div>
|
<div class="theme-option" data-theme="purple">Purple</div>
|
||||||
<div class="theme-option" data-theme="forest">Forest</div>
|
<div class="theme-option" data-theme="forest">Forest</div>
|
||||||
|
|
|
||||||
14
js/api.js
14
js/api.js
|
|
@ -324,6 +324,20 @@ export class LosslessAPI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If album exists but has no releaseDate, try to extract from tracks
|
||||||
|
if (album && !album.releaseDate && tracksSection?.items && tracksSection.items.length > 0) {
|
||||||
|
const firstTrack = tracksSection.items[0];
|
||||||
|
const track = firstTrack.item || firstTrack;
|
||||||
|
|
||||||
|
if (track) {
|
||||||
|
if (track.album && track.album.releaseDate) {
|
||||||
|
album = { ...album, releaseDate: track.album.releaseDate };
|
||||||
|
} else if (track.streamStartDate) {
|
||||||
|
album = { ...album, releaseDate: track.streamStartDate.split('T')[0] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const tracks = (tracksSection?.items || []).map(i => this.prepareTrack(i.item || i));
|
const tracks = (tracksSection?.items || []).map(i => this.prepareTrack(i.item || i));
|
||||||
const result = { album, tracks };
|
const result = { album, tracks };
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -207,10 +207,13 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana
|
||||||
const zip = new JSZip();
|
const zip = new JSZip();
|
||||||
|
|
||||||
const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf';
|
const template = localStorage.getItem('zip-folder-template') || '{albumTitle} - {albumArtist} - monochrome.tf';
|
||||||
|
const releaseDate = album.releaseDate ? new Date(album.releaseDate) : null;
|
||||||
|
const year = (releaseDate && !isNaN(releaseDate.getTime())) ? releaseDate.getFullYear() : '';
|
||||||
|
|
||||||
const folderName = formatTemplate(template, {
|
const folderName = formatTemplate(template, {
|
||||||
albumTitle: album.title,
|
albumTitle: album.title,
|
||||||
albumArtist: album.artist?.name,
|
albumArtist: album.artist?.name,
|
||||||
year: new Date(album.releaseDate).getFullYear()
|
year: year
|
||||||
});
|
});
|
||||||
|
|
||||||
const notification = createBulkDownloadNotification('album', album.title, tracks.length);
|
const notification = createBulkDownloadNotification('album', album.title, tracks.length);
|
||||||
|
|
@ -380,10 +383,13 @@ export async function downloadDiscography(artist, api, quality, lyricsManager =
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { album: fullAlbum, tracks } = await api.getAlbum(album.id);
|
const { album: fullAlbum, tracks } = await api.getAlbum(album.id);
|
||||||
|
const releaseDate = fullAlbum.releaseDate ? new Date(fullAlbum.releaseDate) : null;
|
||||||
|
const year = (releaseDate && !isNaN(releaseDate.getTime())) ? releaseDate.getFullYear() : '';
|
||||||
|
|
||||||
const albumFolder = formatTemplate(template, {
|
const albumFolder = formatTemplate(template, {
|
||||||
albumTitle: fullAlbum.title,
|
albumTitle: fullAlbum.title,
|
||||||
albumArtist: fullAlbum.artist?.name,
|
albumArtist: fullAlbum.artist?.name,
|
||||||
year: new Date(fullAlbum.releaseDate).getFullYear()
|
year: year
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
|
|
|
||||||
47
js/events.js
47
js/events.js
|
|
@ -275,9 +275,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
||||||
contextTrack = trackDataStore.get(trackItem);
|
contextTrack = trackDataStore.get(trackItem);
|
||||||
if (contextTrack) {
|
if (contextTrack) {
|
||||||
const rect = menuBtn.getBoundingClientRect();
|
const rect = menuBtn.getBoundingClientRect();
|
||||||
contextMenu.style.top = `${rect.bottom + 5}px`;
|
positionMenu(contextMenu, rect.left, rect.bottom + 5, rect);
|
||||||
contextMenu.style.left = `${rect.left}px`;
|
|
||||||
contextMenu.style.display = 'block';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
|
@ -307,9 +305,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen
|
||||||
contextTrack = trackDataStore.get(trackItem);
|
contextTrack = trackDataStore.get(trackItem);
|
||||||
|
|
||||||
if (contextTrack) {
|
if (contextTrack) {
|
||||||
contextMenu.style.top = `${e.pageY}px`;
|
positionMenu(contextMenu, e.pageX, e.pageY);
|
||||||
contextMenu.style.left = `${e.pageX}px`;
|
|
||||||
contextMenu.style.display = 'block';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -387,3 +383,42 @@ function formatTime(seconds) {
|
||||||
const s = Math.floor(seconds % 60);
|
const s = Math.floor(seconds % 60);
|
||||||
return `${m}:${String(s).padStart(2, '0')}`;
|
return `${m}:${String(s).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function positionMenu(menu, x, y, anchorRect = null) {
|
||||||
|
// Temporarily show to measure dimensions
|
||||||
|
menu.style.visibility = 'hidden';
|
||||||
|
menu.style.display = 'block';
|
||||||
|
|
||||||
|
const menuWidth = menu.offsetWidth;
|
||||||
|
const menuHeight = menu.offsetHeight;
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
let left = x;
|
||||||
|
let top = y;
|
||||||
|
|
||||||
|
if (anchorRect) {
|
||||||
|
// Adjust horizontal position if it overflows right
|
||||||
|
if (left + menuWidth > windowWidth - 10) { // 10px buffer
|
||||||
|
left = anchorRect.right - menuWidth;
|
||||||
|
if (left < 10) left = 10;
|
||||||
|
}
|
||||||
|
// Adjust vertical position if it overflows bottom
|
||||||
|
if (top + menuHeight > windowHeight - 10) {
|
||||||
|
top = anchorRect.top - menuHeight - 5;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Adjust horizontal position if it overflows right
|
||||||
|
if (left + menuWidth > windowWidth - 10) {
|
||||||
|
left = windowWidth - menuWidth - 10;
|
||||||
|
}
|
||||||
|
// Adjust vertical position if it overflows bottom
|
||||||
|
if (top + menuHeight > windowHeight - 10) {
|
||||||
|
top = y - menuHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.style.top = `${top}px`;
|
||||||
|
menu.style.left = `${left}px`;
|
||||||
|
menu.style.visibility = 'visible';
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -229,8 +229,9 @@ export const themeManager = {
|
||||||
CUSTOM_THEME_KEY: 'monochrome-custom-theme',
|
CUSTOM_THEME_KEY: 'monochrome-custom-theme',
|
||||||
|
|
||||||
defaultThemes: {
|
defaultThemes: {
|
||||||
monochrome: {},
|
light: {},
|
||||||
dark: {},
|
dark: {},
|
||||||
|
monochrome: {},
|
||||||
ocean: {},
|
ocean: {},
|
||||||
purple: {},
|
purple: {},
|
||||||
forest: {}
|
forest: {}
|
||||||
|
|
@ -238,15 +239,21 @@ export const themeManager = {
|
||||||
|
|
||||||
getTheme() {
|
getTheme() {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem(this.STORAGE_KEY) || 'monochrome';
|
return localStorage.getItem(this.STORAGE_KEY) || 'system';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return 'monochrome';
|
return 'system';
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setTheme(theme) {
|
setTheme(theme) {
|
||||||
localStorage.setItem(this.STORAGE_KEY, theme);
|
localStorage.setItem(this.STORAGE_KEY, theme);
|
||||||
document.documentElement.setAttribute('data-theme', theme);
|
|
||||||
|
if (theme === 'system') {
|
||||||
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-theme', theme);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
getCustomTheme() {
|
getCustomTheme() {
|
||||||
|
|
@ -318,3 +325,12 @@ export const lyricsSettings = {
|
||||||
localStorage.setItem(this.DOWNLOAD_WITH_TRACKS, enabled ? 'true' : 'false');
|
localStorage.setItem(this.DOWNLOAD_WITH_TRACKS, enabled ? 'true' : 'false');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// System theme listener
|
||||||
|
if (typeof window !== 'undefined' && window.matchMedia) {
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
|
||||||
|
if (themeManager.getTheme() === 'system') {
|
||||||
|
document.documentElement.setAttribute('data-theme', e.matches ? 'dark' : 'light');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
92
js/ui.js
92
js/ui.js
|
|
@ -23,14 +23,41 @@ export class UIRenderer {
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
createTrackItemHTML(track, index, showCover = false) {
|
adjustTitleFontSize(element, text) {
|
||||||
|
element.classList.remove('long-title', 'very-long-title');
|
||||||
|
if (text.length > 40) {
|
||||||
|
element.classList.add('very-long-title');
|
||||||
|
} else if (text.length > 25) {
|
||||||
|
element.classList.add('long-title');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false) {
|
||||||
const playIconSmall = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
|
const playIconSmall = '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>';
|
||||||
const trackImageHTML = showCover ? `<img src="${this.api.getCoverUrl(track.album?.cover, '80')}" alt="Track Cover" class="track-item-cover" loading="lazy">` : '';
|
const trackImageHTML = showCover ? `<img src="${this.api.getCoverUrl(track.album?.cover, '80')}" alt="Track Cover" class="track-item-cover" loading="lazy">` : '';
|
||||||
const trackNumberHTML = `<div class="track-number">${showCover ? trackImageHTML : index + 1}</div>`;
|
|
||||||
|
let displayIndex;
|
||||||
|
if (hasMultipleDiscs && !showCover) {
|
||||||
|
const discNum = track.volumeNumber ?? track.discNumber ?? 1;
|
||||||
|
displayIndex = `${discNum}-${track.trackNumber}`;
|
||||||
|
} else {
|
||||||
|
displayIndex = index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trackNumberHTML = `<div class="track-number">${showCover ? trackImageHTML : displayIndex}</div>`;
|
||||||
const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : '';
|
const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : '';
|
||||||
const trackArtists = getTrackArtists(track);
|
const trackArtists = getTrackArtists(track);
|
||||||
const trackTitle = getTrackTitle(track);
|
const trackTitle = getTrackTitle(track);
|
||||||
|
|
||||||
|
let yearDisplay = '';
|
||||||
|
const releaseDate = track.album?.releaseDate || track.streamStartDate;
|
||||||
|
if (releaseDate) {
|
||||||
|
const date = new Date(releaseDate);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
yearDisplay = ` • ${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="track-item" data-track-id="${track.id}">
|
<div class="track-item" data-track-id="${track.id}">
|
||||||
${trackNumberHTML}
|
${trackNumberHTML}
|
||||||
|
|
@ -40,7 +67,7 @@ export class UIRenderer {
|
||||||
${trackTitle}
|
${trackTitle}
|
||||||
${explicitBadge}
|
${explicitBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="artist">${trackArtists}</div>
|
<div class="artist">${trackArtists}${yearDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="track-item-duration">${formatTime(track.duration)}</div>
|
<div class="track-item-duration">${formatTime(track.duration)}</div>
|
||||||
|
|
@ -57,13 +84,21 @@ export class UIRenderer {
|
||||||
|
|
||||||
createAlbumCardHTML(album) {
|
createAlbumCardHTML(album) {
|
||||||
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
||||||
|
let yearDisplay = '';
|
||||||
|
if (album.releaseDate) {
|
||||||
|
const date = new Date(album.releaseDate);
|
||||||
|
if (!isNaN(date.getTime())) {
|
||||||
|
yearDisplay = `${date.getFullYear()}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
return `
|
return `
|
||||||
<a href="#album/${album.id}" class="card">
|
<a href="#album/${album.id}" class="card">
|
||||||
<div class="card-image-wrapper">
|
<div class="card-image-wrapper">
|
||||||
<img src="${this.api.getCoverUrl(album.cover, '320')}" alt="${album.title}" class="card-image" loading="lazy">
|
<img src="${this.api.getCoverUrl(album.cover, '320')}" alt="${album.title}" class="card-image" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card-title">${album.title} ${explicitBadge}</h3>
|
<h3 class="card-title">${album.title} ${explicitBadge}</h3>
|
||||||
<p class="card-subtitle">Album • ${album.artist?.name ?? ''}</p>
|
<p class="card-subtitle">${album.artist?.name ?? ''}</p>
|
||||||
|
<p class="card-subtitle">${yearDisplay}</p>
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +110,6 @@ export class UIRenderer {
|
||||||
<img src="${this.api.getArtistPictureUrl(artist.picture, '320')}" alt="${artist.name}" class="card-image" loading="lazy">
|
<img src="${this.api.getArtistPictureUrl(artist.picture, '320')}" alt="${artist.name}" class="card-image" loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
<h3 class="card-title">${artist.name}</h3>
|
<h3 class="card-title">${artist.name}</h3>
|
||||||
<p class="card-subtitle">Artist</p>
|
|
||||||
</a>
|
</a>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -100,7 +134,7 @@ export class UIRenderer {
|
||||||
<div class="skeleton-card ${isArtist ? 'artist' : ''}">
|
<div class="skeleton-card ${isArtist ? 'artist' : ''}">
|
||||||
<div class="skeleton skeleton-card-image"></div>
|
<div class="skeleton skeleton-card-image"></div>
|
||||||
<div class="skeleton skeleton-card-title"></div>
|
<div class="skeleton skeleton-card-title"></div>
|
||||||
<div class="skeleton skeleton-card-subtitle"></div>
|
${!isArtist ? '<div class="skeleton skeleton-card-subtitle"></div>' : ''}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
@ -117,8 +151,11 @@ export class UIRenderer {
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
|
|
||||||
|
// Check if there are multiple discs in the tracks array
|
||||||
|
const hasMultipleDiscs = tracks.some(t => (t.volumeNumber || t.discNumber || 1) > 1);
|
||||||
|
|
||||||
tempDiv.innerHTML = tracks.map((track, i) =>
|
tempDiv.innerHTML = tracks.map((track, i) =>
|
||||||
this.createTrackItemHTML(track, i, showCover)
|
this.createTrackItemHTML(track, i, showCover, hasMultipleDiscs)
|
||||||
).join('');
|
).join('');
|
||||||
|
|
||||||
while (tempDiv.firstChild) {
|
while (tempDiv.firstChild) {
|
||||||
|
|
@ -245,12 +282,14 @@ export class UIRenderer {
|
||||||
const imageEl = document.getElementById('album-detail-image');
|
const imageEl = document.getElementById('album-detail-image');
|
||||||
const titleEl = document.getElementById('album-detail-title');
|
const titleEl = document.getElementById('album-detail-title');
|
||||||
const metaEl = document.getElementById('album-detail-meta');
|
const metaEl = document.getElementById('album-detail-meta');
|
||||||
|
const prodEl = document.getElementById('album-detail-producer');
|
||||||
const tracklistContainer = document.getElementById('album-detail-tracklist');
|
const tracklistContainer = document.getElementById('album-detail-tracklist');
|
||||||
|
|
||||||
imageEl.src = '';
|
imageEl.src = '';
|
||||||
imageEl.style.backgroundColor = 'var(--muted)';
|
imageEl.style.backgroundColor = 'var(--muted)';
|
||||||
titleEl.innerHTML = '<div class="skeleton" style="height: 48px; width: 300px; max-width: 90%;"></div>';
|
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>';
|
metaEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 200px; max-width: 80%;"></div>';
|
||||||
|
prodEl.innerHTML = '<div class="skeleton" style="height: 16px; width: 200px; max-width: 80%;"></div>';
|
||||||
tracklistContainer.innerHTML = `
|
tracklistContainer.innerHTML = `
|
||||||
<div class="track-list-header">
|
<div class="track-list-header">
|
||||||
<span style="width: 40px; text-align: center;">#</span>
|
<span style="width: 40px; text-align: center;">#</span>
|
||||||
|
|
@ -269,16 +308,29 @@ export class UIRenderer {
|
||||||
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
||||||
titleEl.innerHTML = `${album.title} ${explicitBadge}`;
|
titleEl.innerHTML = `${album.title} ${explicitBadge}`;
|
||||||
|
|
||||||
const totalDuration = calculateTotalDuration(tracks);
|
this.adjustTitleFontSize(titleEl, album.title);
|
||||||
const releaseDate = new Date(album.releaseDate);
|
|
||||||
const year = releaseDate.getFullYear();
|
|
||||||
|
|
||||||
const dateDisplay = window.innerWidth > 768
|
const totalDuration = calculateTotalDuration(tracks);
|
||||||
? releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
|
let dateDisplay = '';
|
||||||
: year;
|
if (album.releaseDate) {
|
||||||
|
const releaseDate = new Date(album.releaseDate);
|
||||||
|
if (!isNaN(releaseDate.getTime())) {
|
||||||
|
const year = releaseDate.getFullYear();
|
||||||
|
dateDisplay = window.innerWidth > 768
|
||||||
|
? releaseDate.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
|
||||||
|
: year;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstCopyright = tracks.find(track => track.copyright)?.copyright;
|
||||||
|
|
||||||
metaEl.innerHTML =
|
metaEl.innerHTML =
|
||||||
`By <a href="#artist/${album.artist.id}">${album.artist.name}</a> • ${dateDisplay} • ${tracks.length} tracks • ${formatDuration(totalDuration)}`;
|
(dateDisplay ? `${dateDisplay} • ` : '') +
|
||||||
|
`${tracks.length} tracks • ${formatDuration(totalDuration)}`;
|
||||||
|
|
||||||
|
prodEl.innerHTML =
|
||||||
|
`By <a href="#artist/${album.artist.id}">${album.artist.name}</a>` +
|
||||||
|
(firstCopyright ? ` • ${firstCopyright}` : '');
|
||||||
|
|
||||||
tracklistContainer.innerHTML = `
|
tracklistContainer.innerHTML = `
|
||||||
<div class="track-list-header">
|
<div class="track-list-header">
|
||||||
|
|
@ -288,7 +340,12 @@ export class UIRenderer {
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
tracks.sort((a, b) => a.trackNumber - b.trackNumber);
|
tracks.sort((a, b) => {
|
||||||
|
const discA = a.volumeNumber ?? a.discNumber ?? 1;
|
||||||
|
const discB = b.volumeNumber ?? b.discNumber ?? 1;
|
||||||
|
if (discA !== discB) return discA - discB;
|
||||||
|
return a.trackNumber - b.trackNumber;
|
||||||
|
});
|
||||||
this.renderListWithTracks(tracklistContainer, tracks, false);
|
this.renderListWithTracks(tracklistContainer, tracks, false);
|
||||||
|
|
||||||
recentActivityManager.addAlbum(album);
|
recentActivityManager.addAlbum(album);
|
||||||
|
|
@ -332,6 +389,8 @@ async renderPlaylistPage(playlistId) {
|
||||||
|
|
||||||
titleEl.textContent = playlist.title;
|
titleEl.textContent = playlist.title;
|
||||||
|
|
||||||
|
this.adjustTitleFontSize(titleEl, playlist.title);
|
||||||
|
|
||||||
const totalDuration = calculateTotalDuration(tracks);
|
const totalDuration = calculateTotalDuration(tracks);
|
||||||
|
|
||||||
metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`;
|
metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`;
|
||||||
|
|
@ -377,6 +436,9 @@ async renderPlaylistPage(playlistId) {
|
||||||
imageEl.src = this.api.getArtistPictureUrl(artist.picture, '750');
|
imageEl.src = this.api.getArtistPictureUrl(artist.picture, '750');
|
||||||
imageEl.style.backgroundColor = '';
|
imageEl.style.backgroundColor = '';
|
||||||
nameEl.textContent = artist.name;
|
nameEl.textContent = artist.name;
|
||||||
|
|
||||||
|
this.adjustTitleFontSize(nameEl, artist.name);
|
||||||
|
|
||||||
metaEl.textContent = `${artist.popularity} popularity`;
|
metaEl.textContent = `${artist.popularity} popularity`;
|
||||||
|
|
||||||
this.renderListWithTracks(tracksContainer, artist.tracks, true);
|
this.renderListWithTracks(tracksContainer, artist.tracks, true);
|
||||||
|
|
|
||||||
66
styles.css
66
styles.css
|
|
@ -114,6 +114,27 @@
|
||||||
--explicit-badge: #f59e0b;
|
--explicit-badge: #f59e0b;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root[data-theme="light"] {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #000000;
|
||||||
|
--card: #f4f4f5;
|
||||||
|
--card-foreground: #000000;
|
||||||
|
--primary: #2563eb;
|
||||||
|
--primary-foreground: #ffffff;
|
||||||
|
--secondary: #e4e4e7;
|
||||||
|
--secondary-foreground: #000000;
|
||||||
|
--muted: #e4e4e7;
|
||||||
|
--muted-foreground: #71717a;
|
||||||
|
--border: #e4e4e7;
|
||||||
|
--input: #e4e4e7;
|
||||||
|
--ring: #2563eb;
|
||||||
|
--highlight: #2563eb;
|
||||||
|
--highlight-rgb: 37, 99, 235;
|
||||||
|
--active-highlight: var(--highlight);
|
||||||
|
--explicit-badge: #ef4444;
|
||||||
|
--explicit-badge-foreground: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -166,19 +187,18 @@ kbd {
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
grid-template:
|
grid-template:
|
||||||
"sidebar main" 1fr
|
"sidebar main" 1fr
|
||||||
"player player" auto / 280px 1fr;
|
"player player" auto / 190px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
grid-area: sidebar;
|
grid-area: sidebar;
|
||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
padding: 1.5rem;
|
padding: 1.25rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
z-index: 2000;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
|
|
@ -190,7 +210,7 @@ kbd {
|
||||||
|
|
||||||
.now-playing-bar {
|
.now-playing-bar {
|
||||||
grid-area: player;
|
grid-area: player;
|
||||||
background-color: #050505;
|
background-color: var(--card);
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
padding: var(--spacing-md) var(--spacing-lg);
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
@ -491,7 +511,7 @@ kbd {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background-color: var(--explicit-badge);
|
background-color: var(--explicit-badge);
|
||||||
color: #000;
|
color: var(--explicit-badge-foreground, #000);
|
||||||
font-size: 0.65rem;
|
font-size: 0.65rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 0.15rem 0.35rem;
|
padding: 0.15rem 0.35rem;
|
||||||
|
|
@ -682,6 +702,15 @@ kbd {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.long-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.very-long-title {
|
||||||
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.detail-header-info .meta {
|
.detail-header-info .meta {
|
||||||
|
|
@ -1805,7 +1834,7 @@ input:checked + .slider::before {
|
||||||
/* Responsive Design */
|
/* Responsive Design */
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
.app-container {
|
.app-container {
|
||||||
grid-template-columns: 240px 1fr;
|
grid-template-columns: 160px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-grid {
|
.card-grid {
|
||||||
|
|
@ -1817,6 +1846,14 @@ input:checked + .slider::before {
|
||||||
font-size: 3rem;
|
font-size: 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.long-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.very-long-title {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
padding: var(--spacing-lg);
|
padding: var(--spacing-lg);
|
||||||
}
|
}
|
||||||
|
|
@ -1850,6 +1887,7 @@ input:checked + .slider::before {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
transform: translateX(-100%);
|
transform: translateX(-100%);
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 2000;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar.is-open {
|
.sidebar.is-open {
|
||||||
|
|
@ -1900,6 +1938,14 @@ input:checked + .slider::before {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.long-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.very-long-title {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.detail-header-info .meta {
|
.detail-header-info .meta {
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
gap: 0.35rem;
|
gap: 0.35rem;
|
||||||
|
|
@ -2136,6 +2182,14 @@ input:checked + .slider::before {
|
||||||
font-size: 1.75rem;
|
font-size: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.long-title {
|
||||||
|
font-size: 1.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-header-info .title.very-long-title {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
.search-tab {
|
.search-tab {
|
||||||
padding: var(--spacing-sm) var(--spacing-md);
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue