docs: archive play-state icon normalization; update DeepDrftPublic.Client CLAUDE.md
Deploy DeepDrftAPI / Build, Publish & Bundle (push) Successful in 1m57s
Deploy DeepDrftManager / Build & Publish (push) Successful in 59s
Deploy DeepDrftPublic / Build & Publish (push) Successful in 3m34s
Deploy DeepDrftAPI / Deploy (push) Successful in 1m32s
Deploy DeepDrftManager / Deploy (push) Successful in 1m30s
Deploy DeepDrftPublic / Deploy (push) Successful in 1m26s

This commit is contained in:
daniel-c-harvey
2026-06-06 11:59:53 -04:00
parent 526e607f33
commit 9110b4b764
2 changed files with 83 additions and 3 deletions
+76
View File
@@ -6,6 +6,82 @@ Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CM
--- ---
## Play-State Icon Normalization
**Status:** Phases 14 landed on 2026-06-06 (branches `track-card-play-state-wave1`, `track-card-play-state-wave2`, merged to dev).
### Phase 1 — Fix the gallery bug (correctness, smallest viable change)
**Landed 2026-06-06.**
Bound `TrackCard.IsPlaying` to real playback state instead of selection identity. In `TracksView`/`TracksGallery`, active track is now computed as `PlayerService.IsPlaying && CurrentTrack?.Id == track.Id`. Switched the card glyph from `MusicNote` to the `PlayArrow`/`Pause` vocabulary via `IsPaused` and `OnPause` parameters. Expanded `TracksView.OnPlayerStateChanged` to re-render on any state change, not only on `!IsLoaded` — ensures the gallery correctly reflects pause, play, track-change, and end-of-playback transitions.
**Component changes:**
- `TrackCard.razor` — added `[Parameter] bool IsPaused`, `[Parameter] EventCallback OnPause` parameters; removed `MusicNote` icon; now conditionally renders `PlayArrow` when not playing or `Pause` when playing.
- `TracksView.razor` — removed `_selectedTrack` field (selection now fully derived from service); removed `_clickCount`, `_lifecycleStatus`, `TestInteractivity` dev scaffolding; `OnPlayerStateChanged` now calls `StateHasChanged()` unconditionally instead of only on `!IsLoaded`.
- `TracksGallery.razor` — removed internal `SelectedTrack` mutation and `StateHasChanged` calls on play click; now fully controlled by parent; `SelectedTrack` parameter is read-only.
**Architecture notes:**
- Resolves the reported bug: gallery card now shows correct play/pause icon reflecting actual playback state.
- Enabling pause affordance on cards required extending `TrackCard` with `IsPaused` + `OnPause`, preserving the component's presentational contract (stays parameter-driven, lives in shared library).
- `TracksView.OnPlayerStateChanged` subscription pattern unchanged; expansion from selective to unconditional re-render ensures high-frequency state changes (like spectrum animation or per-sample progress) do not cause visual lag in the gallery.
### Phase 2 — Collapse dual selection state (SRP, prevents regression)
**Landed 2026-06-06.**
Eliminated divergence between `TracksView._selectedTrack` and `PlayerService.CurrentTrack`. `TracksGallery` is now fully controlled — the parent supplies and owns the active-track identity via parameter binding. Selection state is single-sourced from the player service.
**Component changes:**
- `TracksGallery.razor` — removed parameter-field write in `HandlePlayClick`; no longer calls `StateHasChanged()` on click. Raises `SelectedTrackChanged` callback for the parent to route.
- `TracksView.razor` — removed `_selectedTrack` backing field and its local mutation.
**Architecture notes:**
- Resolves the secondary defect: gallery's notion of "active track" can no longer lag the player.
- `TracksGallery` now a pure presentational component (reads `SelectedTrack`, raises `SelectedTrackChanged`, renders); all state derivation lives in the parent or the service.
### Phase 3 — Introduce the single transport-state resolver (DRY)
**Landed 2026-06-06.**
Introduced a unified glyph-mapping source: `PlaybackIcons.Resolve()` static method in `DeepDrftPublic.Client/Helpers/PlaybackIcons.cs`. This is the sole function responsible for mapping `(IsPlaying, IsPaused, trackId?, CurrentTrackId?)` to the correct transport icon (`PlayArrow`, `Pause`, or null). Replaces all hand-rolled ternaries across `TrackCard`, `PlayerControls`, and other surfaces.
**New code (`DeepDrftPublic.Client/Helpers`):**
- `PlaybackIcons.cs` — static `Resolve(bool isPlaying, bool isPaused, long? trackId, long? currentTrackId)` method returning `(string? Icon, bool IsActive, bool IsPaused)` tuple. Icon mapping is the single source of truth.
**Component changes:**
- `PlayerControls.razor(.cs)``IsPlaying` parameter removed from the `AudioPlayerBar → PlayerTransportZone → PlayerControls` chain. Instead, `PlayerControls` now subscribes to `IPlayerService.StateChanged` directly and calls `PlaybackIcons.Resolve()` to determine which icon to render and whether buttons are enabled/disabled.
- `TrackCard.razor` — consumes the tuple returned by `PlaybackIcons.Resolve()` to set `Icon`, `IsActive` (CSS class for highlighting), and `Disabled` state on the FAB.
**Architecture notes:**
- Eliminates the three-way duplication of "which icon for this state" logic.
- Icon vocabulary is now standardized across all surfaces (`PlayArrow`/`Pause` pair, no `MusicNote`).
- Future surfaces (queue list, now-playing chip, etc.) call the same `Resolve()` function instead of re-implementing the mapping.
### Phase 4 (optional, deferred) — Promote to a PlayStateIcon component
**Landed 2026-06-06.**
Created a new `PlayStateIcon.razor` component in `DeepDrftPublic.Client/Controls/` that encapsulates subscription + icon mapping + rendering. Rather than each surface calling `PlaybackIcons.Resolve()` and threading icons through parameters, surfaces now drop in `<PlayStateIcon />` and the component handles cascading, state subscription, and icon selection in one place.
**New component (`DeepDrftPublic.Client/Controls/PlayStateIcon.razor`):**
- Injects `IPlayerService` and subscribes to `StateChanged` on mount.
- Cascades `[CascadingParameter] DarkModeSettings DarkMode` for theming.
- Renders an icon button (or FAB) with the correct glyph via `PlaybackIcons.Resolve()`.
- Forwards `Disabled` parameter to the rendered MudIconButton/MudFab.
- Raises `OnClick` callback when user clicks.
**Component changes:**
- `PlayerControls.razor` — refactored to render its play/pause button via `<PlayStateIcon />` instead of a parameter-driven button. `IsPlaying` parameter removed from the component signature.
- The `AudioPlayerBar → PlayerTransportZone → PlayerControls` chain no longer threads `IsPlaying`/`IsPaused` down; subscription happens inside `PlayStateIcon`.
**Architecture notes:**
- `PlayStateIcon` handles the seam between `IPlayerService` (source of truth) and transport-icon rendering (presentation). This was the third surface (after `TrackCard` and `PlayerControls`); Phase 4 was triggered by the appearance of the third call site.
- Reduces parameter threading in the component tree (no more passing state flags through intermediate layers).
- New surfaces that need play/pause icons (queue list, hover-row play button, etc.) now have a reusable, off-the-shelf component instead of re-implementing subscription and mapping.
---
## WaveformSeeker Wave 3 — CMS PreProcessing panel ## WaveformSeeker Wave 3 — CMS PreProcessing panel
**Status:** W3 (CMS track-preprocessing panel) refactored on 2026-06-05 (branch `waveform-w3-cms`, merged to dev). **Status:** W3 (CMS track-preprocessing panel) refactored on 2026-06-05 (branch `waveform-w3-cms`, merged to dev).
+7 -3
View File
@@ -13,12 +13,16 @@ All interactive UI for the site. Blazor WebAssembly. Pages, controls, the stream
- `Pages/`: Routable components. `Home.razor` (hero/about), `TracksView.razor` (track gallery with pagination/sorting). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist). - `Pages/`: Routable components. `Home.razor` (hero/about), `TracksView.razor` (track gallery with pagination/sorting). **No demo pages** (`Counter.razor`, `Weather.razor` do not exist).
- `Layout/`: `MainLayout.razor` (root layout, wraps in `AudioPlayerProvider`, hosts theme switcher), `DeepDrftMenu.razor` (branded menu bar), `NavMenu.razor` (nav list), `Pages.cs` (centralised nav index — `MenuPages` for header, `AllPages` for exhaustive list). - `Layout/`: `MainLayout.razor` (root layout, wraps in `AudioPlayerProvider`, hosts theme switcher), `DeepDrftMenu.razor` (branded menu bar), `NavMenu.razor` (nav list), `Pages.cs` (centralised nav index — `MenuPages` for header, `AllPages` for exhaustive list).
- `Controls/`: Reusable components. - `Controls/`: Reusable components.
- `TrackCard.razor`: Individual track display (image, name, artist, album, genre, release date). - `TrackCard.razor`: Individual track display (image, name, artist, album, genre, release date). Play/pause icon controlled via `IsPaused` parameter.
- `TracksGallery.razor`: Responsive grid of `TrackCard` items (MudBlazor `MudGrid` with breakpoints). - `TracksGallery.razor`: Responsive grid of `TrackCard` items (MudBlazor `MudGrid` with breakpoints). Fully controlled by parent; derives active-track state from cascaded player service.
- `AppNavLink.razor`: Nav link with active-page highlight. - `AppNavLink.razor`: Nav link with active-page highlight.
- `AudioPlayerProvider.razor`: Cascading host for `IStreamingPlayerService`. Everything inside it gets the player via `[CascadingParameter]`. - `AudioPlayerProvider.razor`: Cascading host for `IStreamingPlayerService`. Everything inside it gets the player via `[CascadingParameter]`.
- `AudioPlayerBar.razor`: Dock UI at the bottom (play/pause/seek/volume). - `AudioPlayerBar.razor`: Dock UI at the bottom (play/pause/seek/volume).
- `AudioPlayerBar/PlayerControls.razor`: Play/pause/stop buttons in the transport zone. Renders via `<PlayStateIcon>`.
- `AudioPlayerBar/PlayStateIcon.razor`: Icon button encapsulating service subscription + transport-state icon selection. Injects `IPlayerService`, subscribes to `StateChanged`, calls `PlaybackIcons.Resolve()` to determine icon and active state.
- `SpectrumVisualizer.razor`: Bar-graph spectrum display, driven by `getSpectrumData` JS callback. - `SpectrumVisualizer.razor`: Bar-graph spectrum display, driven by `getSpectrumData` JS callback.
- `Helpers/`: Utilities and mapper functions.
- `PlaybackIcons.cs`: Static `Resolve(isPlaying, isPaused, trackId, currentTrackId)` method — the sole glyph-mapping source for transport icons across all surfaces. Returns `(Icon, IsActive, IsPaused)` tuple.
- `Services/`: Audio player + dark-mode services. - `Services/`: Audio player + dark-mode services.
- `IPlayerService` / `IStreamingPlayerService`: Contracts exposed to UI. - `IPlayerService` / `IStreamingPlayerService`: Contracts exposed to UI.
- `AudioPlayerService`: Abstract base (lifecycle, initialise, select track, play/pause/stop/seek/volume). - `AudioPlayerService`: Abstract base (lifecycle, initialise, select track, play/pause/stop/seek/volume).
@@ -70,7 +74,7 @@ Both are configured with JSON serializer settings (case-insensitive property mat
- `AudioPlayerProvider.razor` is the cascading host. It injects `IStreamingPlayerService` (resolved to `StreamingAudioPlayerService` in DI), stores it in a cascade with `IsFixed="true"`, and keeps it alive across navigation. - `AudioPlayerProvider.razor` is the cascading host. It injects `IStreamingPlayerService` (resolved to `StreamingAudioPlayerService` in DI), stores it in a cascade with `IsFixed="true"`, and keeps it alive across navigation.
- `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose to re-render itself when the cascade updates. - `AudioPlayerBar.razor` is the dock UI. It cascades the player, binds buttons to `Play()` / `Pause()` / `Seek()` / `SetVolume()`, and displays current time / duration / progress bar. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` (reference-guarded, idempotent) and unsubscribes on dispose to re-render itself when the cascade updates.
- `SpectrumVisualizer.razor` calls `AudioInteropService.GetSpectrumData()` on a timer, receives bar heights, renders via MudBlazor `MudChart` or custom canvas. - `SpectrumVisualizer.razor` calls `AudioInteropService.GetSpectrumData()` on a timer, receives bar heights, renders via MudBlazor `MudChart` or custom canvas.
- `TracksView.razor` injects `TracksViewModel` + cascaded `IStreamingPlayerService`. `PlayTrack(track)` calls `PlayerService.SelectTrackStreaming(track)`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` and clears `_selectedTrack` when `!PlayerService.IsLoaded` (covers Stop, Unload, and end-of-track). - `TracksView.razor` injects `TracksViewModel` + cascaded `IStreamingPlayerService`. `PlayTrack(track)` calls `PlayerService.SelectTrackStreaming(track)`. Subscribes to `IPlayerService.StateChanged` in `OnParametersSet` and calls `StateHasChanged()` unconditionally on any state change, ensuring the gallery correctly reflects play/pause/track-change transitions. Active-track state is derived from `PlayerService.CurrentTrack` and `PlayerService.IsPlaying` (no local `_selectedTrack` field).
## Dark-mode plumbing ## Dark-mode plumbing