feat(ui): add normalization toggle to player bar
Adds 3-state normalization icon between fullscreen and volume controls. Uses mask-image SVGs for theme-compatible coloring. Reduces song card min-width by 72px to accommodate the new control.
This commit is contained in:
1 parent
717eb6fed2
commit
00d0ce7835
9 files changed
+168
-7
No files matched your search
@@ -69,6 +69,9 @@
|
||||
onTrackClick?: () => void;
|
||||
onContextClick?: () => void;
|
||||
queueOpen?: boolean;
|
||||
normalizationEnabled?: boolean;
|
||||
normalizationGain?: number | null;
|
||||
onToggleNormalization?: () => void;
|
||||
}
|
||||
|
||||
let {
|
||||
@@ -110,7 +113,10 @@
|
||||
onAlbumClick,
|
||||
onTrackClick,
|
||||
onContextClick,
|
||||
queueOpen = false
|
||||
queueOpen = false,
|
||||
normalizationEnabled = false,
|
||||
normalizationGain = null,
|
||||
onToggleNormalization
|
||||
}: Props = $props();
|
||||
|
||||
let progressRef: HTMLDivElement;
|
||||
@@ -382,6 +388,25 @@
|
||||
<Maximize2 size={16} />
|
||||
</button>
|
||||
|
||||
<!-- Normalization Toggle -->
|
||||
<button
|
||||
class="control-btn"
|
||||
class:active={normalizationEnabled && normalizationGain !== null && normalizationGain !== 1.0}
|
||||
class:norm-enabled={normalizationEnabled && (normalizationGain === null || normalizationGain === 1.0)}
|
||||
onclick={onToggleNormalization}
|
||||
title={!normalizationEnabled
|
||||
? $t('player.normalizationOff')
|
||||
: normalizationGain !== null && normalizationGain !== 1.0
|
||||
? $t('player.normalizationApplied')
|
||||
: $t('player.normalizationOn')}
|
||||
>
|
||||
<span
|
||||
class="norm-icon"
|
||||
class:norm-on={normalizationEnabled}
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
</button>
|
||||
|
||||
<!-- Volume Control -->
|
||||
<div class="volume-control">
|
||||
<div class="volume-value" class:visible={isDraggingVolume}>{volume}</div>
|
||||
@@ -651,6 +676,31 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Normalization Icon (mask-image for currentColor inheritance) */
|
||||
.norm-icon {
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background-color: currentColor;
|
||||
mask-image: url('/bars-disorder-outlined.svg');
|
||||
mask-size: contain;
|
||||
mask-repeat: no-repeat;
|
||||
mask-position: center;
|
||||
-webkit-mask-image: url('/bars-disorder-outlined.svg');
|
||||
-webkit-mask-size: contain;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
-webkit-mask-position: center;
|
||||
}
|
||||
|
||||
.norm-icon.norm-on {
|
||||
mask-image: url('/bars-normal.svg');
|
||||
-webkit-mask-image: url('/bars-normal.svg');
|
||||
}
|
||||
|
||||
.control-btn.norm-enabled {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
@@ -670,7 +720,7 @@
|
||||
padding: 2px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
min-width: 580px;
|
||||
min-width: 508px;
|
||||
flex: 1;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
@@ -394,7 +394,10 @@
|
||||
"maximize": "Maximieren",
|
||||
"restore": "Wiederherstellen",
|
||||
"minimize": "Minimieren",
|
||||
"exitImmersive": "Immersiv-Modus beenden"
|
||||
"exitImmersive": "Immersiv-Modus beenden",
|
||||
"normalizationOff": "Lautstärkenormalisierung: Aus",
|
||||
"normalizationOn": "Lautstärkenormalisierung: Ein",
|
||||
"normalizationApplied": "Lautstärkenormalisierung: Aktiv"
|
||||
},
|
||||
"audioBadges": {
|
||||
"castingTo": "Übertragung an",
|
||||
|
||||
@@ -408,7 +408,10 @@
|
||||
"maximize": "Maximize",
|
||||
"restore": "Restore",
|
||||
"minimize": "Minimize",
|
||||
"exitImmersive": "Exit Immersive"
|
||||
"exitImmersive": "Exit Immersive",
|
||||
"normalizationOff": "Volume Normalization: Off",
|
||||
"normalizationOn": "Volume Normalization: On",
|
||||
"normalizationApplied": "Volume Normalization: Active"
|
||||
},
|
||||
"audioBadges": {
|
||||
"castingTo": "Casting to",
|
||||
|
||||
@@ -408,7 +408,10 @@
|
||||
"maximize": "Maximizar",
|
||||
"restore": "Restaurar",
|
||||
"minimize": "Minimizar",
|
||||
"exitImmersive": "Salir del modo inmersivo"
|
||||
"exitImmersive": "Salir del modo inmersivo",
|
||||
"normalizationOff": "Normalización de volumen: Desactivada",
|
||||
"normalizationOn": "Normalización de volumen: Activada",
|
||||
"normalizationApplied": "Normalización de volumen: Activa"
|
||||
},
|
||||
"audioBadges": {
|
||||
"castingTo": "Transmitiendo a",
|
||||
|
||||
@@ -394,7 +394,10 @@
|
||||
"maximize": "Agrandir",
|
||||
"restore": "Restaurer",
|
||||
"minimize": "Réduire",
|
||||
"exitImmersive": "Quitter le mode immersif"
|
||||
"exitImmersive": "Quitter le mode immersif",
|
||||
"normalizationOff": "Normalisation du volume : Désactivée",
|
||||
"normalizationOn": "Normalisation du volume : Activée",
|
||||
"normalizationApplied": "Normalisation du volume : Active"
|
||||
},
|
||||
"audioBadges": {
|
||||
"castingTo": "Diffusion vers",
|
||||
|
||||
@@ -76,6 +76,7 @@ interface PlaybackEvent {
|
||||
volume: number;
|
||||
sample_rate: number | null; // Actual stream sample rate in Hz
|
||||
bit_depth: number | null; // Actual stream bit depth
|
||||
normalization_gain: number | null; // Active normalization gain factor (null = not applied)
|
||||
}
|
||||
|
||||
// Queue track from backend (for external track sync)
|
||||
@@ -132,6 +133,7 @@ let castUnsubscribe: (() => void) | null = null;
|
||||
let isAdvancingTrack = false;
|
||||
let isSkipping = false;
|
||||
let queueEnded = false;
|
||||
let normalizationGain: number | null = null; // Current normalization gain (null = not active)
|
||||
|
||||
// Callbacks for track advancement (set by consumer)
|
||||
let onTrackEnded: (() => Promise<void>) | null = null;
|
||||
@@ -188,6 +190,10 @@ export function getIsSkipping(): boolean {
|
||||
return isSkipping;
|
||||
}
|
||||
|
||||
export function getNormalizationGain(): number | null {
|
||||
return normalizationGain;
|
||||
}
|
||||
|
||||
// ============ State Setter ============
|
||||
|
||||
export interface PlayerState {
|
||||
@@ -198,6 +204,7 @@ export interface PlayerState {
|
||||
volume: number;
|
||||
isFavorite: boolean;
|
||||
isSkipping: boolean;
|
||||
normalizationGain: number | null;
|
||||
}
|
||||
|
||||
export function getPlayerState(): PlayerState {
|
||||
@@ -208,7 +215,8 @@ export function getPlayerState(): PlayerState {
|
||||
duration,
|
||||
volume,
|
||||
isFavorite,
|
||||
isSkipping
|
||||
isSkipping,
|
||||
normalizationGain
|
||||
};
|
||||
}
|
||||
|
||||
@@ -547,6 +555,9 @@ async function handlePlaybackEvent(event: PlaybackEvent): Promise<void> {
|
||||
currentTrack.bitDepth = event.bit_depth;
|
||||
}
|
||||
|
||||
// Update normalization gain state
|
||||
normalizationGain = event.normalization_gain;
|
||||
|
||||
notifyListeners();
|
||||
|
||||
// Check if track ended - auto-advance to next
|
||||
|
||||
@@ -531,6 +531,8 @@
|
||||
let duration = $state(0);
|
||||
let volume = $state(getVolume()); // Load persisted volume from localStorage
|
||||
let isFavorite = $state(false);
|
||||
let normalizationEnabled = $state(false);
|
||||
let normalizationGain = $state<number | null>(null);
|
||||
|
||||
// Queue/Shuffle State (from queueStore subscription)
|
||||
let isShuffle = $state(false);
|
||||
@@ -1382,6 +1384,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleNormalization() {
|
||||
const newState = !normalizationEnabled;
|
||||
try {
|
||||
await invoke('set_audio_normalization_enabled', { enabled: newState });
|
||||
normalizationEnabled = newState;
|
||||
} catch (err) {
|
||||
console.error('Failed to toggle normalization:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Add to Playlist handler for Now Playing track
|
||||
function openAddToPlaylistModal() {
|
||||
if (!currentTrack) return;
|
||||
@@ -2530,6 +2542,13 @@
|
||||
// Ignore storage errors
|
||||
}
|
||||
|
||||
// Load normalization enabled state from audio settings
|
||||
invoke<{ normalization_enabled: boolean }>('get_audio_settings').then((settings) => {
|
||||
normalizationEnabled = settings.normalization_enabled;
|
||||
}).catch((err) => {
|
||||
console.error('Failed to load normalization setting:', err);
|
||||
});
|
||||
|
||||
// Set up callback for cast disconnect handoff
|
||||
setOnAskContinueLocally(async (track, position) => {
|
||||
// Ask user if they want to continue locally
|
||||
@@ -2635,6 +2654,7 @@
|
||||
duration = playerState.duration;
|
||||
volume = playerState.volume;
|
||||
isFavorite = playerState.isFavorite;
|
||||
normalizationGain = playerState.normalizationGain;
|
||||
|
||||
// Save position during playback (debounced to every 5s)
|
||||
if (isPlaying && currentTrack && currentTime > 0) {
|
||||
@@ -3353,6 +3373,9 @@
|
||||
}}
|
||||
onContextClick={handleContextNavigation}
|
||||
queueOpen={isQueueOpen}
|
||||
{normalizationEnabled}
|
||||
{normalizationGain}
|
||||
onToggleNormalization={toggleNormalization}
|
||||
onTrackClick={() => {
|
||||
if (currentTrack && !currentTrack.isLocal) {
|
||||
trackInfoTrackId = currentTrack.id;
|
||||
@@ -3370,6 +3393,9 @@
|
||||
queueOpen={isQueueOpen}
|
||||
{volume}
|
||||
onVolumeChange={handleVolumeChange}
|
||||
{normalizationEnabled}
|
||||
{normalizationGain}
|
||||
onToggleNormalization={toggleNormalization}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 592.988 592.988"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<path d="M122.388,285.984H70.373c-32.058,0-58.14,26.083-58.14,58.139v190.225c0,32.059,26.082,58.139,58.14,58.139h52.016
|
||||
c32.059,0,58.14-26.08,58.14-58.139V344.123C180.528,312.066,154.447,285.984,122.388,285.984z M137.688,534.348
|
||||
c0,8.449-6.85,15.299-15.3,15.299H70.373c-8.45,0-15.3-6.85-15.3-15.299V344.123c0-8.449,6.85-15.299,15.3-15.299h52.016
|
||||
c8.45,0,15.3,6.85,15.3,15.299V534.348z"/>
|
||||
<path d="M122.388,592.986H70.373c-32.334,0-58.64-26.306-58.64-58.639V344.123c0-32.334,26.306-58.639,58.64-58.639h52.016
|
||||
c32.334,0,58.64,26.305,58.64,58.639v190.225C181.028,566.681,154.723,592.986,122.388,592.986z M70.373,286.484
|
||||
c-31.783,0-57.64,25.857-57.64,57.639v190.225c0,31.782,25.857,57.639,57.64,57.639h52.016c31.783,0,57.64-25.856,57.64-57.639
|
||||
V344.123c0-31.782-25.857-57.639-57.64-57.639H70.373z M122.388,550.146H70.373c-8.712,0-15.8-7.087-15.8-15.799V344.123
|
||||
c0-8.712,7.088-15.799,15.8-15.799h52.016c8.712,0,15.8,7.087,15.8,15.799v190.225
|
||||
C138.188,543.06,131.101,550.146,122.388,550.146z M70.373,329.324c-8.161,0-14.8,6.639-14.8,14.799v190.225
|
||||
c0,8.16,6.639,14.799,14.8,14.799h52.016c8.161,0,14.8-6.639,14.8-14.799V344.123c0-8.16-6.639-14.799-14.8-14.799H70.373z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M322.502,592.486c32.059,0,58.141-26.08,58.141-58.139V58.64c0-32.059-26.082-58.14-58.141-58.14h-52.016
|
||||
c-32.059,0-58.14,26.082-58.14,58.14v475.708c0,32.059,26.082,58.139,58.14,58.139H322.502z M255.186,534.348V58.64
|
||||
c0-8.45,6.85-15.3,15.3-15.3h52.016c8.449,0,15.301,6.85,15.301,15.3v475.708c0,8.449-6.852,15.299-15.301,15.299h-52.016
|
||||
C262.036,549.646,255.186,542.797,255.186,534.348z"/>
|
||||
<path d="M322.502,592.986h-52.016c-32.334,0-58.64-26.306-58.64-58.639V58.64c0-32.334,26.306-58.64,58.64-58.64h52.016
|
||||
c32.335,0,58.641,26.306,58.641,58.64v475.708C381.143,566.681,354.837,592.986,322.502,592.986z M270.486,1
|
||||
c-31.783,0-57.64,25.857-57.64,57.64v475.708c0,31.782,25.857,57.639,57.64,57.639h52.016c31.783,0,57.641-25.856,57.641-57.639
|
||||
V58.64c0-31.783-25.857-57.64-57.641-57.64H270.486z M322.502,550.146h-52.016c-8.712,0-15.8-7.087-15.8-15.799V58.64
|
||||
c0-8.712,7.088-15.8,15.8-15.8h52.016c8.713,0,15.801,7.088,15.801,15.8v475.708C338.303,543.06,331.215,550.146,322.502,550.146
|
||||
z M270.486,43.84c-8.161,0-14.8,6.639-14.8,14.8v475.708c0,8.16,6.64,14.799,14.8,14.799h52.016
|
||||
c8.161,0,14.801-6.639,14.801-14.799V58.64c0-8.161-6.64-14.8-14.801-14.8H270.486z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path d="M522.615,592.486c32.059,0,58.141-26.08,58.141-58.139V207.961c0-32.059-26.082-58.14-58.141-58.14H470.6
|
||||
c-32.057,0-58.139,26.082-58.139,58.14v326.386c0,32.059,26.082,58.141,58.139,58.141L522.615,592.486L522.615,592.486z
|
||||
M455.301,534.348V207.961c0-8.45,6.85-15.3,15.299-15.3h52.016c8.451,0,15.301,6.85,15.301,15.3v326.386
|
||||
c0,8.449-6.85,15.301-15.301,15.301H470.6C462.15,549.646,455.301,542.797,455.301,534.348z"/>
|
||||
<path d="M470.6,592.988c-32.333,0-58.639-26.306-58.639-58.641V207.961c0-32.334,26.306-58.64,58.639-58.64h52.016
|
||||
c32.335,0,58.641,26.306,58.641,58.64v326.386c0,32.333-26.306,58.639-58.641,58.639L470.6,592.988z M470.6,150.321
|
||||
c-31.782,0-57.639,25.857-57.639,57.64v326.386c0,31.783,25.856,57.641,57.639,57.641l52.016-0.002
|
||||
c31.783,0,57.641-25.856,57.641-57.639V207.961c0-31.783-25.857-57.64-57.641-57.64H470.6z M522.615,550.148H470.6
|
||||
c-8.712-0.002-15.799-7.09-15.799-15.801V207.961c0-8.712,7.087-15.8,15.799-15.8h52.016c8.713,0,15.801,7.088,15.801,15.8
|
||||
v326.386C538.416,543.061,531.328,550.148,522.615,550.148z M470.6,193.161c-8.16,0-14.799,6.639-14.799,14.8v326.386
|
||||
c0,8.159,6.639,14.799,14.799,14.801h52.016c8.161,0,14.801-6.64,14.801-14.801V207.961c0-8.161-6.64-14.8-14.801-14.8H470.6z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg fill="#000000" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="800px" height="800px" viewBox="0 0 978 978" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M0,903c0,33.1,26.9,60,60,60h160c33.1,0,60-26.9,60-60V75c0-33.1-26.9-60-60-60H60C26.9,15,0,41.9,0,75V903z"/>
|
||||
<path d="M409,15c-33.1,0-60,26.9-60,60v828c0,33.1,26.9,60,60,60h160c33.1,0,60-26.9,60-60V75c0-33.1-26.9-60-60-60H409z"/>
|
||||
<path d="M758,963h160c33.1,0,60-26.9,60-60V75c0-33.1-26.9-60-60-60H758c-33.1,0-60,26.9-60,60v828C698,936.1,724.9,963,758,963z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 823 B |
Reference in new issue
Block a user