From c6e6d6a5960e553717f61c925dbccc9843417bf2 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:53:59 +0000 Subject: [PATCH 1/6] Add light theme and fix hardcoded styles - Add light theme CSS variables in styles.css - Add 'Light' option to settings in index.html - Update theme manager defaults in js/storage.js - Replace hardcoded colors with CSS variables for logo and player bar - Add support for explicit badge text color customization --- index.html | 6 ++++-- js/storage.js | 24 ++++++++++++++++++++---- styles.css | 27 ++++++++++++++++++++++++--- 3 files changed, 48 insertions(+), 9 deletions(-) diff --git a/index.html b/index.html index 8b82374..fc7c704 100644 --- a/index.html +++ b/index.html @@ -47,7 +47,7 @@
-
Black
+
System
+
Light
Dark
+
Black
Ocean
Purple
Forest
diff --git a/js/storage.js b/js/storage.js index 20ef4d2..58660e4 100644 --- a/js/storage.js +++ b/js/storage.js @@ -229,8 +229,9 @@ export const themeManager = { CUSTOM_THEME_KEY: 'monochrome-custom-theme', defaultThemes: { - monochrome: {}, + light: {}, dark: {}, + monochrome: {}, ocean: {}, purple: {}, forest: {} @@ -238,15 +239,21 @@ export const themeManager = { getTheme() { try { - return localStorage.getItem(this.STORAGE_KEY) || 'monochrome'; + return localStorage.getItem(this.STORAGE_KEY) || 'system'; } catch (e) { - return 'monochrome'; + return 'system'; } }, setTheme(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() { @@ -318,3 +325,12 @@ export const lyricsSettings = { 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'); + } + }); +} diff --git a/styles.css b/styles.css index 830bbdd..06391e7 100644 --- a/styles.css +++ b/styles.css @@ -114,6 +114,27 @@ --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: #000000; + --explicit-badge-foreground: #ffffff; +} + *, *::before, *::after { box-sizing: border-box; margin: 0; @@ -178,7 +199,6 @@ kbd { flex-direction: column; gap: 2rem; transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1); - z-index: 2000; } .main-content { @@ -190,7 +210,7 @@ kbd { .now-playing-bar { grid-area: player; - background-color: #050505; + background-color: var(--card); border-top: 1px solid var(--border); padding: var(--spacing-md) var(--spacing-lg); display: grid; @@ -491,7 +511,7 @@ kbd { align-items: center; justify-content: center; background-color: var(--explicit-badge); - color: #000; + color: var(--explicit-badge-foreground, #000); font-size: 0.65rem; font-weight: 700; padding: 0.15rem 0.35rem; @@ -1850,6 +1870,7 @@ input:checked + .slider::before { height: 100%; transform: translateX(-100%); box-shadow: 0 0 20px rgba(0, 0, 0, 0.5); + z-index: 2000; } .sidebar.is-open { From 0aee58b823c38e49e0e92c67870eb7df33c024be Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 23:28:09 +0000 Subject: [PATCH 2/6] Fix album track sorting and formatting for multi-disc albums - Updated `renderAlbumPage` in `js/ui.js` to sort tracks by disc/volume number before track number. - Updated `renderListWithTracks` to detect multi-disc albums. - Updated `createTrackItemHTML` to display track numbers as `Disc-Track` (e.g., "1-1") for multi-disc albums when cover art is hidden (album view). --- js/ui.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/js/ui.js b/js/ui.js index 7b72122..293ee6a 100644 --- a/js/ui.js +++ b/js/ui.js @@ -23,10 +23,19 @@ export class UIRenderer { `; } - createTrackItemHTML(track, index, showCover = false) { + createTrackItemHTML(track, index, showCover = false, hasMultipleDiscs = false) { const playIconSmall = ''; const trackImageHTML = showCover ? `Track Cover` : ''; - const trackNumberHTML = `
${showCover ? trackImageHTML : index + 1}
`; + + let displayIndex; + if (hasMultipleDiscs && !showCover) { + const discNum = track.volumeNumber ?? track.discNumber ?? 1; + displayIndex = `${discNum}-${track.trackNumber}`; + } else { + displayIndex = index + 1; + } + + const trackNumberHTML = `
${showCover ? trackImageHTML : displayIndex}
`; const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : ''; const trackArtists = getTrackArtists(track); const trackTitle = getTrackTitle(track); @@ -117,8 +126,11 @@ export class UIRenderer { const fragment = document.createDocumentFragment(); 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) => - this.createTrackItemHTML(track, i, showCover) + this.createTrackItemHTML(track, i, showCover, hasMultipleDiscs) ).join(''); while (tempDiv.firstChild) { @@ -288,7 +300,12 @@ export class UIRenderer {
`; - 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); recentActivityManager.addAlbum(album); From 43f04e74548fd2794080868479f6782c7492391b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:57:02 +0000 Subject: [PATCH 3/6] Fix "Invalid Date" display in album details - Backfill album release date from tracks if missing in API response. - Gracefully handle invalid or missing dates in UI rendering. - Fix potential NaN in download folder naming due to invalid dates. --- index.html | 2 +- js/api.js | 14 ++++++++++++++ js/downloads.js | 10 ++++++++-- js/ui.js | 49 +++++++++++++++++++++++++++++++++++++++---------- 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/index.html b/index.html index fc7c704..f755577 100644 --- a/index.html +++ b/index.html @@ -138,9 +138,9 @@
-
Album

+
${formatTime(track.duration)}
@@ -66,13 +75,21 @@ export class UIRenderer { createAlbumCardHTML(album) { 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 `
${album.title}

${album.title} ${explicitBadge}

-

Album • ${album.artist?.name ?? ''}

+

${album.artist?.name ?? ''}

+

${yearDisplay}

`; } @@ -84,7 +101,6 @@ export class UIRenderer { ${artist.name}

${artist.name}

-

Artist

`; } @@ -109,7 +125,7 @@ export class UIRenderer {
-
+ ${!isArtist ? '
' : ''}
`; } @@ -257,12 +273,14 @@ export class UIRenderer { const imageEl = document.getElementById('album-detail-image'); const titleEl = document.getElementById('album-detail-title'); const metaEl = document.getElementById('album-detail-meta'); + const prodEl = document.getElementById('album-detail-producer'); const tracklistContainer = document.getElementById('album-detail-tracklist'); imageEl.src = ''; imageEl.style.backgroundColor = 'var(--muted)'; titleEl.innerHTML = '
'; metaEl.innerHTML = '
'; + prodEl.innerHTML = '
'; tracklistContainer.innerHTML = `
# @@ -282,15 +300,26 @@ export class UIRenderer { titleEl.innerHTML = `${album.title} ${explicitBadge}`; const totalDuration = calculateTotalDuration(tracks); - const releaseDate = new Date(album.releaseDate); - const year = releaseDate.getFullYear(); + let dateDisplay = ''; + 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 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 = - `By ${album.artist.name} • ${dateDisplay} • ${tracks.length} tracks • ${formatDuration(totalDuration)}`; + (dateDisplay ? `${dateDisplay} • ` : '') + + `${tracks.length} tracks • ${formatDuration(totalDuration)}`; + + prodEl.innerHTML = + `By ${album.artist.name}` + + (firstCopyright ? ` • ${firstCopyright}` : ''); tracklistContainer.innerHTML = `
From ed2d9425eb203d8121d5203128c572bc228ff35a Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 12:50:53 +0100 Subject: [PATCH 4/6] UI: dynamically adjust title font size based on length --- js/ui.js | 16 ++++++++++++++++ styles.css | 35 ++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/js/ui.js b/js/ui.js index 8cbbf7b..d3474cf 100644 --- a/js/ui.js +++ b/js/ui.js @@ -23,6 +23,15 @@ export class UIRenderer { `; } + 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 = ''; const trackImageHTML = showCover ? `Track Cover` : ''; @@ -299,6 +308,8 @@ export class UIRenderer { const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : ''; titleEl.innerHTML = `${album.title} ${explicitBadge}`; + this.adjustTitleFontSize(titleEl, album.title); + const totalDuration = calculateTotalDuration(tracks); let dateDisplay = ''; if (album.releaseDate) { @@ -378,6 +389,8 @@ async renderPlaylistPage(playlistId) { titleEl.textContent = playlist.title; + this.adjustTitleFontSize(titleEl, playlist.title); + const totalDuration = calculateTotalDuration(tracks); metaEl.textContent = `${playlist.numberOfTracks} tracks • ${formatDuration(totalDuration)}`; @@ -423,6 +436,9 @@ async renderPlaylistPage(playlistId) { imageEl.src = this.api.getArtistPictureUrl(artist.picture, '750'); imageEl.style.backgroundColor = ''; nameEl.textContent = artist.name; + + this.adjustTitleFontSize(nameEl, artist.name); + metaEl.textContent = `${artist.popularity} popularity`; this.renderListWithTracks(tracksContainer, artist.tracks, true); diff --git a/styles.css b/styles.css index 06391e7..2968e44 100644 --- a/styles.css +++ b/styles.css @@ -131,7 +131,7 @@ --highlight: #2563eb; --highlight-rgb: 37, 99, 235; --active-highlight: var(--highlight); - --explicit-badge: #000000; + --explicit-badge: #ef4444; --explicit-badge-foreground: #ffffff; } @@ -702,6 +702,15 @@ kbd { align-items: center; gap: 1rem; 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 { @@ -1837,6 +1846,14 @@ input:checked + .slider::before { 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 { padding: var(--spacing-lg); } @@ -1921,6 +1938,14 @@ input:checked + .slider::before { 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 { font-size: 0.85rem; gap: 0.35rem; @@ -2157,6 +2182,14 @@ input:checked + .slider::before { 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 { padding: var(--spacing-sm) var(--spacing-md); font-size: 0.9rem; From 8ef6ecb9e386bac3fcf37ede4b5c8e4ce0832a3d Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 12:53:56 +0100 Subject: [PATCH 5/6] FIX: context menu overflow on screen edges --- js/events.js | 47 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/js/events.js b/js/events.js index 39c239b..209f0d4 100644 --- a/js/events.js +++ b/js/events.js @@ -275,9 +275,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen contextTrack = trackDataStore.get(trackItem); if (contextTrack) { const rect = menuBtn.getBoundingClientRect(); - contextMenu.style.top = `${rect.bottom + 5}px`; - contextMenu.style.left = `${rect.left}px`; - contextMenu.style.display = 'block'; + positionMenu(contextMenu, rect.left, rect.bottom + 5, rect); } } return; @@ -307,9 +305,7 @@ export function initializeTrackInteractions(player, api, mainContent, contextMen contextTrack = trackDataStore.get(trackItem); if (contextTrack) { - contextMenu.style.top = `${e.pageY}px`; - contextMenu.style.left = `${e.pageX}px`; - contextMenu.style.display = 'block'; + positionMenu(contextMenu, e.pageX, e.pageY); } } }); @@ -387,3 +383,42 @@ function formatTime(seconds) { const s = Math.floor(seconds % 60); 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'; +} From 3ee2ed51708511b03de5a5e9c4f8b34b857c2c4d Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Tue, 23 Dec 2025 13:04:20 +0100 Subject: [PATCH 6/6] UI: reduce sidebar width on desktop --- styles.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/styles.css b/styles.css index 2968e44..bf91bcd 100644 --- a/styles.css +++ b/styles.css @@ -187,14 +187,14 @@ kbd { height: 100dvh; grid-template: "sidebar main" 1fr - "player player" auto / 280px 1fr; + "player player" auto / 190px 1fr; } .sidebar { grid-area: sidebar; background-color: var(--background); border-right: 1px solid var(--border); - padding: 1.5rem; + padding: 1.25rem; display: flex; flex-direction: column; gap: 2rem; @@ -1834,7 +1834,7 @@ input:checked + .slider::before { /* Responsive Design */ @media (max-width: 1024px) { .app-container { - grid-template-columns: 240px 1fr; + grid-template-columns: 160px 1fr; } .card-grid {