diff --git a/docs/features/ui.md b/docs/features/ui.md index 3b29249..a9fc991 100644 --- a/docs/features/ui.md +++ b/docs/features/ui.md @@ -176,6 +176,18 @@ The overflow popover itself is capped at `max-h-[calc(100dvh-7rem)]` with `overf The opt-in audio-quality footer ([`AudioQualityFooter`](../../src/components/player/AudioQualityFooter.tsx), pinned via Settings → Appearance → Player bar layout) is a thin strip below the player bar that surfaces the source file specs in compact form (`48 kHz · 256 kb/s · 6 Mo` on the left, `AAC · 24bit · 48kHz` on the right; bitrates ≥ 1000 kbps render as `Mb/s`). When the engine is resampling — source rate ≠ output device rate — the left chunk renders an arrow instead: `48 kHz → 44.1 kHz · …`, so the user can spot the conversion at a glance without opening the popover. The arrow is gated on the device rate being known (the engine reports `0` before the first stream opens); otherwise we fall back to the source rate alone rather than printing a misleading `48 kHz → null`. The Hi-Res pill surfaces when [`isHiRes`](../../src/lib/hiRes.ts) accepts the source bit depth / sample rate combination. +### Hi-Res / DSD badge + +[`HiResBadge`](../../src/components/common/HiResBadge.tsx) is the green pill that decorates track rows, album grid tiles, and the player-bar metadata when the source qualifies as Hi-Res (`isHiRes` — ≥ 24-bit, ≥ 44.1 kHz) or as DSD (`dsdLabel` returns `DSD64` / `DSD128` / …). Three variants: + +| Variant | Used in | Style | +| --------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | +| `overlay` | Album / artist grid covers (default). | Absolute-positioned pill in the cover's top-left corner with a drop shadow. | +| `inline` | TrackTable rows, sidebar lists. | Inline rounded pill next to the title. | +| `text` | Player bar — under the artist name. | Spotify-style minimal green uppercase text, no pill background, blends into the metadata stack. | + +All variants are gated by [`useHiResBadgeVisibility`](../../src/hooks/useHiResBadgeVisibility.ts), which reads `profile_setting['ui.show_hi_res_badge']` (default `true`) and re-reads on the `waveflow:hi-res-badge-visibility` window event. Settings → Appearance ships [`HiResBadgeCard`](../../src/components/views/settings/HiResBadgeCard.tsx) to flip the flag — when off, every mounted `HiResBadge` returns `null` in one render, including the player-bar text label. Per-profile so a kid's profile can hide the audiophile chrome while the audiophile profile keeps it. + Hovering (or keyboard-focusing) the footer opens [`AudioPipelinePopover`](../../src/components/player/AudioPipelinePopover.tsx) — an audiophile-grade breakdown of what the engine is actually doing. #### Sections displayed diff --git a/src/components/common/HiResBadge.tsx b/src/components/common/HiResBadge.tsx index 49dc0d0..f8a085b 100644 --- a/src/components/common/HiResBadge.tsx +++ b/src/components/common/HiResBadge.tsx @@ -1,4 +1,5 @@ import { dsdLabel, isHiRes } from "../../lib/hiRes"; +import { useHiResBadgeVisibility } from "../../hooks/useHiResBadgeVisibility"; /** * Hi-Res Audio badge — shown when the source file is delivered at a @@ -6,18 +7,25 @@ import { dsdLabel, isHiRes } from "../../lib/hiRes"; * DSD (in which case the badge says "DSD64", "DSD128", etc. instead * of "Hi-Res 24-bit"). * - * Two visual variants: - * - `overlay` is intended to sit on top of an album cover (top-left + * Three visual variants: + * - `overlay` (default) sits on top of an album cover (top-left * corner, drop shadow); * - `inline` is for sidebar / row contexts where the badge sits next - * to text. + * to text; + * - `text` is the minimal Spotify-style green text label used under + * the artist name in the player bar — no background pill so it + * nests cleanly inside dense metadata. + * + * Globally hidden by the per-profile + * `profile_setting['ui.show_hi_res_badge']` toggle — flipping that off + * returns `null` everywhere this component is mounted. */ interface HiResBadgeProps { bitDepth: number | null; sampleRate: number | null; /** Codec label from the scanner (e.g. "FLAC", "DSD128"). */ codec?: string | null; - variant?: "overlay" | "inline"; + variant?: "overlay" | "inline" | "text"; /** Override the visible text. Default is "Hi-Res {bitDepth}-bit". */ label?: string; } @@ -29,13 +37,21 @@ export function HiResBadge({ variant = "overlay", label, }: HiResBadgeProps) { + const { visible } = useHiResBadgeVisibility(); // DSD wins over the generic Hi-Res check — a DSF/DFF file reports // bit_depth=1 but is anything but lossy, and the user expects the // rate label (DSD64/128/...) rather than "Hi-Res 1-bit". const dsd = dsdLabel(codec); const isVisible = dsd !== null || isHiRes(bitDepth, sampleRate); - if (!isVisible) return null; + if (!isVisible || !visible) return null; const text = label ?? dsd ?? `Hi-Res ${bitDepth}-bit`; + if (variant === "text") { + return ( + + {text} + + ); + } if (variant === "inline") { return ( diff --git a/src/components/player/PlayerBar.tsx b/src/components/player/PlayerBar.tsx index 7f62338..3c721f4 100644 --- a/src/components/player/PlayerBar.tsx +++ b/src/components/player/PlayerBar.tsx @@ -13,6 +13,7 @@ import { useSleepTimer } from "../../hooks/useSleepTimer"; import { usePlayerBarLayout } from "../../hooks/usePlayerBarLayout"; import { Artwork } from "../common/Artwork"; import { ArtistLink } from "../common/ArtistLink"; +import { HiResBadge } from "../common/HiResBadge"; import { PlaybackControls } from "./PlaybackControls"; import { ProgressBar } from "./ProgressBar"; import { SleepTimerMenu } from "./SleepTimerMenu"; @@ -166,6 +167,19 @@ export function PlayerBar({ onNavigateToArtist }: PlayerBarProps) { (currentTrack?.album_title ?? t("player.inactive")) )} + {/* Spotify-style minimal quality label under the artist. + HiResBadge renders null when the track isn't Hi-Res / + DSD OR when the user has hidden the badge from + Settings → Appearance, so the row collapses naturally + for lossy / lossless-16-bit content. */} + {currentTrack && ( + + )} {currentTrack && !isSpotify && (