From 9110b4b7649f6f1b94a7989d801a1cba7f2b2a40 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sat, 6 Jun 2026 11:59:53 -0400 Subject: [PATCH] docs: archive play-state icon normalization; update DeepDrftPublic.Client CLAUDE.md --- COMPLETED.md | 76 +++++++++++++++++++++++++++++++++ DeepDrftPublic.Client/CLAUDE.md | 10 +++-- 2 files changed, 83 insertions(+), 3 deletions(-) diff --git a/COMPLETED.md b/COMPLETED.md index 2952fd5..3e8d430 100644 --- a/COMPLETED.md +++ b/COMPLETED.md @@ -6,6 +6,82 @@ Newest entries at the top. Group by phase/wave header (mirroring `PLAN.md` / `CM --- +## Play-State Icon Normalization + +**Status:** Phases 1–4 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 `` 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 `` 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 **Status:** W3 (CMS track-preprocessing panel) refactored on 2026-06-05 (branch `waveform-w3-cms`, merged to dev). diff --git a/DeepDrftPublic.Client/CLAUDE.md b/DeepDrftPublic.Client/CLAUDE.md index d223180..5413ff8 100644 --- a/DeepDrftPublic.Client/CLAUDE.md +++ b/DeepDrftPublic.Client/CLAUDE.md @@ -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). - `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. - - `TrackCard.razor`: Individual track display (image, name, artist, album, genre, release date). - - `TracksGallery.razor`: Responsive grid of `TrackCard` items (MudBlazor `MudGrid` with breakpoints). + - `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). Fully controlled by parent; derives active-track state from cascaded player service. - `AppNavLink.razor`: Nav link with active-page highlight. - `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/PlayerControls.razor`: Play/pause/stop buttons in the transport zone. Renders via ``. + - `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. +- `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. - `IPlayerService` / `IStreamingPlayerService`: Contracts exposed to UI. - `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. - `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. -- `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