commit
f3a0e40a1a
4 changed files with 184 additions and 139 deletions
|
|
@ -491,9 +491,6 @@
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<script defer data-domain="monochrome.samidy.com" src="https://plausible.canine.tools/js/script.file-downloads.hash.outbound-links.pageview-props.revenue.tagged-events.js"></script>
|
|
||||||
<script>window.plausible = window.plausible || function() { (window.plausible.q = window.plausible.q || []).push(arguments) }</script>
|
|
||||||
|
|
||||||
<script type="module" src="js/app.js"></script>
|
<script type="module" src="js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -175,12 +175,12 @@ function hideOfflineNotification() {
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', async () => {
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
const api = new LosslessAPI(apiSettings);
|
const api = new LosslessAPI(apiSettings);
|
||||||
const ui = new UIRenderer(api);
|
|
||||||
|
|
||||||
const audioPlayer = document.getElementById('audio-player');
|
const audioPlayer = document.getElementById('audio-player');
|
||||||
const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS';
|
const currentQuality = localStorage.getItem('playback-quality') || 'LOSSLESS';
|
||||||
const player = new Player(audioPlayer, api, currentQuality);
|
const player = new Player(audioPlayer, api, currentQuality);
|
||||||
|
|
||||||
|
const ui = new UIRenderer(api, player);
|
||||||
const scrobbler = new LastFMScrobbler();
|
const scrobbler = new LastFMScrobbler();
|
||||||
const lyricsManager = new LyricsManager(api);
|
const lyricsManager = new LyricsManager(api);
|
||||||
const lyricsPanel = createLyricsPanel();
|
const lyricsPanel = createLyricsPanel();
|
||||||
|
|
|
||||||
|
|
@ -244,10 +244,10 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||||
|
|
||||||
|
try {
|
||||||
const blob = await downloadTrackBlob(track, quality, api);
|
const blob = await downloadTrackBlob(track, quality, api);
|
||||||
zip.file(`${folderName}/${filename}`, blob);
|
zip.file(`${folderName}/${filename}`, blob);
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const meta = buildTrackMetadata(track, api);
|
const meta = buildTrackMetadata(track, api);
|
||||||
const metaFilename = filename.replace(/\.[^.]+$/, '.json');
|
const metaFilename = filename.replace(/\.[^.]+$/, '.json');
|
||||||
|
|
@ -276,6 +276,9 @@ export async function downloadAlbumAsZip(album, tracks, api, quality, lyricsMana
|
||||||
console.log('Could not add lyrics for:', trackTitle);
|
console.log('Could not add lyrics for:', trackTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...');
|
updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...');
|
||||||
|
|
@ -323,6 +326,7 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
updateBulkDownloadProgress(notification, i, tracks.length, trackTitle);
|
||||||
|
|
||||||
|
try {
|
||||||
const blob = await downloadTrackBlob(track, quality, api);
|
const blob = await downloadTrackBlob(track, quality, api);
|
||||||
zip.file(`${folderName}/${filename}`, blob);
|
zip.file(`${folderName}/${filename}`, blob);
|
||||||
|
|
||||||
|
|
@ -354,6 +358,9 @@ export async function downloadPlaylistAsZip(playlist, tracks, api, quality, lyri
|
||||||
console.log('Could not add lyrics for:', trackTitle);
|
console.log('Could not add lyrics for:', trackTitle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to download track ${trackTitle}:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...');
|
updateBulkDownloadProgress(notification, tracks.length, tracks.length, 'Creating ZIP...');
|
||||||
|
|
@ -410,6 +417,8 @@ export async function downloadDiscography(artist, api, quality, lyricsManager =
|
||||||
|
|
||||||
for (const track of tracks) {
|
for (const track of tracks) {
|
||||||
const filename = buildTrackFilename(track, quality);
|
const filename = buildTrackFilename(track, quality);
|
||||||
|
|
||||||
|
try {
|
||||||
const blob = await downloadTrackBlob(track, quality, api);
|
const blob = await downloadTrackBlob(track, quality, api);
|
||||||
zip.file(`${rootFolder}/${albumFolder}/${filename}`, blob);
|
zip.file(`${rootFolder}/${albumFolder}/${filename}`, blob);
|
||||||
|
|
||||||
|
|
@ -439,6 +448,9 @@ export async function downloadDiscography(artist, api, quality, lyricsManager =
|
||||||
console.log('Could not add lyrics for:', track.title);
|
console.log('Could not add lyrics for:', track.title);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Failed to download track ${track.title} in album ${album.title}:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to download album ${album.title}:`, error);
|
console.error(`Failed to download album ${album.title}:`, error);
|
||||||
|
|
|
||||||
74
js/ui.js
74
js/ui.js
|
|
@ -3,8 +3,9 @@ import { SVG_PLAY, SVG_DOWNLOAD, SVG_MENU, formatTime, createPlaceholder, trackD
|
||||||
import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js';
|
import { recentActivityManager, backgroundSettings, trackListSettings } from './storage.js';
|
||||||
|
|
||||||
export class UIRenderer {
|
export class UIRenderer {
|
||||||
constructor(api) {
|
constructor(api, player) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
|
this.player = player;
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
this.searchAbortController = null;
|
this.searchAbortController = null;
|
||||||
}
|
}
|
||||||
|
|
@ -72,6 +73,7 @@ export class UIRenderer {
|
||||||
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);
|
||||||
|
const isCurrentTrack = this.player?.currentTrack?.id === track.id;
|
||||||
|
|
||||||
let yearDisplay = '';
|
let yearDisplay = '';
|
||||||
const releaseDate = track.album?.releaseDate || track.streamStartDate;
|
const releaseDate = track.album?.releaseDate || track.streamStartDate;
|
||||||
|
|
@ -112,7 +114,7 @@ export class UIRenderer {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="track-item" data-track-id="${track.id}">
|
<div class="track-item ${isCurrentTrack ? 'playing' : ''}" data-track-id="${track.id}">
|
||||||
${trackNumberHTML}
|
${trackNumberHTML}
|
||||||
<div class="track-item-info">
|
<div class="track-item-info">
|
||||||
<div class="track-item-details">
|
<div class="track-item-details">
|
||||||
|
|
@ -266,24 +268,60 @@ export class UIRenderer {
|
||||||
if (!color) return;
|
if (!color) return;
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
|
const theme = root.getAttribute('data-theme');
|
||||||
|
const isLightMode = theme === 'light';
|
||||||
|
|
||||||
// Calculate contrast text color
|
let hex = color.replace('#', '');
|
||||||
const hex = color.replace('#', '');
|
// Handle shorthand hex
|
||||||
const r = parseInt(hex.substr(0, 2), 16);
|
if (hex.length === 3) {
|
||||||
const g = parseInt(hex.substr(2, 2), 16);
|
hex = hex.split('').map(char => char + char).join('');
|
||||||
const b = parseInt(hex.substr(4, 2), 16);
|
}
|
||||||
const brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
|
||||||
|
let r = parseInt(hex.substr(0, 2), 16);
|
||||||
|
let g = parseInt(hex.substr(2, 2), 16);
|
||||||
|
let b = parseInt(hex.substr(4, 2), 16);
|
||||||
|
|
||||||
|
// Calculate perceived brightness
|
||||||
|
let brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
|
||||||
|
if (isLightMode) {
|
||||||
|
// In light mode, the background is white.
|
||||||
|
// We need the color (used for text/highlights) to be dark enough.
|
||||||
|
// If brightness is too high (> 150), darken it.
|
||||||
|
while (brightness > 150) {
|
||||||
|
r = Math.floor(r * 0.9);
|
||||||
|
g = Math.floor(g * 0.9);
|
||||||
|
b = Math.floor(b * 0.9);
|
||||||
|
brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// In dark mode, the background is dark.
|
||||||
|
// We need the color to be light enough.
|
||||||
|
// If brightness is too low (< 80), lighten it.
|
||||||
|
while (brightness < 80) {
|
||||||
|
r = Math.min(255, Math.floor(r * 1.15));
|
||||||
|
g = Math.min(255, Math.floor(g * 1.15));
|
||||||
|
b = Math.min(255, Math.floor(b * 1.15));
|
||||||
|
brightness = ((r * 299) + (g * 587) + (b * 114)) / 1000;
|
||||||
|
// Break if we hit white or can't get brighter to avoid infinite loop
|
||||||
|
if (r >= 255 && g >= 255 && b >= 255) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const adjustedColor = `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
||||||
|
|
||||||
|
// Calculate contrast text color for buttons (text on top of the vibrant color)
|
||||||
const foreground = brightness > 128 ? '#000000' : '#ffffff';
|
const foreground = brightness > 128 ? '#000000' : '#ffffff';
|
||||||
|
|
||||||
// Set global CSS variables
|
// Set global CSS variables
|
||||||
root.style.setProperty('--primary', color);
|
root.style.setProperty('--primary', adjustedColor);
|
||||||
root.style.setProperty('--primary-foreground', foreground);
|
root.style.setProperty('--primary-foreground', foreground);
|
||||||
root.style.setProperty('--highlight', color);
|
root.style.setProperty('--highlight', adjustedColor);
|
||||||
root.style.setProperty('--highlight-rgb', `${r}, ${g}, ${b}`);
|
root.style.setProperty('--highlight-rgb', `${r}, ${g}, ${b}`);
|
||||||
root.style.setProperty('--active-highlight', color);
|
root.style.setProperty('--active-highlight', adjustedColor);
|
||||||
root.style.setProperty('--ring', color);
|
root.style.setProperty('--ring', adjustedColor);
|
||||||
|
|
||||||
// Calculate a safe hover color (darken if too light)
|
// Calculate a safe hover color
|
||||||
let hoverColor;
|
let hoverColor;
|
||||||
if (brightness > 200) {
|
if (brightness > 200) {
|
||||||
const dr = Math.floor(r * 0.85);
|
const dr = Math.floor(r * 0.85);
|
||||||
|
|
@ -627,7 +665,7 @@ export class UIRenderer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderPlaylistPage(playlistId) {
|
async renderPlaylistPage(playlistId) {
|
||||||
this.showPage('playlist');
|
this.showPage('playlist');
|
||||||
|
|
||||||
const imageEl = document.getElementById('playlist-detail-image');
|
const imageEl = document.getElementById('playlist-detail-image');
|
||||||
|
|
@ -662,13 +700,11 @@ async renderPlaylistPage(playlistId) {
|
||||||
imageEl.style.backgroundColor = '';
|
imageEl.style.backgroundColor = '';
|
||||||
|
|
||||||
titleEl.textContent = playlist.title;
|
titleEl.textContent = playlist.title;
|
||||||
|
|
||||||
this.adjustTitleFontSize(titleEl, 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)}`;
|
||||||
|
|
||||||
descEl.textContent = playlist.description || '';
|
descEl.textContent = playlist.description || '';
|
||||||
|
|
||||||
tracklistContainer.innerHTML = `
|
tracklistContainer.innerHTML = `
|
||||||
|
|
@ -680,14 +716,14 @@ async renderPlaylistPage(playlistId) {
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.renderListWithTracks(tracklistContainer, tracks, true);
|
this.renderListWithTracks(tracklistContainer, tracks, true);
|
||||||
|
|
||||||
recentActivityManager.addPlaylist(playlist);
|
recentActivityManager.addPlaylist(playlist);
|
||||||
|
|
||||||
document.title = `${playlist.title || 'Artist Mix'} - Monochrome`; } catch (error) {
|
document.title = `${playlist.title || 'Artist Mix'} - Monochrome`;
|
||||||
|
} catch (error) {
|
||||||
console.error("Failed to load playlist:", error);
|
console.error("Failed to load playlist:", error);
|
||||||
tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`);
|
tracklistContainer.innerHTML = createPlaceholder(`Could not load playlist details. ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async renderArtistPage(artistId) {
|
async renderArtistPage(artistId) {
|
||||||
this.showPage('artist');
|
this.showPage('artist');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue