Skip to content

feat(ui): toggle Hi-Res badge + add minimal label in PlayerBar#147

Merged
InstaZDLL merged 3 commits into
mainfrom
feat/hi-res-badge-toggle-and-player-label
May 25, 2026
Merged

feat(ui): toggle Hi-Res badge + add minimal label in PlayerBar#147
InstaZDLL merged 3 commits into
mainfrom
feat/hi-res-badge-toggle-and-player-label

Conversation

@InstaZDLL
Copy link
Copy Markdown
Owner

@InstaZDLL InstaZDLL commented May 25, 2026

What

Two related UX bumps around the Hi-Res / DSD badge:

1. Settings toggle to hide the badge globally

New per-profile setting `ui.show_hi_res_badge` (default on). When flipped off, every mounted `HiResBadge` returns `null` — list rows, album / artist grid overlays, and the new player-bar label collapse in one render. Per-profile so a kid's profile can stay clean while the audiophile profile keeps the pills.

UI lives in `HiResBadgeCard` under Settings → Appearance, next to `WrappedBannerCard`. Backed by `useHiResBadgeVisibility` which mirrors the pattern used by `useWrappedBannerVisibility`: profile setting + window event for cross-mount refresh.

2. Minimal green label under the artist in the PlayerBar

Third variant added to `HiResBadge` — `variant="text"`. No pill background, just a tiny uppercase green text block that nests cleanly into the player-bar metadata column:

<title>
<artist>
HI-RES 24-BIT      ← new

Renders for Hi-Res (≥ 24-bit, ≥ 44.1 kHz) and DSD tracks; renders `null` for lossy / 16-bit lossless / when the user has hidden the badge.

Wiring

  • HiResBadge now reads the visibility hook internally — all existing call sites stay unchanged (`LibraryView`, `PlaylistView`, `AlbumDetailView`, `ArtistDetailView`, `GenreDetailView`, `LikedView`, `TrackPropertiesModal`). No prop drilling.
  • PlayerBar mounts the new `text` variant under the artist link.
  • 17 locales updated with the `settings.hiResBadge.{title,subtitle}` subtree.
  • Docs: `docs/features/ui.md` new "Hi-Res / DSD badge" section explains the three variants + the visibility flag.

Out of scope

The existing two variants (`overlay`, `inline`) keep their visuals unchanged so no track list / album grid is visually disturbed.

Test plan

  • `bun run typecheck` passes.
  • `bun run lint` passes for touched files (pre-existing errors in `secrets/validate-translations.mjs` are unrelated).
  • Play a 24-bit / 96 kHz FLAC → player bar shows `HI-RES 24-BIT` under the artist.
  • Play a DSD64 file → player bar shows `DSD64` instead of the Hi-Res text.
  • Play a 320 kbps MP3 → no quality label appears.
  • Open Settings → Appearance → toggle the new "Hi-Res / DSD badge" off → every list / album / player-bar label disappears immediately (event-driven re-render).
  • Switch profiles → setting is independent per profile.

Summary by CodeRabbit

  • Nouvelles fonctionnalités

    • Badge Hi‑Res / DSD affichable sous le titre/artiste dans la barre de lecture pour pistes éligibles.
    • Carte de réglage « Hi‑Res / DSD » dans Apparence pour activer/désactiver le badge.
    • Nouvelle variante d’affichage "text" pour le badge.
  • Internationalisation

    • Ajout de libellés de réglage pour 18 langues.
  • Documentation

    • Nouvelle section décrivant le badge, ses variantes et le réglage de visibilité.

Review Change Stack

Two related UI bumps:

- New `useHiResBadgeVisibility` hook backed by
  `profile_setting['ui.show_hi_res_badge']` (default ON). All three
  HiResBadge variants now skip render when the flag is off, so the
  toggle is a one-line global hide for users who find the audiophile
  chrome noisy. Per-profile so a kid's profile can stay clean while
  the audiophile profile keeps the pills.

- New `text` variant on HiResBadge — Spotify-style minimal green
  uppercase text, no pill background. Used under the artist name in
  the PlayerBar metadata stack so users instantly know whether the
  current track is being served at Hi-Res 24-bit / DSD spec without
  pinning the audio-quality footer.

- New Settings → Appearance card (`HiResBadgeCard`) hosting the
  toggle, with i18n across all 17 locales.

Implementation is intentionally minimal: the visibility hook lives
inside HiResBadge so existing call sites stay unchanged (no prop
drilling). The pattern mirrors `useWrappedBannerVisibility` —
profile setting + window event for refresh + same default-ON shape.
@InstaZDLL InstaZDLL added scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) scope: docs Docs, README, assets type: feat New feature labels May 25, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 25, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4d7a9044-67af-4d63-87c8-c9a88ba01e56

📥 Commits

Reviewing files that changed from the base of the PR and between efa19bb and bb4cd5e.

📒 Files selected for processing (1)
  • src/hooks/useHiResBadgeVisibility.ts

📝 Walkthrough

Walkthrough

Ajout d'un badge Hi-Res/DSD affichable dans le player, contrôlé globalement par useHiResBadgeVisibility, avec UI de réglage, traductions et documentation.

Changes

Badge Hi-Res/DSD avec contrôle global

Layer / File(s) Résumé
Gestion d'état et persistance utilisateur
src/hooks/useHiResBadgeVisibility.ts
Hook useHiResBadgeVisibility exposant visible booléen et setVisible() asynchrone. Charge la préférence via getProfileSetting('ui.show_hi_res_badge'), persiste via setProfileSetting(), dispatch HI_RES_BADGE_EVENT, et fournit refreshHiResBadgeVisibility().
Composant HiResBadge enrichi
src/components/common/HiResBadge.tsx
Extension du composant avec trois variantes : "overlay" (défaut), "inline", "text". Intègre useHiResBadgeVisibility() pour renvoyer null si masqué; ajoute rendu minimal pour variant === "text".
Intégration dans PlayerBar
src/components/player/PlayerBar.tsx
Affiche le badge sous les infos de piste avec <HiResBadge bitDepth={currentTrack.bit_depth} sampleRate={currentTrack.sample_rate} codec={currentTrack.codec} /> lorsque currentTrack est présent.
Carte de contrôle et Settings
src/components/views/settings/HiResBadgeCard.tsx, src/components/views/SettingsView.tsx
Composant HiResBadgeCard affichant un toggle contrôlé par useHiResBadgeVisibility() avec i18n et icône Sparkles. Intégré dans la section Appearance de SettingsView.
Traductions et documentation
docs/features/ui.md, src/i18n/locales/*
Documentation ajoutée décrivant variantes et visibilité via profile_setting['ui.show_hi_res_badge']. Ajout de settings.hiResBadge.{title,subtitle} dans les locales (ar, de, en, es, fr, hi, id, it, ja, ko, nl, pt-BR, pt, ru, tr, zh-CN, zh-TW).

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • InstaZDLL/WaveFlow#83: Modifie également SettingsView.tsx et restructure l'onglet Appearance, chevauchement probable sur le point d'insertion.
  • InstaZDLL/WaveFlow#82: Ajoute des éléments à l'onglet Appearance de SettingsView, potentiellement adjacent à l'insertion du HiResBadgeCard.

Suggested labels

size: m

Poem

✨ Un badge qui brille pour les pistes Hi‑Res,
Caché ou montré selon le choix de l'usager.
Hook, carte et player accordés en coulisse,
Une bascule orchestre toute la lueur. 🎵

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed Le titre suit précisément Conventional Commits avec un scope en kebab-case et décrit clairement les deux changements principaux : ajout d'un toggle pour le badge Hi-Res et l'ajout d'un label minimal dans la PlayerBar.
Description check ✅ Passed La description est complète et structurée : elle couvre le « What » (deux améliorations UX), le « How I tested » (checklist manuelle détaillée), explique le wiring technique, clarifie le scope, et confirme que les checks lint/typecheck sont passés.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/hi-res-badge-toggle-and-player-label

Comment @coderabbitai help to get the list of available commands and usage tips.

@InstaZDLL InstaZDLL added the size: l 200-500 lines label May 25, 2026
@InstaZDLL InstaZDLL self-assigned this May 25, 2026
@InstaZDLL InstaZDLL enabled auto-merge (squash) May 25, 2026 01:58
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/common/HiResBadge.tsx`:
- Around line 40-47: The HiResBadge component currently calls
useHiResBadgeVisibility for each instance causing N listeners; remove the local
hook call inside HiResBadge (reference HiResBadge and useHiResBadgeVisibility)
and instead accept a boolean prop visible (e.g. prop name visible) that the
parent provides; implement a single provider/store (e.g.
HiResBadgeVisibilityProvider or a shared hook) at a higher level to subscribe
once to profile_setting and compute visibility, then pass that visible prop into
each HiResBadge instance and remove any per-badge Tauri calls and listener
setup.

In `@src/hooks/useHiResBadgeVisibility.ts`:
- Around line 58-65: La mise à jour optimiste dans la fonction setVisible met à
jour l'état local avant d'écrire avec setProfileSetting, mais en cas d'échec
l'UI ne revient pas en arrière ; modifiez setVisible (et l'utilisation de KEY,
setProfileSetting, HI_RES_BADGE_EVENT et window.dispatchEvent) pour soit
effectuer l setVisibleState(next) après un write réussi, soit conserver la
valeur précédente (capturée avant le changement) et la restaurer dans le bloc
catch (p.ex. setVisibleState(previousValue)) après l'échec afin d'éviter une
divergence entre l'UI et l'état persistant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 15374187-8139-4434-8582-09d88c399c10

📥 Commits

Reviewing files that changed from the base of the PR and between 7e2a86f and 284e0d5.

📒 Files selected for processing (23)
  • docs/features/ui.md
  • src/components/common/HiResBadge.tsx
  • src/components/player/PlayerBar.tsx
  • src/components/views/SettingsView.tsx
  • src/components/views/settings/HiResBadgeCard.tsx
  • src/hooks/useHiResBadgeVisibility.ts
  • src/i18n/locales/ar.json
  • src/i18n/locales/de.json
  • src/i18n/locales/en.json
  • src/i18n/locales/es.json
  • src/i18n/locales/fr.json
  • src/i18n/locales/hi.json
  • src/i18n/locales/id.json
  • src/i18n/locales/it.json
  • src/i18n/locales/ja.json
  • src/i18n/locales/ko.json
  • src/i18n/locales/nl.json
  • src/i18n/locales/pt-BR.json
  • src/i18n/locales/pt.json
  • src/i18n/locales/ru.json
  • src/i18n/locales/tr.json
  • src/i18n/locales/zh-CN.json
  • src/i18n/locales/zh-TW.json

Comment thread src/components/common/HiResBadge.tsx
Comment thread src/hooks/useHiResBadgeVisibility.ts
Two findings from the PR #147 review:

1. Every mounted HiResBadge called the hook, which fired its own
   `useEffect` with a Tauri `getProfileSetting` read + a window
   event listener. On a virtualised library view with ~20-50 badges
   mounted at once that's 20-50 redundant fetches plus 20-50
   listeners for a single boolean.

   Refactor the hook to a module-level store + `useSyncExternalStore`:
   the first subscriber kicks off the one-and-only Tauri read, the
   window listener is attached lazily (and only once), and every
   `HiResBadge` shares the same `currentVisible` value through cheap
   `Set<callback>` notifications. Call sites stay unchanged — same
   public interface, same `visible`/`setVisible` shape.

2. The optimistic `setVisible` flipped the local state before the
   `setProfileSetting` write and never rolled back on failure, so a
   rejected Tauri call left the UI showing a value that didn't match
   what was actually persisted. Capture the previous value before
   the flip and restore it inside the catch so the visible state
   tracks the truth on the disk.

`refreshHiResBadgeVisibility` is also now exported for callers that
need to force a re-read (profile switch — separate follow-up since
the original hook had the same limitation, and the no-op cost of the
new cache between switches is benign).
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/hooks/useHiResBadgeVisibility.ts`:
- Line 126: The call to window.dispatchEvent in setVisible can throw in non-DOM
environments (SSR/tests) because the earlier guard only checked
addEventListener; update setVisible to first ensure a DOM-backed dispatch is
available (e.g., verify typeof window !== 'undefined' && typeof
window.dispatchEvent === 'function' or wrap the call in a safe try/catch) before
invoking window.dispatchEvent(new CustomEvent(HI_RES_BADGE_EVENT)); reference
the setVisible function and the HI_RES_BADGE_EVENT symbol when making the change
so the dispatch is properly protected.
- Around line 42-49: hydrateFromBackend can apply stale async results over newer
state and calls window.dispatchEvent without SSR/test guards; fix by
implementing a latest-read-wins guard: before awaiting getProfileSetting(KEY)
capture/advance a request token (e.g., a local incrementing id or timestamp
stored on the module) and after parseVisible only set currentVisible and call
notify() if the token matches the module's latest token, referencing
hydrateFromBackend, currentVisible, getProfileSetting, parseVisible, notify and
HI_RES_BADGE_EVENT; also wrap any window.dispatchEvent(...) calls with a guard
(typeof window !== "undefined") similar to ensureWindowListener to avoid crashes
in non-window environments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 300bc820-408a-4d4b-8462-68f84c399cc1

📥 Commits

Reviewing files that changed from the base of the PR and between 284e0d5 and efa19bb.

📒 Files selected for processing (1)
  • src/hooks/useHiResBadgeVisibility.ts

Comment thread src/hooks/useHiResBadgeVisibility.ts
Comment thread src/hooks/useHiResBadgeVisibility.ts Outdated
Two findings from the latest review:

- `hydrateFromBackend` could apply a stale `getProfileSetting` reply
  if two reads raced (e.g. the settings toggle dispatching the
  window event right next to a profile-switch
  `refreshHiResBadgeVisibility`). Capture a monotonic token at the
  top of the function and bail when a newer read has already
  superseded the in-flight one before committing to `currentVisible`.

- `window.dispatchEvent` in `setVisible` was unguarded, so a
  non-DOM caller (Node tests, hypothetical future SSR) would crash.
  Wrap with the same `typeof window !== "undefined"` check that
  `ensureWindowListener` already uses.

Skipped finding: the CodeRabbit "fan-out coûteux" pass ran on the
pre-`efa19bb` revision where each badge held its own listener +
Tauri call. The store-with-useSyncExternalStore refactor already
collapses that to one fetch + one listener total, so prop-drilling
a `visible` flag would add noise without any measurable win.
@InstaZDLL InstaZDLL merged commit 85c877b into main May 25, 2026
13 checks passed
@InstaZDLL InstaZDLL deleted the feat/hi-res-badge-toggle-and-player-label branch May 25, 2026 02:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: docs Docs, README, assets scope: frontend React/Vite frontend (src/) scope: i18n Translations (src/i18n/) size: l 200-500 lines type: feat New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant