From e1a44b35023448f5e6647f08103e04b7f82998f9 Mon Sep 17 00:00:00 2001 From: Julien Maille Date: Fri, 16 Jan 2026 20:54:58 +0100 Subject: [PATCH] feat: add Hi-Res and Lossless quality indicators to queue and play bar --- index.html | 10 ++++++++++ js/player.js | 14 +++++++++----- js/settings.js | 13 +++++++++++++ js/storage.js | 17 +++++++++++++++++ js/ui.js | 9 +++++++-- js/utils.js | 12 +++++++++++- styles.css | 12 ++++++++++++ 7 files changed, 79 insertions(+), 8 deletions(-) diff --git a/index.html b/index.html index 3646bcb..5652ab3 100644 --- a/index.html +++ b/index.html @@ -1126,6 +1126,16 @@ +
+
+ Show Quality Badges + Display "HR" badge for Hi-Res tracks +
+ +
Gapless Playback diff --git a/js/player.js b/js/player.js index 72ad1e8..4412a82 100644 --- a/js/player.js +++ b/js/player.js @@ -1,6 +1,6 @@ //js/player.js import { MediaPlayer } from 'dashjs'; -import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle, getTrackArtistsHTML } from './utils.js'; +import { REPEAT_MODE, formatTime, getTrackArtists, getTrackTitle, getTrackArtistsHTML, createQualityBadgeHTML } from './utils.js'; import { queueManager, replayGainSettings } from './storage.js'; export class Player { @@ -119,7 +119,10 @@ export class Player { const artistEl = document.querySelector('.now-playing-bar .artist'); if (coverEl) coverEl.src = this.api.getCoverUrl(track.album?.cover); - if (titleEl) titleEl.textContent = trackTitle; + if (titleEl) { + const qualityBadge = createQualityBadgeHTML(track); + titleEl.innerHTML = `${trackTitle} ${qualityBadge}`; + } if (artistEl) artistEl.innerHTML = trackArtistsHTML + yearDisplay; const mixBtn = document.getElementById('now-playing-mix-btn'); @@ -232,7 +235,8 @@ export class Player { } } - async playTrackFromQueue(startTime = 0) { + async playTrack(track, options = {}) { + const { startTime = 0 } = options; const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue; if (this.currentQueueIndex < 0 || this.currentQueueIndex >= currentQueue.length) { return; @@ -240,7 +244,6 @@ export class Player { this.saveQueueState(); - const track = currentQueue[this.currentQueueIndex]; this.currentTrack = track; const trackTitle = getTrackTitle(track); @@ -256,7 +259,8 @@ export class Player { } document.querySelector('.now-playing-bar .cover').src = this.api.getCoverUrl(track.album?.cover); - document.querySelector('.now-playing-bar .title').textContent = trackTitle; + const qualityBadge = createQualityBadgeHTML(track); + document.querySelector('.now-playing-bar .title').innerHTML = `${trackTitle} ${qualityBadge}`; document.querySelector('.now-playing-bar .artist').innerHTML = trackArtistsHTML + yearDisplay; const mixBtn = document.getElementById('now-playing-mix-btn'); diff --git a/js/settings.js b/js/settings.js index 4650278..4acd6cb 100644 --- a/js/settings.js +++ b/js/settings.js @@ -11,6 +11,7 @@ import { replayGainSettings, smoothScrollingSettings, downloadQualitySettings, + qualityBadgeSettings, } from './storage.js'; import { db } from './db.js'; import { authManager } from './accounts/auth.js'; @@ -272,6 +273,18 @@ export function initializeSettings(scrobbler, player, api, ui) { }); } + // Quality Badge Settings + const showQualityBadgesToggle = document.getElementById('show-quality-badges-toggle'); + if (showQualityBadgesToggle) { + showQualityBadgesToggle.checked = qualityBadgeSettings.isEnabled(); + showQualityBadgesToggle.addEventListener('change', (e) => { + qualityBadgeSettings.setEnabled(e.target.checked); + // Re-render to reflect changes + ui.renderLibraryPage(); + if (window.renderQueueFunction) window.renderQueueFunction(); + }); + } + // ReplayGain Settings const replayGainMode = document.getElementById('replay-gain-mode'); if (replayGainMode) { diff --git a/js/storage.js b/js/storage.js index f620707..ebdefdf 100644 --- a/js/storage.js +++ b/js/storage.js @@ -552,6 +552,23 @@ export const smoothScrollingSettings = { }, }; +export const qualityBadgeSettings = { + STORAGE_KEY: 'show-quality-badges', + + isEnabled() { + try { + const val = localStorage.getItem(this.STORAGE_KEY); + return val === null ? true : val === 'true'; + } catch { + return true; + } + }, + + setEnabled(enabled) { + localStorage.setItem(this.STORAGE_KEY, enabled ? 'true' : 'false'); + }, +}; + export const queueManager = { STORAGE_KEY: 'monochrome-queue', diff --git a/js/ui.js b/js/ui.js index 5ca00a7..bde3bff 100644 --- a/js/ui.js +++ b/js/ui.js @@ -10,6 +10,7 @@ import { hasExplicitContent, getTrackArtists, getTrackTitle, + createQualityBadgeHTML, calculateTotalDuration, formatDuration, escapeHtml, @@ -176,6 +177,7 @@ export class UIRenderer { const trackNumberHTML = `
${showCover ? trackImageHTML : displayIndex}
`; const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : ''; + const qualityBadge = createQualityBadgeHTML(track); const trackArtists = getTrackArtists(track); const trackTitle = getTrackTitle(track); const isCurrentTrack = this.player?.currentTrack?.id === track.id; @@ -233,6 +235,7 @@ export class UIRenderer {
${escapeHtml(trackTitle)} ${explicitBadge} + ${qualityBadge}
${escapeHtml(trackArtists)}${yearDisplay}
@@ -401,6 +404,7 @@ export class UIRenderer { createAlbumCardHTML(album) { const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : ''; + const qualityBadge = createQualityBadgeHTML(album); let yearDisplay = ''; if (album.releaseDate) { const date = new Date(album.releaseDate); @@ -417,7 +421,7 @@ export class UIRenderer { type: 'album', id: album.id, href: `#album/${album.id}`, - title: `${escapeHtml(album.title)} ${explicitBadge}`, + title: `${escapeHtml(album.title)} ${explicitBadge} ${qualityBadge}`, subtitle: `${escapeHtml(album.artist?.name ?? '')} • ${yearDisplay}${typeLabel}`, imageHTML: `${escapeHtml(album.title)}`, actionButtonsHTML: ` @@ -629,7 +633,8 @@ export class UIRenderer { const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280'); image.src = coverUrl; - title.textContent = track.title; + const qualityBadge = createQualityBadgeHTML(track); + title.innerHTML = `${escapeHtml(track.title)} ${qualityBadge}`; artist.textContent = getTrackArtists(track); if (nextTrack) { diff --git a/js/utils.js b/js/utils.js index 34192cc..6cc7b70 100644 --- a/js/utils.js +++ b/js/utils.js @@ -1,4 +1,5 @@ //js/utils.js +import { qualityBadgeSettings } from './storage.js'; export const QUALITY = 'LOSSLESS'; @@ -123,10 +124,19 @@ export const normalizeQualityToken = (value) => { return quality; } } - return null; }; +export const createQualityBadgeHTML = (track) => { + if (!qualityBadgeSettings.isEnabled()) return ''; + + const quality = deriveTrackQuality(track); + if (quality === 'HI_RES_LOSSLESS') { + return 'HR'; + } + return ''; +}; + export const deriveQualityFromTags = (rawTags) => { if (!Array.isArray(rawTags)) return null; diff --git a/styles.css b/styles.css index 189181e..6add99e 100644 --- a/styles.css +++ b/styles.css @@ -872,6 +872,18 @@ body.has-page-background .track-item:hover { line-height: 1; } +.quality-hires { + background-color: var(--highlight); + color: var(--primary-foreground); + font-size: 0.6rem; + font-weight: 700; + padding: 0.15rem 0.3rem; + border-radius: 3px; + margin-left: 0.5rem; + vertical-align: middle; + line-height: 1; +} + .track-list { display: flex; flex-direction: column;