feat: add Hi-Res and Lossless quality indicators to queue and play bar
This commit is contained in:
parent
3ddcec9211
commit
e1a44b3502
7 changed files with 79 additions and 8 deletions
10
index.html
10
index.html
|
|
@ -1126,6 +1126,16 @@
|
||||||
<option value="LOW">AAC 96kbps</option>
|
<option value="LOW">AAC 96kbps</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Show Quality Badges</span>
|
||||||
|
<span class="description">Display "HR" badge for Hi-Res tracks</span>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="show-quality-badges-toggle" checked />
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="setting-item">
|
<div class="setting-item">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<span class="label">Gapless Playback</span>
|
<span class="label">Gapless Playback</span>
|
||||||
|
|
|
||||||
14
js/player.js
14
js/player.js
|
|
@ -1,6 +1,6 @@
|
||||||
//js/player.js
|
//js/player.js
|
||||||
import { MediaPlayer } from 'dashjs';
|
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';
|
import { queueManager, replayGainSettings } from './storage.js';
|
||||||
|
|
||||||
export class Player {
|
export class Player {
|
||||||
|
|
@ -119,7 +119,10 @@ export class Player {
|
||||||
const artistEl = document.querySelector('.now-playing-bar .artist');
|
const artistEl = document.querySelector('.now-playing-bar .artist');
|
||||||
|
|
||||||
if (coverEl) coverEl.src = this.api.getCoverUrl(track.album?.cover);
|
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;
|
if (artistEl) artistEl.innerHTML = trackArtistsHTML + yearDisplay;
|
||||||
|
|
||||||
const mixBtn = document.getElementById('now-playing-mix-btn');
|
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;
|
const currentQueue = this.shuffleActive ? this.shuffledQueue : this.queue;
|
||||||
if (this.currentQueueIndex < 0 || this.currentQueueIndex >= currentQueue.length) {
|
if (this.currentQueueIndex < 0 || this.currentQueueIndex >= currentQueue.length) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -240,7 +244,6 @@ export class Player {
|
||||||
|
|
||||||
this.saveQueueState();
|
this.saveQueueState();
|
||||||
|
|
||||||
const track = currentQueue[this.currentQueueIndex];
|
|
||||||
this.currentTrack = track;
|
this.currentTrack = track;
|
||||||
|
|
||||||
const trackTitle = getTrackTitle(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 .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;
|
document.querySelector('.now-playing-bar .artist').innerHTML = trackArtistsHTML + yearDisplay;
|
||||||
|
|
||||||
const mixBtn = document.getElementById('now-playing-mix-btn');
|
const mixBtn = document.getElementById('now-playing-mix-btn');
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
replayGainSettings,
|
replayGainSettings,
|
||||||
smoothScrollingSettings,
|
smoothScrollingSettings,
|
||||||
downloadQualitySettings,
|
downloadQualitySettings,
|
||||||
|
qualityBadgeSettings,
|
||||||
} from './storage.js';
|
} from './storage.js';
|
||||||
import { db } from './db.js';
|
import { db } from './db.js';
|
||||||
import { authManager } from './accounts/auth.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
|
// ReplayGain Settings
|
||||||
const replayGainMode = document.getElementById('replay-gain-mode');
|
const replayGainMode = document.getElementById('replay-gain-mode');
|
||||||
if (replayGainMode) {
|
if (replayGainMode) {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
export const queueManager = {
|
||||||
STORAGE_KEY: 'monochrome-queue',
|
STORAGE_KEY: 'monochrome-queue',
|
||||||
|
|
||||||
|
|
|
||||||
9
js/ui.js
9
js/ui.js
|
|
@ -10,6 +10,7 @@ import {
|
||||||
hasExplicitContent,
|
hasExplicitContent,
|
||||||
getTrackArtists,
|
getTrackArtists,
|
||||||
getTrackTitle,
|
getTrackTitle,
|
||||||
|
createQualityBadgeHTML,
|
||||||
calculateTotalDuration,
|
calculateTotalDuration,
|
||||||
formatDuration,
|
formatDuration,
|
||||||
escapeHtml,
|
escapeHtml,
|
||||||
|
|
@ -176,6 +177,7 @@ export class UIRenderer {
|
||||||
|
|
||||||
const trackNumberHTML = `<div class="track-number">${showCover ? trackImageHTML : displayIndex}</div>`;
|
const trackNumberHTML = `<div class="track-number">${showCover ? trackImageHTML : displayIndex}</div>`;
|
||||||
const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : '';
|
const explicitBadge = hasExplicitContent(track) ? this.createExplicitBadge() : '';
|
||||||
|
const qualityBadge = createQualityBadgeHTML(track);
|
||||||
const trackArtists = getTrackArtists(track);
|
const trackArtists = getTrackArtists(track);
|
||||||
const trackTitle = getTrackTitle(track);
|
const trackTitle = getTrackTitle(track);
|
||||||
const isCurrentTrack = this.player?.currentTrack?.id === track.id;
|
const isCurrentTrack = this.player?.currentTrack?.id === track.id;
|
||||||
|
|
@ -233,6 +235,7 @@ export class UIRenderer {
|
||||||
<div class="title">
|
<div class="title">
|
||||||
${escapeHtml(trackTitle)}
|
${escapeHtml(trackTitle)}
|
||||||
${explicitBadge}
|
${explicitBadge}
|
||||||
|
${qualityBadge}
|
||||||
</div>
|
</div>
|
||||||
<div class="artist">${escapeHtml(trackArtists)}${yearDisplay}</div>
|
<div class="artist">${escapeHtml(trackArtists)}${yearDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -401,6 +404,7 @@ export class UIRenderer {
|
||||||
|
|
||||||
createAlbumCardHTML(album) {
|
createAlbumCardHTML(album) {
|
||||||
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
const explicitBadge = hasExplicitContent(album) ? this.createExplicitBadge() : '';
|
||||||
|
const qualityBadge = createQualityBadgeHTML(album);
|
||||||
let yearDisplay = '';
|
let yearDisplay = '';
|
||||||
if (album.releaseDate) {
|
if (album.releaseDate) {
|
||||||
const date = new Date(album.releaseDate);
|
const date = new Date(album.releaseDate);
|
||||||
|
|
@ -417,7 +421,7 @@ export class UIRenderer {
|
||||||
type: 'album',
|
type: 'album',
|
||||||
id: album.id,
|
id: album.id,
|
||||||
href: `#album/${album.id}`,
|
href: `#album/${album.id}`,
|
||||||
title: `${escapeHtml(album.title)} ${explicitBadge}`,
|
title: `${escapeHtml(album.title)} ${explicitBadge} ${qualityBadge}`,
|
||||||
subtitle: `${escapeHtml(album.artist?.name ?? '')} • ${yearDisplay}${typeLabel}`,
|
subtitle: `${escapeHtml(album.artist?.name ?? '')} • ${yearDisplay}${typeLabel}`,
|
||||||
imageHTML: `<img src="${this.api.getCoverUrl(album.cover)}" alt="${escapeHtml(album.title)}" class="card-image" loading="lazy">`,
|
imageHTML: `<img src="${this.api.getCoverUrl(album.cover)}" alt="${escapeHtml(album.title)}" class="card-image" loading="lazy">`,
|
||||||
actionButtonsHTML: `
|
actionButtonsHTML: `
|
||||||
|
|
@ -629,7 +633,8 @@ export class UIRenderer {
|
||||||
|
|
||||||
const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');
|
const coverUrl = this.api.getCoverUrl(track.album?.cover, '1280');
|
||||||
image.src = coverUrl;
|
image.src = coverUrl;
|
||||||
title.textContent = track.title;
|
const qualityBadge = createQualityBadgeHTML(track);
|
||||||
|
title.innerHTML = `${escapeHtml(track.title)} ${qualityBadge}`;
|
||||||
artist.textContent = getTrackArtists(track);
|
artist.textContent = getTrackArtists(track);
|
||||||
|
|
||||||
if (nextTrack) {
|
if (nextTrack) {
|
||||||
|
|
|
||||||
12
js/utils.js
12
js/utils.js
|
|
@ -1,4 +1,5 @@
|
||||||
//js/utils.js
|
//js/utils.js
|
||||||
|
import { qualityBadgeSettings } from './storage.js';
|
||||||
|
|
||||||
export const QUALITY = 'LOSSLESS';
|
export const QUALITY = 'LOSSLESS';
|
||||||
|
|
||||||
|
|
@ -123,10 +124,19 @@ export const normalizeQualityToken = (value) => {
|
||||||
return quality;
|
return quality;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const createQualityBadgeHTML = (track) => {
|
||||||
|
if (!qualityBadgeSettings.isEnabled()) return '';
|
||||||
|
|
||||||
|
const quality = deriveTrackQuality(track);
|
||||||
|
if (quality === 'HI_RES_LOSSLESS') {
|
||||||
|
return '<span class="quality-badge quality-hires" title="Hi-Res Lossless">HR</span>';
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
export const deriveQualityFromTags = (rawTags) => {
|
export const deriveQualityFromTags = (rawTags) => {
|
||||||
if (!Array.isArray(rawTags)) return null;
|
if (!Array.isArray(rawTags)) return null;
|
||||||
|
|
||||||
|
|
|
||||||
12
styles.css
12
styles.css
|
|
@ -872,6 +872,18 @@ body.has-page-background .track-item:hover {
|
||||||
line-height: 1;
|
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 {
|
.track-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue