10 KiB
CLAUDE.md - DeepDrftPublic.Client
Guidance for working in the DeepDrftPublic.Client project (the Blazor WebAssembly assembly).
See the root CLAUDE.md for full architecture overview. This file covers what is specific to this project.
One-line purpose
All interactive UI for the site. Blazor WebAssembly. Pages, controls, the streaming audio player stack, theme/dark-mode plumbing, HTTP clients for both backends.
Actual structure
Pages/: Routable components.Home.razor(hero/about),TracksView.razor(track gallery with pagination/sorting). No demo pages (Counter.razor,Weather.razordo not exist).Layout/:MainLayout.razor(root layout, wraps inAudioPlayerProvider, hosts theme switcher),DeepDrftMenu.razor(branded menu bar),NavMenu.razor(nav list),Pages.cs(centralised nav index —MenuPagesfor header,AllPagesfor exhaustive list).Controls/: Reusable components.TrackCard.razor: Individual track display (image, name, artist, album, genre, release date).TracksGallery.razor: Responsive grid ofTrackCarditems (MudBlazorMudGridwith breakpoints).AppNavLink.razor: Nav link with active-page highlight.AudioPlayerProvider.razor: Cascading host forIPlayerService. Everything inside it gets the player via[CascadingParameter].AudioPlayerBar.razor: Dock UI at the bottom (play/pause/seek/volume).SpectrumVisualizer.razor: Bar-graph spectrum display, driven bygetSpectrumDataJS callback.
Services/: Audio player + dark-mode services.IPlayerService/IStreamingPlayerService: Contracts exposed to UI.AudioPlayerService: Abstract base (lifecycle, initialise, select track, play/pause/stop/seek/volume).StreamingAudioPlayerService: Production implementation. Chunked stream fromTrackMediaClient, adaptive 16–64 KB buffer, early-playback, seek-beyond-buffer via offset request to the content API.AudioInteropService: JS interop wrapper overwindow.DeepDrftAudio. ManagesDotNetObjectReferencelifetimes for progress, end-of-playback, spectrum callbacks.- Dark-mode services:
DarkModeServiceBase(cookie name constant),DarkModeCookieService(JS cookie read/write).
Clients/: HTTP API clients (both target DeepDrftAPI).TrackClient: SQL metadata API. Uses namedIHttpClientFactoryclient"DeepDrft.API". Sendspageparam (notpageNumber). Deserializes response as barePagedResult<TrackDto>(not wrapped in ApiResultDto envelope).TrackMediaClient: Content API. Uses namedIHttpClientFactoryclient"DeepDrft.Content". Methods likeGetAudioStreamAsync(trackId, offset)→Stream.
ViewModels/: Component state.TracksViewModel: Scoped. Holds current page, page size, sort column, descending flag.SetPage(pageNumber)callsTrackClient.GetPageAsyncand updates. Registered inStartup.ConfigureDomainServices.
Common/: Shared utilities.DarkModeSettings.cs:[PersistentState]-annotated class (single source of truth for dark mode in the client). Registered scoped.DDIcons.cs: Hand-rolled SVG icons (gas-lamp lit/unlit for dark mode toggle).
Program.cs: WASM entry point. CallsStartup.ConfigureApiHttpClient,ConfigureContentServices,ConfigureDomainServices._Imports.razor: Global using statements and component imports.
Two HTTP clients pattern
Both clients are configured in Startup.cs (static methods called from both server and WASM Program.cs):
TrackClientuses"DeepDrft.API"(base address fromappsettings.jsonApiUrls:SqlApi, points to DeepDrftAPI). Fetches paginated metadata.TrackMediaClientuses"DeepDrft.Content"(base address fromappsettings.jsonApiUrls:ContentApi, points to DeepDrftAPI). Streams audio bytes, optionally with offset.
Both are configured with JSON serializer settings (case-insensitive property matching). The dual-client pattern keeps concerns separated: one for structured data, one for binary streaming. Both target DeepDrftAPI (they may share the same host URL in production).
Audio player stack (deepest part of the codebase)
Contracts
IPlayerService: Initialize, SelectTrack, Play, Pause, Stop, Seek, SetVolume. Sync interface.IStreamingPlayerService: Extends above. SelectTrackStreaming(track) starts the chunked stream flow.
Implementation
AudioPlayerService(abstract base): Lifecycle. Stores current track, playback state, volume. Derived classes implementSelectTrackStreaming/SelectTrackImmediate.StreamingAudioPlayerService(production): Constructor takesTrackMediaClient,AudioInteropService, logger.SelectTrackStreaming:- Calls
TrackMediaClient.GetAudioStreamAsync(trackId, offset: 0). StreamingAudioPlayerService.StreamAudioAsyncreads chunks (16–64 KB adaptive), pushes each viaAudioInteropService.ProcessStreamingChunkAsync(JS interop call).- TypeScript
StreamDecoderparses WAV header (first chunk), decodes subsequent chunks toAudioBuffers. PlaybackSchedulerschedules buffers on Web AudioAudioContext.- Playback starts as soon as a configurable min buffer count is queued.
- Seek beyond buffer: if seek target is past the decoded range,
Seek(position)callsTrackMediaClient.GetAudioStreamAsync(trackId, offset: byteOffset). Server'sWavOffsetServicesynthesises a new 44-byte WAV header and streams from the offset. Player tears down and re-initialises decoder for the new stream.
- Calls
Interop bridge
AudioInteropService.ProcessStreamingChunkAsync(chunk)calls JSwindow.DeepDrftAudio.processStreamingChunk(chunk)and awaits the Promise.AudioInteropServicealso manages callback registrations for progress (fired byPlaybackScheduler), end-of-playback (fired byPlaybackScheduler), and spectrum data (fired bySpectrumAnalyzer). Each callback is aDotNetObjectReferenceto a delegate.
Component integration
AudioPlayerProvider.razoris the cascading host. It injectsIPlayerService(resolved toStreamingAudioPlayerServicein DI), stores it in a cascade, and keeps it alive across navigation.AudioPlayerBar.razoris the dock UI. It cascades the player, binds buttons toPlay()/Pause()/Seek()/SetVolume(), and displays current time / duration / progress bar.SpectrumVisualizer.razorcallsAudioInteropService.GetSpectrumData()on a timer, receives bar heights, renders via MudBlazorMudChartor custom canvas.TracksView.razorinjectsTracksViewModel+ cascadedIPlayerService.PlayTrack(track)callsPlayerService.SelectTrack(track)(which resolves toStreamingAudioPlayerService.SelectTrackStreaming(track)).
Dark-mode plumbing
DarkModeSettings(Common/):[PersistentState]-annotated class withIsDarkModeproperty. Registered scoped inStartup.ConfigureDomainServices. Single source of truth in the client.DarkModeServiceBase: Holds the cookie name constant ("darkMode").DarkModeCookieService: Reads/writes the cookie via JS (document.cookieinterop). CallsDarkModeSettings.IsDarkMode = valuewhen the cookie changes or user toggles the button.- Server-side
DarkModeService(inDeepDrftPublic, not here): Reads the cookie during prerender, seeds theDarkModeSettingsinstance, rounds it throughPersistentComponentStateto the client. MainLayout.razor: Wraps entire layout inCascadingValueofDarkModeSettings, so all children see the current dark-mode state. The dark-mode toggle button (hand-rolled lit/unlit gas-lamp icon fromDDIcons.cs) callsDarkModeCookieService.ToggleDarkModeAsync().
The flow ensures the first paint uses the correct theme (no flash), and toggling the button persists the setting to a 365-day cookie.
MVVM convention
Component state lives in ViewModels (registered scoped in DI). Components render and dispatch only.
TracksViewModel: Holds page number, page size, sort column, descending flag.SetPage(pageNumber)is the command.TracksView.razorinjects it and callsSetPage.- New VMs go in
ViewModels/and register inStartup.ConfigureDomainServices.
Theming convention
- Bespoke
PaletteLight/PaletteDarkdefined inline inMainLayout.razor(MudBlazor theme objects). - CSS classes prefixed
deepdrft-live inDeepDrftPublic/wwwroot/styles/deepdrft-styles.css(shared across server and client). - Custom SVG icons:
Common/DDIcons.cs(hand-rolled gas-lamp, etc.).
Development commands
# The client runs as part of the DeepDrftPublic host:
dotnet run --project DeepDrftPublic
# Watch during development (rebuilds WASM as you change .cs/.razor/.ts files):
dotnet watch run --project DeepDrftPublic
# Build just the client (for verification):
dotnet build DeepDrftPublic.Client
# Run client-specific tests (if any; currently none exist):
dotnet test DeepDrftTests/
Configuration
Program.cs: Entry point. CallsStartup.ConfigureApiHttpClient(registers named clients),ConfigureContentServices(same),ConfigureDomainServices(registers services likeTracksViewModel,DarkModeSettings,AudioPlayerService).- Both
Startupmethods are static and called from both the serverDeepDrftPublic/Program.csand the clientProgram.cs, ensuring prerender and runtime DI are identical. - No
appsettings.jsonin the WASM assembly — config comes from the serverappsettings.jsonvia HTTP or is hardcoded.
Important patterns
- Cascading parameters:
AudioPlayerProvidercascadesIPlayerService. All children (includingMainLayoutand pages) access it via[CascadingParameter] IPlayerService Player { get; set; }. - Result types: Clients return
ApiResult<T>from NetBlocks. UI checksSuccessbefore usingValue. - Async/await: All operations are async.
- Stream consumption:
TrackMediaClient.GetAudioStreamAsyncreturns aStream(not fully buffered).StreamingAudioPlayerServicereads it in chunks to avoid memory pressure on large files.
When working with this project, maintain the separation between presentation (Razor components) and logic (ViewModels/Clients), follow the established audio player architecture, and respect the dark-mode round-trip (cookie → DarkModeSettings → PersistentComponentState → client).